From 023a7364f3053ec77070c4b986ff5526f6e56675 Mon Sep 17 00:00:00 2001 From: Greg Taschuk Date: Tue, 3 Nov 2020 12:50:03 -0800 Subject: [PATCH] added WIP support for the redemption of multiple tokens from a single contract --- merkle/contracts/MerkleRedeem.sol | 67 +++++++++------- merkle/migrations/2_deploy_redeem.js | 2 +- merkle/test/high-volume-redeem.js | 17 ++-- merkle/test/redeem.js | 112 +++++++++++++++++---------- 4 files changed, 122 insertions(+), 76 deletions(-) diff --git a/merkle/contracts/MerkleRedeem.sol b/merkle/contracts/MerkleRedeem.sol index 90e9a21..7f2f46a 100644 --- a/merkle/contracts/MerkleRedeem.sol +++ b/merkle/contracts/MerkleRedeem.sol @@ -7,45 +7,52 @@ import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; contract MerkleRedeem is Ownable { - IERC20 public token; + //IERC20 public token; - event Claimed(address _claimant, uint256 _balance); + event Claimed(address _erc20, address _claimant, uint256 _balance); // Recorded weeks - mapping(uint => bytes32) public weekMerkleRoots; - mapping(uint => mapping(address => bool)) public claimed; + // token => week => user + mapping(address => mapping(uint => bytes32)) public weekMerkleRoots; + // token => week => user + mapping(address => mapping(uint => mapping(address => bool))) public claimed; constructor( - address _token + address[] memory _tokens ) public { - token = IERC20(_token); + // TODO - put these in a set and add a modifier to check token is in contract + //token = IERC20(_token); } function disburse( - address _liquidityProvider, + address _erc20, + address _user, uint _balance ) private { + IERC20 token = IERC20(_erc20); if (_balance > 0) { - emit Claimed(_liquidityProvider, _balance); - require(token.transfer(_liquidityProvider, _balance), "ERR_TRANSFER_FAILED"); + emit Claimed(_erc20, _user, _balance); + require(token.transfer(_user, _balance), "ERR_TRANSFER_FAILED"); } } + // you can trigger a claim for another user function claimWeek( - address _liquidityProvider, + address _user, + address _erc20, uint _week, uint _claimedBalance, bytes32[] memory _merkleProof ) public { - require(!claimed[_week][_liquidityProvider]); - require(verifyClaim(_liquidityProvider, _week, _claimedBalance, _merkleProof), 'Incorrect merkle proof'); + require(!claimed[_erc20][_week][_user]); + require(verifyClaim(_user, _erc20, _week, _claimedBalance, _merkleProof), 'Incorrect merkle proof'); - claimed[_week][_liquidityProvider] = true; - disburse(_liquidityProvider, _claimedBalance); + claimed[_erc20][_week][_user] = true; + disburse(_erc20, _user, _claimedBalance); } struct Claim { @@ -55,7 +62,8 @@ contract MerkleRedeem is Ownable { } function claimWeeks( - address _liquidityProvider, + address _user, + address _erc20, Claim[] memory claims ) public @@ -65,17 +73,18 @@ contract MerkleRedeem is Ownable { for(uint i = 0; i < claims.length; i++) { claim = claims[i]; - require(!claimed[claim.week][_liquidityProvider]); - require(verifyClaim(_liquidityProvider, claim.week, claim.balance, claim.merkleProof), 'Incorrect merkle proof'); + require(!claimed[_erc20][claim.week][_user]); + require(verifyClaim(_user,_erc20, claim.week, claim.balance, claim.merkleProof), 'Incorrect merkle proof'); totalBalance += claim.balance; - claimed[claim.week][_liquidityProvider] = true; + claimed[_erc20][claim.week][_user] = true; } - disburse(_liquidityProvider, totalBalance); + disburse(_erc20, _user, totalBalance); } function claimStatus( - address _liquidityProvider, + address _user, + address _erc20, uint _begin, uint _end ) @@ -86,12 +95,13 @@ contract MerkleRedeem is Ownable { uint size = 1 + _end - _begin; bool[] memory arr = new bool[](size); for(uint i = 0; i < size; i++) { - arr[i] = claimed[_begin + i][_liquidityProvider]; + arr[i] = claimed[_erc20][_begin + i][_user]; } return arr; } function merkleRoots( + address _erc20, uint _begin, uint _end ) @@ -102,13 +112,14 @@ contract MerkleRedeem is Ownable { uint size = 1 + _end - _begin; bytes32[] memory arr = new bytes32[](size); for(uint i = 0; i < size; i++) { - arr[i] = weekMerkleRoots[_begin + i]; + arr[i] = weekMerkleRoots[_erc20][_begin + i]; } return arr; } function verifyClaim( - address _liquidityProvider, + address _user, + address _erc20, uint _week, uint _claimedBalance, bytes32[] memory _merkleProof @@ -117,11 +128,12 @@ contract MerkleRedeem is Ownable { view returns (bool valid) { - bytes32 leaf = keccak256(abi.encodePacked(_liquidityProvider, _claimedBalance)); - return MerkleProof.verify(_merkleProof, weekMerkleRoots[_week], leaf); + bytes32 leaf = keccak256(abi.encodePacked(_user, _claimedBalance)); + return MerkleProof.verify(_merkleProof, weekMerkleRoots[_erc20][_week], leaf); } function seedAllocations( + address _erc20, uint _week, bytes32 _merkleRoot, uint _totalAllocation @@ -129,9 +141,10 @@ contract MerkleRedeem is Ownable { external onlyOwner { - require(weekMerkleRoots[_week] == bytes32(0), "cannot rewrite merkle root"); - weekMerkleRoots[_week] = _merkleRoot; + require(weekMerkleRoots[_erc20][_week] == bytes32(0), "cannot rewrite merkle root"); + weekMerkleRoots[_erc20][_week] = _merkleRoot; + IERC20 token = IERC20(_erc20); require(token.transferFrom(msg.sender, address(this), _totalAllocation), "ERR_TRANSFER_FAILED"); } } diff --git a/merkle/migrations/2_deploy_redeem.js b/merkle/migrations/2_deploy_redeem.js index f2da130..1deeeaa 100644 --- a/merkle/migrations/2_deploy_redeem.js +++ b/merkle/migrations/2_deploy_redeem.js @@ -9,7 +9,7 @@ module.exports = (deployer, network, accounts) => { const token = await TToken.deployed(); await token.mint(admin, utils.toWei("145000")); - await deployer.deploy(Redeem, token.address); + await deployer.deploy(Redeem, [token.address]); const redeem = await Redeem.deployed(); await token.transfer(redeem.address, utils.toWei("20000")); diff --git a/merkle/test/high-volume-redeem.js b/merkle/test/high-volume-redeem.js index 88874c2..dca9c31 100644 --- a/merkle/test/high-volume-redeem.js +++ b/merkle/test/high-volume-redeem.js @@ -23,7 +23,7 @@ contract("MerkleRedeem - High Volume", accounts => { await tbal.mint(admin, utils.toWei("1450000")); TBAL = tbal.address; - redeem = await Redeem.new(TBAL); + redeem = await Redeem.new([TBAL]); REDEEM = redeem.address; await tbal.approve(REDEEM, MAX); @@ -42,11 +42,12 @@ contract("MerkleRedeem - High Volume", accounts => { const merkleTree = new MerkleTree(elements); const root = merkleTree.getHexRoot(); - await redeem.seedAllocations(1, root, utils.toWei("145000")); + await redeem.seedAllocations(TBAL, 1, root, utils.toWei("145000")); const proof36 = merkleTree.getHexProof(elements[36]); let result = await redeem.verifyClaim( addresses[36], + TBAL, 1, utils.toWei("360"), proof36 @@ -56,6 +57,7 @@ contract("MerkleRedeem - High Volume", accounts => { const proof48 = merkleTree.getHexProof(elements[48]); result = await redeem.verifyClaim( addresses[48], + TBAL, 1, utils.toWei("480"), proof48 @@ -97,25 +99,25 @@ contract("MerkleRedeem - High Volume", accounts => { beforeEach(async () => { let lastBlock = await web3.eth.getBlock("latest"); - await redeem.seedAllocations(1, root1, utils.toWei("145000")); + await redeem.seedAllocations(TBAL, 1, root1, utils.toWei("145000")); await increaseTime(7); lastBlock = await web3.eth.getBlock("latest"); let lastBlockHash = "0xb6801f31f93d990dfe65d67d3479c3853d5fafd7a7f2b8fad9e68084d8d409e0"; // set this manually to simplify testing - await redeem.seedAllocations(2, root2, utils.toWei("145000")); + await redeem.seedAllocations(TBAL, 2, root2, utils.toWei("145000")); await increaseTime(7); lastBlock = await web3.eth.getBlock("latest"); - await redeem.seedAllocations(3, root3, utils.toWei("145000")); + await redeem.seedAllocations(TBAL, 3, root3, utils.toWei("145000")); await increaseTime(7); lastBlock = await web3.eth.getBlock("latest"); - await redeem.seedAllocations(4, root4, utils.toWei("145000")); + await redeem.seedAllocations(TBAL, 4, root4, utils.toWei("145000")); await increaseTime(7); lastBlock = await web3.eth.getBlock("latest"); - await redeem.seedAllocations(5, root5, utils.toWei("145000")); + await redeem.seedAllocations(TBAL, 5, root5, utils.toWei("145000")); }); it("Allows the user to claim multiple weeks at once", async () => { @@ -129,6 +131,7 @@ contract("MerkleRedeem - High Volume", accounts => { await redeem.claimWeeks( accounts[1], + TBAL, [ [1, claimBalance1, proof1], [2, claimBalance2, proof2], diff --git a/merkle/test/redeem.js b/merkle/test/redeem.js index 42cb0ce..a48c73a 100644 --- a/merkle/test/redeem.js +++ b/merkle/test/redeem.js @@ -16,33 +16,30 @@ contract("MerkleRedeem", accounts => { let tbal; let TBAL; + let tbal2; + let TBAL2; + const MAX = utils.toTwosComplement(-1); // these are deterministic because accounts are deterministic for the ganache mnemonic const endingBlockHash = "0x76e2419510611ed9dceb203644e997aae76fb195d6420f8bee64368b14303312"; - const expectedOffsetSeconds = [ - 581200, - 171952, - 63845, - 503077, - 284922, - 44468, - 25715, - 559291, - 98173, - 588157 - ]; beforeEach(async () => { tbal = await TToken.new("Test Bal", "TBAL", 18); await tbal.mint(admin, utils.toWei("1450000")); TBAL = tbal.address; - redeem = await Redeem.new(TBAL); + // TODO add tests using a second token + tbal2 = await TToken.new("Test Bal 2", "TBAL", 18); + await tbal2.mint(admin, utils.toWei("1450000")); + TBAL2 = tbal2.address; + + redeem = await Redeem.new([TBAL, TBAL2]); REDEEM = redeem.address; await tbal.approve(REDEEM, MAX); + await tbal2.approve(REDEEM, MAX); }); it("stores an allocation", async () => { @@ -53,11 +50,17 @@ contract("MerkleRedeem", accounts => { const merkleTree = new MerkleTree(elements); const root = merkleTree.getHexRoot(); - await redeem.seedAllocations(1, root, utils.toWei("145000")); + await redeem.seedAllocations(TBAL, 1, root, utils.toWei("145000")); const proof = merkleTree.getHexProof(elements[0]); - let result = await redeem.verifyClaim(accounts[0], 1, claimBalance, proof); + let result = await redeem.verifyClaim( + accounts[0], + TBAL, + 1, + claimBalance, + proof + ); assert(result, "user should have an allocation"); }); @@ -69,7 +72,7 @@ contract("MerkleRedeem", accounts => { const merkleTree = new MerkleTree(elements); const root = merkleTree.getHexRoot(); - await redeem.seedAllocations(1, root, utils.toWei("145000")); + await redeem.seedAllocations(TBAL, 1, root, utils.toWei("145000")); // construct tree to attempt to override the allocation const elements2 = [ @@ -80,7 +83,7 @@ contract("MerkleRedeem", accounts => { const root2 = merkleTree.getHexRoot(); await truffleAssert.reverts( - redeem.seedAllocations(1, root2, utils.toWei("145000")) + redeem.seedAllocations(TBAL, 1, root2, utils.toWei("145000")) ); }); @@ -97,11 +100,12 @@ contract("MerkleRedeem", accounts => { const merkleTree = new MerkleTree(elements); const root = merkleTree.getHexRoot(); - await redeem.seedAllocations(1, root, utils.toWei("145000")); + await redeem.seedAllocations(TBAL, 1, root, utils.toWei("145000")); let proof0 = merkleTree.getHexProof(elements[0]); let result = await redeem.verifyClaim( accounts[0], + TBAL, 1, claimBalance0, proof0 @@ -109,7 +113,13 @@ contract("MerkleRedeem", accounts => { assert(result, "account 0 should have an allocation"); let proof1 = merkleTree.getHexProof(elements[1]); - result = await redeem.verifyClaim(accounts[1], 1, claimBalance1, proof1); + result = await redeem.verifyClaim( + accounts[1], + TBAL, + 1, + claimBalance1, + proof1 + ); assert(result, "account 1 should have an allocation"); }); @@ -129,7 +139,7 @@ contract("MerkleRedeem", accounts => { const merkleProof = merkleTree.getHexProof(elements[0]); await truffleAssert.reverts( - redeem.claimWeek(accounts[1], 1, claimedBalance, merkleProof, { + redeem.claimWeek(accounts[1], TBAL, 1, claimedBalance, merkleProof, { from: accounts[1] }) ); @@ -148,20 +158,27 @@ contract("MerkleRedeem", accounts => { let lastBlockHash = "0x7c1b1e7c2eaddafdf52250cba9679e5b30014a9d86a0e2af17ec4cee24a5fc80"; - await redeem.seedAllocations(1, root, utils.toWei("145000")); + await redeem.seedAllocations(TBAL, 1, root, utils.toWei("145000")); }); it("Allows the user to claimWeek", async () => { let claimedBalance = utils.toWei("1000"); const merkleProof = merkleTree.getHexProof(elements[0]); - await redeem.claimWeek(accounts[1], 1, claimedBalance, merkleProof, { - from: accounts[1] - }); + await redeem.claimWeek( + accounts[1], + TBAL, + 1, + claimedBalance, + merkleProof, + { + from: accounts[1] + } + ); let result = await tbal.balanceOf(accounts[1]); assert(result == claimedBalance, "user should have an allocation"); - result = await redeem.claimed(1, accounts[1]); + result = await redeem.claimed(TBAL, 1, accounts[1]); assert(result == true, "claim should be marked as claimed"); }); @@ -171,7 +188,7 @@ contract("MerkleRedeem", accounts => { const merkleProof = merkleTree.getHexProof(elements[0]); await truffleAssert.reverts( - redeem.claimWeek(accounts[2], 1, claimedBalance, merkleProof, { + redeem.claimWeek(accounts[2], TBAL, 1, claimedBalance, merkleProof, { from: accounts[2] }) ); @@ -182,7 +199,7 @@ contract("MerkleRedeem", accounts => { let claimedBalance = utils.toWei("666"); const merkleProof = merkleTree.getHexProof(elements[0]); await truffleAssert.reverts( - redeem.claimWeek(accounts[1], 1, claimedBalance, merkleProof, { + redeem.claimWeek(accounts[1], TBAL, 1, claimedBalance, merkleProof, { from: accounts[1] }) ); @@ -193,12 +210,19 @@ contract("MerkleRedeem", accounts => { let claimedBalance = utils.toWei("1000"); const merkleProof = merkleTree.getHexProof(elements[0]); - await redeem.claimWeek(accounts[1], 1, claimedBalance, merkleProof, { - from: accounts[1] - }); + await redeem.claimWeek( + accounts[1], + TBAL, + 1, + claimedBalance, + merkleProof, + { + from: accounts[1] + } + ); await truffleAssert.reverts( - redeem.claimWeek(accounts[1], 1, claimedBalance, merkleProof, { + redeem.claimWeek(accounts[1], TBAL, 1, claimedBalance, merkleProof, { from: accounts[1] }) ); @@ -219,13 +243,13 @@ contract("MerkleRedeem", accounts => { beforeEach(async () => { let lastBlock = await web3.eth.getBlock("latest"); - await redeem.seedAllocations(1, root1, utils.toWei("145000")); + await redeem.seedAllocations(TBAL, 1, root1, utils.toWei("145000")); await increaseTime(7); lastBlock = await web3.eth.getBlock("latest"); let lastBlockHash = "0xb6801f31f93d990dfe65d67d3479c3853d5fafd7a7f2b8fad9e68084d8d409e0"; // set this manually to simplify testing - await redeem.seedAllocations(2, root2, utils.toWei("145000")); + await redeem.seedAllocations(TBAL, 2, root2, utils.toWei("145000")); }); it("Allows the user to claim once the time has past", async () => { @@ -235,12 +259,12 @@ contract("MerkleRedeem", accounts => { let claimedBalance2 = utils.toWei("1234"); const proof1 = merkleTree1.getHexProof(elements1[0]); - await redeem.claimWeek(accounts[1], 1, claimedBalance1, proof1, { + await redeem.claimWeek(accounts[1], TBAL, 1, claimedBalance1, proof1, { from: accounts[1] }); const proof2 = merkleTree2.getHexProof(elements2[0]); - await redeem.claimWeek(accounts[1], 2, claimedBalance2, proof2, { + await redeem.claimWeek(accounts[1], TBAL, 2, claimedBalance2, proof2, { from: accounts[1] }); @@ -262,6 +286,7 @@ contract("MerkleRedeem", accounts => { await redeem.claimWeeks( accounts[1], + TBAL, [[1, claimedBalance1, proof1], [2, claimedBalance2, proof2]], { from: accounts[1] } ); @@ -275,7 +300,7 @@ contract("MerkleRedeem", accounts => { it("Returns an array of week claims", async () => { let expectedResult = [false, false]; - let result = await redeem.claimStatus(accounts[1], 1, 2); + let result = await redeem.claimStatus(accounts[1], TBAL, 1, 2); assert.deepEqual( result, expectedResult, @@ -285,12 +310,17 @@ contract("MerkleRedeem", accounts => { const proof1 = merkleTree1.getHexProof(elements1[0]); await increaseTime(8); - await redeem.claimWeeks(accounts[1], [[1, claimedBalance1, proof1]], { - from: accounts[1] - }); + await redeem.claimWeeks( + accounts[1], + TBAL, + [[1, claimedBalance1, proof1]], + { + from: accounts[1] + } + ); expectedResult = [true, false]; - result = await redeem.claimStatus(accounts[1], 1, 2); + result = await redeem.claimStatus(accounts[1], TBAL, 1, 2); assert.deepEqual( result, expectedResult, @@ -300,7 +330,7 @@ contract("MerkleRedeem", accounts => { it("Returns an array of merkle roots", async () => { let expectedResult = [root1, root2]; - let result = await redeem.merkleRoots(1, 2); + let result = await redeem.merkleRoots(TBAL, 1, 2); assert.deepEqual( result, expectedResult,