-
Notifications
You must be signed in to change notification settings - Fork 48
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Create HATAirdrop #542
Merged
Merged
Create HATAirdrop #542
Changes from all commits
Commits
Show all changes
18 commits
Select commit
Hold shift + click to select a range
01328dd
Create HATAirdrop
ben-kaufman dab98e2
Lint
ben-kaufman 805573a
Redeem to token lock
ben-kaufman 5bec34a
Add Airdrop Factory
ben-kaufman 564d8a3
Update factory event
ben-kaufman d27d856
Hold funds in factory
ben-kaufman 64b8c3c
Update HATAirdropFactory.sol
ben-kaufman 9cf2794
Avoid deploying lock if time passed
ben-kaufman c6e2993
Allow specifying implementation on deployment
ben-kaufman d7d97b0
Disallow redeeming for others
ben-kaufman c957604
Gas optimization
ben-kaufman b7efd2f
Revert "Disallow redeeming for others"
ben-kaufman 99dbca8
Redeem from factory
ben-kaufman d037f55
Lint
ben-kaufman b99c8c4
Make init data customizable
ben-kaufman 136a041
Add initialize docstring
ben-kaufman 6a0b7bf
Add interface for IHATAirdrop contracts
ben-kaufman 652566f
Update HATAirdrop.sol docstring
ben-kaufman File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity 0.8.16; | ||
|
||
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; | ||
import "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol"; | ||
import "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; | ||
import "@openzeppelin/contracts-upgradeable/utils/cryptography/MerkleProofUpgradeable.sol"; | ||
import "../tokenlock/ITokenLockFactory.sol"; | ||
import "./interfaces/IHATAirdrop.sol"; | ||
|
||
/* | ||
An airdrop contract that transfers tokens based on a merkle tree. | ||
*/ | ||
contract HATAirdrop is IHATAirdrop, Initializable { | ||
error CannotRedeemBeforeStartTime(); | ||
error CannotRedeemAfterDeadline(); | ||
error LeafAlreadyRedeemed(); | ||
error InvalidMerkleProof(); | ||
error CannotRecoverBeforeDeadline(); | ||
error RedeemerMustBeBeneficiary(); | ||
|
||
using SafeERC20Upgradeable for IERC20Upgradeable; | ||
|
||
bytes32 public root; | ||
uint256 public startTime; | ||
uint256 public deadline; | ||
uint256 public lockEndTime; | ||
uint256 public periods; | ||
jellegerbrandy marked this conversation as resolved.
Show resolved
Hide resolved
|
||
IERC20Upgradeable public token; | ||
ITokenLockFactory public tokenLockFactory; | ||
address public factory; | ||
|
||
mapping (bytes32 => bool) public leafRedeemed; | ||
|
||
event MerkleTreeSet(string _merkleTreeIPFSRef, bytes32 _root, uint256 _startTime, uint256 _deadline); | ||
event TokensRedeemed(address indexed _account, address indexed _tokenLock, uint256 _amount); | ||
|
||
constructor () { | ||
_disableInitializers(); | ||
} | ||
|
||
/** | ||
* @notice Initialize a HATAirdrop instance | ||
* @param _merkleTreeIPFSRef new merkle tree ipfs reference. | ||
* @param _root new merkle tree root to use for verifying airdrop data. | ||
* @param _startTime start of the redeem period and of the token lock (if exists) | ||
* @param _deadline end time to redeem from the contract | ||
* @param _lockEndTime end time for the token lock contract. If this date is in the past, the tokens will be transferred directly to the user and no token lock will be created | ||
* @param _periods number of periods of the token lock contract (if exists) | ||
* @param _token the token to be airdropped | ||
* @param _tokenLockFactory the token lock factory to use to deploy the token locks | ||
*/ | ||
function initialize( | ||
jellegerbrandy marked this conversation as resolved.
Show resolved
Hide resolved
|
||
string memory _merkleTreeIPFSRef, | ||
jellegerbrandy marked this conversation as resolved.
Show resolved
Hide resolved
|
||
bytes32 _root, | ||
uint256 _startTime, | ||
uint256 _deadline, | ||
uint256 _lockEndTime, | ||
uint256 _periods, | ||
IERC20Upgradeable _token, | ||
ITokenLockFactory _tokenLockFactory | ||
) external initializer { | ||
root = _root; | ||
startTime = _startTime; | ||
deadline = _deadline; | ||
lockEndTime = _lockEndTime; | ||
periods = _periods; | ||
token = _token; | ||
tokenLockFactory = _tokenLockFactory; | ||
factory = msg.sender; | ||
emit MerkleTreeSet(_merkleTreeIPFSRef, _root, _startTime, _deadline); | ||
} | ||
|
||
function redeem(address _account, uint256 _amount, bytes32[] calldata _proof) external { | ||
if (msg.sender != _account && msg.sender != factory) { | ||
revert RedeemerMustBeBeneficiary(); | ||
} | ||
// solhint-disable-next-line not-rely-on-time | ||
if (block.timestamp < startTime) revert CannotRedeemBeforeStartTime(); | ||
// solhint-disable-next-line not-rely-on-time | ||
if (block.timestamp > deadline) revert CannotRedeemAfterDeadline(); | ||
bytes32 leaf = _leaf(_account, _amount); | ||
if (leafRedeemed[leaf]) revert LeafAlreadyRedeemed(); | ||
if(!_verify(_proof, leaf)) revert InvalidMerkleProof(); | ||
leafRedeemed[leaf] = true; | ||
|
||
address _tokenLock = address(0); | ||
// solhint-disable-next-line not-rely-on-time | ||
if (lockEndTime > block.timestamp) { | ||
_tokenLock = tokenLockFactory.createTokenLock( | ||
address(token), | ||
0x0000000000000000000000000000000000000000, | ||
_account, | ||
_amount, | ||
startTime, | ||
lockEndTime, | ||
periods, | ||
0, | ||
0, | ||
false, | ||
true | ||
); | ||
token.safeTransferFrom(factory, _tokenLock, _amount); | ||
} else { | ||
token.safeTransferFrom(factory, _account, _amount); | ||
} | ||
|
||
emit TokensRedeemed(_account, _tokenLock, _amount); | ||
} | ||
|
||
function _verify(bytes32[] calldata proof, bytes32 leaf) internal view returns (bool) { | ||
return MerkleProofUpgradeable.verifyCalldata(proof, root, leaf); | ||
} | ||
|
||
function _leaf(address _account, uint256 _amount) internal pure returns (bytes32) { | ||
return keccak256(abi.encodePacked(_account, _amount)); | ||
} | ||
} |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
// SPDX-License-Identifier: MIT | ||
// Disclaimer https://github.com/hats-finance/hats-contracts/blob/main/DISCLAIMER.md | ||
|
||
pragma solidity 0.8.16; | ||
|
||
import "@openzeppelin/contracts/access/Ownable.sol"; | ||
import "@openzeppelin/contracts/proxy/Clones.sol"; | ||
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; | ||
import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; | ||
import "./interfaces/IHATAirdrop.sol"; | ||
|
||
contract HATAirdropFactory is Ownable { | ||
error RedeemDataArraysLengthMismatch(); | ||
error ContractIsNotHATAirdrop(); | ||
error HATAirdropInitializationFailed(); | ||
|
||
using SafeERC20 for IERC20; | ||
|
||
mapping(address => bool) public isAirdrop; | ||
|
||
event TokensWithdrawn(address indexed _owner, uint256 _amount); | ||
event HATAirdropCreated(address indexed _hatAirdrop, bytes _initData, IERC20 _token, uint256 _totalAmount); | ||
|
||
function withdrawTokens(IERC20 _token, uint256 _amount) external onlyOwner { | ||
address owner = msg.sender; | ||
_token.safeTransfer(owner, _amount); | ||
emit TokensWithdrawn(owner, _amount); | ||
} | ||
|
||
function redeemMultipleAirdrops(IHATAirdrop[] calldata _airdrops, uint256[] calldata _amounts, bytes32[][] calldata _proofs) external { | ||
if (_airdrops.length != _amounts.length || _airdrops.length != _proofs.length) { | ||
revert RedeemDataArraysLengthMismatch(); | ||
} | ||
|
||
address caller = msg.sender; | ||
for (uint256 i = 0; i < _airdrops.length;) { | ||
if (!isAirdrop[address(_airdrops[i])]) { | ||
revert ContractIsNotHATAirdrop(); | ||
} | ||
|
||
_airdrops[i].redeem(caller, _amounts[i], _proofs[i]); | ||
|
||
unchecked { | ||
++i; | ||
} | ||
} | ||
} | ||
|
||
function createHATAirdrop( | ||
address _implementation, | ||
bytes calldata _initData, | ||
IERC20 _token, | ||
uint256 _totalAmount | ||
) external onlyOwner returns (address result) { | ||
result = Clones.cloneDeterministic(_implementation, keccak256(_initData)); | ||
|
||
// solhint-disable-next-line avoid-low-level-calls | ||
(bool success,) = result.call(_initData); | ||
|
||
if (!success) { | ||
revert HATAirdropInitializationFailed(); | ||
} | ||
|
||
isAirdrop[result] = true; | ||
|
||
_token.safeApprove(result, _totalAmount); | ||
|
||
emit HATAirdropCreated(result, _initData, _token, _totalAmount); | ||
} | ||
|
||
function predictHATAirdropAddress( | ||
address _implementation, | ||
bytes calldata _initData | ||
) external view returns (address) { | ||
return Clones.predictDeterministicAddress(_implementation, keccak256(_initData)); | ||
} | ||
} |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
// SPDX-License-Identifier: MIT | ||
// Disclaimer https://github.com/hats-finance/hats-contracts/blob/main/DISCLAIMER.md | ||
|
||
pragma solidity 0.8.16; | ||
|
||
|
||
interface IHATAirdrop { | ||
function redeem(address _account, uint256 _amount, bytes32[] calldata _proof) external; | ||
} |
12 changes: 12 additions & 0 deletions
12
docs/dodoc/elin/contracts-upgradeable/utils/cryptography/MerkleProofUpgradeable.md
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
# MerkleProofUpgradeable | ||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
*These functions deal with verification of Merkle Tree proofs. The tree and the proofs can be generated using our https://github.com/OpenZeppelin/merkle-tree[JavaScript library]. You will find a quickstart guide in the readme. WARNING: You should avoid using leaf values that are 64 bytes long prior to hashing, or use a hash function other than keccak256 for hashing leaves. This is because the concatenation of a sorted pair of internal nodes in the merkle tree could be reinterpreted as a leaf value. OpenZeppelin's JavaScript library generates merkle trees that are safe against this attack out of the box.* | ||
|
||
|
||
|
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we should consider lock length
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So here we can either just start from a certain date to a certain date, or we can start on claim and end after X time. I implemented the former but we the latter is also a valid option, we just need to decide what behavior we want here.