From 28a09d41572a2c6173f4c56dba9b86cfff6c5c49 Mon Sep 17 00:00:00 2001 From: boqdan <304771+bowd@users.noreply.github.com> Date: Thu, 26 Sep 2024 12:46:52 +0200 Subject: [PATCH] Create and deploy a revertible distributor for testing (#220) * feat: add a testing merkle tree distributor that can be unclaimed * feat: add full contracts to the testing file --- bin/verify-celoscan.ts | 1 - contracts/TestingMerkleDistributor.sol | 130 ++++++++++++++++++ .../dev-DeployTestingMerkleDistributor.sol | 21 +++ .../dev-ToggleTestingDistributorClaimed.sol | 27 ++++ script/interfaces/IGovernor.sol | 2 +- script/utils/mento/Chain.sol | 2 +- script/utils/mento/Contracts.sol | 2 +- script/utils/mento/ExecuteProposal.sol | 2 +- script/utils/mento/GovernanceHelper.sol | 2 +- script/utils/mento/Oracles.sol | 2 +- script/utils/mento/PassProposal.sol | 2 +- script/utils/mento/QueueProposal.sol | 2 +- script/utils/mento/Script.sol | 2 +- script/utils/mento/SimulateUpgrade.sol | 2 +- 14 files changed, 188 insertions(+), 11 deletions(-) create mode 100644 contracts/TestingMerkleDistributor.sol create mode 100644 script/dev/dev-DeployTestingMerkleDistributor.sol create mode 100644 script/dev/dev-ToggleTestingDistributorClaimed.sol diff --git a/bin/verify-celoscan.ts b/bin/verify-celoscan.ts index ba65566a..76e5e3dc 100644 --- a/bin/verify-celoscan.ts +++ b/bin/verify-celoscan.ts @@ -245,7 +245,6 @@ function makeStandardJson(metadata: Metadata, sources: Sources, libraryMap: Reco settings: { viaIR: true, metadata: { - appendCBOR: true, bytecodeHash: "none", useLiteralContent: true, }, diff --git a/contracts/TestingMerkleDistributor.sol b/contracts/TestingMerkleDistributor.sol new file mode 100644 index 00000000..5e3abdaf --- /dev/null +++ b/contracts/TestingMerkleDistributor.sol @@ -0,0 +1,130 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity =0.8.17; + +import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; +import { IERC20, SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { MerkleProof } from "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol"; +import { IMerkleDistributor } from "merkle-distributor/interfaces/IMerkleDistributor.sol"; + +error EndTimeInPast(); +error ClaimWindowFinished(); +error NoWithdrawDuringClaim(); +error AlreadyClaimed(); +error InvalidProof(); + +contract MerkleDistributor is IMerkleDistributor { + using SafeERC20 for IERC20; + + address public immutable override token; + bytes32 public immutable override merkleRoot; + + // This is a packed array of booleans. + mapping(uint256 => uint256) private claimedBitMap; + + constructor(address token_, bytes32 merkleRoot_) { + token = token_; + merkleRoot = merkleRoot_; + } + + function isClaimed(uint256 index) public view virtual override returns (bool) { + uint256 claimedWordIndex = index / 256; + uint256 claimedBitIndex = index % 256; + uint256 claimedWord = claimedBitMap[claimedWordIndex]; + uint256 mask = (1 << claimedBitIndex); + return claimedWord & mask == mask; + } + + function _setClaimed(uint256 index) private { + uint256 claimedWordIndex = index / 256; + uint256 claimedBitIndex = index % 256; + claimedBitMap[claimedWordIndex] = claimedBitMap[claimedWordIndex] | (1 << claimedBitIndex); + } + + function claim( + uint256 index, + address account, + uint256 amount, + bytes32[] calldata merkleProof + ) public virtual override { + if (isClaimed(index)) revert AlreadyClaimed(); + + // Verify the merkle proof. + bytes32 node = keccak256(abi.encodePacked(index, account, amount)); + if (!MerkleProof.verify(merkleProof, merkleRoot, node)) revert InvalidProof(); + + // Mark it claimed and send the token. + _setClaimed(index); + IERC20(token).safeTransfer(account, amount); + + emit Claimed(index, account, amount); + } +} + +contract MerkleDistributorWithDeadline is MerkleDistributor, Ownable { + using SafeERC20 for IERC20; + + uint256 public immutable endTime; + + constructor(address token_, bytes32 merkleRoot_, uint256 endTime_) MerkleDistributor(token_, merkleRoot_) { + if (endTime_ <= block.timestamp) revert EndTimeInPast(); + endTime = endTime_; + } + + function claim( + uint256 index, + address account, + uint256 amount, + bytes32[] calldata merkleProof + ) public virtual override { + if (block.timestamp > endTime) revert ClaimWindowFinished(); + super.claim(index, account, amount, merkleProof); + } + + function withdraw() external onlyOwner { + if (block.timestamp < endTime) revert NoWithdrawDuringClaim(); + IERC20(token).safeTransfer(msg.sender, IERC20(token).balanceOf(address(this))); + } +} + +contract TestingMerkleDistributor is MerkleDistributorWithDeadline { + // This is a packed array of booleans. + mapping(uint256 => uint256) private claimedBitMap; + + constructor( + address token_, + bytes32 merkleRoot_, + uint256 endTime_ + ) MerkleDistributorWithDeadline(token_, merkleRoot_, endTime_) {} + + function setClaimed(uint256 index, bool claimed) public onlyOwner { + _setClaimed(index, claimed); + } + + function _setClaimed(uint256 index, bool claimed) public { + uint256 claimedWordIndex = index / 256; + uint256 claimedBitIndex = index % 256; + if (claimed) { + claimedBitMap[claimedWordIndex] = claimedBitMap[claimedWordIndex] | (1 << claimedBitIndex); + } else { + claimedBitMap[claimedWordIndex] = claimedBitMap[claimedWordIndex] & ~(1 << claimedBitIndex); + } + } + + function isClaimed(uint256 index) public view virtual override returns (bool) { + uint256 claimedWordIndex = index / 256; + uint256 claimedBitIndex = index % 256; + uint256 claimedWord = claimedBitMap[claimedWordIndex]; + uint256 mask = (1 << claimedBitIndex); + return claimedWord & mask == mask; + } + + function claim( + uint256 index, + address account, + uint256 amount, + bytes32[] calldata merkleProof + ) public virtual override { + super.claim(index, account, amount, merkleProof); + _setClaimed(index, true); + } +} diff --git a/script/dev/dev-DeployTestingMerkleDistributor.sol b/script/dev/dev-DeployTestingMerkleDistributor.sol new file mode 100644 index 00000000..4c16711a --- /dev/null +++ b/script/dev/dev-DeployTestingMerkleDistributor.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity =0.8.17; + +import { Script } from "script/utils/mento/Script.sol"; +import { Chain as ChainLib } from "script/utils/mento/Chain.sol"; +import { Contracts } from "script/utils/mento/Contracts.sol"; +import { TestingMerkleDistributor } from "contracts/TestingMerkleDistributor.sol"; + +contract DeployTestingMerkleDistributor is Script { + using Contracts for Contracts.Cache; + bytes32 constant MERKLE_ROOT = 0xc8148553672963b66a8972e21bff370b4c8dba9ce7f108282edf784c76875a43; + + function run() public { + address token = contracts.celoRegistry("StableToken"); + vm.startBroadcast(ChainLib.deployerPrivateKey()); + { + new TestingMerkleDistributor(token, MERKLE_ROOT, block.timestamp + 1 weeks); + } + vm.stopBroadcast(); + } +} diff --git a/script/dev/dev-ToggleTestingDistributorClaimed.sol b/script/dev/dev-ToggleTestingDistributorClaimed.sol new file mode 100644 index 00000000..9f60eeae --- /dev/null +++ b/script/dev/dev-ToggleTestingDistributorClaimed.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.18; + +import { console } from "forge-std-next/console.sol"; +import { Script } from "script/utils/mento/Script.sol"; +import { Chain as ChainLib } from "script/utils/mento/Chain.sol"; + +interface IDistributor { + function setClaimed(uint256 index, bool claimed) external; +} + +/* + * How to run: + * env DISTRIBUTOR=0x... CLAIMER_INDEX=0x0 yarn script:dev -n celo -s ToggleTestingDistributorClaimed + */ +contract ToggleTestingDistributorClaimed is Script { + function run() public { + address distributor = vm.envAddress("DISTRIBUTOR"); + uint256 index = vm.envUint("CLAIMER_INDEX"); + + vm.startBroadcast(ChainLib.deployerPrivateKey()); + { + IDistributor(distributor).setClaimed(index, false); + } + vm.stopBroadcast(); + } +} diff --git a/script/interfaces/IGovernor.sol b/script/interfaces/IGovernor.sol index d45dc2e2..c409614c 100644 --- a/script/interfaces/IGovernor.sol +++ b/script/interfaces/IGovernor.sol @@ -1,4 +1,4 @@ -pragma solidity 0.8.18; +pragma solidity ^0.8; /** * @dev Interface of the Bravo Compatible Governor. diff --git a/script/utils/mento/Chain.sol b/script/utils/mento/Chain.sol index cdf723a4..31cbe8c0 100644 --- a/script/utils/mento/Chain.sol +++ b/script/utils/mento/Chain.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.8.18; +pragma solidity ^0.8; import { Vm } from "forge-std/Vm.sol"; diff --git a/script/utils/mento/Contracts.sol b/script/utils/mento/Contracts.sol index 12bf8010..b0667d96 100644 --- a/script/utils/mento/Contracts.sol +++ b/script/utils/mento/Contracts.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.8.18; +pragma solidity ^0.8; import { console } from "forge-std/console.sol"; diff --git a/script/utils/mento/ExecuteProposal.sol b/script/utils/mento/ExecuteProposal.sol index e82f4830..5d37f433 100644 --- a/script/utils/mento/ExecuteProposal.sol +++ b/script/utils/mento/ExecuteProposal.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.8.18; +pragma solidity ^0.8; import { Script } from "./Script.sol"; import { IGovernanceFactory } from "../../interfaces/IGovernanceFactory.sol"; diff --git a/script/utils/mento/GovernanceHelper.sol b/script/utils/mento/GovernanceHelper.sol index 974cd39d..3f7e2488 100644 --- a/script/utils/mento/GovernanceHelper.sol +++ b/script/utils/mento/GovernanceHelper.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.8.18; +pragma solidity ^0.8; import { Script, console2 } from "forge-std/Script.sol"; import { ICeloGovernance } from "../../interfaces/ICeloGovernance.sol"; diff --git a/script/utils/mento/Oracles.sol b/script/utils/mento/Oracles.sol index 1c080031..fb69cbac 100644 --- a/script/utils/mento/Oracles.sol +++ b/script/utils/mento/Oracles.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later // solhint-disable func-visibility -pragma solidity ^0.8.18; +pragma solidity ^0.8; import { IChainlinkRelayer } from "mento-core-2.5.0/interfaces/IChainlinkRelayer.sol"; diff --git a/script/utils/mento/PassProposal.sol b/script/utils/mento/PassProposal.sol index 3594b56f..ab43642d 100644 --- a/script/utils/mento/PassProposal.sol +++ b/script/utils/mento/PassProposal.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.8.18; +pragma solidity ^0.8; import { Script } from "./Script.sol"; diff --git a/script/utils/mento/QueueProposal.sol b/script/utils/mento/QueueProposal.sol index 4834c012..523f4108 100644 --- a/script/utils/mento/QueueProposal.sol +++ b/script/utils/mento/QueueProposal.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.8.18; +pragma solidity ^0.8; import { Script } from "./Script.sol"; import { IGovernanceFactory } from "../../interfaces/IGovernanceFactory.sol"; diff --git a/script/utils/mento/Script.sol b/script/utils/mento/Script.sol index a4949abe..23061524 100644 --- a/script/utils/mento/Script.sol +++ b/script/utils/mento/Script.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.8.18; +pragma solidity ^0.8; import { Script as BaseScript } from "forge-std/Script.sol"; import { FixidityLib } from "../FixidityLib.sol"; diff --git a/script/utils/mento/SimulateUpgrade.sol b/script/utils/mento/SimulateUpgrade.sol index efb23b2a..9c48ca60 100644 --- a/script/utils/mento/SimulateUpgrade.sol +++ b/script/utils/mento/SimulateUpgrade.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later // solhint-disable func-name-mixedcase, contract-name-camelcase -pragma solidity ^0.8.18; +pragma solidity ^0.8; import { console } from "forge-std-next/console.sol"; import { Contracts } from "script/utils/mento/Contracts.sol";