-
-
Save Picodes/db3251f15c1319d974cf5c090c65195f to your computer and use it in GitHub Desktop.
Reward Distributor
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// SPDX-License-Identifier: GPL-3.0 | |
pragma solidity 0.8.12; | |
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; | |
import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; | |
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; | |
import "../interfaces/ITreasury.sol"; | |
struct MerkleTree { | |
// Root of a Merkle tree which leaves are (address user, address token, uint amount) | |
// representing an amount of tokens owed to user. | |
// The Merkle tree is assumed to have only increasing amounts: that is to say if a user can claim 1, | |
// then after the amount associated in the Merkle tree for this token should be x > 1 | |
bytes32 merkleRoot; | |
// Ipfs hash of the tree data | |
bytes32 ipfsHash; | |
} | |
/// @title MerkleRootDistributor | |
/// @notice Allows the DAO to distribute rewards through Merkle Roots | |
/// @author Angle Core Team | |
contract MerkleRootDistributor is Initializable { | |
using SafeERC20 for IERC20; | |
/// @notice Tree of claimable tokens through this contract | |
MerkleTree public tree; | |
/// @notice Treasury contract handling access control | |
ITreasury public treasury; | |
/// @notice Mapping user -> token -> amount to track claimed amounts | |
mapping(address => mapping(address => uint256)) public claimed; | |
/// @notice Trusted EOAs to update the merkle root | |
mapping(address => uint256) public trusted; | |
uint256[46] private __gap; | |
// ================================== Events =================================== | |
event TrustedToggled(address indexed eoa, bool trust); | |
event Recovered(address indexed token, address indexed to, uint256 amount); | |
event TreeUpdated(bytes32 merkleRoot, bytes32 ipfsHash); | |
event Claimed(address user, address token, uint256 amount); | |
// ================================== Errors =================================== | |
error InvalidLengths(); | |
error InvalidProof(); | |
error NotGovernorOrGuardian(); | |
error NotTrusted(); | |
error ZeroAddress(); | |
// ================================= Modifiers ================================= | |
/// @notice Checks whether the `msg.sender` has the governor role or the guardian role | |
modifier onlyGovernorOrGuardian() { | |
if (!treasury.isGovernorOrGuardian(msg.sender)) revert NotGovernorOrGuardian(); | |
_; | |
} | |
/// @notice Checks whether the `msg.sender` is a trusted address to change the Merkle root of the contract | |
modifier onlyTrusted() { | |
if (!treasury.isGovernorOrGuardian(msg.sender) && trusted[msg.sender] != 1) revert NotTrusted(); | |
_; | |
} | |
// ============================ Constructor ==================================== | |
constructor() initializer {} | |
function initialize(ITreasury _treasury) public initializer { | |
if(address(_treasury) == address(0)) revert ZeroAddress(); | |
treasury = _treasury; | |
} | |
// =========================== Main Function =================================== | |
/// @notice Claims rewards for a given set of users | |
/// @dev Anyone may call this function for anyone else, funds go to destination regardless, it's just a question of | |
/// who provides the proof and pays the gas: `msg.sender` is not used in this function | |
/// @param users Recipient of tokens | |
/// @param tokens ERC20 claimed | |
/// @param amounts Amount of tokens that will be sent to the corresponding users | |
/// @param proofs Array of hashes bridging from leaf (hash of user | token | amount) to Merkle root | |
function claim( | |
address[] calldata users, | |
address[] calldata tokens, | |
uint256[] calldata amounts, | |
bytes32[][] calldata proofs | |
) public { | |
if ( | |
users.length == 0 || | |
users.length != tokens.length || | |
users.length != amounts.length || | |
users.length != proofs.length | |
) revert InvalidLengths(); | |
for (uint256 i = 0; i < users.length; i++) { | |
address user = users[i]; | |
address token = tokens[i]; | |
uint256 amount = amounts[i]; | |
// Verifying proof | |
bytes32 leaf = keccak256(abi.encode(user, token, amount)); | |
if (!_verifyProof(leaf, proofs[i])) revert InvalidProof(); | |
// Closing reentrancy gate here | |
uint256 toSend = amount - claimed[user][token]; | |
claimed[user][token] = amount; | |
IERC20(token).safeTransfer(user, toSend); | |
emit Claimed(user, token, toSend); | |
} | |
} | |
// =========================== Governance Functions ============================ | |
/// @notice Adds or removes trusted EOA | |
function toggleTrusted(address eoa) external onlyGovernorOrGuardian { | |
uint256 trustedStatus = 1 - trusted[eoa]; | |
trusted[eoa] = trustedStatus; | |
emit TrustedToggled(eoa, trustedStatus == 1); | |
} | |
/// @notice Updates Merkle Tree | |
function updateTree(MerkleTree calldata _tree) external onlyTrusted { | |
tree = _tree; | |
emit TreeUpdated(_tree.merkleRoot, _tree.ipfsHash); | |
} | |
/// @notice Recovers any ERC20 token | |
function recoverERC20( | |
address tokenAddress, | |
address to, | |
uint256 amountToRecover | |
) external onlyGovernorOrGuardian { | |
IERC20(tokenAddress).safeTransfer(to, amountToRecover); | |
emit Recovered(tokenAddress, to, amountToRecover); | |
} | |
// =========================== Internal Functions ============================== | |
/// @notice Checks the validity of a proof | |
/// @param leaf Hashed leaf data, the starting point of the proof | |
/// @param proof Array of hashes forming a hash chain from leaf to root | |
/// @return true If proof is correct, else false | |
function _verifyProof(bytes32 leaf, bytes32[] memory proof) internal view returns (bool) { | |
bytes32 currentHash = leaf; | |
for (uint256 i = 0; i < proof.length; i += 1) { | |
if (currentHash < proof[i]) { | |
currentHash = keccak256(abi.encode(currentHash, proof[i])); | |
} else { | |
currentHash = keccak256(abi.encode(proof[i], currentHash)); | |
} | |
} | |
return currentHash == tree.merkleRoot; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment