From 01328dd799ccef8e21fc00dc04ff551fc2e0a8b0 Mon Sep 17 00:00:00 2001 From: benk10 Date: Wed, 22 Nov 2023 10:45:35 -0500 Subject: [PATCH 01/18] Create HATAirdrop --- contracts/tge/HATAirdrop.sol | 75 +++++ .../dodoc/elin/contracts/utils/StorageSlot.md | 12 - .../elin/contracts/utils/math/SignedMath.md | 12 - docs/dodoc/tge/HATAirdrop.md | 309 ++++++++++++++++++ test/tge/airdropData.json | 8 + test/tge/hatairdrop.js | 194 +++++++++++ 6 files changed, 586 insertions(+), 24 deletions(-) create mode 100644 contracts/tge/HATAirdrop.sol delete mode 100644 docs/dodoc/elin/contracts/utils/StorageSlot.md delete mode 100644 docs/dodoc/elin/contracts/utils/math/SignedMath.md create mode 100644 docs/dodoc/tge/HATAirdrop.md create mode 100644 test/tge/airdropData.json create mode 100644 test/tge/hatairdrop.js diff --git a/contracts/tge/HATAirdrop.sol b/contracts/tge/HATAirdrop.sol new file mode 100644 index 00000000..ccecab09 --- /dev/null +++ b/contracts/tge/HATAirdrop.sol @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.16; + +import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol"; + +/* +An airdrop contract that transfers tokens based on a merkle tree. +*/ +contract HATAirdrop is Ownable { + error CannotRedeemBeforeStartTime(); + error CannotRedeemAfterDeadline(); + error LeafAlreadyRedeemed(); + error InvalidMerkleProof(); + error CannotRecoverBeforeDeadline(); + + using SafeERC20 for IERC20; + + bytes32 public root; + uint256 public startTime; + uint256 public deadline; + IERC20 public token; + + mapping (bytes32 => bool) public leafRedeemed; + + event MerkleTreeSet(string _merkleTreeIPFSRef, bytes32 _root, uint256 _startTime, uint256 _deadline); + event TokensRedeemed(address _account, uint256 _amount); + event TokensRecovered(address _owner, uint256 _amount); + + constructor( + string memory _merkleTreeIPFSRef, + bytes32 _root, + uint256 _startTime, + uint256 _deadline, + IERC20 _token + ) { + root = _root; + startTime = _startTime; + deadline = _deadline; + token = _token; + emit MerkleTreeSet(_merkleTreeIPFSRef, _root, _startTime, _deadline); + } + + function redeem(address _account, uint256 _amount, bytes32[] calldata _proof) external { + // 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; + token.safeTransfer(_account, _amount); + emit TokensRedeemed(_account, _amount); + } + + function recoverTokens() external onlyOwner { + // solhint-disable-next-line not-rely-on-time + if (block.timestamp <= deadline) revert CannotRecoverBeforeDeadline(); + address owner = owner(); + uint256 amount = token.balanceOf(address(this)); + token.safeTransfer(owner, amount); + emit TokensRecovered(owner, amount); + } + + function _verify(bytes32[] calldata proof, bytes32 leaf) internal view returns (bool) { + return MerkleProof.verifyCalldata(proof, root, leaf); + } + + function _leaf(address _account, uint256 _amount) internal pure returns (bytes32) { + return keccak256(abi.encodePacked(_account, _amount)); + } +} diff --git a/docs/dodoc/elin/contracts/utils/StorageSlot.md b/docs/dodoc/elin/contracts/utils/StorageSlot.md deleted file mode 100644 index 45728aaf..00000000 --- a/docs/dodoc/elin/contracts/utils/StorageSlot.md +++ /dev/null @@ -1,12 +0,0 @@ -# StorageSlot - - - - - - - -*Library for reading and writing primitive types to specific storage slots. Storage slots are often used to avoid storage conflict when dealing with upgradeable contracts. This library helps with reading and writing to such slots without the need for inline assembly. The functions in this library return Slot structs that contain a `value` member that can be used to read or write. Example usage to set ERC1967 implementation slot: ```solidity contract ERC1967 { bytes32 internal constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; function _getImplementation() internal view returns (address) { return StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value; } function _setImplementation(address newImplementation) internal { require(Address.isContract(newImplementation), "ERC1967: new implementation is not a contract"); StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value = newImplementation; } } ``` _Available since v4.1 for `address`, `bool`, `bytes32`, `uint256`._ _Available since v4.9 for `string`, `bytes`._* - - - diff --git a/docs/dodoc/elin/contracts/utils/math/SignedMath.md b/docs/dodoc/elin/contracts/utils/math/SignedMath.md deleted file mode 100644 index 35f29531..00000000 --- a/docs/dodoc/elin/contracts/utils/math/SignedMath.md +++ /dev/null @@ -1,12 +0,0 @@ -# SignedMath - - - - - - - -*Standard signed math utilities missing in the Solidity language.* - - - diff --git a/docs/dodoc/tge/HATAirdrop.md b/docs/dodoc/tge/HATAirdrop.md new file mode 100644 index 00000000..5742495b --- /dev/null +++ b/docs/dodoc/tge/HATAirdrop.md @@ -0,0 +1,309 @@ +# HATAirdrop + + + + + + + + + +## Methods + +### deadline + +```solidity +function deadline() external view returns (uint256) +``` + + + + + + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | uint256 | undefined | + +### leafRedeemed + +```solidity +function leafRedeemed(bytes32) external view returns (bool) +``` + + + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| _0 | bytes32 | undefined | + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | bool | undefined | + +### owner + +```solidity +function owner() external view returns (address) +``` + + + +*Returns the address of the current owner.* + + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | address | undefined | + +### recoverTokens + +```solidity +function recoverTokens() external nonpayable +``` + + + + + + +### redeem + +```solidity +function redeem(address _account, uint256 _amount, bytes32[] _proof) external nonpayable +``` + + + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| _account | address | undefined | +| _amount | uint256 | undefined | +| _proof | bytes32[] | undefined | + +### renounceOwnership + +```solidity +function renounceOwnership() external nonpayable +``` + + + +*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.* + + +### root + +```solidity +function root() external view returns (bytes32) +``` + + + + + + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | bytes32 | undefined | + +### startTime + +```solidity +function startTime() external view returns (uint256) +``` + + + + + + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | uint256 | undefined | + +### token + +```solidity +function token() external view returns (contract IERC20) +``` + + + + + + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | contract IERC20 | undefined | + +### transferOwnership + +```solidity +function transferOwnership(address newOwner) external nonpayable +``` + + + +*Transfers ownership of the contract to a new account (`newOwner`). Can only be called by the current owner.* + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| newOwner | address | undefined | + + + +## Events + +### MerkleTreeSet + +```solidity +event MerkleTreeSet(string _merkleTreeIPFSRef, bytes32 _root, uint256 _startTime, uint256 _deadline) +``` + + + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| _merkleTreeIPFSRef | string | undefined | +| _root | bytes32 | undefined | +| _startTime | uint256 | undefined | +| _deadline | uint256 | undefined | + +### OwnershipTransferred + +```solidity +event OwnershipTransferred(address indexed previousOwner, address indexed newOwner) +``` + + + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| previousOwner `indexed` | address | undefined | +| newOwner `indexed` | address | undefined | + +### TokensRecovered + +```solidity +event TokensRecovered(address _owner, uint256 _amount) +``` + + + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| _owner | address | undefined | +| _amount | uint256 | undefined | + +### TokensRedeemed + +```solidity +event TokensRedeemed(address _account, uint256 _amount) +``` + + + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| _account | address | undefined | +| _amount | uint256 | undefined | + + + +## Errors + +### CannotRecoverBeforeDeadline + +```solidity +error CannotRecoverBeforeDeadline() +``` + + + + + + +### CannotRedeemAfterDeadline + +```solidity +error CannotRedeemAfterDeadline() +``` + + + + + + +### CannotRedeemBeforeStartTime + +```solidity +error CannotRedeemBeforeStartTime() +``` + + + + + + +### InvalidMerkleProof + +```solidity +error InvalidMerkleProof() +``` + + + + + + +### LeafAlreadyRedeemed + +```solidity +error LeafAlreadyRedeemed() +``` + + + + + + + diff --git a/test/tge/airdropData.json b/test/tge/airdropData.json new file mode 100644 index 00000000..97c6d61a --- /dev/null +++ b/test/tge/airdropData.json @@ -0,0 +1,8 @@ +{ + "0xbAAEa72417f4dC3E0f52a1783B0913d0f3516634": "10", + "0x9214574DC772B6FD34da960054d0baF29b0c9f9e": "25", + "0x078ad5270b0240d8529f600f35a271fc6e2b2bd8": "25", + "0x60a47eD557d4cD1472FDbffcE8b85956F0555A7A": "15", + "0xe629e116ff404e9074ef1992ef2b147247a49e81": "30", + "0x8F402318BB49776f5017d2FB12c90D0B0acAAaE8": "20" +} diff --git a/test/tge/hatairdrop.js b/test/tge/hatairdrop.js new file mode 100644 index 00000000..9ca01164 --- /dev/null +++ b/test/tge/hatairdrop.js @@ -0,0 +1,194 @@ +const utils = require("../utils.js"); +const ERC20Mock = artifacts.require("./ERC20Mock.sol"); +const { contract, web3 } = require("hardhat"); +const { + setup, + advanceToSafetyPeriod, + submitClaim, + assertFunctionRaisesException, + ZERO_ADDRESS, +} = require("../common.js"); +const airdropData = require('./airdropData.json'); +const { assert } = require("chai"); +const { default: MerkleTree } = require("merkletreejs"); +const keccak256 = require("keccak256"); + +function hashTokens(account, amount) { + return Buffer.from( + ethers.utils.solidityKeccak256( + ['address', 'uint256'], + [account, amount] + ).slice(2), + 'hex' + ); +} + +contract("HATAirdrop", (accounts) => { + + let hatAirdrop; + let token; + let merkleTree; + let hashes = []; + let totalAmount = 0; + + async function setupHATAirdrop() { + token = await ERC20Mock.new("Staking", "STK"); + + for (const [account, amount] of Object.entries(airdropData)) { + totalAmount += parseInt(amount); + hashes.push(hashTokens(account, amount)); + } + + merkleTree = new MerkleTree(hashes, keccak256, { sortPairs: true }); + + const HATAirdrop = artifacts.require("./HATAirdrop.sol"); + hatAirdrop = await HATAirdrop.new( + "QmSUXfYsk9HgrMBa7tgp3MBm8FGwDF9hnVaR9C1PMoFdS3", + merkleTree.getHexRoot(), + (await web3.eth.getBlock("latest")).timestamp + 7 * 24 * 3600, + (await web3.eth.getBlock("latest")).timestamp + (7 + 365) * 24 * 3600, + token.address + ); + + await token.mint(hatAirdrop.address, totalAmount); + } + + + it("Redeem all", async () => { + await setupHATAirdrop(); + + await utils.increaseTime(7 * 24 * 3600); + + for (const [account, amount] of Object.entries(airdropData)) { + let currentBalance = await token.balanceOf(hatAirdrop.address); + const proof = merkleTree.getHexProof(hashTokens(account, amount)); + let tx = await hatAirdrop.redeem(account, amount, proof); + assert.equal(tx.logs[0].event, "TokensRedeemed"); + assert.equal(tx.logs[0].args._account.toLowerCase(), account.toLowerCase()); + assert.equal(tx.logs[0].args._amount, amount); + assert.equal(await token.balanceOf(account), amount); + assert.equal((await token.balanceOf(hatAirdrop.address)).toString(), currentBalance.sub(web3.utils.toBN(amount)).toString()); + } + + assert.equal((await token.balanceOf(hatAirdrop.address)).toString(), "0"); + }); + + it("Cannot redeem before start time", async () => { + await setupHATAirdrop(); + + const [account, amount] = Object.entries(airdropData)[0]; + const proof = merkleTree.getHexProof(hashTokens(account, amount)); + await assertFunctionRaisesException( + hatAirdrop.redeem(account, amount, proof), + "CannotRedeemBeforeStartTime" + ); + + await utils.increaseTime(7 * 24 * 3600); + + let tx = await hatAirdrop.redeem(account, amount, proof); + assert.equal(tx.logs[0].event, "TokensRedeemed"); + assert.equal(tx.logs[0].args._account.toLowerCase(), account.toLowerCase()); + assert.equal(tx.logs[0].args._amount, amount); + }); + + it("Cannot redeem after deadline", async () => { + await setupHATAirdrop(); + + const [account, amount] = Object.entries(airdropData)[0]; + const proof = merkleTree.getHexProof(hashTokens(account, amount)); + + await utils.increaseTime(7 * 24 * 3600); + + let tx = await hatAirdrop.redeem(account, amount, proof); + assert.equal(tx.logs[0].event, "TokensRedeemed"); + assert.equal(tx.logs[0].args._account.toLowerCase(), account.toLowerCase()); + assert.equal(tx.logs[0].args._amount, amount); + + await utils.increaseTime(365 * 24 * 3600); + + const [account2, amount2] = Object.entries(airdropData)[1]; + const proof2 = merkleTree.getHexProof(hashTokens(account2, amount2)); + + await assertFunctionRaisesException( + hatAirdrop.redeem(account2, amount2, proof2), + "CannotRedeemAfterDeadline" + ); + }); + + it("Cannot redeem twice", async () => { + await setupHATAirdrop(); + + const [account, amount] = Object.entries(airdropData)[0]; + const proof = merkleTree.getHexProof(hashTokens(account, amount)); + + await utils.increaseTime(7 * 24 * 3600); + + let tx = await hatAirdrop.redeem(account, amount, proof); + assert.equal(tx.logs[0].event, "TokensRedeemed"); + assert.equal(tx.logs[0].args._account.toLowerCase(), account.toLowerCase()); + assert.equal(tx.logs[0].args._amount, amount); + + await assertFunctionRaisesException( + hatAirdrop.redeem(account, amount, proof), + "LeafAlreadyRedeemed" + ); + }); + + it("Cannot redeem invalid proof", async () => { + await setupHATAirdrop(); + + const [account, amount] = Object.entries(airdropData)[0]; + + await utils.increaseTime(7 * 24 * 3600); + + await assertFunctionRaisesException( + hatAirdrop.redeem(account, amount, [web3.utils.randomHex(32)]), + "InvalidMerkleProof" + ); + + const proof = merkleTree.getHexProof(hashTokens(account, amount)); + + await assertFunctionRaisesException( + hatAirdrop.redeem(account, "0", proof), + "InvalidMerkleProof" + ); + + let tx = await hatAirdrop.redeem(account, amount, proof); + assert.equal(tx.logs[0].event, "TokensRedeemed"); + assert.equal(tx.logs[0].args._account.toLowerCase(), account.toLowerCase()); + assert.equal(tx.logs[0].args._amount, amount); + }); + + it("Recover tokens", async () => { + await setupHATAirdrop(); + + await assertFunctionRaisesException( + hatAirdrop.recoverTokens({ from: accounts[1] }), + "Ownable: caller is not the owner" + ); + + await assertFunctionRaisesException( + hatAirdrop.recoverTokens(), + "CannotRecoverBeforeDeadline" + ); + + await utils.increaseTime(7 * 24 * 3600); + + const [account, amount] = Object.entries(airdropData)[0]; + const proof = merkleTree.getHexProof(hashTokens(account, amount)); + + let tx = await hatAirdrop.redeem(account, amount, proof); + assert.equal(tx.logs[0].event, "TokensRedeemed"); + assert.equal(tx.logs[0].args._account.toLowerCase(), account.toLowerCase()); + assert.equal(tx.logs[0].args._amount, amount); + + await utils.increaseTime(365 * 24 * 3600); + + tx = await hatAirdrop.recoverTokens(); + + assert.equal(tx.logs[0].event, "TokensRecovered"); + assert.equal(tx.logs[0].args._owner.toLowerCase(), accounts[0].toLowerCase()); + assert.equal(tx.logs[0].args._amount.toString(), web3.utils.toBN((totalAmount - parseInt(amount))).toString()); + assert.equal((await token.balanceOf(hatAirdrop.address)).toString(), "0"); + }); +}); From dab98e287b00bed2e3d2f9228b393bcb4ca761d8 Mon Sep 17 00:00:00 2001 From: benk10 Date: Wed, 22 Nov 2023 10:49:57 -0500 Subject: [PATCH 02/18] Lint --- test/tge/hatairdrop.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/test/tge/hatairdrop.js b/test/tge/hatairdrop.js index 9ca01164..2a4ae362 100644 --- a/test/tge/hatairdrop.js +++ b/test/tge/hatairdrop.js @@ -2,11 +2,7 @@ const utils = require("../utils.js"); const ERC20Mock = artifacts.require("./ERC20Mock.sol"); const { contract, web3 } = require("hardhat"); const { - setup, - advanceToSafetyPeriod, - submitClaim, assertFunctionRaisesException, - ZERO_ADDRESS, } = require("../common.js"); const airdropData = require('./airdropData.json'); const { assert } = require("chai"); From 805573a2ca4ff105ac850db7db1f34c3db5a76d0 Mon Sep 17 00:00:00 2001 From: benk10 Date: Mon, 4 Dec 2023 12:20:21 -0500 Subject: [PATCH 03/18] Redeem to token lock --- contracts/tge/HATAirdrop.sol | 38 +++++++++++++----- docs/dodoc/tge/HATAirdrop.md | 43 +++++++++++++++++++-- test/tge/hatairdrop.js | 74 +++++++++++++++++++++++++----------- 3 files changed, 120 insertions(+), 35 deletions(-) diff --git a/contracts/tge/HATAirdrop.sol b/contracts/tge/HATAirdrop.sol index ccecab09..cdaebbfc 100644 --- a/contracts/tge/HATAirdrop.sol +++ b/contracts/tge/HATAirdrop.sol @@ -5,6 +5,7 @@ import "@openzeppelin/contracts/access/Ownable.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol"; +import "../tokenlock/TokenLockFactory.sol"; /* An airdrop contract that transfers tokens based on a merkle tree. @@ -18,28 +19,34 @@ contract HATAirdrop is Ownable { using SafeERC20 for IERC20; - bytes32 public root; - uint256 public startTime; - uint256 public deadline; - IERC20 public token; + bytes32 public immutable root; + uint256 public immutable startTime; + uint256 public immutable deadline; + uint256 public immutable periods; + IERC20 public immutable token; + TokenLockFactory public immutable tokenLockFactory; mapping (bytes32 => bool) public leafRedeemed; event MerkleTreeSet(string _merkleTreeIPFSRef, bytes32 _root, uint256 _startTime, uint256 _deadline); - event TokensRedeemed(address _account, uint256 _amount); - event TokensRecovered(address _owner, uint256 _amount); + event TokensRedeemed(address indexed _account, address indexed _tokenLock, uint256 _amount); + event TokensRecovered(address indexed _owner, uint256 _amount); constructor( string memory _merkleTreeIPFSRef, bytes32 _root, uint256 _startTime, uint256 _deadline, - IERC20 _token + uint256 _periods, + IERC20 _token, + TokenLockFactory _tokenLockFactory ) { root = _root; startTime = _startTime; deadline = _deadline; + periods = _periods; token = _token; + tokenLockFactory = _tokenLockFactory; emit MerkleTreeSet(_merkleTreeIPFSRef, _root, _startTime, _deadline); } @@ -52,8 +59,21 @@ contract HATAirdrop is Ownable { if (leafRedeemed[leaf]) revert LeafAlreadyRedeemed(); if(!_verify(_proof, leaf)) revert InvalidMerkleProof(); leafRedeemed[leaf] = true; - token.safeTransfer(_account, _amount); - emit TokensRedeemed(_account, _amount); + address _tokenLock = tokenLockFactory.createTokenLock( + address(token), + 0x0000000000000000000000000000000000000000, + _account, + _amount, + startTime, + deadline, + periods, + 0, + 0, + false, + true + ); + token.safeTransfer(_tokenLock, _amount); + emit TokensRedeemed(_account, _tokenLock, _amount); } function recoverTokens() external onlyOwner { diff --git a/docs/dodoc/tge/HATAirdrop.md b/docs/dodoc/tge/HATAirdrop.md index 5742495b..e9c6bb53 100644 --- a/docs/dodoc/tge/HATAirdrop.md +++ b/docs/dodoc/tge/HATAirdrop.md @@ -66,6 +66,23 @@ function owner() external view returns (address) |---|---|---| | _0 | address | undefined | +### periods + +```solidity +function periods() external view returns (uint256) +``` + + + + + + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | uint256 | undefined | + ### recoverTokens ```solidity @@ -157,6 +174,23 @@ function token() external view returns (contract IERC20) |---|---|---| | _0 | contract IERC20 | undefined | +### tokenLockFactory + +```solidity +function tokenLockFactory() external view returns (contract TokenLockFactory) +``` + + + + + + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | contract TokenLockFactory | undefined | + ### transferOwnership ```solidity @@ -216,7 +250,7 @@ event OwnershipTransferred(address indexed previousOwner, address indexed newOwn ### TokensRecovered ```solidity -event TokensRecovered(address _owner, uint256 _amount) +event TokensRecovered(address indexed _owner, uint256 _amount) ``` @@ -227,13 +261,13 @@ event TokensRecovered(address _owner, uint256 _amount) | Name | Type | Description | |---|---|---| -| _owner | address | undefined | +| _owner `indexed` | address | undefined | | _amount | uint256 | undefined | ### TokensRedeemed ```solidity -event TokensRedeemed(address _account, uint256 _amount) +event TokensRedeemed(address indexed _account, address indexed _tokenLock, uint256 _amount) ``` @@ -244,7 +278,8 @@ event TokensRedeemed(address _account, uint256 _amount) | Name | Type | Description | |---|---|---| -| _account | address | undefined | +| _account `indexed` | address | undefined | +| _tokenLock `indexed` | address | undefined | | _amount | uint256 | undefined | diff --git a/test/tge/hatairdrop.js b/test/tge/hatairdrop.js index 2a4ae362..06c79122 100644 --- a/test/tge/hatairdrop.js +++ b/test/tge/hatairdrop.js @@ -1,5 +1,7 @@ const utils = require("../utils.js"); const ERC20Mock = artifacts.require("./ERC20Mock.sol"); +const TokenLockFactory = artifacts.require("./TokenLockFactory.sol"); +const HATTokenLock = artifacts.require("./HATTokenLock.sol"); const { contract, web3 } = require("hardhat"); const { assertFunctionRaisesException, @@ -26,6 +28,10 @@ contract("HATAirdrop", (accounts) => { let merkleTree; let hashes = []; let totalAmount = 0; + let tokenLockFactory; + let startTime; + let endTime; + let periods; async function setupHATAirdrop() { token = await ERC20Mock.new("Staking", "STK"); @@ -36,14 +42,20 @@ contract("HATAirdrop", (accounts) => { } merkleTree = new MerkleTree(hashes, keccak256, { sortPairs: true }); + tokenLockFactory = await TokenLockFactory.new((await HATTokenLock.new()).address, accounts[0]); + startTime = (await web3.eth.getBlock("latest")).timestamp + 7 * 24 * 3600; + endTime = (await web3.eth.getBlock("latest")).timestamp + (7 + 365) * 24 * 3600; + periods = 90; const HATAirdrop = artifacts.require("./HATAirdrop.sol"); hatAirdrop = await HATAirdrop.new( "QmSUXfYsk9HgrMBa7tgp3MBm8FGwDF9hnVaR9C1PMoFdS3", merkleTree.getHexRoot(), - (await web3.eth.getBlock("latest")).timestamp + 7 * 24 * 3600, - (await web3.eth.getBlock("latest")).timestamp + (7 + 365) * 24 * 3600, - token.address + startTime, + endTime, + periods, + token.address, + tokenLockFactory.address ); await token.mint(hatAirdrop.address, totalAmount); @@ -59,11 +71,24 @@ contract("HATAirdrop", (accounts) => { let currentBalance = await token.balanceOf(hatAirdrop.address); const proof = merkleTree.getHexProof(hashTokens(account, amount)); let tx = await hatAirdrop.redeem(account, amount, proof); - assert.equal(tx.logs[0].event, "TokensRedeemed"); - assert.equal(tx.logs[0].args._account.toLowerCase(), account.toLowerCase()); - assert.equal(tx.logs[0].args._amount, amount); - assert.equal(await token.balanceOf(account), amount); + assert.equal(tx.logs[1].event, "TokensRedeemed"); + assert.equal(tx.logs[1].args._account.toLowerCase(), account.toLowerCase()); + assert.equal(tx.logs[1].args._amount, amount); + assert.equal(await token.balanceOf(tx.logs[1].args._tokenLock), amount); assert.equal((await token.balanceOf(hatAirdrop.address)).toString(), currentBalance.sub(web3.utils.toBN(amount)).toString()); + + let tokenLock = await HATTokenLock.at(tx.logs[1].args._tokenLock); + assert.equal(await tokenLock.startTime(), startTime); + assert.equal(await tokenLock.endTime(), endTime); + assert.equal(await tokenLock.periods(), periods); + assert.equal(await tokenLock.owner(), "0x0000000000000000000000000000000000000000"); + assert.equal((await tokenLock.beneficiary()).toLowerCase(), account.toLowerCase()); + assert.equal(await tokenLock.managedAmount(), amount); + assert.equal(await tokenLock.token(), token.address); + assert.equal(await tokenLock.releaseStartTime(), 0); + assert.equal(await tokenLock.vestingCliffTime(), 0); + assert.equal(await tokenLock.revocable(), false); + assert.equal(await tokenLock.canDelegate(), true); } assert.equal((await token.balanceOf(hatAirdrop.address)).toString(), "0"); @@ -82,9 +107,10 @@ contract("HATAirdrop", (accounts) => { await utils.increaseTime(7 * 24 * 3600); let tx = await hatAirdrop.redeem(account, amount, proof); - assert.equal(tx.logs[0].event, "TokensRedeemed"); - assert.equal(tx.logs[0].args._account.toLowerCase(), account.toLowerCase()); - assert.equal(tx.logs[0].args._amount, amount); + assert.equal(tx.logs[1].event, "TokensRedeemed"); + assert.equal(tx.logs[1].args._account.toLowerCase(), account.toLowerCase()); + assert.equal(tx.logs[1].args._amount, amount); + assert.equal(await token.balanceOf(tx.logs[1].args._tokenLock), amount); }); it("Cannot redeem after deadline", async () => { @@ -96,9 +122,10 @@ contract("HATAirdrop", (accounts) => { await utils.increaseTime(7 * 24 * 3600); let tx = await hatAirdrop.redeem(account, amount, proof); - assert.equal(tx.logs[0].event, "TokensRedeemed"); - assert.equal(tx.logs[0].args._account.toLowerCase(), account.toLowerCase()); - assert.equal(tx.logs[0].args._amount, amount); + assert.equal(tx.logs[1].event, "TokensRedeemed"); + assert.equal(tx.logs[1].args._account.toLowerCase(), account.toLowerCase()); + assert.equal(tx.logs[1].args._amount, amount); + assert.equal(await token.balanceOf(tx.logs[1].args._tokenLock), amount); await utils.increaseTime(365 * 24 * 3600); @@ -120,9 +147,10 @@ contract("HATAirdrop", (accounts) => { await utils.increaseTime(7 * 24 * 3600); let tx = await hatAirdrop.redeem(account, amount, proof); - assert.equal(tx.logs[0].event, "TokensRedeemed"); - assert.equal(tx.logs[0].args._account.toLowerCase(), account.toLowerCase()); - assert.equal(tx.logs[0].args._amount, amount); + assert.equal(tx.logs[1].event, "TokensRedeemed"); + assert.equal(tx.logs[1].args._account.toLowerCase(), account.toLowerCase()); + assert.equal(tx.logs[1].args._amount, amount); + assert.equal(await token.balanceOf(tx.logs[1].args._tokenLock), amount); await assertFunctionRaisesException( hatAirdrop.redeem(account, amount, proof), @@ -150,9 +178,10 @@ contract("HATAirdrop", (accounts) => { ); let tx = await hatAirdrop.redeem(account, amount, proof); - assert.equal(tx.logs[0].event, "TokensRedeemed"); - assert.equal(tx.logs[0].args._account.toLowerCase(), account.toLowerCase()); - assert.equal(tx.logs[0].args._amount, amount); + assert.equal(tx.logs[1].event, "TokensRedeemed"); + assert.equal(tx.logs[1].args._account.toLowerCase(), account.toLowerCase()); + assert.equal(tx.logs[1].args._amount, amount); + assert.equal(await token.balanceOf(tx.logs[1].args._tokenLock), amount); }); it("Recover tokens", async () => { @@ -174,9 +203,10 @@ contract("HATAirdrop", (accounts) => { const proof = merkleTree.getHexProof(hashTokens(account, amount)); let tx = await hatAirdrop.redeem(account, amount, proof); - assert.equal(tx.logs[0].event, "TokensRedeemed"); - assert.equal(tx.logs[0].args._account.toLowerCase(), account.toLowerCase()); - assert.equal(tx.logs[0].args._amount, amount); + assert.equal(tx.logs[1].event, "TokensRedeemed"); + assert.equal(tx.logs[1].args._account.toLowerCase(), account.toLowerCase()); + assert.equal(tx.logs[1].args._amount, amount); + assert.equal(await token.balanceOf(tx.logs[1].args._tokenLock), amount); await utils.increaseTime(365 * 24 * 3600); From 5bec34a487b40d11a032de25ddfa55f45179f6e6 Mon Sep 17 00:00:00 2001 From: benk10 Date: Tue, 2 Jan 2024 15:31:29 -0300 Subject: [PATCH 04/18] Add Airdrop Factory --- contracts/tge/HATAirdrop.sol | 80 ++++++++----- contracts/tge/HATAirdropFactory.sol | 78 ++++++++++++ .../cryptography/MerkleProofUpgradeable.md | 12 ++ docs/dodoc/tge/HATAirdrop.md | 65 +++++++++- docs/dodoc/tge/HATAirdropFactory.md | 111 ++++++++++++++++++ test/tge/hatairdrop.js | 80 ++++++++++++- 6 files changed, 385 insertions(+), 41 deletions(-) create mode 100644 contracts/tge/HATAirdropFactory.sol create mode 100644 docs/dodoc/elin/contracts-upgradeable/utils/cryptography/MerkleProofUpgradeable.md create mode 100644 docs/dodoc/tge/HATAirdropFactory.md diff --git a/contracts/tge/HATAirdrop.sol b/contracts/tge/HATAirdrop.sol index cdaebbfc..5128c3d8 100644 --- a/contracts/tge/HATAirdrop.sol +++ b/contracts/tge/HATAirdrop.sol @@ -1,30 +1,31 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.16; -import "@openzeppelin/contracts/access/Ownable.sol"; -import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol"; -import "../tokenlock/TokenLockFactory.sol"; +import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.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"; /* An airdrop contract that transfers tokens based on a merkle tree. */ -contract HATAirdrop is Ownable { +contract HATAirdrop is OwnableUpgradeable { error CannotRedeemBeforeStartTime(); error CannotRedeemAfterDeadline(); error LeafAlreadyRedeemed(); error InvalidMerkleProof(); error CannotRecoverBeforeDeadline(); - using SafeERC20 for IERC20; + using SafeERC20Upgradeable for IERC20Upgradeable; - bytes32 public immutable root; - uint256 public immutable startTime; - uint256 public immutable deadline; - uint256 public immutable periods; - IERC20 public immutable token; - TokenLockFactory public immutable tokenLockFactory; + bytes32 public root; + uint256 public startTime; + uint256 public deadline; + uint256 public lockEndTime; + uint256 public periods; + IERC20Upgradeable public token; + ITokenLockFactory public tokenLockFactory; mapping (bytes32 => bool) public leafRedeemed; @@ -32,18 +33,26 @@ contract HATAirdrop is Ownable { event TokensRedeemed(address indexed _account, address indexed _tokenLock, uint256 _amount); event TokensRecovered(address indexed _owner, uint256 _amount); - constructor( + constructor () { + _disableInitializers(); + } + + function initialize( + address _owner, string memory _merkleTreeIPFSRef, bytes32 _root, uint256 _startTime, uint256 _deadline, + uint256 _lockEndTime, uint256 _periods, - IERC20 _token, - TokenLockFactory _tokenLockFactory - ) { + IERC20Upgradeable _token, + ITokenLockFactory _tokenLockFactory + ) external initializer { + _transferOwnership(_owner); root = _root; startTime = _startTime; deadline = _deadline; + lockEndTime = _lockEndTime; periods = _periods; token = _token; tokenLockFactory = _tokenLockFactory; @@ -59,20 +68,27 @@ contract HATAirdrop is Ownable { if (leafRedeemed[leaf]) revert LeafAlreadyRedeemed(); if(!_verify(_proof, leaf)) revert InvalidMerkleProof(); leafRedeemed[leaf] = true; - address _tokenLock = tokenLockFactory.createTokenLock( - address(token), - 0x0000000000000000000000000000000000000000, - _account, - _amount, - startTime, - deadline, - periods, - 0, - 0, - false, - true - ); - token.safeTransfer(_tokenLock, _amount); + + address _tokenLock = address(0); + if (lockEndTime != 0) { + _tokenLock = tokenLockFactory.createTokenLock( + address(token), + 0x0000000000000000000000000000000000000000, + _account, + _amount, + startTime, + lockEndTime, + periods, + 0, + 0, + false, + true + ); + token.safeTransfer(_tokenLock, _amount); + } else { + token.safeTransfer(_account, _amount); + } + emit TokensRedeemed(_account, _tokenLock, _amount); } @@ -86,7 +102,7 @@ contract HATAirdrop is Ownable { } function _verify(bytes32[] calldata proof, bytes32 leaf) internal view returns (bool) { - return MerkleProof.verifyCalldata(proof, root, leaf); + return MerkleProofUpgradeable.verifyCalldata(proof, root, leaf); } function _leaf(address _account, uint256 _amount) internal pure returns (bytes32) { diff --git a/contracts/tge/HATAirdropFactory.sol b/contracts/tge/HATAirdropFactory.sol new file mode 100644 index 00000000..c8a7870a --- /dev/null +++ b/contracts/tge/HATAirdropFactory.sol @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: MIT +// Disclaimer https://github.com/hats-finance/hats-contracts/blob/main/DISCLAIMER.md + +pragma solidity 0.8.16; + +import "@openzeppelin/contracts/proxy/Clones.sol"; +import "./HATAirdrop.sol"; + +contract HATAirdropFactory { + address public immutable implementation; + event HATAirdropCreated(address indexed _hatAirdrop); + + constructor (address _implementation) { + implementation = _implementation; + } + + function createHATAirdrop( + address _owner, + string memory _merkleTreeIPFSRef, + bytes32 _root, + uint256 _startTime, + uint256 _deadline, + uint256 _lockEndTime, + uint256 _periods, + IERC20Upgradeable _token, + ITokenLockFactory _tokenLockFactory + ) external returns (address result) { + result = Clones.cloneDeterministic(implementation, keccak256(abi.encodePacked( + _owner, + _merkleTreeIPFSRef, + _root, + _startTime, + _deadline, + _lockEndTime, + _periods, + _token, + _tokenLockFactory + ))); + + HATAirdrop(payable(result)).initialize( + _owner, + _merkleTreeIPFSRef, + _root, + _startTime, + _deadline, + _lockEndTime, + _periods, + _token, + _tokenLockFactory + ); + + emit HATAirdropCreated(result); + } + + function predictHATAirdropAddress( + address _owner, + string memory _merkleTreeIPFSRef, + bytes32 _root, + uint256 _startTime, + uint256 _deadline, + uint256 _lockEndTime, + uint256 _periods, + IERC20 _token, + ITokenLockFactory _tokenLockFactory + ) public view returns (address) { + return Clones.predictDeterministicAddress(implementation, keccak256(abi.encodePacked( + _owner, + _merkleTreeIPFSRef, + _root, + _startTime, + _deadline, + _lockEndTime, + _periods, + _token, + _tokenLockFactory + ))); + } +} diff --git a/docs/dodoc/elin/contracts-upgradeable/utils/cryptography/MerkleProofUpgradeable.md b/docs/dodoc/elin/contracts-upgradeable/utils/cryptography/MerkleProofUpgradeable.md new file mode 100644 index 00000000..91f11e5f --- /dev/null +++ b/docs/dodoc/elin/contracts-upgradeable/utils/cryptography/MerkleProofUpgradeable.md @@ -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.* + + + diff --git a/docs/dodoc/tge/HATAirdrop.md b/docs/dodoc/tge/HATAirdrop.md index e9c6bb53..27ed8906 100644 --- a/docs/dodoc/tge/HATAirdrop.md +++ b/docs/dodoc/tge/HATAirdrop.md @@ -27,6 +27,30 @@ function deadline() external view returns (uint256) |---|---|---| | _0 | uint256 | undefined | +### initialize + +```solidity +function initialize(address _owner, string _merkleTreeIPFSRef, bytes32 _root, uint256 _startTime, uint256 _deadline, uint256 _lockEndTime, uint256 _periods, contract IERC20Upgradeable _token, contract ITokenLockFactory _tokenLockFactory) external nonpayable +``` + + + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| _owner | address | undefined | +| _merkleTreeIPFSRef | string | undefined | +| _root | bytes32 | undefined | +| _startTime | uint256 | undefined | +| _deadline | uint256 | undefined | +| _lockEndTime | uint256 | undefined | +| _periods | uint256 | undefined | +| _token | contract IERC20Upgradeable | undefined | +| _tokenLockFactory | contract ITokenLockFactory | undefined | + ### leafRedeemed ```solidity @@ -49,6 +73,23 @@ function leafRedeemed(bytes32) external view returns (bool) |---|---|---| | _0 | bool | undefined | +### lockEndTime + +```solidity +function lockEndTime() external view returns (uint256) +``` + + + + + + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | uint256 | undefined | + ### owner ```solidity @@ -160,7 +201,7 @@ function startTime() external view returns (uint256) ### token ```solidity -function token() external view returns (contract IERC20) +function token() external view returns (contract IERC20Upgradeable) ``` @@ -172,12 +213,12 @@ function token() external view returns (contract IERC20) | Name | Type | Description | |---|---|---| -| _0 | contract IERC20 | undefined | +| _0 | contract IERC20Upgradeable | undefined | ### tokenLockFactory ```solidity -function tokenLockFactory() external view returns (contract TokenLockFactory) +function tokenLockFactory() external view returns (contract ITokenLockFactory) ``` @@ -189,7 +230,7 @@ function tokenLockFactory() external view returns (contract TokenLockFactory) | Name | Type | Description | |---|---|---| -| _0 | contract TokenLockFactory | undefined | +| _0 | contract ITokenLockFactory | undefined | ### transferOwnership @@ -211,6 +252,22 @@ function transferOwnership(address newOwner) external nonpayable ## Events +### Initialized + +```solidity +event Initialized(uint8 version) +``` + + + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| version | uint8 | undefined | + ### MerkleTreeSet ```solidity diff --git a/docs/dodoc/tge/HATAirdropFactory.md b/docs/dodoc/tge/HATAirdropFactory.md new file mode 100644 index 00000000..3a3eaa3b --- /dev/null +++ b/docs/dodoc/tge/HATAirdropFactory.md @@ -0,0 +1,111 @@ +# HATAirdropFactory + + + + + + + + + +## Methods + +### createHATAirdrop + +```solidity +function createHATAirdrop(address _owner, string _merkleTreeIPFSRef, bytes32 _root, uint256 _startTime, uint256 _deadline, uint256 _lockEndTime, uint256 _periods, contract IERC20Upgradeable _token, contract ITokenLockFactory _tokenLockFactory) external nonpayable returns (address result) +``` + + + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| _owner | address | undefined | +| _merkleTreeIPFSRef | string | undefined | +| _root | bytes32 | undefined | +| _startTime | uint256 | undefined | +| _deadline | uint256 | undefined | +| _lockEndTime | uint256 | undefined | +| _periods | uint256 | undefined | +| _token | contract IERC20Upgradeable | undefined | +| _tokenLockFactory | contract ITokenLockFactory | undefined | + +#### Returns + +| Name | Type | Description | +|---|---|---| +| result | address | undefined | + +### implementation + +```solidity +function implementation() external view returns (address) +``` + + + + + + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | address | undefined | + +### predictHATAirdropAddress + +```solidity +function predictHATAirdropAddress(address _owner, string _merkleTreeIPFSRef, bytes32 _root, uint256 _startTime, uint256 _deadline, uint256 _lockEndTime, uint256 _periods, contract IERC20 _token, contract ITokenLockFactory _tokenLockFactory) external view returns (address) +``` + + + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| _owner | address | undefined | +| _merkleTreeIPFSRef | string | undefined | +| _root | bytes32 | undefined | +| _startTime | uint256 | undefined | +| _deadline | uint256 | undefined | +| _lockEndTime | uint256 | undefined | +| _periods | uint256 | undefined | +| _token | contract IERC20 | undefined | +| _tokenLockFactory | contract ITokenLockFactory | undefined | + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | address | undefined | + + + +## Events + +### HATAirdropCreated + +```solidity +event HATAirdropCreated(address indexed _hatAirdrop) +``` + + + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| _hatAirdrop `indexed` | address | undefined | + + + diff --git a/test/tge/hatairdrop.js b/test/tge/hatairdrop.js index 06c79122..300a691a 100644 --- a/test/tge/hatairdrop.js +++ b/test/tge/hatairdrop.js @@ -2,9 +2,11 @@ const utils = require("../utils.js"); const ERC20Mock = artifacts.require("./ERC20Mock.sol"); const TokenLockFactory = artifacts.require("./TokenLockFactory.sol"); const HATTokenLock = artifacts.require("./HATTokenLock.sol"); +const HATAirdrop = artifacts.require("./HATAirdrop.sol"); +const HATAirdropFactory = artifacts.require("./HATAirdropFactory.sol"); const { contract, web3 } = require("hardhat"); const { - assertFunctionRaisesException, + assertFunctionRaisesException, assertVMException, ZERO_ADDRESS, } = require("../common.js"); const airdropData = require('./airdropData.json'); const { assert } = require("chai"); @@ -31,11 +33,14 @@ contract("HATAirdrop", (accounts) => { let tokenLockFactory; let startTime; let endTime; + let lockEndTime; let periods; - async function setupHATAirdrop() { + async function setupHATAirdrop(useLock = true) { token = await ERC20Mock.new("Staking", "STK"); + totalAmount = 0; + for (const [account, amount] of Object.entries(airdropData)) { totalAmount += parseInt(amount); hashes.push(hashTokens(account, amount)); @@ -45,22 +50,67 @@ contract("HATAirdrop", (accounts) => { tokenLockFactory = await TokenLockFactory.new((await HATTokenLock.new()).address, accounts[0]); startTime = (await web3.eth.getBlock("latest")).timestamp + 7 * 24 * 3600; endTime = (await web3.eth.getBlock("latest")).timestamp + (7 + 365) * 24 * 3600; + if (useLock) { + lockEndTime = (await web3.eth.getBlock("latest")).timestamp + (7 + 365) * 24 * 3600; + } else { + lockEndTime = 0; + } periods = 90; - - const HATAirdrop = artifacts.require("./HATAirdrop.sol"); - hatAirdrop = await HATAirdrop.new( + + let hatAirdropImplementation = await HATAirdrop.new(); + let hatAirdropFactory = await HATAirdropFactory.new(hatAirdropImplementation.address); + + let airdropAddress = await hatAirdropFactory.predictHATAirdropAddress( + accounts[0], + "QmSUXfYsk9HgrMBa7tgp3MBm8FGwDF9hnVaR9C1PMoFdS3", + merkleTree.getHexRoot(), + startTime, + endTime, + lockEndTime, + periods, + token.address, + tokenLockFactory.address + ); + + let tx = await hatAirdropFactory.createHATAirdrop( + accounts[0], "QmSUXfYsk9HgrMBa7tgp3MBm8FGwDF9hnVaR9C1PMoFdS3", merkleTree.getHexRoot(), startTime, endTime, + lockEndTime, periods, token.address, tokenLockFactory.address ); + assert.equal(tx.logs[0].event, "HATAirdropCreated"); + assert.equal(tx.logs[0].args._hatAirdrop, airdropAddress); + hatAirdrop = await HATAirdrop.at(airdropAddress); + await token.mint(hatAirdrop.address, totalAmount); } + it("Cannot initialize twice", async () => { + await setupHATAirdrop(); + + try { + await hatAirdrop.initialize( + await hatAirdrop.owner(), + "QmSUXfYsk9HgrMBa7tgp3MBm8FGwDF9hnVaR9C1PMoFdS3", + await hatAirdrop.root(), + await hatAirdrop.startTime(), + await hatAirdrop.deadline(), + await hatAirdrop.lockEndTime(), + await hatAirdrop.periods(), + await hatAirdrop.token(), + await hatAirdrop.tokenLockFactory() + ); + assert(false, "cannot initialize twice"); + } catch (ex) { + assertVMException(ex, "Initializable: contract is already initialized"); + } + }); it("Redeem all", async () => { await setupHATAirdrop(); @@ -94,6 +144,26 @@ contract("HATAirdrop", (accounts) => { assert.equal((await token.balanceOf(hatAirdrop.address)).toString(), "0"); }); + it("Redeem all no lock", async () => { + await setupHATAirdrop(false); + + await utils.increaseTime(7 * 24 * 3600); + + for (const [account, amount] of Object.entries(airdropData)) { + let currentBalance = await token.balanceOf(hatAirdrop.address); + const proof = merkleTree.getHexProof(hashTokens(account, amount)); + let tx = await hatAirdrop.redeem(account, amount, proof); + assert.equal(tx.logs[0].event, "TokensRedeemed"); + assert.equal(tx.logs[0].args._account.toLowerCase(), account.toLowerCase()); + assert.equal(tx.logs[0].args._tokenLock, ZERO_ADDRESS); + assert.equal(tx.logs[0].args._amount, amount); + assert.equal(await token.balanceOf(account), amount); + assert.equal((await token.balanceOf(hatAirdrop.address)).toString(), currentBalance.sub(web3.utils.toBN(amount)).toString()); + } + + assert.equal((await token.balanceOf(hatAirdrop.address)).toString(), "0"); + }); + it("Cannot redeem before start time", async () => { await setupHATAirdrop(); From 564d8a3db1a2bed2084e43825d057375fcb6c40a Mon Sep 17 00:00:00 2001 From: benk10 Date: Tue, 2 Jan 2024 18:21:05 -0300 Subject: [PATCH 05/18] Update factory event --- contracts/tge/HATAirdropFactory.sol | 26 ++++++++++++++++++++++++-- docs/dodoc/tge/HATAirdropFactory.md | 11 ++++++++++- test/tge/hatairdrop.js | 9 +++++++++ 3 files changed, 43 insertions(+), 3 deletions(-) diff --git a/contracts/tge/HATAirdropFactory.sol b/contracts/tge/HATAirdropFactory.sol index c8a7870a..2ee21319 100644 --- a/contracts/tge/HATAirdropFactory.sol +++ b/contracts/tge/HATAirdropFactory.sol @@ -8,7 +8,18 @@ import "./HATAirdrop.sol"; contract HATAirdropFactory { address public immutable implementation; - event HATAirdropCreated(address indexed _hatAirdrop); + event HATAirdropCreated( + address indexed _hatAirdrop, + address _owner, + string _merkleTreeIPFSRef, + bytes32 _root, + uint256 _startTime, + uint256 _deadline, + uint256 _lockEndTime, + uint256 _periods, + IERC20Upgradeable _token, + ITokenLockFactory _tokenLockFactory + ); constructor (address _implementation) { implementation = _implementation; @@ -49,7 +60,18 @@ contract HATAirdropFactory { _tokenLockFactory ); - emit HATAirdropCreated(result); + emit HATAirdropCreated( + result, + _owner, + _merkleTreeIPFSRef, + _root, + _startTime, + _deadline, + _lockEndTime, + _periods, + _token, + _tokenLockFactory + ); } function predictHATAirdropAddress( diff --git a/docs/dodoc/tge/HATAirdropFactory.md b/docs/dodoc/tge/HATAirdropFactory.md index 3a3eaa3b..d3485e13 100644 --- a/docs/dodoc/tge/HATAirdropFactory.md +++ b/docs/dodoc/tge/HATAirdropFactory.md @@ -94,7 +94,7 @@ function predictHATAirdropAddress(address _owner, string _merkleTreeIPFSRef, byt ### HATAirdropCreated ```solidity -event HATAirdropCreated(address indexed _hatAirdrop) +event HATAirdropCreated(address indexed _hatAirdrop, address _owner, string _merkleTreeIPFSRef, bytes32 _root, uint256 _startTime, uint256 _deadline, uint256 _lockEndTime, uint256 _periods, contract IERC20Upgradeable _token, contract ITokenLockFactory _tokenLockFactory) ``` @@ -106,6 +106,15 @@ event HATAirdropCreated(address indexed _hatAirdrop) | Name | Type | Description | |---|---|---| | _hatAirdrop `indexed` | address | undefined | +| _owner | address | undefined | +| _merkleTreeIPFSRef | string | undefined | +| _root | bytes32 | undefined | +| _startTime | uint256 | undefined | +| _deadline | uint256 | undefined | +| _lockEndTime | uint256 | undefined | +| _periods | uint256 | undefined | +| _token | contract IERC20Upgradeable | undefined | +| _tokenLockFactory | contract ITokenLockFactory | undefined | diff --git a/test/tge/hatairdrop.js b/test/tge/hatairdrop.js index 300a691a..57e5d761 100644 --- a/test/tge/hatairdrop.js +++ b/test/tge/hatairdrop.js @@ -86,6 +86,15 @@ contract("HATAirdrop", (accounts) => { assert.equal(tx.logs[0].event, "HATAirdropCreated"); assert.equal(tx.logs[0].args._hatAirdrop, airdropAddress); + assert.equal(tx.logs[0].args._owner, accounts[0]); + assert.equal(tx.logs[0].args._merkleTreeIPFSRef, "QmSUXfYsk9HgrMBa7tgp3MBm8FGwDF9hnVaR9C1PMoFdS3"); + assert.equal(tx.logs[0].args._root, merkleTree.getHexRoot()); + assert.equal(tx.logs[0].args._startTime, startTime); + assert.equal(tx.logs[0].args._deadline, endTime); + assert.equal(tx.logs[0].args._lockEndTime, lockEndTime); + assert.equal(tx.logs[0].args._periods, periods); + assert.equal(tx.logs[0].args._token, token.address); + assert.equal(tx.logs[0].args._tokenLockFactory, tokenLockFactory.address); hatAirdrop = await HATAirdrop.at(airdropAddress); await token.mint(hatAirdrop.address, totalAmount); From d27d856e5b79eba1110aa5d7454ac52f1ed1b718 Mon Sep 17 00:00:00 2001 From: benk10 Date: Mon, 8 Jan 2024 13:10:40 -0300 Subject: [PATCH 06/18] Hold funds in factory --- contracts/tge/HATAirdrop.sol | 22 ++--- contracts/tge/HATAirdropFactory.sol | 31 ++++--- docs/dodoc/tge/HATAirdrop.md | 109 ++++------------------ docs/dodoc/tge/HATAirdropFactory.md | 138 ++++++++++++++++++++++++++-- test/tge/hatairdrop.js | 131 ++++++++++++++++---------- 5 files changed, 259 insertions(+), 172 deletions(-) diff --git a/contracts/tge/HATAirdrop.sol b/contracts/tge/HATAirdrop.sol index 5128c3d8..6bf89cfa 100644 --- a/contracts/tge/HATAirdrop.sol +++ b/contracts/tge/HATAirdrop.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.16; -import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +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"; @@ -10,7 +10,7 @@ import "../tokenlock/ITokenLockFactory.sol"; /* An airdrop contract that transfers tokens based on a merkle tree. */ -contract HATAirdrop is OwnableUpgradeable { +contract HATAirdrop is Initializable { error CannotRedeemBeforeStartTime(); error CannotRedeemAfterDeadline(); error LeafAlreadyRedeemed(); @@ -26,19 +26,18 @@ contract HATAirdrop is OwnableUpgradeable { uint256 public periods; 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); - event TokensRecovered(address indexed _owner, uint256 _amount); constructor () { _disableInitializers(); } function initialize( - address _owner, string memory _merkleTreeIPFSRef, bytes32 _root, uint256 _startTime, @@ -48,7 +47,6 @@ contract HATAirdrop is OwnableUpgradeable { IERC20Upgradeable _token, ITokenLockFactory _tokenLockFactory ) external initializer { - _transferOwnership(_owner); root = _root; startTime = _startTime; deadline = _deadline; @@ -56,6 +54,7 @@ contract HATAirdrop is OwnableUpgradeable { periods = _periods; token = _token; tokenLockFactory = _tokenLockFactory; + factory = msg.sender; emit MerkleTreeSet(_merkleTreeIPFSRef, _root, _startTime, _deadline); } @@ -84,23 +83,14 @@ contract HATAirdrop is OwnableUpgradeable { false, true ); - token.safeTransfer(_tokenLock, _amount); + token.safeTransferFrom(factory, _tokenLock, _amount); } else { - token.safeTransfer(_account, _amount); + token.safeTransferFrom(factory, _account, _amount); } emit TokensRedeemed(_account, _tokenLock, _amount); } - function recoverTokens() external onlyOwner { - // solhint-disable-next-line not-rely-on-time - if (block.timestamp <= deadline) revert CannotRecoverBeforeDeadline(); - address owner = owner(); - uint256 amount = token.balanceOf(address(this)); - token.safeTransfer(owner, amount); - emit TokensRecovered(owner, amount); - } - function _verify(bytes32[] calldata proof, bytes32 leaf) internal view returns (bool) { return MerkleProofUpgradeable.verifyCalldata(proof, root, leaf); } diff --git a/contracts/tge/HATAirdropFactory.sol b/contracts/tge/HATAirdropFactory.sol index 2ee21319..407580ed 100644 --- a/contracts/tge/HATAirdropFactory.sol +++ b/contracts/tge/HATAirdropFactory.sol @@ -3,20 +3,26 @@ pragma solidity 0.8.16; +import "@openzeppelin/contracts/access/Ownable.sol"; import "@openzeppelin/contracts/proxy/Clones.sol"; import "./HATAirdrop.sol"; -contract HATAirdropFactory { - address public immutable implementation; +contract HATAirdropFactory is Ownable { + using SafeERC20Upgradeable for IERC20Upgradeable; + + address public implementation; + + event ImplementationUpdated(address indexed _newImplementation); + event TokensWithdrawn(address indexed _owner, uint256 _amount); event HATAirdropCreated( address indexed _hatAirdrop, - address _owner, string _merkleTreeIPFSRef, bytes32 _root, uint256 _startTime, uint256 _deadline, uint256 _lockEndTime, uint256 _periods, + uint256 _totalAmount, IERC20Upgradeable _token, ITokenLockFactory _tokenLockFactory ); @@ -25,19 +31,23 @@ contract HATAirdropFactory { implementation = _implementation; } + address owner = owner(); + _token.safeTransfer(owner, _amount); + emit TokensWithdrawn(owner, _amount); + } + function createHATAirdrop( - address _owner, string memory _merkleTreeIPFSRef, bytes32 _root, uint256 _startTime, uint256 _deadline, uint256 _lockEndTime, uint256 _periods, + uint256 _totalAmount, IERC20Upgradeable _token, ITokenLockFactory _tokenLockFactory - ) external returns (address result) { + ) external onlyOwner returns (address result) { result = Clones.cloneDeterministic(implementation, keccak256(abi.encodePacked( - _owner, _merkleTreeIPFSRef, _root, _startTime, @@ -49,7 +59,6 @@ contract HATAirdropFactory { ))); HATAirdrop(payable(result)).initialize( - _owner, _merkleTreeIPFSRef, _root, _startTime, @@ -60,22 +69,23 @@ contract HATAirdropFactory { _tokenLockFactory ); + _token.safeApprove(result, _totalAmount); + emit HATAirdropCreated( result, - _owner, _merkleTreeIPFSRef, _root, _startTime, _deadline, _lockEndTime, _periods, + _totalAmount, _token, _tokenLockFactory ); } function predictHATAirdropAddress( - address _owner, string memory _merkleTreeIPFSRef, bytes32 _root, uint256 _startTime, @@ -84,9 +94,8 @@ contract HATAirdropFactory { uint256 _periods, IERC20 _token, ITokenLockFactory _tokenLockFactory - ) public view returns (address) { + ) external view returns (address) { return Clones.predictDeterministicAddress(implementation, keccak256(abi.encodePacked( - _owner, _merkleTreeIPFSRef, _root, _startTime, diff --git a/docs/dodoc/tge/HATAirdrop.md b/docs/dodoc/tge/HATAirdrop.md index 27ed8906..58a66cb4 100644 --- a/docs/dodoc/tge/HATAirdrop.md +++ b/docs/dodoc/tge/HATAirdrop.md @@ -27,10 +27,27 @@ function deadline() external view returns (uint256) |---|---|---| | _0 | uint256 | undefined | +### factory + +```solidity +function factory() external view returns (address) +``` + + + + + + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | address | undefined | + ### initialize ```solidity -function initialize(address _owner, string _merkleTreeIPFSRef, bytes32 _root, uint256 _startTime, uint256 _deadline, uint256 _lockEndTime, uint256 _periods, contract IERC20Upgradeable _token, contract ITokenLockFactory _tokenLockFactory) external nonpayable +function initialize(string _merkleTreeIPFSRef, bytes32 _root, uint256 _startTime, uint256 _deadline, uint256 _lockEndTime, uint256 _periods, contract IERC20Upgradeable _token, contract ITokenLockFactory _tokenLockFactory) external nonpayable ``` @@ -41,7 +58,6 @@ function initialize(address _owner, string _merkleTreeIPFSRef, bytes32 _root, ui | Name | Type | Description | |---|---|---| -| _owner | address | undefined | | _merkleTreeIPFSRef | string | undefined | | _root | bytes32 | undefined | | _startTime | uint256 | undefined | @@ -90,23 +106,6 @@ function lockEndTime() external view returns (uint256) |---|---|---| | _0 | uint256 | undefined | -### owner - -```solidity -function owner() external view returns (address) -``` - - - -*Returns the address of the current owner.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined | - ### periods ```solidity @@ -124,17 +123,6 @@ function periods() external view returns (uint256) |---|---|---| | _0 | uint256 | undefined | -### recoverTokens - -```solidity -function recoverTokens() external nonpayable -``` - - - - - - ### redeem ```solidity @@ -153,17 +141,6 @@ function redeem(address _account, uint256 _amount, bytes32[] _proof) external no | _amount | uint256 | undefined | | _proof | bytes32[] | undefined | -### renounceOwnership - -```solidity -function renounceOwnership() external nonpayable -``` - - - -*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.* - - ### root ```solidity @@ -232,22 +209,6 @@ function tokenLockFactory() external view returns (contract ITokenLockFactory) |---|---|---| | _0 | contract ITokenLockFactory | undefined | -### transferOwnership - -```solidity -function transferOwnership(address newOwner) external nonpayable -``` - - - -*Transfers ownership of the contract to a new account (`newOwner`). Can only be called by the current owner.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| newOwner | address | undefined | - ## Events @@ -287,40 +248,6 @@ event MerkleTreeSet(string _merkleTreeIPFSRef, bytes32 _root, uint256 _startTime | _startTime | uint256 | undefined | | _deadline | uint256 | undefined | -### OwnershipTransferred - -```solidity -event OwnershipTransferred(address indexed previousOwner, address indexed newOwner) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| previousOwner `indexed` | address | undefined | -| newOwner `indexed` | address | undefined | - -### TokensRecovered - -```solidity -event TokensRecovered(address indexed _owner, uint256 _amount) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _owner `indexed` | address | undefined | -| _amount | uint256 | undefined | - ### TokensRedeemed ```solidity diff --git a/docs/dodoc/tge/HATAirdropFactory.md b/docs/dodoc/tge/HATAirdropFactory.md index d3485e13..ee39fdde 100644 --- a/docs/dodoc/tge/HATAirdropFactory.md +++ b/docs/dodoc/tge/HATAirdropFactory.md @@ -13,7 +13,7 @@ ### createHATAirdrop ```solidity -function createHATAirdrop(address _owner, string _merkleTreeIPFSRef, bytes32 _root, uint256 _startTime, uint256 _deadline, uint256 _lockEndTime, uint256 _periods, contract IERC20Upgradeable _token, contract ITokenLockFactory _tokenLockFactory) external nonpayable returns (address result) +function createHATAirdrop(string _merkleTreeIPFSRef, bytes32 _root, uint256 _startTime, uint256 _deadline, uint256 _lockEndTime, uint256 _periods, uint256 _totalAmount, contract IERC20Upgradeable _token, contract ITokenLockFactory _tokenLockFactory) external nonpayable returns (address result) ``` @@ -24,13 +24,13 @@ function createHATAirdrop(address _owner, string _merkleTreeIPFSRef, bytes32 _ro | Name | Type | Description | |---|---|---| -| _owner | address | undefined | | _merkleTreeIPFSRef | string | undefined | | _root | bytes32 | undefined | | _startTime | uint256 | undefined | | _deadline | uint256 | undefined | | _lockEndTime | uint256 | undefined | | _periods | uint256 | undefined | +| _totalAmount | uint256 | undefined | | _token | contract IERC20Upgradeable | undefined | | _tokenLockFactory | contract ITokenLockFactory | undefined | @@ -51,6 +51,23 @@ function implementation() external view returns (address) +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | address | undefined | + +### owner + +```solidity +function owner() external view returns (address) +``` + + + +*Returns the address of the current owner.* + + #### Returns | Name | Type | Description | @@ -60,7 +77,7 @@ function implementation() external view returns (address) ### predictHATAirdropAddress ```solidity -function predictHATAirdropAddress(address _owner, string _merkleTreeIPFSRef, bytes32 _root, uint256 _startTime, uint256 _deadline, uint256 _lockEndTime, uint256 _periods, contract IERC20 _token, contract ITokenLockFactory _tokenLockFactory) external view returns (address) +function predictHATAirdropAddress(string _merkleTreeIPFSRef, bytes32 _root, uint256 _startTime, uint256 _deadline, uint256 _lockEndTime, uint256 _periods, contract IERC20 _token, contract ITokenLockFactory _tokenLockFactory) external view returns (address) ``` @@ -71,7 +88,6 @@ function predictHATAirdropAddress(address _owner, string _merkleTreeIPFSRef, byt | Name | Type | Description | |---|---|---| -| _owner | address | undefined | | _merkleTreeIPFSRef | string | undefined | | _root | bytes32 | undefined | | _startTime | uint256 | undefined | @@ -87,6 +103,66 @@ function predictHATAirdropAddress(address _owner, string _merkleTreeIPFSRef, byt |---|---|---| | _0 | address | undefined | +### renounceOwnership + +```solidity +function renounceOwnership() external nonpayable +``` + + + +*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 + +```solidity +function transferOwnership(address newOwner) external nonpayable +``` + + + +*Transfers ownership of the contract to a new account (`newOwner`). Can only be called by the current owner.* + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| newOwner | address | undefined | + +### updateImplementation + +```solidity +function updateImplementation(address _newImplementation) external nonpayable +``` + + + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| _newImplementation | address | undefined | + +### withdrawTokens + +```solidity +function withdrawTokens(contract IERC20Upgradeable _token, uint256 _amount) external nonpayable +``` + + + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| _token | contract IERC20Upgradeable | undefined | +| _amount | uint256 | undefined | + ## Events @@ -94,7 +170,7 @@ function predictHATAirdropAddress(address _owner, string _merkleTreeIPFSRef, byt ### HATAirdropCreated ```solidity -event HATAirdropCreated(address indexed _hatAirdrop, address _owner, string _merkleTreeIPFSRef, bytes32 _root, uint256 _startTime, uint256 _deadline, uint256 _lockEndTime, uint256 _periods, contract IERC20Upgradeable _token, contract ITokenLockFactory _tokenLockFactory) +event HATAirdropCreated(address indexed _hatAirdrop, string _merkleTreeIPFSRef, bytes32 _root, uint256 _startTime, uint256 _deadline, uint256 _lockEndTime, uint256 _periods, uint256 _totalAmount, contract IERC20Upgradeable _token, contract ITokenLockFactory _tokenLockFactory) ``` @@ -106,15 +182,65 @@ event HATAirdropCreated(address indexed _hatAirdrop, address _owner, string _mer | Name | Type | Description | |---|---|---| | _hatAirdrop `indexed` | address | undefined | -| _owner | address | undefined | | _merkleTreeIPFSRef | string | undefined | | _root | bytes32 | undefined | | _startTime | uint256 | undefined | | _deadline | uint256 | undefined | | _lockEndTime | uint256 | undefined | | _periods | uint256 | undefined | +| _totalAmount | uint256 | undefined | | _token | contract IERC20Upgradeable | undefined | | _tokenLockFactory | contract ITokenLockFactory | undefined | +### ImplementationUpdated + +```solidity +event ImplementationUpdated(address indexed _newImplementation) +``` + + + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| _newImplementation `indexed` | address | undefined | + +### OwnershipTransferred + +```solidity +event OwnershipTransferred(address indexed previousOwner, address indexed newOwner) +``` + + + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| previousOwner `indexed` | address | undefined | +| newOwner `indexed` | address | undefined | + +### TokensWithdrawn + +```solidity +event TokensWithdrawn(address indexed _owner, uint256 _amount) +``` + + + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| _owner `indexed` | address | undefined | +| _amount | uint256 | undefined | + diff --git a/test/tge/hatairdrop.js b/test/tge/hatairdrop.js index 57e5d761..3c76a02f 100644 --- a/test/tge/hatairdrop.js +++ b/test/tge/hatairdrop.js @@ -25,7 +25,9 @@ function hashTokens(account, amount) { contract("HATAirdrop", (accounts) => { + let hatAirdropFactory; let hatAirdrop; + let hatAirdropImplementation; let token; let merkleTree; let hashes = []; @@ -57,11 +59,10 @@ contract("HATAirdrop", (accounts) => { } periods = 90; - let hatAirdropImplementation = await HATAirdrop.new(); - let hatAirdropFactory = await HATAirdropFactory.new(hatAirdropImplementation.address); + hatAirdropImplementation = await HATAirdrop.new(); + hatAirdropFactory = await HATAirdropFactory.new(hatAirdropImplementation.address); let airdropAddress = await hatAirdropFactory.predictHATAirdropAddress( - accounts[0], "QmSUXfYsk9HgrMBa7tgp3MBm8FGwDF9hnVaR9C1PMoFdS3", merkleTree.getHexRoot(), startTime, @@ -73,39 +74,78 @@ contract("HATAirdrop", (accounts) => { ); let tx = await hatAirdropFactory.createHATAirdrop( - accounts[0], "QmSUXfYsk9HgrMBa7tgp3MBm8FGwDF9hnVaR9C1PMoFdS3", merkleTree.getHexRoot(), startTime, endTime, lockEndTime, periods, + totalAmount, token.address, tokenLockFactory.address ); assert.equal(tx.logs[0].event, "HATAirdropCreated"); assert.equal(tx.logs[0].args._hatAirdrop, airdropAddress); - assert.equal(tx.logs[0].args._owner, accounts[0]); assert.equal(tx.logs[0].args._merkleTreeIPFSRef, "QmSUXfYsk9HgrMBa7tgp3MBm8FGwDF9hnVaR9C1PMoFdS3"); assert.equal(tx.logs[0].args._root, merkleTree.getHexRoot()); assert.equal(tx.logs[0].args._startTime, startTime); assert.equal(tx.logs[0].args._deadline, endTime); assert.equal(tx.logs[0].args._lockEndTime, lockEndTime); assert.equal(tx.logs[0].args._periods, periods); + assert.equal(tx.logs[0].args._totalAmount, totalAmount); assert.equal(tx.logs[0].args._token, token.address); assert.equal(tx.logs[0].args._tokenLockFactory, tokenLockFactory.address); hatAirdrop = await HATAirdrop.at(airdropAddress); - await token.mint(hatAirdrop.address, totalAmount); + await token.mint(hatAirdropFactory.address, totalAmount); } + it("Update implementation", async () => { + await setupHATAirdrop(); + + await assertFunctionRaisesException( + hatAirdropFactory.updateImplementation( + accounts[3], + { from: accounts[1] } + ), + "Ownable: caller is not the owner" + ); + + assert.equal(await hatAirdropFactory.implementation(), hatAirdropImplementation.address); + + let tx = await hatAirdropFactory.updateImplementation(accounts[3]); + assert.equal(tx.logs[0].event, "ImplementationUpdated"); + assert.equal(tx.logs[0].args._newImplementation, accounts[3]); + + assert.equal(await hatAirdropFactory.implementation(), accounts[3]); + }); + + it("Only owner can create airdrops", async () => { + await setupHATAirdrop(); + + await assertFunctionRaisesException( + hatAirdropFactory.createHATAirdrop( + "QmSUXfYsk9HgrMBa7tgp3MBm8FGwDF9hnVaR9C1PMoFdS3", + merkleTree.getHexRoot(), + startTime, + endTime, + lockEndTime, + periods, + totalAmount, + token.address, + tokenLockFactory.address, + { from: accounts[1] } + ), + "Ownable: caller is not the owner" + ); + }); + it("Cannot initialize twice", async () => { await setupHATAirdrop(); try { await hatAirdrop.initialize( - await hatAirdrop.owner(), "QmSUXfYsk9HgrMBa7tgp3MBm8FGwDF9hnVaR9C1PMoFdS3", await hatAirdrop.root(), await hatAirdrop.startTime(), @@ -127,16 +167,16 @@ contract("HATAirdrop", (accounts) => { await utils.increaseTime(7 * 24 * 3600); for (const [account, amount] of Object.entries(airdropData)) { - let currentBalance = await token.balanceOf(hatAirdrop.address); + let currentBalance = await token.balanceOf(hatAirdropFactory.address); const proof = merkleTree.getHexProof(hashTokens(account, amount)); let tx = await hatAirdrop.redeem(account, amount, proof); - assert.equal(tx.logs[1].event, "TokensRedeemed"); - assert.equal(tx.logs[1].args._account.toLowerCase(), account.toLowerCase()); - assert.equal(tx.logs[1].args._amount, amount); - assert.equal(await token.balanceOf(tx.logs[1].args._tokenLock), amount); - assert.equal((await token.balanceOf(hatAirdrop.address)).toString(), currentBalance.sub(web3.utils.toBN(amount)).toString()); + assert.equal(tx.logs[0].event, "TokensRedeemed"); + assert.equal(tx.logs[0].args._account.toLowerCase(), account.toLowerCase()); + assert.equal(tx.logs[0].args._amount, amount); + assert.equal(await token.balanceOf(tx.logs[0].args._tokenLock), amount); + assert.equal((await token.balanceOf(hatAirdropFactory.address)).toString(), currentBalance.sub(web3.utils.toBN(amount)).toString()); - let tokenLock = await HATTokenLock.at(tx.logs[1].args._tokenLock); + let tokenLock = await HATTokenLock.at(tx.logs[0].args._tokenLock); assert.equal(await tokenLock.startTime(), startTime); assert.equal(await tokenLock.endTime(), endTime); assert.equal(await tokenLock.periods(), periods); @@ -150,7 +190,7 @@ contract("HATAirdrop", (accounts) => { assert.equal(await tokenLock.canDelegate(), true); } - assert.equal((await token.balanceOf(hatAirdrop.address)).toString(), "0"); + assert.equal((await token.balanceOf(hatAirdropFactory.address)).toString(), "0"); }); it("Redeem all no lock", async () => { @@ -159,7 +199,7 @@ contract("HATAirdrop", (accounts) => { await utils.increaseTime(7 * 24 * 3600); for (const [account, amount] of Object.entries(airdropData)) { - let currentBalance = await token.balanceOf(hatAirdrop.address); + let currentBalance = await token.balanceOf(hatAirdropFactory.address); const proof = merkleTree.getHexProof(hashTokens(account, amount)); let tx = await hatAirdrop.redeem(account, amount, proof); assert.equal(tx.logs[0].event, "TokensRedeemed"); @@ -167,10 +207,10 @@ contract("HATAirdrop", (accounts) => { assert.equal(tx.logs[0].args._tokenLock, ZERO_ADDRESS); assert.equal(tx.logs[0].args._amount, amount); assert.equal(await token.balanceOf(account), amount); - assert.equal((await token.balanceOf(hatAirdrop.address)).toString(), currentBalance.sub(web3.utils.toBN(amount)).toString()); + assert.equal((await token.balanceOf(hatAirdropFactory.address)).toString(), currentBalance.sub(web3.utils.toBN(amount)).toString()); } - assert.equal((await token.balanceOf(hatAirdrop.address)).toString(), "0"); + assert.equal((await token.balanceOf(hatAirdropFactory.address)).toString(), "0"); }); it("Cannot redeem before start time", async () => { @@ -186,10 +226,10 @@ contract("HATAirdrop", (accounts) => { await utils.increaseTime(7 * 24 * 3600); let tx = await hatAirdrop.redeem(account, amount, proof); - assert.equal(tx.logs[1].event, "TokensRedeemed"); - assert.equal(tx.logs[1].args._account.toLowerCase(), account.toLowerCase()); - assert.equal(tx.logs[1].args._amount, amount); - assert.equal(await token.balanceOf(tx.logs[1].args._tokenLock), amount); + assert.equal(tx.logs[0].event, "TokensRedeemed"); + assert.equal(tx.logs[0].args._account.toLowerCase(), account.toLowerCase()); + assert.equal(tx.logs[0].args._amount, amount); + assert.equal(await token.balanceOf(tx.logs[0].args._tokenLock), amount); }); it("Cannot redeem after deadline", async () => { @@ -201,10 +241,10 @@ contract("HATAirdrop", (accounts) => { await utils.increaseTime(7 * 24 * 3600); let tx = await hatAirdrop.redeem(account, amount, proof); - assert.equal(tx.logs[1].event, "TokensRedeemed"); - assert.equal(tx.logs[1].args._account.toLowerCase(), account.toLowerCase()); - assert.equal(tx.logs[1].args._amount, amount); - assert.equal(await token.balanceOf(tx.logs[1].args._tokenLock), amount); + assert.equal(tx.logs[0].event, "TokensRedeemed"); + assert.equal(tx.logs[0].args._account.toLowerCase(), account.toLowerCase()); + assert.equal(tx.logs[0].args._amount, amount); + assert.equal(await token.balanceOf(tx.logs[0].args._tokenLock), amount); await utils.increaseTime(365 * 24 * 3600); @@ -226,10 +266,10 @@ contract("HATAirdrop", (accounts) => { await utils.increaseTime(7 * 24 * 3600); let tx = await hatAirdrop.redeem(account, amount, proof); - assert.equal(tx.logs[1].event, "TokensRedeemed"); - assert.equal(tx.logs[1].args._account.toLowerCase(), account.toLowerCase()); - assert.equal(tx.logs[1].args._amount, amount); - assert.equal(await token.balanceOf(tx.logs[1].args._tokenLock), amount); + assert.equal(tx.logs[0].event, "TokensRedeemed"); + assert.equal(tx.logs[0].args._account.toLowerCase(), account.toLowerCase()); + assert.equal(tx.logs[0].args._amount, amount); + assert.equal(await token.balanceOf(tx.logs[0].args._tokenLock), amount); await assertFunctionRaisesException( hatAirdrop.redeem(account, amount, proof), @@ -257,43 +297,38 @@ contract("HATAirdrop", (accounts) => { ); let tx = await hatAirdrop.redeem(account, amount, proof); - assert.equal(tx.logs[1].event, "TokensRedeemed"); - assert.equal(tx.logs[1].args._account.toLowerCase(), account.toLowerCase()); - assert.equal(tx.logs[1].args._amount, amount); - assert.equal(await token.balanceOf(tx.logs[1].args._tokenLock), amount); + assert.equal(tx.logs[0].event, "TokensRedeemed"); + assert.equal(tx.logs[0].args._account.toLowerCase(), account.toLowerCase()); + assert.equal(tx.logs[0].args._amount, amount); + assert.equal(await token.balanceOf(tx.logs[0].args._tokenLock), amount); }); - it("Recover tokens", async () => { + it("Withdraw tokens", async () => { await setupHATAirdrop(); await assertFunctionRaisesException( - hatAirdrop.recoverTokens({ from: accounts[1] }), + hatAirdropFactory.withdrawTokens(token.address, 1, { from: accounts[1] }), "Ownable: caller is not the owner" ); - await assertFunctionRaisesException( - hatAirdrop.recoverTokens(), - "CannotRecoverBeforeDeadline" - ); - await utils.increaseTime(7 * 24 * 3600); const [account, amount] = Object.entries(airdropData)[0]; const proof = merkleTree.getHexProof(hashTokens(account, amount)); let tx = await hatAirdrop.redeem(account, amount, proof); - assert.equal(tx.logs[1].event, "TokensRedeemed"); - assert.equal(tx.logs[1].args._account.toLowerCase(), account.toLowerCase()); - assert.equal(tx.logs[1].args._amount, amount); - assert.equal(await token.balanceOf(tx.logs[1].args._tokenLock), amount); + assert.equal(tx.logs[0].event, "TokensRedeemed"); + assert.equal(tx.logs[0].args._account.toLowerCase(), account.toLowerCase()); + assert.equal(tx.logs[0].args._amount, amount); + assert.equal(await token.balanceOf(tx.logs[0].args._tokenLock), amount); await utils.increaseTime(365 * 24 * 3600); - tx = await hatAirdrop.recoverTokens(); + tx = await hatAirdropFactory.withdrawTokens(token.address, web3.utils.toBN((totalAmount - parseInt(amount)))); - assert.equal(tx.logs[0].event, "TokensRecovered"); + assert.equal(tx.logs[0].event, "TokensWithdrawn"); assert.equal(tx.logs[0].args._owner.toLowerCase(), accounts[0].toLowerCase()); assert.equal(tx.logs[0].args._amount.toString(), web3.utils.toBN((totalAmount - parseInt(amount))).toString()); - assert.equal((await token.balanceOf(hatAirdrop.address)).toString(), "0"); + assert.equal((await token.balanceOf(hatAirdropFactory.address)).toString(), "0"); }); }); From 64b8c3c57f678c21b72f918c6f898cde3eec1295 Mon Sep 17 00:00:00 2001 From: benk10 Date: Mon, 8 Jan 2024 13:10:57 -0300 Subject: [PATCH 07/18] Update HATAirdropFactory.sol --- contracts/tge/HATAirdropFactory.sol | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/contracts/tge/HATAirdropFactory.sol b/contracts/tge/HATAirdropFactory.sol index 407580ed..f4afc8af 100644 --- a/contracts/tge/HATAirdropFactory.sol +++ b/contracts/tge/HATAirdropFactory.sol @@ -31,6 +31,12 @@ contract HATAirdropFactory is Ownable { implementation = _implementation; } + function updateImplementation(address _newImplementation) external onlyOwner { + implementation = _newImplementation; + emit ImplementationUpdated(_newImplementation); + } + + function withdrawTokens(IERC20Upgradeable _token, uint256 _amount) external onlyOwner { address owner = owner(); _token.safeTransfer(owner, _amount); emit TokensWithdrawn(owner, _amount); From 9cf2794c9fb544b5eed71e444e245d2107d9ec9c Mon Sep 17 00:00:00 2001 From: benk10 Date: Wed, 10 Jan 2024 19:10:17 -0300 Subject: [PATCH 08/18] Avoid deploying lock if time passed --- contracts/tge/HATAirdrop.sol | 3 ++- test/tge/hatairdrop.js | 51 ++++++++++++++++++++++++++++++++++-- 2 files changed, 51 insertions(+), 3 deletions(-) diff --git a/contracts/tge/HATAirdrop.sol b/contracts/tge/HATAirdrop.sol index 6bf89cfa..7ff1b087 100644 --- a/contracts/tge/HATAirdrop.sol +++ b/contracts/tge/HATAirdrop.sol @@ -69,7 +69,8 @@ contract HATAirdrop is Initializable { leafRedeemed[leaf] = true; address _tokenLock = address(0); - if (lockEndTime != 0) { + // solhint-disable-next-line not-rely-on-time + if (lockEndTime > block.timestamp) { _tokenLock = tokenLockFactory.createTokenLock( address(token), 0x0000000000000000000000000000000000000000, diff --git a/test/tge/hatairdrop.js b/test/tge/hatairdrop.js index 3c76a02f..eba6b3e4 100644 --- a/test/tge/hatairdrop.js +++ b/test/tge/hatairdrop.js @@ -53,7 +53,7 @@ contract("HATAirdrop", (accounts) => { startTime = (await web3.eth.getBlock("latest")).timestamp + 7 * 24 * 3600; endTime = (await web3.eth.getBlock("latest")).timestamp + (7 + 365) * 24 * 3600; if (useLock) { - lockEndTime = (await web3.eth.getBlock("latest")).timestamp + (7 + 365) * 24 * 3600; + lockEndTime = (await web3.eth.getBlock("latest")).timestamp + 14 * 24 * 3600; } else { lockEndTime = 0; } @@ -178,7 +178,7 @@ contract("HATAirdrop", (accounts) => { let tokenLock = await HATTokenLock.at(tx.logs[0].args._tokenLock); assert.equal(await tokenLock.startTime(), startTime); - assert.equal(await tokenLock.endTime(), endTime); + assert.equal(await tokenLock.endTime(), lockEndTime); assert.equal(await tokenLock.periods(), periods); assert.equal(await tokenLock.owner(), "0x0000000000000000000000000000000000000000"); assert.equal((await tokenLock.beneficiary()).toLowerCase(), account.toLowerCase()); @@ -213,6 +213,53 @@ contract("HATAirdrop", (accounts) => { assert.equal((await token.balanceOf(hatAirdropFactory.address)).toString(), "0"); }); + it("Redeem after lock ended does not deploy lock", async () => { + await setupHATAirdrop(); + + await utils.increaseTime(7 * 24 * 3600); + + const dataLength = Object.entries(airdropData).length; + for (const [account, amount] of Object.entries(airdropData).slice(0, dataLength / 2)) { + let currentBalance = await token.balanceOf(hatAirdropFactory.address); + const proof = merkleTree.getHexProof(hashTokens(account, amount)); + let tx = await hatAirdrop.redeem(account, amount, proof); + assert.equal(tx.logs[0].event, "TokensRedeemed"); + assert.equal(tx.logs[0].args._account.toLowerCase(), account.toLowerCase()); + assert.equal(tx.logs[0].args._amount, amount); + assert.equal(await token.balanceOf(tx.logs[0].args._tokenLock), amount); + assert.equal((await token.balanceOf(hatAirdropFactory.address)).toString(), currentBalance.sub(web3.utils.toBN(amount)).toString()); + + let tokenLock = await HATTokenLock.at(tx.logs[0].args._tokenLock); + assert.equal(await tokenLock.startTime(), startTime); + assert.equal(await tokenLock.endTime(), lockEndTime); + assert.equal(await tokenLock.periods(), periods); + assert.equal(await tokenLock.owner(), "0x0000000000000000000000000000000000000000"); + assert.equal((await tokenLock.beneficiary()).toLowerCase(), account.toLowerCase()); + assert.equal(await tokenLock.managedAmount(), amount); + assert.equal(await tokenLock.token(), token.address); + assert.equal(await tokenLock.releaseStartTime(), 0); + assert.equal(await tokenLock.vestingCliffTime(), 0); + assert.equal(await tokenLock.revocable(), false); + assert.equal(await tokenLock.canDelegate(), true); + } + + await utils.increaseTime(7 * 24 * 3600); + + for (const [account, amount] of Object.entries(airdropData).slice(dataLength / 2)) { + let currentBalance = await token.balanceOf(hatAirdropFactory.address); + const proof = merkleTree.getHexProof(hashTokens(account, amount)); + let tx = await hatAirdrop.redeem(account, amount, proof); + assert.equal(tx.logs[0].event, "TokensRedeemed"); + assert.equal(tx.logs[0].args._account.toLowerCase(), account.toLowerCase()); + assert.equal(tx.logs[0].args._tokenLock, ZERO_ADDRESS); + assert.equal(tx.logs[0].args._amount, amount); + assert.equal(await token.balanceOf(account), amount); + assert.equal((await token.balanceOf(hatAirdropFactory.address)).toString(), currentBalance.sub(web3.utils.toBN(amount)).toString()); + } + + assert.equal((await token.balanceOf(hatAirdropFactory.address)).toString(), "0"); + }); + it("Cannot redeem before start time", async () => { await setupHATAirdrop(); From c6e2993408a3c934a1ec38357aefc0dc2a3824be Mon Sep 17 00:00:00 2001 From: benk10 Date: Wed, 10 Jan 2024 19:13:31 -0300 Subject: [PATCH 09/18] Allow specifying implementation on deployment --- contracts/tge/HATAirdropFactory.sol | 18 +++------- docs/dodoc/tge/HATAirdropFactory.md | 55 +++-------------------------- test/tge/hatairdrop.js | 25 +++---------- 3 files changed, 12 insertions(+), 86 deletions(-) diff --git a/contracts/tge/HATAirdropFactory.sol b/contracts/tge/HATAirdropFactory.sol index f4afc8af..0f317617 100644 --- a/contracts/tge/HATAirdropFactory.sol +++ b/contracts/tge/HATAirdropFactory.sol @@ -10,9 +10,6 @@ import "./HATAirdrop.sol"; contract HATAirdropFactory is Ownable { using SafeERC20Upgradeable for IERC20Upgradeable; - address public implementation; - - event ImplementationUpdated(address indexed _newImplementation); event TokensWithdrawn(address indexed _owner, uint256 _amount); event HATAirdropCreated( address indexed _hatAirdrop, @@ -27,15 +24,6 @@ contract HATAirdropFactory is Ownable { ITokenLockFactory _tokenLockFactory ); - constructor (address _implementation) { - implementation = _implementation; - } - - function updateImplementation(address _newImplementation) external onlyOwner { - implementation = _newImplementation; - emit ImplementationUpdated(_newImplementation); - } - function withdrawTokens(IERC20Upgradeable _token, uint256 _amount) external onlyOwner { address owner = owner(); _token.safeTransfer(owner, _amount); @@ -43,6 +31,7 @@ contract HATAirdropFactory is Ownable { } function createHATAirdrop( + address _implementation, string memory _merkleTreeIPFSRef, bytes32 _root, uint256 _startTime, @@ -53,7 +42,7 @@ contract HATAirdropFactory is Ownable { IERC20Upgradeable _token, ITokenLockFactory _tokenLockFactory ) external onlyOwner returns (address result) { - result = Clones.cloneDeterministic(implementation, keccak256(abi.encodePacked( + result = Clones.cloneDeterministic(_implementation, keccak256(abi.encodePacked( _merkleTreeIPFSRef, _root, _startTime, @@ -92,6 +81,7 @@ contract HATAirdropFactory is Ownable { } function predictHATAirdropAddress( + address _implementation, string memory _merkleTreeIPFSRef, bytes32 _root, uint256 _startTime, @@ -101,7 +91,7 @@ contract HATAirdropFactory is Ownable { IERC20 _token, ITokenLockFactory _tokenLockFactory ) external view returns (address) { - return Clones.predictDeterministicAddress(implementation, keccak256(abi.encodePacked( + return Clones.predictDeterministicAddress(_implementation, keccak256(abi.encodePacked( _merkleTreeIPFSRef, _root, _startTime, diff --git a/docs/dodoc/tge/HATAirdropFactory.md b/docs/dodoc/tge/HATAirdropFactory.md index ee39fdde..72f2a72b 100644 --- a/docs/dodoc/tge/HATAirdropFactory.md +++ b/docs/dodoc/tge/HATAirdropFactory.md @@ -13,7 +13,7 @@ ### createHATAirdrop ```solidity -function createHATAirdrop(string _merkleTreeIPFSRef, bytes32 _root, uint256 _startTime, uint256 _deadline, uint256 _lockEndTime, uint256 _periods, uint256 _totalAmount, contract IERC20Upgradeable _token, contract ITokenLockFactory _tokenLockFactory) external nonpayable returns (address result) +function createHATAirdrop(address _implementation, string _merkleTreeIPFSRef, bytes32 _root, uint256 _startTime, uint256 _deadline, uint256 _lockEndTime, uint256 _periods, uint256 _totalAmount, contract IERC20Upgradeable _token, contract ITokenLockFactory _tokenLockFactory) external nonpayable returns (address result) ``` @@ -24,6 +24,7 @@ function createHATAirdrop(string _merkleTreeIPFSRef, bytes32 _root, uint256 _sta | Name | Type | Description | |---|---|---| +| _implementation | address | undefined | | _merkleTreeIPFSRef | string | undefined | | _root | bytes32 | undefined | | _startTime | uint256 | undefined | @@ -40,23 +41,6 @@ function createHATAirdrop(string _merkleTreeIPFSRef, bytes32 _root, uint256 _sta |---|---|---| | result | address | undefined | -### implementation - -```solidity -function implementation() external view returns (address) -``` - - - - - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined | - ### owner ```solidity @@ -77,7 +61,7 @@ function owner() external view returns (address) ### predictHATAirdropAddress ```solidity -function predictHATAirdropAddress(string _merkleTreeIPFSRef, bytes32 _root, uint256 _startTime, uint256 _deadline, uint256 _lockEndTime, uint256 _periods, contract IERC20 _token, contract ITokenLockFactory _tokenLockFactory) external view returns (address) +function predictHATAirdropAddress(address _implementation, string _merkleTreeIPFSRef, bytes32 _root, uint256 _startTime, uint256 _deadline, uint256 _lockEndTime, uint256 _periods, contract IERC20 _token, contract ITokenLockFactory _tokenLockFactory) external view returns (address) ``` @@ -88,6 +72,7 @@ function predictHATAirdropAddress(string _merkleTreeIPFSRef, bytes32 _root, uint | Name | Type | Description | |---|---|---| +| _implementation | address | undefined | | _merkleTreeIPFSRef | string | undefined | | _root | bytes32 | undefined | | _startTime | uint256 | undefined | @@ -130,22 +115,6 @@ function transferOwnership(address newOwner) external nonpayable |---|---|---| | newOwner | address | undefined | -### updateImplementation - -```solidity -function updateImplementation(address _newImplementation) external nonpayable -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _newImplementation | address | undefined | - ### withdrawTokens ```solidity @@ -192,22 +161,6 @@ event HATAirdropCreated(address indexed _hatAirdrop, string _merkleTreeIPFSRef, | _token | contract IERC20Upgradeable | undefined | | _tokenLockFactory | contract ITokenLockFactory | undefined | -### ImplementationUpdated - -```solidity -event ImplementationUpdated(address indexed _newImplementation) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _newImplementation `indexed` | address | undefined | - ### OwnershipTransferred ```solidity diff --git a/test/tge/hatairdrop.js b/test/tge/hatairdrop.js index eba6b3e4..f22e7d0e 100644 --- a/test/tge/hatairdrop.js +++ b/test/tge/hatairdrop.js @@ -60,9 +60,10 @@ contract("HATAirdrop", (accounts) => { periods = 90; hatAirdropImplementation = await HATAirdrop.new(); - hatAirdropFactory = await HATAirdropFactory.new(hatAirdropImplementation.address); + hatAirdropFactory = await HATAirdropFactory.new(); let airdropAddress = await hatAirdropFactory.predictHATAirdropAddress( + hatAirdropImplementation.address, "QmSUXfYsk9HgrMBa7tgp3MBm8FGwDF9hnVaR9C1PMoFdS3", merkleTree.getHexRoot(), startTime, @@ -74,6 +75,7 @@ contract("HATAirdrop", (accounts) => { ); let tx = await hatAirdropFactory.createHATAirdrop( + hatAirdropImplementation.address, "QmSUXfYsk9HgrMBa7tgp3MBm8FGwDF9hnVaR9C1PMoFdS3", merkleTree.getHexRoot(), startTime, @@ -101,31 +103,12 @@ contract("HATAirdrop", (accounts) => { await token.mint(hatAirdropFactory.address, totalAmount); } - it("Update implementation", async () => { - await setupHATAirdrop(); - - await assertFunctionRaisesException( - hatAirdropFactory.updateImplementation( - accounts[3], - { from: accounts[1] } - ), - "Ownable: caller is not the owner" - ); - - assert.equal(await hatAirdropFactory.implementation(), hatAirdropImplementation.address); - - let tx = await hatAirdropFactory.updateImplementation(accounts[3]); - assert.equal(tx.logs[0].event, "ImplementationUpdated"); - assert.equal(tx.logs[0].args._newImplementation, accounts[3]); - - assert.equal(await hatAirdropFactory.implementation(), accounts[3]); - }); - it("Only owner can create airdrops", async () => { await setupHATAirdrop(); await assertFunctionRaisesException( hatAirdropFactory.createHATAirdrop( + hatAirdropImplementation.address, "QmSUXfYsk9HgrMBa7tgp3MBm8FGwDF9hnVaR9C1PMoFdS3", merkleTree.getHexRoot(), startTime, From d7d97b037778af97ff8ea13c41e6ea2f00443577 Mon Sep 17 00:00:00 2001 From: benk10 Date: Wed, 10 Jan 2024 19:24:52 -0300 Subject: [PATCH 10/18] Disallow redeeming for others --- contracts/tge/HATAirdrop.sol | 11 ++++++----- docs/dodoc/tge/HATAirdrop.md | 3 +-- test/tge/airdropData.json | 12 ++++++------ test/tge/hatairdrop.js | 35 +++++++++++++++++++++-------------- 4 files changed, 34 insertions(+), 27 deletions(-) diff --git a/contracts/tge/HATAirdrop.sol b/contracts/tge/HATAirdrop.sol index 7ff1b087..c6c7bc75 100644 --- a/contracts/tge/HATAirdrop.sol +++ b/contracts/tge/HATAirdrop.sol @@ -58,12 +58,13 @@ contract HATAirdrop is Initializable { emit MerkleTreeSet(_merkleTreeIPFSRef, _root, _startTime, _deadline); } - function redeem(address _account, uint256 _amount, bytes32[] calldata _proof) external { + function redeem(uint256 _amount, bytes32[] calldata _proof) external { + address account = msg.sender; // 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); + bytes32 leaf = _leaf(account, _amount); if (leafRedeemed[leaf]) revert LeafAlreadyRedeemed(); if(!_verify(_proof, leaf)) revert InvalidMerkleProof(); leafRedeemed[leaf] = true; @@ -74,7 +75,7 @@ contract HATAirdrop is Initializable { _tokenLock = tokenLockFactory.createTokenLock( address(token), 0x0000000000000000000000000000000000000000, - _account, + account, _amount, startTime, lockEndTime, @@ -86,10 +87,10 @@ contract HATAirdrop is Initializable { ); token.safeTransferFrom(factory, _tokenLock, _amount); } else { - token.safeTransferFrom(factory, _account, _amount); + token.safeTransferFrom(factory, account, _amount); } - emit TokensRedeemed(_account, _tokenLock, _amount); + emit TokensRedeemed(account, _tokenLock, _amount); } function _verify(bytes32[] calldata proof, bytes32 leaf) internal view returns (bool) { diff --git a/docs/dodoc/tge/HATAirdrop.md b/docs/dodoc/tge/HATAirdrop.md index 58a66cb4..d4ee8d25 100644 --- a/docs/dodoc/tge/HATAirdrop.md +++ b/docs/dodoc/tge/HATAirdrop.md @@ -126,7 +126,7 @@ function periods() external view returns (uint256) ### redeem ```solidity -function redeem(address _account, uint256 _amount, bytes32[] _proof) external nonpayable +function redeem(uint256 _amount, bytes32[] _proof) external nonpayable ``` @@ -137,7 +137,6 @@ function redeem(address _account, uint256 _amount, bytes32[] _proof) external no | Name | Type | Description | |---|---|---| -| _account | address | undefined | | _amount | uint256 | undefined | | _proof | bytes32[] | undefined | diff --git a/test/tge/airdropData.json b/test/tge/airdropData.json index 97c6d61a..d29ca428 100644 --- a/test/tge/airdropData.json +++ b/test/tge/airdropData.json @@ -1,8 +1,8 @@ { - "0xbAAEa72417f4dC3E0f52a1783B0913d0f3516634": "10", - "0x9214574DC772B6FD34da960054d0baF29b0c9f9e": "25", - "0x078ad5270b0240d8529f600f35a271fc6e2b2bd8": "25", - "0x60a47eD557d4cD1472FDbffcE8b85956F0555A7A": "15", - "0xe629e116ff404e9074ef1992ef2b147247a49e81": "30", - "0x8F402318BB49776f5017d2FB12c90D0B0acAAaE8": "20" + "0xc783df8a850f42e7F7e57013759C285caa701eB6": "10", + "0xeAD9C93b79Ae7C1591b1FB5323BD777E86e150d4": "25", + "0xE5904695748fe4A84b40b3fc79De2277660BD1D3": "25", + "0x92561F28Ec438Ee9831D00D1D59fbDC981b762b2": "15", + "0x2fFd013AaA7B5a7DA93336C2251075202b33FB2B": "30", + "0x9FC9C2DfBA3b6cF204C37a5F690619772b926e39": "20" } diff --git a/test/tge/hatairdrop.js b/test/tge/hatairdrop.js index f22e7d0e..f20162b6 100644 --- a/test/tge/hatairdrop.js +++ b/test/tge/hatairdrop.js @@ -149,10 +149,12 @@ contract("HATAirdrop", (accounts) => { await utils.increaseTime(7 * 24 * 3600); + let i = 0; for (const [account, amount] of Object.entries(airdropData)) { let currentBalance = await token.balanceOf(hatAirdropFactory.address); const proof = merkleTree.getHexProof(hashTokens(account, amount)); - let tx = await hatAirdrop.redeem(account, amount, proof); + let tx = await hatAirdrop.redeem(amount, proof, { from: accounts[i] }); + i++; assert.equal(tx.logs[0].event, "TokensRedeemed"); assert.equal(tx.logs[0].args._account.toLowerCase(), account.toLowerCase()); assert.equal(tx.logs[0].args._amount, amount); @@ -181,10 +183,12 @@ contract("HATAirdrop", (accounts) => { await utils.increaseTime(7 * 24 * 3600); + let i = 0; for (const [account, amount] of Object.entries(airdropData)) { let currentBalance = await token.balanceOf(hatAirdropFactory.address); const proof = merkleTree.getHexProof(hashTokens(account, amount)); - let tx = await hatAirdrop.redeem(account, amount, proof); + let tx = await hatAirdrop.redeem(amount, proof, { from: accounts[i] }); + i++; assert.equal(tx.logs[0].event, "TokensRedeemed"); assert.equal(tx.logs[0].args._account.toLowerCase(), account.toLowerCase()); assert.equal(tx.logs[0].args._tokenLock, ZERO_ADDRESS); @@ -202,10 +206,12 @@ contract("HATAirdrop", (accounts) => { await utils.increaseTime(7 * 24 * 3600); const dataLength = Object.entries(airdropData).length; + let i = 0; for (const [account, amount] of Object.entries(airdropData).slice(0, dataLength / 2)) { let currentBalance = await token.balanceOf(hatAirdropFactory.address); const proof = merkleTree.getHexProof(hashTokens(account, amount)); - let tx = await hatAirdrop.redeem(account, amount, proof); + let tx = await hatAirdrop.redeem(amount, proof, { from: accounts[i] }); + i++; assert.equal(tx.logs[0].event, "TokensRedeemed"); assert.equal(tx.logs[0].args._account.toLowerCase(), account.toLowerCase()); assert.equal(tx.logs[0].args._amount, amount); @@ -231,7 +237,8 @@ contract("HATAirdrop", (accounts) => { for (const [account, amount] of Object.entries(airdropData).slice(dataLength / 2)) { let currentBalance = await token.balanceOf(hatAirdropFactory.address); const proof = merkleTree.getHexProof(hashTokens(account, amount)); - let tx = await hatAirdrop.redeem(account, amount, proof); + let tx = await hatAirdrop.redeem(amount, proof, { from: accounts[i] }); + i++; assert.equal(tx.logs[0].event, "TokensRedeemed"); assert.equal(tx.logs[0].args._account.toLowerCase(), account.toLowerCase()); assert.equal(tx.logs[0].args._tokenLock, ZERO_ADDRESS); @@ -249,13 +256,13 @@ contract("HATAirdrop", (accounts) => { const [account, amount] = Object.entries(airdropData)[0]; const proof = merkleTree.getHexProof(hashTokens(account, amount)); await assertFunctionRaisesException( - hatAirdrop.redeem(account, amount, proof), + hatAirdrop.redeem(amount, proof), "CannotRedeemBeforeStartTime" ); await utils.increaseTime(7 * 24 * 3600); - let tx = await hatAirdrop.redeem(account, amount, proof); + let tx = await hatAirdrop.redeem(amount, proof); assert.equal(tx.logs[0].event, "TokensRedeemed"); assert.equal(tx.logs[0].args._account.toLowerCase(), account.toLowerCase()); assert.equal(tx.logs[0].args._amount, amount); @@ -270,7 +277,7 @@ contract("HATAirdrop", (accounts) => { await utils.increaseTime(7 * 24 * 3600); - let tx = await hatAirdrop.redeem(account, amount, proof); + let tx = await hatAirdrop.redeem(amount, proof); assert.equal(tx.logs[0].event, "TokensRedeemed"); assert.equal(tx.logs[0].args._account.toLowerCase(), account.toLowerCase()); assert.equal(tx.logs[0].args._amount, amount); @@ -282,7 +289,7 @@ contract("HATAirdrop", (accounts) => { const proof2 = merkleTree.getHexProof(hashTokens(account2, amount2)); await assertFunctionRaisesException( - hatAirdrop.redeem(account2, amount2, proof2), + hatAirdrop.redeem(amount2, proof2, { from: accounts[1] }), "CannotRedeemAfterDeadline" ); }); @@ -295,14 +302,14 @@ contract("HATAirdrop", (accounts) => { await utils.increaseTime(7 * 24 * 3600); - let tx = await hatAirdrop.redeem(account, amount, proof); + let tx = await hatAirdrop.redeem(amount, proof); assert.equal(tx.logs[0].event, "TokensRedeemed"); assert.equal(tx.logs[0].args._account.toLowerCase(), account.toLowerCase()); assert.equal(tx.logs[0].args._amount, amount); assert.equal(await token.balanceOf(tx.logs[0].args._tokenLock), amount); await assertFunctionRaisesException( - hatAirdrop.redeem(account, amount, proof), + hatAirdrop.redeem(amount, proof), "LeafAlreadyRedeemed" ); }); @@ -315,18 +322,18 @@ contract("HATAirdrop", (accounts) => { await utils.increaseTime(7 * 24 * 3600); await assertFunctionRaisesException( - hatAirdrop.redeem(account, amount, [web3.utils.randomHex(32)]), + hatAirdrop.redeem(amount, [web3.utils.randomHex(32)]), "InvalidMerkleProof" ); const proof = merkleTree.getHexProof(hashTokens(account, amount)); await assertFunctionRaisesException( - hatAirdrop.redeem(account, "0", proof), + hatAirdrop.redeem("0", proof), "InvalidMerkleProof" ); - let tx = await hatAirdrop.redeem(account, amount, proof); + let tx = await hatAirdrop.redeem(amount, proof); assert.equal(tx.logs[0].event, "TokensRedeemed"); assert.equal(tx.logs[0].args._account.toLowerCase(), account.toLowerCase()); assert.equal(tx.logs[0].args._amount, amount); @@ -346,7 +353,7 @@ contract("HATAirdrop", (accounts) => { const [account, amount] = Object.entries(airdropData)[0]; const proof = merkleTree.getHexProof(hashTokens(account, amount)); - let tx = await hatAirdrop.redeem(account, amount, proof); + let tx = await hatAirdrop.redeem(amount, proof); assert.equal(tx.logs[0].event, "TokensRedeemed"); assert.equal(tx.logs[0].args._account.toLowerCase(), account.toLowerCase()); assert.equal(tx.logs[0].args._amount, amount); From c957604e0614aaf17d211da804f6fa4a4101b994 Mon Sep 17 00:00:00 2001 From: benk10 Date: Wed, 10 Jan 2024 19:25:28 -0300 Subject: [PATCH 11/18] Gas optimization --- contracts/tge/HATAirdropFactory.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/tge/HATAirdropFactory.sol b/contracts/tge/HATAirdropFactory.sol index 0f317617..fec825fb 100644 --- a/contracts/tge/HATAirdropFactory.sol +++ b/contracts/tge/HATAirdropFactory.sol @@ -25,7 +25,7 @@ contract HATAirdropFactory is Ownable { ); function withdrawTokens(IERC20Upgradeable _token, uint256 _amount) external onlyOwner { - address owner = owner(); + address owner = msg.sender; _token.safeTransfer(owner, _amount); emit TokensWithdrawn(owner, _amount); } From b7efd2f7f4c4eb471dece3d2a69b74343746f778 Mon Sep 17 00:00:00 2001 From: benk10 Date: Thu, 11 Jan 2024 16:11:03 -0300 Subject: [PATCH 12/18] Revert "Disallow redeeming for others" This reverts commit d7d97b037778af97ff8ea13c41e6ea2f00443577. --- contracts/tge/HATAirdrop.sol | 11 +++++------ docs/dodoc/tge/HATAirdrop.md | 3 ++- test/tge/airdropData.json | 12 ++++++------ test/tge/hatairdrop.js | 35 ++++++++++++++--------------------- 4 files changed, 27 insertions(+), 34 deletions(-) diff --git a/contracts/tge/HATAirdrop.sol b/contracts/tge/HATAirdrop.sol index c6c7bc75..7ff1b087 100644 --- a/contracts/tge/HATAirdrop.sol +++ b/contracts/tge/HATAirdrop.sol @@ -58,13 +58,12 @@ contract HATAirdrop is Initializable { emit MerkleTreeSet(_merkleTreeIPFSRef, _root, _startTime, _deadline); } - function redeem(uint256 _amount, bytes32[] calldata _proof) external { - address account = msg.sender; + function redeem(address _account, uint256 _amount, bytes32[] calldata _proof) external { // 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); + bytes32 leaf = _leaf(_account, _amount); if (leafRedeemed[leaf]) revert LeafAlreadyRedeemed(); if(!_verify(_proof, leaf)) revert InvalidMerkleProof(); leafRedeemed[leaf] = true; @@ -75,7 +74,7 @@ contract HATAirdrop is Initializable { _tokenLock = tokenLockFactory.createTokenLock( address(token), 0x0000000000000000000000000000000000000000, - account, + _account, _amount, startTime, lockEndTime, @@ -87,10 +86,10 @@ contract HATAirdrop is Initializable { ); token.safeTransferFrom(factory, _tokenLock, _amount); } else { - token.safeTransferFrom(factory, account, _amount); + token.safeTransferFrom(factory, _account, _amount); } - emit TokensRedeemed(account, _tokenLock, _amount); + emit TokensRedeemed(_account, _tokenLock, _amount); } function _verify(bytes32[] calldata proof, bytes32 leaf) internal view returns (bool) { diff --git a/docs/dodoc/tge/HATAirdrop.md b/docs/dodoc/tge/HATAirdrop.md index d4ee8d25..58a66cb4 100644 --- a/docs/dodoc/tge/HATAirdrop.md +++ b/docs/dodoc/tge/HATAirdrop.md @@ -126,7 +126,7 @@ function periods() external view returns (uint256) ### redeem ```solidity -function redeem(uint256 _amount, bytes32[] _proof) external nonpayable +function redeem(address _account, uint256 _amount, bytes32[] _proof) external nonpayable ``` @@ -137,6 +137,7 @@ function redeem(uint256 _amount, bytes32[] _proof) external nonpayable | Name | Type | Description | |---|---|---| +| _account | address | undefined | | _amount | uint256 | undefined | | _proof | bytes32[] | undefined | diff --git a/test/tge/airdropData.json b/test/tge/airdropData.json index d29ca428..97c6d61a 100644 --- a/test/tge/airdropData.json +++ b/test/tge/airdropData.json @@ -1,8 +1,8 @@ { - "0xc783df8a850f42e7F7e57013759C285caa701eB6": "10", - "0xeAD9C93b79Ae7C1591b1FB5323BD777E86e150d4": "25", - "0xE5904695748fe4A84b40b3fc79De2277660BD1D3": "25", - "0x92561F28Ec438Ee9831D00D1D59fbDC981b762b2": "15", - "0x2fFd013AaA7B5a7DA93336C2251075202b33FB2B": "30", - "0x9FC9C2DfBA3b6cF204C37a5F690619772b926e39": "20" + "0xbAAEa72417f4dC3E0f52a1783B0913d0f3516634": "10", + "0x9214574DC772B6FD34da960054d0baF29b0c9f9e": "25", + "0x078ad5270b0240d8529f600f35a271fc6e2b2bd8": "25", + "0x60a47eD557d4cD1472FDbffcE8b85956F0555A7A": "15", + "0xe629e116ff404e9074ef1992ef2b147247a49e81": "30", + "0x8F402318BB49776f5017d2FB12c90D0B0acAAaE8": "20" } diff --git a/test/tge/hatairdrop.js b/test/tge/hatairdrop.js index f20162b6..f22e7d0e 100644 --- a/test/tge/hatairdrop.js +++ b/test/tge/hatairdrop.js @@ -149,12 +149,10 @@ contract("HATAirdrop", (accounts) => { await utils.increaseTime(7 * 24 * 3600); - let i = 0; for (const [account, amount] of Object.entries(airdropData)) { let currentBalance = await token.balanceOf(hatAirdropFactory.address); const proof = merkleTree.getHexProof(hashTokens(account, amount)); - let tx = await hatAirdrop.redeem(amount, proof, { from: accounts[i] }); - i++; + let tx = await hatAirdrop.redeem(account, amount, proof); assert.equal(tx.logs[0].event, "TokensRedeemed"); assert.equal(tx.logs[0].args._account.toLowerCase(), account.toLowerCase()); assert.equal(tx.logs[0].args._amount, amount); @@ -183,12 +181,10 @@ contract("HATAirdrop", (accounts) => { await utils.increaseTime(7 * 24 * 3600); - let i = 0; for (const [account, amount] of Object.entries(airdropData)) { let currentBalance = await token.balanceOf(hatAirdropFactory.address); const proof = merkleTree.getHexProof(hashTokens(account, amount)); - let tx = await hatAirdrop.redeem(amount, proof, { from: accounts[i] }); - i++; + let tx = await hatAirdrop.redeem(account, amount, proof); assert.equal(tx.logs[0].event, "TokensRedeemed"); assert.equal(tx.logs[0].args._account.toLowerCase(), account.toLowerCase()); assert.equal(tx.logs[0].args._tokenLock, ZERO_ADDRESS); @@ -206,12 +202,10 @@ contract("HATAirdrop", (accounts) => { await utils.increaseTime(7 * 24 * 3600); const dataLength = Object.entries(airdropData).length; - let i = 0; for (const [account, amount] of Object.entries(airdropData).slice(0, dataLength / 2)) { let currentBalance = await token.balanceOf(hatAirdropFactory.address); const proof = merkleTree.getHexProof(hashTokens(account, amount)); - let tx = await hatAirdrop.redeem(amount, proof, { from: accounts[i] }); - i++; + let tx = await hatAirdrop.redeem(account, amount, proof); assert.equal(tx.logs[0].event, "TokensRedeemed"); assert.equal(tx.logs[0].args._account.toLowerCase(), account.toLowerCase()); assert.equal(tx.logs[0].args._amount, amount); @@ -237,8 +231,7 @@ contract("HATAirdrop", (accounts) => { for (const [account, amount] of Object.entries(airdropData).slice(dataLength / 2)) { let currentBalance = await token.balanceOf(hatAirdropFactory.address); const proof = merkleTree.getHexProof(hashTokens(account, amount)); - let tx = await hatAirdrop.redeem(amount, proof, { from: accounts[i] }); - i++; + let tx = await hatAirdrop.redeem(account, amount, proof); assert.equal(tx.logs[0].event, "TokensRedeemed"); assert.equal(tx.logs[0].args._account.toLowerCase(), account.toLowerCase()); assert.equal(tx.logs[0].args._tokenLock, ZERO_ADDRESS); @@ -256,13 +249,13 @@ contract("HATAirdrop", (accounts) => { const [account, amount] = Object.entries(airdropData)[0]; const proof = merkleTree.getHexProof(hashTokens(account, amount)); await assertFunctionRaisesException( - hatAirdrop.redeem(amount, proof), + hatAirdrop.redeem(account, amount, proof), "CannotRedeemBeforeStartTime" ); await utils.increaseTime(7 * 24 * 3600); - let tx = await hatAirdrop.redeem(amount, proof); + let tx = await hatAirdrop.redeem(account, amount, proof); assert.equal(tx.logs[0].event, "TokensRedeemed"); assert.equal(tx.logs[0].args._account.toLowerCase(), account.toLowerCase()); assert.equal(tx.logs[0].args._amount, amount); @@ -277,7 +270,7 @@ contract("HATAirdrop", (accounts) => { await utils.increaseTime(7 * 24 * 3600); - let tx = await hatAirdrop.redeem(amount, proof); + let tx = await hatAirdrop.redeem(account, amount, proof); assert.equal(tx.logs[0].event, "TokensRedeemed"); assert.equal(tx.logs[0].args._account.toLowerCase(), account.toLowerCase()); assert.equal(tx.logs[0].args._amount, amount); @@ -289,7 +282,7 @@ contract("HATAirdrop", (accounts) => { const proof2 = merkleTree.getHexProof(hashTokens(account2, amount2)); await assertFunctionRaisesException( - hatAirdrop.redeem(amount2, proof2, { from: accounts[1] }), + hatAirdrop.redeem(account2, amount2, proof2), "CannotRedeemAfterDeadline" ); }); @@ -302,14 +295,14 @@ contract("HATAirdrop", (accounts) => { await utils.increaseTime(7 * 24 * 3600); - let tx = await hatAirdrop.redeem(amount, proof); + let tx = await hatAirdrop.redeem(account, amount, proof); assert.equal(tx.logs[0].event, "TokensRedeemed"); assert.equal(tx.logs[0].args._account.toLowerCase(), account.toLowerCase()); assert.equal(tx.logs[0].args._amount, amount); assert.equal(await token.balanceOf(tx.logs[0].args._tokenLock), amount); await assertFunctionRaisesException( - hatAirdrop.redeem(amount, proof), + hatAirdrop.redeem(account, amount, proof), "LeafAlreadyRedeemed" ); }); @@ -322,18 +315,18 @@ contract("HATAirdrop", (accounts) => { await utils.increaseTime(7 * 24 * 3600); await assertFunctionRaisesException( - hatAirdrop.redeem(amount, [web3.utils.randomHex(32)]), + hatAirdrop.redeem(account, amount, [web3.utils.randomHex(32)]), "InvalidMerkleProof" ); const proof = merkleTree.getHexProof(hashTokens(account, amount)); await assertFunctionRaisesException( - hatAirdrop.redeem("0", proof), + hatAirdrop.redeem(account, "0", proof), "InvalidMerkleProof" ); - let tx = await hatAirdrop.redeem(amount, proof); + let tx = await hatAirdrop.redeem(account, amount, proof); assert.equal(tx.logs[0].event, "TokensRedeemed"); assert.equal(tx.logs[0].args._account.toLowerCase(), account.toLowerCase()); assert.equal(tx.logs[0].args._amount, amount); @@ -353,7 +346,7 @@ contract("HATAirdrop", (accounts) => { const [account, amount] = Object.entries(airdropData)[0]; const proof = merkleTree.getHexProof(hashTokens(account, amount)); - let tx = await hatAirdrop.redeem(amount, proof); + let tx = await hatAirdrop.redeem(account, amount, proof); assert.equal(tx.logs[0].event, "TokensRedeemed"); assert.equal(tx.logs[0].args._account.toLowerCase(), account.toLowerCase()); assert.equal(tx.logs[0].args._amount, amount); From 99dbca8201f83992f7912b4462ceba011eac5584 Mon Sep 17 00:00:00 2001 From: benk10 Date: Thu, 11 Jan 2024 17:17:39 -0300 Subject: [PATCH 13/18] Redeem from factory --- contracts/tge/HATAirdrop.sol | 4 + contracts/tge/HATAirdropFactory.sol | 27 ++++++ docs/dodoc/tge/HATAirdrop.md | 11 +++ docs/dodoc/tge/HATAirdropFactory.md | 65 +++++++++++++++ test/tge/airdropData.json | 12 +-- test/tge/hatairdrop.js | 125 ++++++++++++++++++++++++---- 6 files changed, 223 insertions(+), 21 deletions(-) diff --git a/contracts/tge/HATAirdrop.sol b/contracts/tge/HATAirdrop.sol index 7ff1b087..2f754bbe 100644 --- a/contracts/tge/HATAirdrop.sol +++ b/contracts/tge/HATAirdrop.sol @@ -16,6 +16,7 @@ contract HATAirdrop is Initializable { error LeafAlreadyRedeemed(); error InvalidMerkleProof(); error CannotRecoverBeforeDeadline(); + error RedeemerMustBeBeneficiary(); using SafeERC20Upgradeable for IERC20Upgradeable; @@ -59,6 +60,9 @@ contract HATAirdrop is Initializable { } 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 diff --git a/contracts/tge/HATAirdropFactory.sol b/contracts/tge/HATAirdropFactory.sol index fec825fb..16d86568 100644 --- a/contracts/tge/HATAirdropFactory.sol +++ b/contracts/tge/HATAirdropFactory.sol @@ -8,8 +8,13 @@ import "@openzeppelin/contracts/proxy/Clones.sol"; import "./HATAirdrop.sol"; contract HATAirdropFactory is Ownable { + error RedeemDataArraysLengthMismatch(); + error ContractIsNotHATAirdrop(); + using SafeERC20Upgradeable for IERC20Upgradeable; + mapping(address => bool) public isAirdrop; + event TokensWithdrawn(address indexed _owner, uint256 _amount); event HATAirdropCreated( address indexed _hatAirdrop, @@ -30,6 +35,25 @@ contract HATAirdropFactory is Ownable { emit TokensWithdrawn(owner, _amount); } + function redeemMultipleAirdrops(HATAirdrop[] 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(); + } + + HATAirdrop(_airdrops[i]).redeem(caller, _amounts[i], _proofs[i]); + + unchecked { + ++i; + } + } + } + function createHATAirdrop( address _implementation, string memory _merkleTreeIPFSRef, @@ -53,6 +77,7 @@ contract HATAirdropFactory is Ownable { _tokenLockFactory ))); + // TODO: Change this to generic bytes instead of harcoded function signature HATAirdrop(payable(result)).initialize( _merkleTreeIPFSRef, _root, @@ -64,6 +89,8 @@ contract HATAirdropFactory is Ownable { _tokenLockFactory ); + isAirdrop[result] = true; + _token.safeApprove(result, _totalAmount); emit HATAirdropCreated( diff --git a/docs/dodoc/tge/HATAirdrop.md b/docs/dodoc/tge/HATAirdrop.md index 58a66cb4..4c5acde3 100644 --- a/docs/dodoc/tge/HATAirdrop.md +++ b/docs/dodoc/tge/HATAirdrop.md @@ -325,4 +325,15 @@ error LeafAlreadyRedeemed() +### RedeemerMustBeBeneficiary + +```solidity +error RedeemerMustBeBeneficiary() +``` + + + + + + diff --git a/docs/dodoc/tge/HATAirdropFactory.md b/docs/dodoc/tge/HATAirdropFactory.md index 72f2a72b..c718b0fe 100644 --- a/docs/dodoc/tge/HATAirdropFactory.md +++ b/docs/dodoc/tge/HATAirdropFactory.md @@ -41,6 +41,28 @@ function createHATAirdrop(address _implementation, string _merkleTreeIPFSRef, by |---|---|---| | result | address | undefined | +### isAirdrop + +```solidity +function isAirdrop(address) external view returns (bool) +``` + + + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| _0 | address | undefined | + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | bool | undefined | + ### owner ```solidity @@ -88,6 +110,24 @@ function predictHATAirdropAddress(address _implementation, string _merkleTreeIPF |---|---|---| | _0 | address | undefined | +### redeemMultipleAirdrops + +```solidity +function redeemMultipleAirdrops(contract HATAirdrop[] _airdrops, uint256[] _amounts, bytes32[][] _proofs) external nonpayable +``` + + + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| _airdrops | contract HATAirdrop[] | undefined | +| _amounts | uint256[] | undefined | +| _proofs | bytes32[][] | undefined | + ### renounceOwnership ```solidity @@ -197,3 +237,28 @@ event TokensWithdrawn(address indexed _owner, uint256 _amount) +## Errors + +### ContractIsNotHATAirdrop + +```solidity +error ContractIsNotHATAirdrop() +``` + + + + + + +### RedeemDataArraysLengthMismatch + +```solidity +error RedeemDataArraysLengthMismatch() +``` + + + + + + + diff --git a/test/tge/airdropData.json b/test/tge/airdropData.json index 97c6d61a..d29ca428 100644 --- a/test/tge/airdropData.json +++ b/test/tge/airdropData.json @@ -1,8 +1,8 @@ { - "0xbAAEa72417f4dC3E0f52a1783B0913d0f3516634": "10", - "0x9214574DC772B6FD34da960054d0baF29b0c9f9e": "25", - "0x078ad5270b0240d8529f600f35a271fc6e2b2bd8": "25", - "0x60a47eD557d4cD1472FDbffcE8b85956F0555A7A": "15", - "0xe629e116ff404e9074ef1992ef2b147247a49e81": "30", - "0x8F402318BB49776f5017d2FB12c90D0B0acAAaE8": "20" + "0xc783df8a850f42e7F7e57013759C285caa701eB6": "10", + "0xeAD9C93b79Ae7C1591b1FB5323BD777E86e150d4": "25", + "0xE5904695748fe4A84b40b3fc79De2277660BD1D3": "25", + "0x92561F28Ec438Ee9831D00D1D59fbDC981b762b2": "15", + "0x2fFd013AaA7B5a7DA93336C2251075202b33FB2B": "30", + "0x9FC9C2DfBA3b6cF204C37a5F690619772b926e39": "20" } diff --git a/test/tge/hatairdrop.js b/test/tge/hatairdrop.js index f22e7d0e..50c46211 100644 --- a/test/tge/hatairdrop.js +++ b/test/tge/hatairdrop.js @@ -149,10 +149,12 @@ contract("HATAirdrop", (accounts) => { await utils.increaseTime(7 * 24 * 3600); + let i = 0; for (const [account, amount] of Object.entries(airdropData)) { let currentBalance = await token.balanceOf(hatAirdropFactory.address); const proof = merkleTree.getHexProof(hashTokens(account, amount)); - let tx = await hatAirdrop.redeem(account, amount, proof); + let tx = await hatAirdrop.redeem(accounts[i], amount, proof, { from: accounts[i] }); + i++; assert.equal(tx.logs[0].event, "TokensRedeemed"); assert.equal(tx.logs[0].args._account.toLowerCase(), account.toLowerCase()); assert.equal(tx.logs[0].args._amount, amount); @@ -176,15 +178,84 @@ contract("HATAirdrop", (accounts) => { assert.equal((await token.balanceOf(hatAirdropFactory.address)).toString(), "0"); }); + it("Redeem all from multiple airdrops", async () => { + await setupHATAirdrop(); + + let tx = await hatAirdropFactory.createHATAirdrop( + hatAirdropImplementation.address, + "QmSUXfYsk9HgrMBa7tgp3MBm8FGwDF9hnVaR9C1PMoFdS3", + merkleTree.getHexRoot(), + startTime, + endTime, + lockEndTime + 1, + periods, + totalAmount, + token.address, + tokenLockFactory.address + ); + + const hatAirdrop2 = await HATAirdrop.at(tx.logs[0].args._hatAirdrop); + + await token.mint(hatAirdropFactory.address, totalAmount); + + await utils.increaseTime(7 * 24 * 3600); + + let i = 0; + for (const [account, amount] of Object.entries(airdropData)) { + let currentBalance = await token.balanceOf(hatAirdropFactory.address); + const proof = merkleTree.getHexProof(hashTokens(account, amount)); + await hatAirdropFactory.redeemMultipleAirdrops([hatAirdrop.address, hatAirdrop2.address], [amount, amount], [proof, proof], { from: accounts[i] }); + i++; + assert.equal((await token.balanceOf(hatAirdropFactory.address)).toString(), currentBalance.sub(web3.utils.toBN(amount * 2)).toString()); + } + + assert.equal((await token.balanceOf(hatAirdropFactory.address)).toString(), "0"); + }); + + it("Cannot redeem with factory if contract is not deployed by the factory", async () => { + await setupHATAirdrop(); + + await utils.increaseTime(7 * 24 * 3600); + + const [account, amount] = Object.entries(airdropData)[0]; + const proof = merkleTree.getHexProof(hashTokens(account, amount)); + + await assertFunctionRaisesException( + hatAirdropFactory.redeemMultipleAirdrops([hatAirdrop.address, accounts[0]], [amount, amount], [proof, proof], { from: accounts[0] }), + "ContractIsNotHATAirdrop" + ); + }); + + it("Cannot redeem with factory if arrays lenngths mismatch", async () => { + await setupHATAirdrop(); + + await utils.increaseTime(7 * 24 * 3600); + + const [account, amount] = Object.entries(airdropData)[0]; + const proof = merkleTree.getHexProof(hashTokens(account, amount)); + + await assertFunctionRaisesException( + hatAirdropFactory.redeemMultipleAirdrops([hatAirdrop.address], [amount, amount], [proof, proof], { from: accounts[0] }), + "RedeemDataArraysLengthMismatch" + ); + + await assertFunctionRaisesException( + hatAirdropFactory.redeemMultipleAirdrops([hatAirdrop.address], [amount], [proof, proof], { from: accounts[0] }), + "RedeemDataArraysLengthMismatch" + ); + }); + it("Redeem all no lock", async () => { await setupHATAirdrop(false); await utils.increaseTime(7 * 24 * 3600); + let i = 0; for (const [account, amount] of Object.entries(airdropData)) { let currentBalance = await token.balanceOf(hatAirdropFactory.address); const proof = merkleTree.getHexProof(hashTokens(account, amount)); - let tx = await hatAirdrop.redeem(account, amount, proof); + let tx = await hatAirdrop.redeem(accounts[i], amount, proof, { from: accounts[i] }); + i++; assert.equal(tx.logs[0].event, "TokensRedeemed"); assert.equal(tx.logs[0].args._account.toLowerCase(), account.toLowerCase()); assert.equal(tx.logs[0].args._tokenLock, ZERO_ADDRESS); @@ -202,10 +273,12 @@ contract("HATAirdrop", (accounts) => { await utils.increaseTime(7 * 24 * 3600); const dataLength = Object.entries(airdropData).length; + let i = 0; for (const [account, amount] of Object.entries(airdropData).slice(0, dataLength / 2)) { let currentBalance = await token.balanceOf(hatAirdropFactory.address); const proof = merkleTree.getHexProof(hashTokens(account, amount)); - let tx = await hatAirdrop.redeem(account, amount, proof); + let tx = await hatAirdrop.redeem(accounts[i], amount, proof, { from: accounts[i] }); + i++; assert.equal(tx.logs[0].event, "TokensRedeemed"); assert.equal(tx.logs[0].args._account.toLowerCase(), account.toLowerCase()); assert.equal(tx.logs[0].args._amount, amount); @@ -231,7 +304,8 @@ contract("HATAirdrop", (accounts) => { for (const [account, amount] of Object.entries(airdropData).slice(dataLength / 2)) { let currentBalance = await token.balanceOf(hatAirdropFactory.address); const proof = merkleTree.getHexProof(hashTokens(account, amount)); - let tx = await hatAirdrop.redeem(account, amount, proof); + let tx = await hatAirdrop.redeem(accounts[i], amount, proof, { from: accounts[i] }); + i++; assert.equal(tx.logs[0].event, "TokensRedeemed"); assert.equal(tx.logs[0].args._account.toLowerCase(), account.toLowerCase()); assert.equal(tx.logs[0].args._tokenLock, ZERO_ADDRESS); @@ -249,13 +323,13 @@ contract("HATAirdrop", (accounts) => { const [account, amount] = Object.entries(airdropData)[0]; const proof = merkleTree.getHexProof(hashTokens(account, amount)); await assertFunctionRaisesException( - hatAirdrop.redeem(account, amount, proof), + hatAirdrop.redeem(accounts[0], amount, proof), "CannotRedeemBeforeStartTime" ); await utils.increaseTime(7 * 24 * 3600); - let tx = await hatAirdrop.redeem(account, amount, proof); + let tx = await hatAirdrop.redeem(accounts[0], amount, proof); assert.equal(tx.logs[0].event, "TokensRedeemed"); assert.equal(tx.logs[0].args._account.toLowerCase(), account.toLowerCase()); assert.equal(tx.logs[0].args._amount, amount); @@ -270,7 +344,7 @@ contract("HATAirdrop", (accounts) => { await utils.increaseTime(7 * 24 * 3600); - let tx = await hatAirdrop.redeem(account, amount, proof); + let tx = await hatAirdrop.redeem(accounts[0], amount, proof); assert.equal(tx.logs[0].event, "TokensRedeemed"); assert.equal(tx.logs[0].args._account.toLowerCase(), account.toLowerCase()); assert.equal(tx.logs[0].args._amount, amount); @@ -282,11 +356,32 @@ contract("HATAirdrop", (accounts) => { const proof2 = merkleTree.getHexProof(hashTokens(account2, amount2)); await assertFunctionRaisesException( - hatAirdrop.redeem(account2, amount2, proof2), + hatAirdrop.redeem(accounts[1], amount2, proof2, { from: accounts[1] }), "CannotRedeemAfterDeadline" ); }); + it("Cannot redeem for another account", async () => { + await setupHATAirdrop(); + + const [account, amount] = Object.entries(airdropData)[0]; + const proof = merkleTree.getHexProof(hashTokens(account, amount)); + + await utils.increaseTime(7 * 24 * 3600); + + await assertFunctionRaisesException( + hatAirdrop.redeem(accounts[0], amount, proof, { from: accounts[1] }), + "RedeemerMustBeBeneficiary" + ); + + let tx = await hatAirdrop.redeem(accounts[0], amount, proof); + assert.equal(tx.logs[0].event, "TokensRedeemed"); + assert.equal(tx.logs[0].args._account.toLowerCase(), account.toLowerCase()); + assert.equal(tx.logs[0].args._amount, amount); + assert.equal(await token.balanceOf(tx.logs[0].args._tokenLock), amount); + + }); + it("Cannot redeem twice", async () => { await setupHATAirdrop(); @@ -295,14 +390,14 @@ contract("HATAirdrop", (accounts) => { await utils.increaseTime(7 * 24 * 3600); - let tx = await hatAirdrop.redeem(account, amount, proof); + let tx = await hatAirdrop.redeem(accounts[0], amount, proof); assert.equal(tx.logs[0].event, "TokensRedeemed"); assert.equal(tx.logs[0].args._account.toLowerCase(), account.toLowerCase()); assert.equal(tx.logs[0].args._amount, amount); assert.equal(await token.balanceOf(tx.logs[0].args._tokenLock), amount); await assertFunctionRaisesException( - hatAirdrop.redeem(account, amount, proof), + hatAirdrop.redeem(accounts[0], amount, proof), "LeafAlreadyRedeemed" ); }); @@ -315,18 +410,18 @@ contract("HATAirdrop", (accounts) => { await utils.increaseTime(7 * 24 * 3600); await assertFunctionRaisesException( - hatAirdrop.redeem(account, amount, [web3.utils.randomHex(32)]), + hatAirdrop.redeem(accounts[0], amount, [web3.utils.randomHex(32)]), "InvalidMerkleProof" ); const proof = merkleTree.getHexProof(hashTokens(account, amount)); await assertFunctionRaisesException( - hatAirdrop.redeem(account, "0", proof), + hatAirdrop.redeem(accounts[0], "0", proof), "InvalidMerkleProof" ); - let tx = await hatAirdrop.redeem(account, amount, proof); + let tx = await hatAirdrop.redeem(accounts[0], amount, proof); assert.equal(tx.logs[0].event, "TokensRedeemed"); assert.equal(tx.logs[0].args._account.toLowerCase(), account.toLowerCase()); assert.equal(tx.logs[0].args._amount, amount); @@ -346,7 +441,7 @@ contract("HATAirdrop", (accounts) => { const [account, amount] = Object.entries(airdropData)[0]; const proof = merkleTree.getHexProof(hashTokens(account, amount)); - let tx = await hatAirdrop.redeem(account, amount, proof); + let tx = await hatAirdrop.redeem(accounts[0], amount, proof); assert.equal(tx.logs[0].event, "TokensRedeemed"); assert.equal(tx.logs[0].args._account.toLowerCase(), account.toLowerCase()); assert.equal(tx.logs[0].args._amount, amount); @@ -361,4 +456,4 @@ contract("HATAirdrop", (accounts) => { assert.equal(tx.logs[0].args._amount.toString(), web3.utils.toBN((totalAmount - parseInt(amount))).toString()); assert.equal((await token.balanceOf(hatAirdropFactory.address)).toString(), "0"); }); -}); +}); \ No newline at end of file From d037f55baef5f08b572e1b0ed26e0c902ae88c3a Mon Sep 17 00:00:00 2001 From: benk10 Date: Thu, 11 Jan 2024 17:20:32 -0300 Subject: [PATCH 14/18] Lint --- test/tge/hatairdrop.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/tge/hatairdrop.js b/test/tge/hatairdrop.js index 50c46211..b68b48cb 100644 --- a/test/tge/hatairdrop.js +++ b/test/tge/hatairdrop.js @@ -456,4 +456,4 @@ contract("HATAirdrop", (accounts) => { assert.equal(tx.logs[0].args._amount.toString(), web3.utils.toBN((totalAmount - parseInt(amount))).toString()); assert.equal((await token.balanceOf(hatAirdropFactory.address)).toString(), "0"); }); -}); \ No newline at end of file +}); From b99c8c4968bf78e98aed0d05c1ecf7fc98402a50 Mon Sep 17 00:00:00 2001 From: benk10 Date: Thu, 11 Jan 2024 17:43:53 -0300 Subject: [PATCH 15/18] Make init data customizable --- contracts/tge/HATAirdropFactory.sol | 87 +++++------------------------ docs/dodoc/tge/HATAirdropFactory.md | 46 +++++++-------- test/tge/hatairdrop.js | 65 +++++++++++---------- 3 files changed, 70 insertions(+), 128 deletions(-) diff --git a/contracts/tge/HATAirdropFactory.sol b/contracts/tge/HATAirdropFactory.sol index 16d86568..1f4c14d3 100644 --- a/contracts/tge/HATAirdropFactory.sol +++ b/contracts/tge/HATAirdropFactory.sol @@ -10,24 +10,14 @@ import "./HATAirdrop.sol"; contract HATAirdropFactory is Ownable { error RedeemDataArraysLengthMismatch(); error ContractIsNotHATAirdrop(); + error HATAirdropInitializationFailed(); using SafeERC20Upgradeable for IERC20Upgradeable; mapping(address => bool) public isAirdrop; event TokensWithdrawn(address indexed _owner, uint256 _amount); - event HATAirdropCreated( - address indexed _hatAirdrop, - string _merkleTreeIPFSRef, - bytes32 _root, - uint256 _startTime, - uint256 _deadline, - uint256 _lockEndTime, - uint256 _periods, - uint256 _totalAmount, - IERC20Upgradeable _token, - ITokenLockFactory _tokenLockFactory - ); + event HATAirdropCreated(address indexed _hatAirdrop, bytes _initData, IERC20Upgradeable _token, uint256 _totalAmount); function withdrawTokens(IERC20Upgradeable _token, uint256 _amount) external onlyOwner { address owner = msg.sender; @@ -56,77 +46,30 @@ contract HATAirdropFactory is Ownable { function createHATAirdrop( address _implementation, - string memory _merkleTreeIPFSRef, - bytes32 _root, - uint256 _startTime, - uint256 _deadline, - uint256 _lockEndTime, - uint256 _periods, - uint256 _totalAmount, + bytes calldata _initData, IERC20Upgradeable _token, - ITokenLockFactory _tokenLockFactory + uint256 _totalAmount ) external onlyOwner returns (address result) { - result = Clones.cloneDeterministic(_implementation, keccak256(abi.encodePacked( - _merkleTreeIPFSRef, - _root, - _startTime, - _deadline, - _lockEndTime, - _periods, - _token, - _tokenLockFactory - ))); - - // TODO: Change this to generic bytes instead of harcoded function signature - HATAirdrop(payable(result)).initialize( - _merkleTreeIPFSRef, - _root, - _startTime, - _deadline, - _lockEndTime, - _periods, - _token, - _tokenLockFactory - ); + 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, - _merkleTreeIPFSRef, - _root, - _startTime, - _deadline, - _lockEndTime, - _periods, - _totalAmount, - _token, - _tokenLockFactory - ); + emit HATAirdropCreated(result, _initData, _token, _totalAmount); } function predictHATAirdropAddress( address _implementation, - string memory _merkleTreeIPFSRef, - bytes32 _root, - uint256 _startTime, - uint256 _deadline, - uint256 _lockEndTime, - uint256 _periods, - IERC20 _token, - ITokenLockFactory _tokenLockFactory + bytes calldata _initData ) external view returns (address) { - return Clones.predictDeterministicAddress(_implementation, keccak256(abi.encodePacked( - _merkleTreeIPFSRef, - _root, - _startTime, - _deadline, - _lockEndTime, - _periods, - _token, - _tokenLockFactory - ))); + return Clones.predictDeterministicAddress(_implementation, keccak256(_initData)); } } diff --git a/docs/dodoc/tge/HATAirdropFactory.md b/docs/dodoc/tge/HATAirdropFactory.md index c718b0fe..b3e286b0 100644 --- a/docs/dodoc/tge/HATAirdropFactory.md +++ b/docs/dodoc/tge/HATAirdropFactory.md @@ -13,7 +13,7 @@ ### createHATAirdrop ```solidity -function createHATAirdrop(address _implementation, string _merkleTreeIPFSRef, bytes32 _root, uint256 _startTime, uint256 _deadline, uint256 _lockEndTime, uint256 _periods, uint256 _totalAmount, contract IERC20Upgradeable _token, contract ITokenLockFactory _tokenLockFactory) external nonpayable returns (address result) +function createHATAirdrop(address _implementation, bytes _initData, contract IERC20Upgradeable _token, uint256 _totalAmount) external nonpayable returns (address result) ``` @@ -25,15 +25,9 @@ function createHATAirdrop(address _implementation, string _merkleTreeIPFSRef, by | Name | Type | Description | |---|---|---| | _implementation | address | undefined | -| _merkleTreeIPFSRef | string | undefined | -| _root | bytes32 | undefined | -| _startTime | uint256 | undefined | -| _deadline | uint256 | undefined | -| _lockEndTime | uint256 | undefined | -| _periods | uint256 | undefined | -| _totalAmount | uint256 | undefined | +| _initData | bytes | undefined | | _token | contract IERC20Upgradeable | undefined | -| _tokenLockFactory | contract ITokenLockFactory | undefined | +| _totalAmount | uint256 | undefined | #### Returns @@ -83,7 +77,7 @@ function owner() external view returns (address) ### predictHATAirdropAddress ```solidity -function predictHATAirdropAddress(address _implementation, string _merkleTreeIPFSRef, bytes32 _root, uint256 _startTime, uint256 _deadline, uint256 _lockEndTime, uint256 _periods, contract IERC20 _token, contract ITokenLockFactory _tokenLockFactory) external view returns (address) +function predictHATAirdropAddress(address _implementation, bytes _initData) external view returns (address) ``` @@ -95,14 +89,7 @@ function predictHATAirdropAddress(address _implementation, string _merkleTreeIPF | Name | Type | Description | |---|---|---| | _implementation | address | undefined | -| _merkleTreeIPFSRef | string | undefined | -| _root | bytes32 | undefined | -| _startTime | uint256 | undefined | -| _deadline | uint256 | undefined | -| _lockEndTime | uint256 | undefined | -| _periods | uint256 | undefined | -| _token | contract IERC20 | undefined | -| _tokenLockFactory | contract ITokenLockFactory | undefined | +| _initData | bytes | undefined | #### Returns @@ -179,7 +166,7 @@ function withdrawTokens(contract IERC20Upgradeable _token, uint256 _amount) exte ### HATAirdropCreated ```solidity -event HATAirdropCreated(address indexed _hatAirdrop, string _merkleTreeIPFSRef, bytes32 _root, uint256 _startTime, uint256 _deadline, uint256 _lockEndTime, uint256 _periods, uint256 _totalAmount, contract IERC20Upgradeable _token, contract ITokenLockFactory _tokenLockFactory) +event HATAirdropCreated(address indexed _hatAirdrop, bytes _initData, contract IERC20Upgradeable _token, uint256 _totalAmount) ``` @@ -191,15 +178,9 @@ event HATAirdropCreated(address indexed _hatAirdrop, string _merkleTreeIPFSRef, | Name | Type | Description | |---|---|---| | _hatAirdrop `indexed` | address | undefined | -| _merkleTreeIPFSRef | string | undefined | -| _root | bytes32 | undefined | -| _startTime | uint256 | undefined | -| _deadline | uint256 | undefined | -| _lockEndTime | uint256 | undefined | -| _periods | uint256 | undefined | -| _totalAmount | uint256 | undefined | +| _initData | bytes | undefined | | _token | contract IERC20Upgradeable | undefined | -| _tokenLockFactory | contract ITokenLockFactory | undefined | +| _totalAmount | uint256 | undefined | ### OwnershipTransferred @@ -250,6 +231,17 @@ error ContractIsNotHATAirdrop() +### HATAirdropInitializationFailed + +```solidity +error HATAirdropInitializationFailed() +``` + + + + + + ### RedeemDataArraysLengthMismatch ```solidity diff --git a/test/tge/hatairdrop.js b/test/tge/hatairdrop.js index b68b48cb..69eac049 100644 --- a/test/tge/hatairdrop.js +++ b/test/tge/hatairdrop.js @@ -4,6 +4,7 @@ const TokenLockFactory = artifacts.require("./TokenLockFactory.sol"); const HATTokenLock = artifacts.require("./HATTokenLock.sol"); const HATAirdrop = artifacts.require("./HATAirdrop.sol"); const HATAirdropFactory = artifacts.require("./HATAirdropFactory.sol"); +const IHATAirdrop = new ethers.utils.Interface(HATAirdrop.abi); const { contract, web3 } = require("hardhat"); const { assertFunctionRaisesException, assertVMException, ZERO_ADDRESS, @@ -37,6 +38,7 @@ contract("HATAirdrop", (accounts) => { let endTime; let lockEndTime; let periods; + let initData; async function setupHATAirdrop(useLock = true) { token = await ERC20Mock.new("Staking", "STK"); @@ -62,8 +64,7 @@ contract("HATAirdrop", (accounts) => { hatAirdropImplementation = await HATAirdrop.new(); hatAirdropFactory = await HATAirdropFactory.new(); - let airdropAddress = await hatAirdropFactory.predictHATAirdropAddress( - hatAirdropImplementation.address, + initData = IHATAirdrop.encodeFunctionData("initialize", [ "QmSUXfYsk9HgrMBa7tgp3MBm8FGwDF9hnVaR9C1PMoFdS3", merkleTree.getHexRoot(), startTime, @@ -72,32 +73,25 @@ contract("HATAirdrop", (accounts) => { periods, token.address, tokenLockFactory.address + ]); + + let airdropAddress = await hatAirdropFactory.predictHATAirdropAddress( + hatAirdropImplementation.address, + initData ); let tx = await hatAirdropFactory.createHATAirdrop( hatAirdropImplementation.address, - "QmSUXfYsk9HgrMBa7tgp3MBm8FGwDF9hnVaR9C1PMoFdS3", - merkleTree.getHexRoot(), - startTime, - endTime, - lockEndTime, - periods, - totalAmount, + initData, token.address, - tokenLockFactory.address + totalAmount ); assert.equal(tx.logs[0].event, "HATAirdropCreated"); assert.equal(tx.logs[0].args._hatAirdrop, airdropAddress); - assert.equal(tx.logs[0].args._merkleTreeIPFSRef, "QmSUXfYsk9HgrMBa7tgp3MBm8FGwDF9hnVaR9C1PMoFdS3"); - assert.equal(tx.logs[0].args._root, merkleTree.getHexRoot()); - assert.equal(tx.logs[0].args._startTime, startTime); - assert.equal(tx.logs[0].args._deadline, endTime); - assert.equal(tx.logs[0].args._lockEndTime, lockEndTime); - assert.equal(tx.logs[0].args._periods, periods); - assert.equal(tx.logs[0].args._totalAmount, totalAmount); + assert.equal(tx.logs[0].args._initData, initData); assert.equal(tx.logs[0].args._token, token.address); - assert.equal(tx.logs[0].args._tokenLockFactory, tokenLockFactory.address); + assert.equal(tx.logs[0].args._totalAmount, totalAmount); hatAirdrop = await HATAirdrop.at(airdropAddress); await token.mint(hatAirdropFactory.address, totalAmount); @@ -109,15 +103,9 @@ contract("HATAirdrop", (accounts) => { await assertFunctionRaisesException( hatAirdropFactory.createHATAirdrop( hatAirdropImplementation.address, - "QmSUXfYsk9HgrMBa7tgp3MBm8FGwDF9hnVaR9C1PMoFdS3", - merkleTree.getHexRoot(), - startTime, - endTime, - lockEndTime, - periods, - totalAmount, + initData, token.address, - tokenLockFactory.address, + totalAmount, { from: accounts[1] } ), "Ownable: caller is not the owner" @@ -181,17 +169,22 @@ contract("HATAirdrop", (accounts) => { it("Redeem all from multiple airdrops", async () => { await setupHATAirdrop(); - let tx = await hatAirdropFactory.createHATAirdrop( - hatAirdropImplementation.address, + const initData2 = IHATAirdrop.encodeFunctionData("initialize", [ "QmSUXfYsk9HgrMBa7tgp3MBm8FGwDF9hnVaR9C1PMoFdS3", merkleTree.getHexRoot(), startTime, endTime, lockEndTime + 1, periods, - totalAmount, token.address, tokenLockFactory.address + ]); + + let tx = await hatAirdropFactory.createHATAirdrop( + hatAirdropImplementation.address, + initData2, + token.address, + totalAmount ); const hatAirdrop2 = await HATAirdrop.at(tx.logs[0].args._hatAirdrop); @@ -212,6 +205,20 @@ contract("HATAirdrop", (accounts) => { assert.equal((await token.balanceOf(hatAirdropFactory.address)).toString(), "0"); }); + it("Factory deployment fails if init fails", async () => { + await setupHATAirdrop(); + + await assertFunctionRaisesException( + hatAirdropFactory.createHATAirdrop( + hatAirdropFactory.address, + initData, + token.address, + totalAmount + ), + "HATAirdropInitializationFailed" + ); + }); + it("Cannot redeem with factory if contract is not deployed by the factory", async () => { await setupHATAirdrop(); From 136a041dc2bf6afc60da7da6d4caa17c21ceeb17 Mon Sep 17 00:00:00 2001 From: benk10 Date: Thu, 11 Jan 2024 17:53:52 -0300 Subject: [PATCH 16/18] Add initialize docstring --- contracts/tge/HATAirdrop.sol | 11 +++++++++++ docs/dodoc/tge/HATAirdrop.md | 18 +++++++++--------- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/contracts/tge/HATAirdrop.sol b/contracts/tge/HATAirdrop.sol index 2f754bbe..8225243e 100644 --- a/contracts/tge/HATAirdrop.sol +++ b/contracts/tge/HATAirdrop.sol @@ -38,6 +38,17 @@ contract HATAirdrop is Initializable { _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 exists) + * @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( string memory _merkleTreeIPFSRef, bytes32 _root, diff --git a/docs/dodoc/tge/HATAirdrop.md b/docs/dodoc/tge/HATAirdrop.md index 4c5acde3..50e00dfa 100644 --- a/docs/dodoc/tge/HATAirdrop.md +++ b/docs/dodoc/tge/HATAirdrop.md @@ -50,7 +50,7 @@ function factory() external view returns (address) function initialize(string _merkleTreeIPFSRef, bytes32 _root, uint256 _startTime, uint256 _deadline, uint256 _lockEndTime, uint256 _periods, contract IERC20Upgradeable _token, contract ITokenLockFactory _tokenLockFactory) external nonpayable ``` - +Initialize a HATAirdrop instance @@ -58,14 +58,14 @@ function initialize(string _merkleTreeIPFSRef, bytes32 _root, uint256 _startTime | Name | Type | Description | |---|---|---| -| _merkleTreeIPFSRef | string | undefined | -| _root | bytes32 | undefined | -| _startTime | uint256 | undefined | -| _deadline | uint256 | undefined | -| _lockEndTime | uint256 | undefined | -| _periods | uint256 | undefined | -| _token | contract IERC20Upgradeable | undefined | -| _tokenLockFactory | contract ITokenLockFactory | undefined | +| _merkleTreeIPFSRef | string | new merkle tree ipfs reference. | +| _root | bytes32 | new merkle tree root to use for verifying airdrop data. | +| _startTime | uint256 | start of the redeem period and of the token lock (if exists) | +| _deadline | uint256 | end time to redeem from the contract | +| _lockEndTime | uint256 | end time for the token lock contract (if exists) | +| _periods | uint256 | number of periods of the token lock contract (if exists) | +| _token | contract IERC20Upgradeable | the token to be airdropped | +| _tokenLockFactory | contract ITokenLockFactory | the token lock factory to use to deploy the token locks | ### leafRedeemed From 6a0b7bf3a4d424029b9aa287fcd1568e8ad3470e Mon Sep 17 00:00:00 2001 From: benk10 Date: Thu, 11 Jan 2024 17:58:57 -0300 Subject: [PATCH 17/18] Add interface for IHATAirdrop contracts --- contracts/tge/HATAirdrop.sol | 3 ++- contracts/tge/HATAirdropFactory.sol | 16 +++++++----- contracts/tge/interfaces/IHATAirdrop.sol | 9 +++++++ docs/dodoc/tge/HATAirdropFactory.md | 16 ++++++------ docs/dodoc/tge/interfaces/IHATAirdrop.md | 33 ++++++++++++++++++++++++ 5 files changed, 61 insertions(+), 16 deletions(-) create mode 100644 contracts/tge/interfaces/IHATAirdrop.sol create mode 100644 docs/dodoc/tge/interfaces/IHATAirdrop.md diff --git a/contracts/tge/HATAirdrop.sol b/contracts/tge/HATAirdrop.sol index 8225243e..a57a1ff4 100644 --- a/contracts/tge/HATAirdrop.sol +++ b/contracts/tge/HATAirdrop.sol @@ -6,11 +6,12 @@ import "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeab 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 Initializable { +contract HATAirdrop is IHATAirdrop, Initializable { error CannotRedeemBeforeStartTime(); error CannotRedeemAfterDeadline(); error LeafAlreadyRedeemed(); diff --git a/contracts/tge/HATAirdropFactory.sol b/contracts/tge/HATAirdropFactory.sol index 1f4c14d3..23c33f12 100644 --- a/contracts/tge/HATAirdropFactory.sol +++ b/contracts/tge/HATAirdropFactory.sol @@ -5,27 +5,29 @@ pragma solidity 0.8.16; import "@openzeppelin/contracts/access/Ownable.sol"; import "@openzeppelin/contracts/proxy/Clones.sol"; -import "./HATAirdrop.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 SafeERC20Upgradeable for IERC20Upgradeable; + using SafeERC20 for IERC20; mapping(address => bool) public isAirdrop; event TokensWithdrawn(address indexed _owner, uint256 _amount); - event HATAirdropCreated(address indexed _hatAirdrop, bytes _initData, IERC20Upgradeable _token, uint256 _totalAmount); + event HATAirdropCreated(address indexed _hatAirdrop, bytes _initData, IERC20 _token, uint256 _totalAmount); - function withdrawTokens(IERC20Upgradeable _token, uint256 _amount) external onlyOwner { + function withdrawTokens(IERC20 _token, uint256 _amount) external onlyOwner { address owner = msg.sender; _token.safeTransfer(owner, _amount); emit TokensWithdrawn(owner, _amount); } - function redeemMultipleAirdrops(HATAirdrop[] calldata _airdrops, uint256[] calldata _amounts, bytes32[][] calldata _proofs) external { + function redeemMultipleAirdrops(IHATAirdrop[] calldata _airdrops, uint256[] calldata _amounts, bytes32[][] calldata _proofs) external { if (_airdrops.length != _amounts.length || _airdrops.length != _proofs.length) { revert RedeemDataArraysLengthMismatch(); } @@ -36,7 +38,7 @@ contract HATAirdropFactory is Ownable { revert ContractIsNotHATAirdrop(); } - HATAirdrop(_airdrops[i]).redeem(caller, _amounts[i], _proofs[i]); + _airdrops[i].redeem(caller, _amounts[i], _proofs[i]); unchecked { ++i; @@ -47,7 +49,7 @@ contract HATAirdropFactory is Ownable { function createHATAirdrop( address _implementation, bytes calldata _initData, - IERC20Upgradeable _token, + IERC20 _token, uint256 _totalAmount ) external onlyOwner returns (address result) { result = Clones.cloneDeterministic(_implementation, keccak256(_initData)); diff --git a/contracts/tge/interfaces/IHATAirdrop.sol b/contracts/tge/interfaces/IHATAirdrop.sol new file mode 100644 index 00000000..a9c5f2f1 --- /dev/null +++ b/contracts/tge/interfaces/IHATAirdrop.sol @@ -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; +} \ No newline at end of file diff --git a/docs/dodoc/tge/HATAirdropFactory.md b/docs/dodoc/tge/HATAirdropFactory.md index b3e286b0..7a5a5b5c 100644 --- a/docs/dodoc/tge/HATAirdropFactory.md +++ b/docs/dodoc/tge/HATAirdropFactory.md @@ -13,7 +13,7 @@ ### createHATAirdrop ```solidity -function createHATAirdrop(address _implementation, bytes _initData, contract IERC20Upgradeable _token, uint256 _totalAmount) external nonpayable returns (address result) +function createHATAirdrop(address _implementation, bytes _initData, contract IERC20 _token, uint256 _totalAmount) external nonpayable returns (address result) ``` @@ -26,7 +26,7 @@ function createHATAirdrop(address _implementation, bytes _initData, contract IER |---|---|---| | _implementation | address | undefined | | _initData | bytes | undefined | -| _token | contract IERC20Upgradeable | undefined | +| _token | contract IERC20 | undefined | | _totalAmount | uint256 | undefined | #### Returns @@ -100,7 +100,7 @@ function predictHATAirdropAddress(address _implementation, bytes _initData) exte ### redeemMultipleAirdrops ```solidity -function redeemMultipleAirdrops(contract HATAirdrop[] _airdrops, uint256[] _amounts, bytes32[][] _proofs) external nonpayable +function redeemMultipleAirdrops(contract IHATAirdrop[] _airdrops, uint256[] _amounts, bytes32[][] _proofs) external nonpayable ``` @@ -111,7 +111,7 @@ function redeemMultipleAirdrops(contract HATAirdrop[] _airdrops, uint256[] _amou | Name | Type | Description | |---|---|---| -| _airdrops | contract HATAirdrop[] | undefined | +| _airdrops | contract IHATAirdrop[] | undefined | | _amounts | uint256[] | undefined | | _proofs | bytes32[][] | undefined | @@ -145,7 +145,7 @@ function transferOwnership(address newOwner) external nonpayable ### withdrawTokens ```solidity -function withdrawTokens(contract IERC20Upgradeable _token, uint256 _amount) external nonpayable +function withdrawTokens(contract IERC20 _token, uint256 _amount) external nonpayable ``` @@ -156,7 +156,7 @@ function withdrawTokens(contract IERC20Upgradeable _token, uint256 _amount) exte | Name | Type | Description | |---|---|---| -| _token | contract IERC20Upgradeable | undefined | +| _token | contract IERC20 | undefined | | _amount | uint256 | undefined | @@ -166,7 +166,7 @@ function withdrawTokens(contract IERC20Upgradeable _token, uint256 _amount) exte ### HATAirdropCreated ```solidity -event HATAirdropCreated(address indexed _hatAirdrop, bytes _initData, contract IERC20Upgradeable _token, uint256 _totalAmount) +event HATAirdropCreated(address indexed _hatAirdrop, bytes _initData, contract IERC20 _token, uint256 _totalAmount) ``` @@ -179,7 +179,7 @@ event HATAirdropCreated(address indexed _hatAirdrop, bytes _initData, contract I |---|---|---| | _hatAirdrop `indexed` | address | undefined | | _initData | bytes | undefined | -| _token | contract IERC20Upgradeable | undefined | +| _token | contract IERC20 | undefined | | _totalAmount | uint256 | undefined | ### OwnershipTransferred diff --git a/docs/dodoc/tge/interfaces/IHATAirdrop.md b/docs/dodoc/tge/interfaces/IHATAirdrop.md new file mode 100644 index 00000000..3edd6a87 --- /dev/null +++ b/docs/dodoc/tge/interfaces/IHATAirdrop.md @@ -0,0 +1,33 @@ +# IHATAirdrop + + + + + + + + + +## Methods + +### redeem + +```solidity +function redeem(address _account, uint256 _amount, bytes32[] _proof) external nonpayable +``` + + + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| _account | address | undefined | +| _amount | uint256 | undefined | +| _proof | bytes32[] | undefined | + + + + From 652566fa1bedf1ed5f3504cfb8c2e47e39a5a6e9 Mon Sep 17 00:00:00 2001 From: benk10 Date: Mon, 15 Jan 2024 07:28:28 -0300 Subject: [PATCH 18/18] Update HATAirdrop.sol docstring Co-authored-by: Jelle Signed-off-by: benk10 --- contracts/tge/HATAirdrop.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/tge/HATAirdrop.sol b/contracts/tge/HATAirdrop.sol index a57a1ff4..945e8318 100644 --- a/contracts/tge/HATAirdrop.sol +++ b/contracts/tge/HATAirdrop.sol @@ -45,7 +45,7 @@ contract HATAirdrop is IHATAirdrop, Initializable { * @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 exists) + * @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