MerkleDistributor
MerkleDistributor
#
Inspired by:
- https://github.com/pie-dao/vested-token-migration-app
- https://github.com/Uniswap/merkle-distributor
- https://github.com/balancer-labs/erc20-redeemable
Allows an owner to distribute any reward ERC20 to claimants according to Merkle roots. The owner can specify multiple Merkle roots distributions with customized reward currencies.
The Merkle trees are not validated in any way, so the system assumes the contract owner behaves honestly.
#
FunctionssetWindow(uint256 rewardsToDeposit, address rewardToken, bytes32 merkleRoot, string ipfsHash) (external)
Set merkle root for the next available window index and seed allocations.
Callable only by owner of this contract. Caller must have approved this contract to transfer
rewardsToDeposit
amount of rewardToken
or this call will fail. Importantly, we assume that the
owner of this contract correctly chooses an amount rewardsToDeposit
that is sufficient to cover all
claims within the merkleRoot
. Otherwise, a race condition can be created. This situation can occur
because we do not segregate reward balances by window, for code simplicity purposes.
(If rewardsToDeposit
is purposefully insufficient to payout all claims, then the admin must
subsequently transfer in rewards or the following situation can occur).
Example race situation:
- Window 1 Tree: Owner sets `rewardsToDeposit=100` and insert proofs that give claimant A 50 tokens and claimant B 51 tokens. The owner has made an error by not setting the `rewardsToDeposit` correctly to 101. - Window 2 Tree: Owner sets `rewardsToDeposit=1` and insert proofs that give claimant A 1 token. The owner correctly set `rewardsToDeposit` this time. - At this point contract owns 100 + 1 = 101 tokens. Now, imagine the following sequence: (1) Claimant A claims 50 tokens for Window 1, contract now has 101 - 50 = 51 tokens. (2) Claimant B claims 51 tokens for Window 1, contract now has 51 - 51 = 0 tokens. (3) Claimant A tries to claim 1 token for Window 2 but fails because contract has 0 tokens. - In summary, the contract owner created a race for step(2) and step(3) in which the first claim would succeed and the second claim would fail, even though both claimants would expect their claims to succeed.
#
Parameters:- rewardsToDeposit: amount of rewards to deposit to seed this allocation.
- rewardToken: ERC20 reward token.
- merkleRoot: merkle root describing allocation.
- ipfsHash: hash of IPFS object, conveniently stored for clients
deleteWindow(uint256 windowIndex) (external)
Delete merkle root at window index.
Callable only by owner. Likely to be followed by a withdrawRewards call to clear contract state.
#
Parameters:- windowIndex: merkle root index to delete.
withdrawRewards(address rewardCurrency, uint256 amount) (external)
Emergency method that transfers rewards out of the contract if the contract was configured improperly.
Callable only by owner.
#
Parameters:- rewardCurrency: rewards to withdraw from contract.
- amount: amount of rewards to withdraw.
claimMulti(struct MerkleDistributor.Claim[] claims) (external)
Batch claims to reduce gas versus individual submitting all claims. Method will fail if any individual claims within the batch would fail.
Optimistically tries to batch together consecutive claims for the same account and same reward token to reduce gas. Therefore, the most gas-cost-optimal way to use this method is to pass in an array of claims sorted by account and reward currency.
#
Parameters:- claims: array of claims to claim.
claim(struct MerkleDistributor.Claim _claim) (public)
Claim amount of reward tokens for account, as described by Claim input object.
If the _claim
's amount
, accountIndex
, and account
do not exactly match the
values stored in the merkle root for the _claim
's windowIndex
this method
will revert.
#
Parameters:- _claim: claim object describing amount, accountIndex, account, window index, and merkle proof.
isClaimed(uint256 windowIndex, uint256 accountIndex) โ bool (public)
Returns True if the claim for accountIndex
has already been completed for the Merkle root at
windowIndex
.
This method will only work as intended if all accountIndex
's are unique for a given windowIndex
.
The onus is on the Owner of this contract to submit only valid Merkle roots.
#
Parameters:- windowIndex: merkle root to check.
- accountIndex: account index to check within window index.
verifyClaim(struct MerkleDistributor.Claim _claim) โ bool valid (public)
Returns True if leaf described by {account, amount, accountIndex} is stored in Merkle root at given window index.
#
Parameters:- _claim: claim object describing amount, accountIndex, account, window index, and merkle proof.
constructor() (internal)
Initializes the contract setting the deployer as the initial owner.
owner() โ address (public)
Returns the address of the current owner.
renounceOwnership() (public)
Leaves the contract without owner. It will not be possible to call
onlyOwner
functions anymore. Can only be called by the current owner.
NOTE: Renouncing ownership will leave the contract without an owner,
thereby removing any functionality that is only available to the owner.
transferOwnership(address newOwner) (public)
Transfers ownership of the contract to a new account (newOwner
).
Can only be called by the current owner.
_transferOwnership(address newOwner) (internal)
Transfers ownership of the contract to a new account (newOwner
).
Internal function without access restriction.
_msgSender() โ address (internal)
_msgData() โ bytes (internal)
#
EventsClaimed(address caller, uint256 windowIndex, address account, uint256 accountIndex, uint256 amount, address rewardToken)
CreatedWindow(uint256 windowIndex, uint256 rewardsDeposited, address rewardToken, address owner)
WithdrawRewards(address owner, uint256 amount, address currency)
DeleteWindow(uint256 windowIndex, address owner)
OwnershipTransferred(address previousOwner, address newOwner)
#
ModifiersonlyOwner()
Throws if called by any account other than the owner.