From 955894ed79c09b0d375d5753769a6abb39312a42 Mon Sep 17 00:00:00 2001 From: Ezequiel Date: Thu, 26 Oct 2023 15:21:00 -0300 Subject: [PATCH 01/13] chore: update dependencies --- .gitmodules | 3 --- contracts/lib/ds-test | 2 +- contracts/lib/openzeppelin-contracts | 2 +- contracts/lib/solmate | 1 - 4 files changed, 2 insertions(+), 6 deletions(-) delete mode 160000 contracts/lib/solmate diff --git a/.gitmodules b/.gitmodules index 738831f..287443a 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,9 +1,6 @@ [submodule "contracts/lib/ds-test"] path = contracts/lib/ds-test url = https://github.com/dapphub/ds-test -[submodule "contracts/lib/solmate"] - path = contracts/lib/solmate - url = https://github.com/rari-capital/solmate [submodule "contracts/lib/openzeppelin-contracts"] path = contracts/lib/openzeppelin-contracts url = https://github.com/OpenZeppelin/openzeppelin-contracts diff --git a/contracts/lib/ds-test b/contracts/lib/ds-test index 0a5da56..e282159 160000 --- a/contracts/lib/ds-test +++ b/contracts/lib/ds-test @@ -1 +1 @@ -Subproject commit 0a5da56b0d65960e6a994d2ec8245e6edd38c248 +Subproject commit e282159d5170298eb2455a6c05280ab5a73a4ef0 diff --git a/contracts/lib/openzeppelin-contracts b/contracts/lib/openzeppelin-contracts index e3391cd..2ec2ed9 160000 --- a/contracts/lib/openzeppelin-contracts +++ b/contracts/lib/openzeppelin-contracts @@ -1 +1 @@ -Subproject commit e3391cd65f57c6b16c1d1be17e52380d6517ded1 +Subproject commit 2ec2ed96950a6f67a83ea72596bb464b5f4ebd88 diff --git a/contracts/lib/solmate b/contracts/lib/solmate deleted file mode 160000 index fab1075..0000000 --- a/contracts/lib/solmate +++ /dev/null @@ -1 +0,0 @@ -Subproject commit fab107565a51674f3a3b5bfdaacc67f6179b1a9b From c2af39e3c912c05f6632dee22b2dcafb6071a8d2 Mon Sep 17 00:00:00 2001 From: Ezequiel Date: Thu, 26 Oct 2023 15:51:46 -0300 Subject: [PATCH 02/13] feat: use external token in claim, and format code --- .gitmodules | 3 - contracts/foundry.toml | 6 + contracts/lib/openzeppelin-contracts | 1 - contracts/src/MerkleClaimERC20.sol | 111 ++++---- contracts/src/OverlayV1Token.sol | 51 ++++ contracts/src/interfaces/IOverlayV1Token.sol | 20 ++ contracts/src/test/MerkleClaimERC20.t.sol | 260 +++++++++--------- .../src/test/utils/MerkleClaimERC20Test.sol | 58 ++-- .../src/test/utils/MerkleClaimERC20User.sol | 49 ++-- 9 files changed, 316 insertions(+), 243 deletions(-) create mode 100644 contracts/foundry.toml delete mode 160000 contracts/lib/openzeppelin-contracts create mode 100644 contracts/src/OverlayV1Token.sol create mode 100644 contracts/src/interfaces/IOverlayV1Token.sol diff --git a/.gitmodules b/.gitmodules index 287443a..7ccd2eb 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,3 @@ [submodule "contracts/lib/ds-test"] path = contracts/lib/ds-test url = https://github.com/dapphub/ds-test -[submodule "contracts/lib/openzeppelin-contracts"] - path = contracts/lib/openzeppelin-contracts - url = https://github.com/OpenZeppelin/openzeppelin-contracts diff --git a/contracts/foundry.toml b/contracts/foundry.toml new file mode 100644 index 0000000..4ff40c4 --- /dev/null +++ b/contracts/foundry.toml @@ -0,0 +1,6 @@ +[profile.default] +src = "src" +out = "out" +libs = ["lib"] + +# See more config options https://github.com/foundry-rs/foundry/tree/master/config \ No newline at end of file diff --git a/contracts/lib/openzeppelin-contracts b/contracts/lib/openzeppelin-contracts deleted file mode 160000 index 2ec2ed9..0000000 --- a/contracts/lib/openzeppelin-contracts +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 2ec2ed96950a6f67a83ea72596bb464b5f4ebd88 diff --git a/contracts/src/MerkleClaimERC20.sol b/contracts/src/MerkleClaimERC20.sol index ff90286..ac9fda2 100644 --- a/contracts/src/MerkleClaimERC20.sol +++ b/contracts/src/MerkleClaimERC20.sol @@ -1,79 +1,80 @@ // SPDX-License-Identifier: AGPL-3.0-only -pragma solidity >=0.8.0; +pragma solidity 0.8.19; /// ============ Imports ============ -import { ERC20 } from "@solmate/tokens/ERC20.sol"; // Solmate: ERC20 -import { MerkleProof } from "@openzeppelin/utils/cryptography/MerkleProof.sol"; // OZ: MerkleProof +import {IERC20} from "@openzeppelin/token/ERC20/IERC20.sol"; +import {MerkleProof} from "@openzeppelin/utils/cryptography/MerkleProof.sol"; /// @title MerkleClaimERC20 -/// @notice ERC20 claimable by members of a merkle tree /// @author Anish Agnihotri -/// @dev Solmate ERC20 includes unused _burn logic that can be removed to optimize deployment cost -contract MerkleClaimERC20 is ERC20 { +contract MerkleClaimERC20 { + /// ============ Immutable storage ============ - /// ============ Immutable storage ============ + /// @notice ERC20-claimee inclusion root + bytes32 public immutable merkleRoot; - /// @notice ERC20-claimee inclusion root - bytes32 public immutable merkleRoot; + /// @notice Contract address of airdropped token + IERC20 public immutable token; - /// ============ Mutable storage ============ + /// ============ Mutable storage ============ - /// @notice Mapping of addresses who have claimed tokens - mapping(address => bool) public hasClaimed; + /// @notice Mapping of addresses who have claimed tokens + mapping(address => bool) public hasClaimed; - /// ============ Errors ============ + /// ============ Errors ============ - /// @notice Thrown if address has already claimed - error AlreadyClaimed(); - /// @notice Thrown if address/amount are not part of Merkle tree - error NotInMerkle(); + /// @notice Thrown if address has already claimed + error AlreadyClaimed(); - /// ============ Constructor ============ + /// @notice Thrown if address/amount are not part of Merkle tree + error NotInMerkle(); - /// @notice Creates a new MerkleClaimERC20 contract - /// @param _name of token - /// @param _symbol of token - /// @param _decimals of token - /// @param _merkleRoot of claimees - constructor( - string memory _name, - string memory _symbol, - uint8 _decimals, - bytes32 _merkleRoot - ) ERC20(_name, _symbol, _decimals) { - merkleRoot = _merkleRoot; // Update root - } + /// @notice Thrown if claim contract doesn't have enough tokens to payout + error NotEnoughRewards(); - /// ============ Events ============ + /// ============ Constructor ============ - /// @notice Emitted after a successful token claim - /// @param to recipient of claim - /// @param amount of tokens claimed - event Claim(address indexed to, uint256 amount); + /// @notice Creates a new MerkleClaimERC20 contract + /// @param _token address of airdropped token + /// @param _merkleRoot merkle root of claimees + constructor(IERC20 _token, bytes32 _merkleRoot) { + token = _token; + merkleRoot = _merkleRoot; + } - /// ============ Functions ============ + /// ============ Events ============ - /// @notice Allows claiming tokens if address is part of merkle tree - /// @param to address of claimee - /// @param amount of tokens owed to claimee - /// @param proof merkle proof to prove address and amount are in tree - function claim(address to, uint256 amount, bytes32[] calldata proof) external { - // Throw if address has already claimed tokens - if (hasClaimed[to]) revert AlreadyClaimed(); + /// @notice Emitted after a successful token claim + /// @param to recipient of claim + /// @param amount of tokens claimed + event Claim(address indexed to, uint256 amount); - // Verify merkle proof, or revert if not in tree - bytes32 leaf = keccak256(abi.encodePacked(to, amount)); - bool isValidLeaf = MerkleProof.verify(proof, merkleRoot, leaf); - if (!isValidLeaf) revert NotInMerkle(); + /// ============ Functions ============ - // Set address to claimed - hasClaimed[to] = true; + /// @notice Allows claiming tokens if address is part of merkle tree + /// @param to address of claimee + /// @param amount of tokens owed to claimee + /// @param proof merkle proof to prove address and amount are in tree + function claim(address to, uint256 amount, bytes32[] calldata proof) external { + // Throw if address has already claimed tokens + if (hasClaimed[to]) revert AlreadyClaimed(); - // Mint tokens to address - _mint(to, amount); + // Throw if the contract doesn't hold enough tokens for claimee + if (amount > token.balanceOf(address(this))) revert NotEnoughRewards(); - // Emit claim event - emit Claim(to, amount); - } + // Verify merkle proof, or revert if not in tree + bytes32 leaf = keccak256(abi.encodePacked(to, amount)); + bool isValidLeaf = MerkleProof.verify(proof, merkleRoot, leaf); + if (!isValidLeaf) revert NotInMerkle(); + + // Set address to claimed + hasClaimed[to] = true; + + // Award tokens to address + token.transfer(to, amount); + + // Emit claim event + emit Claim(to, amount); + } } diff --git a/contracts/src/OverlayV1Token.sol b/contracts/src/OverlayV1Token.sol new file mode 100644 index 0000000..5f078c7 --- /dev/null +++ b/contracts/src/OverlayV1Token.sol @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +import "@openzeppelin/token/ERC20/ERC20.sol"; +import "@openzeppelin/access/AccessControlEnumerable.sol"; + +import "./interfaces/IOverlayV1Token.sol"; + +contract OverlayV1Token is IOverlayV1Token, AccessControlEnumerable, ERC20("Overlay", "OVL") { + /// @notice indicates whether transfers are allowed for everyone or only whitelisted addresses + bool public transfersLocked; + + constructor() { + // Only whitelisted addresses can transfer by default + transfersLocked = true; + _grantRole(DEFAULT_ADMIN_ROLE, msg.sender); + } + + modifier onlyMinter() { + require(hasRole(MINTER_ROLE, msg.sender), "ERC20: !minter"); + _; + } + + modifier onlyBurner() { + require(hasRole(BURNER_ROLE, msg.sender), "ERC20: !burner"); + _; + } + + function mint(address _recipient, uint256 _amount) external onlyMinter { + _mint(_recipient, _amount); + } + + function burn(uint256 _amount) external onlyBurner { + _burn(msg.sender, _amount); + } + + function unlockTransfers() external onlyRole(DEFAULT_ADMIN_ROLE) { + transfersLocked = false; + } + + function _beforeTokenTransfer(address from, address to, uint256) + internal + view + override + { + require( + !transfersLocked || hasRole(TRANSFER_ROLE, from) || hasRole(TRANSFER_ROLE, to), + "ERC20: cannot transfer" + ); + } +} diff --git a/contracts/src/interfaces/IOverlayV1Token.sol b/contracts/src/interfaces/IOverlayV1Token.sol new file mode 100644 index 0000000..5930d08 --- /dev/null +++ b/contracts/src/interfaces/IOverlayV1Token.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +import "@openzeppelin/token/ERC20/IERC20.sol"; +import "@openzeppelin/access/IAccessControlEnumerable.sol"; + +bytes32 constant MINTER_ROLE = keccak256("MINTER"); +bytes32 constant BURNER_ROLE = keccak256("BURNER"); +bytes32 constant GOVERNOR_ROLE = keccak256("GOVERNOR"); +bytes32 constant GUARDIAN_ROLE = keccak256("GUARDIAN"); +bytes32 constant TRANSFER_ROLE = keccak256("TRANSFER"); + +interface IOverlayV1Token is IAccessControlEnumerable, IERC20 { + // mint/burn + function mint(address _recipient, uint256 _amount) external; + + function burn(uint256 _amount) external; + + function unlockTransfers() external; +} diff --git a/contracts/src/test/MerkleClaimERC20.t.sol b/contracts/src/test/MerkleClaimERC20.t.sol index bfc1eee..0df0d2a 100644 --- a/contracts/src/test/MerkleClaimERC20.t.sol +++ b/contracts/src/test/MerkleClaimERC20.t.sol @@ -3,139 +3,139 @@ pragma solidity >=0.8.0; /// ============ Imports ============ -import { MerkleClaimERC20Test } from "./utils/MerkleClaimERC20Test.sol"; // Test scaffolding +import {MerkleClaimERC20Test} from "./utils/MerkleClaimERC20Test.sol"; // Test scaffolding /// @title Tests /// @notice MerkleClaimERC20 tests /// @author Anish Agnihotri contract Tests is MerkleClaimERC20Test { - /// @notice Allow Alice to claim 100e18 tokens - function testAliceClaim() public { - // Setup correct proof for Alice - bytes32[] memory aliceProof = new bytes32[](1); - aliceProof[0] = 0xceeae64152a2deaf8c661fccd5645458ba20261b16d2f6e090fe908b0ac9ca88; - - // Collect Alice balance of tokens before claim - uint256 alicePreBalance = ALICE.tokenBalance(); - - // Claim tokens - ALICE.claim( - // Claiming for Alice - address(ALICE), - // 100 tokens - 100e18, - // With valid proof - aliceProof - ); - - // Collect Alice balance of tokens after claim - uint256 alicePostBalance = ALICE.tokenBalance(); - - // Assert Alice balance before + 100 tokens = after balance - assertEq(alicePostBalance, alicePreBalance + 100e18); - } - - /// @notice Prevent Alice from claiming twice - function testFailAliceClaimTwice() public { - // Setup correct proof for Alice - bytes32[] memory aliceProof = new bytes32[](1); - aliceProof[0] = 0xceeae64152a2deaf8c661fccd5645458ba20261b16d2f6e090fe908b0ac9ca88; - - // Claim tokens - ALICE.claim( - // Claiming for Alice - address(ALICE), - // 100 tokens - 100e18, - // With valid proof - aliceProof - ); - - // Claim tokens again - ALICE.claim( - // Claiming for Alice - address(ALICE), - // 100 tokens - 100e18, - // With valid proof - aliceProof - ); - } - - /// @notice Prevent Alice from claiming with invalid proof - function testFailAliceClaimInvalidProof() public { - // Setup incorrect proof for Alice - bytes32[] memory aliceProof = new bytes32[](1); - aliceProof[0] = 0xc11ae64152a2deaf8c661fccd5645458ba20261b16d2f6e090fe908b0ac9ca88; - - // Claim tokens - ALICE.claim( - // Claiming for Alice - address(ALICE), - // 100 tokens - 100e18, - // With valid proof - aliceProof - ); - } - - /// @notice Prevent Alice from claiming with invalid amount - function testFailAliceClaimInvalidAmount() public { - // Setup correct proof for Alice - bytes32[] memory aliceProof = new bytes32[](1); - aliceProof[0] = 0xceeae64152a2deaf8c661fccd5645458ba20261b16d2f6e090fe908b0ac9ca88; - - // Claim tokens - ALICE.claim( - // Claiming for Alice - address(ALICE), - // Incorrect: 1000 tokens - 1000e18, - // With valid proof (for 100 tokens) - aliceProof - ); - } - - /// @notice Prevent Bob from claiming - function testFailBobClaim() public { - // Setup correct proof for Alice - bytes32[] memory aliceProof = new bytes32[](1); - aliceProof[0] = 0xceeae64152a2deaf8c661fccd5645458ba20261b16d2f6e090fe908b0ac9ca88; - - // Claim tokens - BOB.claim( - // Claiming for Bob - address(BOB), - // 100 tokens - 100e18, - // With valid proof (for Alice) - aliceProof - ); - } - - /// @notice Let Bob claim on behalf of Alice - function testBobClaimForAlice() public { - // Setup correct proof for Alice - bytes32[] memory aliceProof = new bytes32[](1); - aliceProof[0] = 0xceeae64152a2deaf8c661fccd5645458ba20261b16d2f6e090fe908b0ac9ca88; - - // Collect Alice balance of tokens before claim - uint256 alicePreBalance = ALICE.tokenBalance(); - - // Claim tokens - BOB.claim( - // Claiming for Alice - address(ALICE), - // 100 tokens - 100e18, - // With valid proof (for Alice) - aliceProof - ); - - // Collect Alice balance of tokens after claim - uint256 alicePostBalance = ALICE.tokenBalance(); - - // Assert Alice balance before + 100 tokens = after balance - assertEq(alicePostBalance, alicePreBalance + 100e18); - } + /// @notice Allow Alice to claim 100e18 tokens + function testAliceClaim() public { + // Setup correct proof for Alice + bytes32[] memory aliceProof = new bytes32[](1); + aliceProof[0] = 0xceeae64152a2deaf8c661fccd5645458ba20261b16d2f6e090fe908b0ac9ca88; + + // Collect Alice balance of tokens before claim + uint256 alicePreBalance = ALICE.tokenBalance(); + + // Claim tokens + ALICE.claim( + // Claiming for Alice + address(ALICE), + // 100 tokens + 100e18, + // With valid proof + aliceProof + ); + + // Collect Alice balance of tokens after claim + uint256 alicePostBalance = ALICE.tokenBalance(); + + // Assert Alice balance before + 100 tokens = after balance + assertEq(alicePostBalance, alicePreBalance + 100e18); + } + + /// @notice Prevent Alice from claiming twice + function testFailAliceClaimTwice() public { + // Setup correct proof for Alice + bytes32[] memory aliceProof = new bytes32[](1); + aliceProof[0] = 0xceeae64152a2deaf8c661fccd5645458ba20261b16d2f6e090fe908b0ac9ca88; + + // Claim tokens + ALICE.claim( + // Claiming for Alice + address(ALICE), + // 100 tokens + 100e18, + // With valid proof + aliceProof + ); + + // Claim tokens again + ALICE.claim( + // Claiming for Alice + address(ALICE), + // 100 tokens + 100e18, + // With valid proof + aliceProof + ); + } + + /// @notice Prevent Alice from claiming with invalid proof + function testFailAliceClaimInvalidProof() public { + // Setup incorrect proof for Alice + bytes32[] memory aliceProof = new bytes32[](1); + aliceProof[0] = 0xc11ae64152a2deaf8c661fccd5645458ba20261b16d2f6e090fe908b0ac9ca88; + + // Claim tokens + ALICE.claim( + // Claiming for Alice + address(ALICE), + // 100 tokens + 100e18, + // With valid proof + aliceProof + ); + } + + /// @notice Prevent Alice from claiming with invalid amount + function testFailAliceClaimInvalidAmount() public { + // Setup correct proof for Alice + bytes32[] memory aliceProof = new bytes32[](1); + aliceProof[0] = 0xceeae64152a2deaf8c661fccd5645458ba20261b16d2f6e090fe908b0ac9ca88; + + // Claim tokens + ALICE.claim( + // Claiming for Alice + address(ALICE), + // Incorrect: 1000 tokens + 1000e18, + // With valid proof (for 100 tokens) + aliceProof + ); + } + + /// @notice Prevent Bob from claiming + function testFailBobClaim() public { + // Setup correct proof for Alice + bytes32[] memory aliceProof = new bytes32[](1); + aliceProof[0] = 0xceeae64152a2deaf8c661fccd5645458ba20261b16d2f6e090fe908b0ac9ca88; + + // Claim tokens + BOB.claim( + // Claiming for Bob + address(BOB), + // 100 tokens + 100e18, + // With valid proof (for Alice) + aliceProof + ); + } + + /// @notice Let Bob claim on behalf of Alice + function testBobClaimForAlice() public { + // Setup correct proof for Alice + bytes32[] memory aliceProof = new bytes32[](1); + aliceProof[0] = 0xceeae64152a2deaf8c661fccd5645458ba20261b16d2f6e090fe908b0ac9ca88; + + // Collect Alice balance of tokens before claim + uint256 alicePreBalance = ALICE.tokenBalance(); + + // Claim tokens + BOB.claim( + // Claiming for Alice + address(ALICE), + // 100 tokens + 100e18, + // With valid proof (for Alice) + aliceProof + ); + + // Collect Alice balance of tokens after claim + uint256 alicePostBalance = ALICE.tokenBalance(); + + // Assert Alice balance before + 100 tokens = after balance + assertEq(alicePostBalance, alicePreBalance + 100e18); + } } diff --git a/contracts/src/test/utils/MerkleClaimERC20Test.sol b/contracts/src/test/utils/MerkleClaimERC20Test.sol index 114e3e9..7954318 100644 --- a/contracts/src/test/utils/MerkleClaimERC20Test.sol +++ b/contracts/src/test/utils/MerkleClaimERC20Test.sol @@ -3,38 +3,38 @@ pragma solidity >=0.8.0; /// ============ Imports ============ -import { DSTest } from "ds-test/test.sol"; // DSTest -import { MerkleClaimERC20 } from "../../MerkleClaimERC20.sol"; // MerkleClaimERC20 -import { MerkleClaimERC20User } from "./MerkleClaimERC20User.sol"; // MerkleClaimERC20 user +import {DSTest} from "ds-test/test.sol"; // DSTest +import {MerkleClaimERC20} from "../../MerkleClaimERC20.sol"; // MerkleClaimERC20 +import {OverlayV1Token} from "../../OverlayV1Token.sol"; // OverlayV1Token +import {MerkleClaimERC20User} from "./MerkleClaimERC20User.sol"; // MerkleClaimERC20 user /// @title MerkleClaimERC20Test /// @notice Scaffolding for MerkleClaimERC20 tests /// @author Anish Agnihotri contract MerkleClaimERC20Test is DSTest { - - /// ============ Storage ============ - - /// @dev MerkleClaimERC20 contract - MerkleClaimERC20 internal TOKEN; - /// @dev User: Alice (in merkle tree) - MerkleClaimERC20User internal ALICE; - /// @dev User: Bob (not in merkle tree) - MerkleClaimERC20User internal BOB; - - /// ============ Setup test suite ============ - - function setUp() public virtual { - // Create airdrop token - TOKEN = new MerkleClaimERC20( - "My Token", - "MT", - 18, - // Merkle root containing ALICE with 100e18 tokens but no BOB - 0xd0aa6a4e5b4e13462921d7518eebdb7b297a7877d6cfe078b0c318827392fb55 - ); - - // Setup airdrop users - ALICE = new MerkleClaimERC20User(TOKEN); // 0x185a4dc360ce69bdccee33b3784b0282f7961aea - BOB = new MerkleClaimERC20User(TOKEN); // 0xefc56627233b02ea95bae7e19f648d7dcd5bb132 - } + /// ============ Storage ============ + + /// @dev MerkleClaimERC20 contract + MerkleClaimERC20 internal TOKEN; + /// @dev User: Alice (in merkle tree) + MerkleClaimERC20User internal ALICE; + /// @dev User: Bob (not in merkle tree) + MerkleClaimERC20User internal BOB; + + /// ============ Setup test suite ============ + + function setUp() public virtual { + address ovl = address(new OverlayV1Token()); + + // Create airdrop token + TOKEN = new MerkleClaimERC20( + ovl, + // Merkle root containing ALICE with 100e18 tokens but no BOB + 0xd0aa6a4e5b4e13462921d7518eebdb7b297a7877d6cfe078b0c318827392fb55 + ); + + // Setup airdrop users + ALICE = new MerkleClaimERC20User(TOKEN); // 0x185a4dc360ce69bdccee33b3784b0282f7961aea + BOB = new MerkleClaimERC20User(TOKEN); // 0xefc56627233b02ea95bae7e19f648d7dcd5bb132 + } } diff --git a/contracts/src/test/utils/MerkleClaimERC20User.sol b/contracts/src/test/utils/MerkleClaimERC20User.sol index e7d5bfe..e35d578 100644 --- a/contracts/src/test/utils/MerkleClaimERC20User.sol +++ b/contracts/src/test/utils/MerkleClaimERC20User.sol @@ -3,40 +3,39 @@ pragma solidity >=0.8.0; /// ============ Imports ============ -import { MerkleClaimERC20 } from "../../MerkleClaimERC20.sol"; // MerkleClaimERC20 +import {MerkleClaimERC20} from "../../MerkleClaimERC20.sol"; // MerkleClaimERC20 /// @title MerkleClaimERC20User /// @notice Mock MerkleClaimERC20 user /// @author Anish Agnihotri contract MerkleClaimERC20User { + /// ============ Immutable storage ============ - /// ============ Immutable storage ============ + /// @dev MerkleClaimERC20 contract + MerkleClaimERC20 internal immutable TOKEN; - /// @dev MerkleClaimERC20 contract - MerkleClaimERC20 immutable internal TOKEN; + /// ============ Constructor ============ - /// ============ Constructor ============ + /// @notice Creates a new MerkleClaimERC20User + /// @param _TOKEN MerkleClaimERC20 contract + constructor(MerkleClaimERC20 _TOKEN) { + TOKEN = _TOKEN; + } - /// @notice Creates a new MerkleClaimERC20User - /// @param _TOKEN MerkleClaimERC20 contract - constructor(MerkleClaimERC20 _TOKEN) { - TOKEN = _TOKEN; - } + /// ============ Helper functions ============ - /// ============ Helper functions ============ + /// @notice Returns users' token balance + function tokenBalance() public view returns (uint256) { + return TOKEN.token().balanceOf(address(this)); + } - /// @notice Returns users' token balance - function tokenBalance() public view returns (uint256) { - return TOKEN.balanceOf(address(this)); - } + /// ============ Inherited functionality ============ - /// ============ Inherited functionality ============ - - /// @notice Allows user to claim tokens from contract - /// @param to address of claimee - /// @param amount of tokens owed to claimee - /// @param proof merkle proof to prove address and amount are in tree - function claim(address to, uint256 amount, bytes32[] calldata proof) public { - TOKEN.claim(to, amount, proof); - } -} \ No newline at end of file + /// @notice Allows user to claim tokens from contract + /// @param to address of claimee + /// @param amount of tokens owed to claimee + /// @param proof merkle proof to prove address and amount are in tree + function claim(address to, uint256 amount, bytes32[] calldata proof) public { + TOKEN.claim(to, amount, proof); + } +} From b6e01f18296b847af71374692c9e69428ccc7b93 Mon Sep 17 00:00:00 2001 From: Ezequiel Date: Thu, 26 Oct 2023 15:52:07 -0300 Subject: [PATCH 03/13] forge install: openzeppelin-contracts v4.9.3 --- .gitmodules | 3 +++ contracts/lib/openzeppelin-contracts | 1 + 2 files changed, 4 insertions(+) create mode 160000 contracts/lib/openzeppelin-contracts diff --git a/.gitmodules b/.gitmodules index 7ccd2eb..287443a 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "contracts/lib/ds-test"] path = contracts/lib/ds-test url = https://github.com/dapphub/ds-test +[submodule "contracts/lib/openzeppelin-contracts"] + path = contracts/lib/openzeppelin-contracts + url = https://github.com/OpenZeppelin/openzeppelin-contracts diff --git a/contracts/lib/openzeppelin-contracts b/contracts/lib/openzeppelin-contracts new file mode 160000 index 0000000..fd81a96 --- /dev/null +++ b/contracts/lib/openzeppelin-contracts @@ -0,0 +1 @@ +Subproject commit fd81a96f01cc42ef1c9a5399364968d0e07e9e90 From ce01a83c21ba6632cc25624f5a21ecc389cbd48c Mon Sep 17 00:00:00 2001 From: Ezequiel Date: Thu, 26 Oct 2023 15:53:55 -0300 Subject: [PATCH 04/13] fix: compile errors --- contracts/src/test/utils/MerkleClaimERC20Test.sol | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/contracts/src/test/utils/MerkleClaimERC20Test.sol b/contracts/src/test/utils/MerkleClaimERC20Test.sol index 7954318..1952143 100644 --- a/contracts/src/test/utils/MerkleClaimERC20Test.sol +++ b/contracts/src/test/utils/MerkleClaimERC20Test.sol @@ -4,6 +4,7 @@ pragma solidity >=0.8.0; /// ============ Imports ============ import {DSTest} from "ds-test/test.sol"; // DSTest +import {IERC20} from "@openzeppelin/token/ERC20/IERC20.sol"; import {MerkleClaimERC20} from "../../MerkleClaimERC20.sol"; // MerkleClaimERC20 import {OverlayV1Token} from "../../OverlayV1Token.sol"; // OverlayV1Token import {MerkleClaimERC20User} from "./MerkleClaimERC20User.sol"; // MerkleClaimERC20 user @@ -28,7 +29,7 @@ contract MerkleClaimERC20Test is DSTest { // Create airdrop token TOKEN = new MerkleClaimERC20( - ovl, + IERC20(ovl), // Merkle root containing ALICE with 100e18 tokens but no BOB 0xd0aa6a4e5b4e13462921d7518eebdb7b297a7877d6cfe078b0c318827392fb55 ); From 7e6e19db0b94d313f98f116d1ac9eaaab25c77dd Mon Sep 17 00:00:00 2001 From: Ezequiel Date: Thu, 26 Oct 2023 16:19:59 -0300 Subject: [PATCH 05/13] chore: forge init --- contracts/.github/workflows/test.yml | 34 +++++++++ contracts/.gitignore | 14 ++++ contracts/README.md | 74 +++++++++++++------ contracts/script/Counter.s.sol | 12 +++ contracts/src/Counter.sol | 14 ++++ .../src/test/utils/MerkleClaimERC20Test.sol | 16 +++- contracts/test/Counter.t.sol | 24 ++++++ 7 files changed, 162 insertions(+), 26 deletions(-) create mode 100644 contracts/.github/workflows/test.yml create mode 100644 contracts/.gitignore create mode 100644 contracts/script/Counter.s.sol create mode 100644 contracts/src/Counter.sol create mode 100644 contracts/test/Counter.t.sol diff --git a/contracts/.github/workflows/test.yml b/contracts/.github/workflows/test.yml new file mode 100644 index 0000000..09880b1 --- /dev/null +++ b/contracts/.github/workflows/test.yml @@ -0,0 +1,34 @@ +name: test + +on: workflow_dispatch + +env: + FOUNDRY_PROFILE: ci + +jobs: + check: + strategy: + fail-fast: true + + name: Foundry project + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + submodules: recursive + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + with: + version: nightly + + - name: Run Forge build + run: | + forge --version + forge build --sizes + id: build + + - name: Run Forge tests + run: | + forge test -vvv + id: test diff --git a/contracts/.gitignore b/contracts/.gitignore new file mode 100644 index 0000000..85198aa --- /dev/null +++ b/contracts/.gitignore @@ -0,0 +1,14 @@ +# Compiler files +cache/ +out/ + +# Ignores development broadcast logs +!/broadcast +/broadcast/*/31337/ +/broadcast/**/dry-run/ + +# Docs +docs/ + +# Dotenv file +.env diff --git a/contracts/README.md b/contracts/README.md index ae8da10..9265b45 100644 --- a/contracts/README.md +++ b/contracts/README.md @@ -1,36 +1,66 @@ -# MerkleClaimERC20 +## Foundry -ERC20 token claimable by members of a [Merkle tree](https://en.wikipedia.org/wiki/Merkle_tree). Useful for conducting Airdrops. Utilizes [Solmate ERC20](https://github.com/Rari-Capital/solmate/blob/main/src/tokens/ERC20.sol) for modern ERC20 token implementation. +**Foundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.** -## Test +Foundry consists of: -Tests use [Foundry: Forge](https://github.com/gakonst/foundry). +- **Forge**: Ethereum testing framework (like Truffle, Hardhat and DappTools). +- **Cast**: Swiss army knife for interacting with EVM smart contracts, sending transactions and getting chain data. +- **Anvil**: Local Ethereum node, akin to Ganache, Hardhat Network. +- **Chisel**: Fast, utilitarian, and verbose solidity REPL. -Install Foundry using the installation steps in the README of the linked repo. +## Documentation -### Run tests +https://book.getfoundry.sh/ -```bash -# Go to contracts directory, if not already there -cd contracts/ +## Usage -# Get dependencies -forge update +### Build -# Run tests -forge test --root . -# Run tests with stack traces -forge test --root . -vvvv +```shell +$ forge build ``` -## Deploy +### Test -Follow the `forge create` instructions ([CLI README](https://github.com/gakonst/foundry/blob/master/cli/README.md#build)) to deploy your contracts or use [Remix](https://remix.ethereum.org/). +```shell +$ forge test +``` + +### Format + +```shell +$ forge fmt +``` + +### Gas Snapshots + +```shell +$ forge snapshot +``` + +### Anvil -You can specify the token `name`, `symbol`, `decimals`, and airdrop `merkleRoot` upon deploy. +```shell +$ anvil +``` + +### Deploy + +```shell +$ forge script script/Counter.s.sol:CounterScript --rpc-url --private-key +``` -## Credits +### Cast -- [@brockelmore](https://github.com/Anish-Agnihotri/merkle-airdrop-starter/issues?q=is%3Apr+author%3Abrockelmore) for [#1](https://github.com/Anish-Agnihotri/merkle-airdrop-starter/pull/1) -- [@transmissions11](https://github.com/Anish-Agnihotri/merkle-airdrop-starter/issues?q=is%3Apr+author%3Atransmissions11) for [#2](https://github.com/Anish-Agnihotri/merkle-airdrop-starter/pull/2) -- [@devanonon](https://github.com/Anish-Agnihotri/merkle-airdrop-starter/issues?q=is%3Apr+author%3Adevanonon) for [#3](https://github.com/Anish-Agnihotri/merkle-airdrop-starter/pull/8) +```shell +$ cast +``` + +### Help + +```shell +$ forge --help +$ anvil --help +$ cast --help +``` diff --git a/contracts/script/Counter.s.sol b/contracts/script/Counter.s.sol new file mode 100644 index 0000000..1a47b40 --- /dev/null +++ b/contracts/script/Counter.s.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {Script, console2} from "forge-std/Script.sol"; + +contract CounterScript is Script { + function setUp() public {} + + function run() public { + vm.broadcast(); + } +} diff --git a/contracts/src/Counter.sol b/contracts/src/Counter.sol new file mode 100644 index 0000000..aded799 --- /dev/null +++ b/contracts/src/Counter.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +contract Counter { + uint256 public number; + + function setNumber(uint256 newNumber) public { + number = newNumber; + } + + function increment() public { + number++; + } +} diff --git a/contracts/src/test/utils/MerkleClaimERC20Test.sol b/contracts/src/test/utils/MerkleClaimERC20Test.sol index 1952143..ce48b79 100644 --- a/contracts/src/test/utils/MerkleClaimERC20Test.sol +++ b/contracts/src/test/utils/MerkleClaimERC20Test.sol @@ -3,7 +3,7 @@ pragma solidity >=0.8.0; /// ============ Imports ============ -import {DSTest} from "ds-test/test.sol"; // DSTest +import {Test} from "forge-std/Test.sol"; import {IERC20} from "@openzeppelin/token/ERC20/IERC20.sol"; import {MerkleClaimERC20} from "../../MerkleClaimERC20.sol"; // MerkleClaimERC20 import {OverlayV1Token} from "../../OverlayV1Token.sol"; // OverlayV1Token @@ -12,15 +12,15 @@ import {MerkleClaimERC20User} from "./MerkleClaimERC20User.sol"; // MerkleClaimE /// @title MerkleClaimERC20Test /// @notice Scaffolding for MerkleClaimERC20 tests /// @author Anish Agnihotri -contract MerkleClaimERC20Test is DSTest { +contract MerkleClaimERC20Test is Test { /// ============ Storage ============ /// @dev MerkleClaimERC20 contract MerkleClaimERC20 internal TOKEN; /// @dev User: Alice (in merkle tree) - MerkleClaimERC20User internal ALICE; + MerkleClaimERC20User internal ALICE = MerkleClaimERC20User(0x185a4dc360CE69bDCceE33b3784B0282f7961aea); /// @dev User: Bob (not in merkle tree) - MerkleClaimERC20User internal BOB; + MerkleClaimERC20User internal BOB = MerkleClaimERC20User(0xEFc56627233b02eA95bAE7e19F648d7DcD5Bb132); /// ============ Setup test suite ============ @@ -34,8 +34,16 @@ contract MerkleClaimERC20Test is DSTest { 0xd0aa6a4e5b4e13462921d7518eebdb7b297a7877d6cfe078b0c318827392fb55 ); + deal(ovl, address(TOKEN), 100e18); // total amount of tokens to be claimed + // Setup airdrop users ALICE = new MerkleClaimERC20User(TOKEN); // 0x185a4dc360ce69bdccee33b3784b0282f7961aea BOB = new MerkleClaimERC20User(TOKEN); // 0xefc56627233b02ea95bae7e19f648d7dcd5bb132 + + // deployCodeTo("MerkleClaimERC20.sol", abi.encode(TOKEN), address(ALICE)); + // deployCodeTo("MerkleClaimERC20.sol", abi.encode(TOKEN), address(BOB)); + + emit log_named_address("Alice", address(ALICE)); + emit log_named_address("Bob", address(BOB)); } } diff --git a/contracts/test/Counter.t.sol b/contracts/test/Counter.t.sol new file mode 100644 index 0000000..e9b9e6a --- /dev/null +++ b/contracts/test/Counter.t.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {Test, console2} from "forge-std/Test.sol"; +import {Counter} from "../src/Counter.sol"; + +contract CounterTest is Test { + Counter public counter; + + function setUp() public { + counter = new Counter(); + counter.setNumber(0); + } + + function test_Increment() public { + counter.increment(); + assertEq(counter.number(), 1); + } + + function testFuzz_SetNumber(uint256 x) public { + counter.setNumber(x); + assertEq(counter.number(), x); + } +} From 6c756ea7dc99791adab94bde1963097ec32752bd Mon Sep 17 00:00:00 2001 From: Ezequiel Date: Thu, 26 Oct 2023 16:20:03 -0300 Subject: [PATCH 06/13] forge install: forge-std v1.7.1 --- .gitmodules | 3 +++ contracts/lib/forge-std | 1 + 2 files changed, 4 insertions(+) create mode 160000 contracts/lib/forge-std diff --git a/.gitmodules b/.gitmodules index 287443a..d4e9962 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,6 @@ [submodule "contracts/lib/openzeppelin-contracts"] path = contracts/lib/openzeppelin-contracts url = https://github.com/OpenZeppelin/openzeppelin-contracts +[submodule "contracts/lib/forge-std"] + path = contracts/lib/forge-std + url = https://github.com/foundry-rs/forge-std diff --git a/contracts/lib/forge-std b/contracts/lib/forge-std new file mode 160000 index 0000000..f73c73d --- /dev/null +++ b/contracts/lib/forge-std @@ -0,0 +1 @@ +Subproject commit f73c73d2018eb6a111f35e4dae7b4f27401e9421 From f576e22fdf5dfce9f98379cc1d3443ca77b7dbd8 Mon Sep 17 00:00:00 2001 From: Ezequiel Date: Thu, 26 Oct 2023 16:22:03 -0300 Subject: [PATCH 07/13] chore: forge init --- contracts/remappings.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/contracts/remappings.txt b/contracts/remappings.txt index 1e28f55..802d475 100644 --- a/contracts/remappings.txt +++ b/contracts/remappings.txt @@ -1,3 +1,4 @@ ds-test/=lib/ds-test/src/ +forge-std/=lib/forge-std/src/ @solmate/=lib/solmate/src/ @openzeppelin/=lib/openzeppelin-contracts/contracts/ \ No newline at end of file From 99ed416099069c1780b661e8542c3a1c2403cf2d Mon Sep 17 00:00:00 2001 From: Ezequiel Date: Thu, 26 Oct 2023 16:30:58 -0300 Subject: [PATCH 08/13] fix: broken tests --- contracts/script/Counter.s.sol | 12 ---------- contracts/src/Counter.sol | 14 ----------- .../src/test/utils/MerkleClaimERC20Test.sol | 22 +++++++---------- contracts/test/Counter.t.sol | 24 ------------------- 4 files changed, 9 insertions(+), 63 deletions(-) delete mode 100644 contracts/script/Counter.s.sol delete mode 100644 contracts/src/Counter.sol delete mode 100644 contracts/test/Counter.t.sol diff --git a/contracts/script/Counter.s.sol b/contracts/script/Counter.s.sol deleted file mode 100644 index 1a47b40..0000000 --- a/contracts/script/Counter.s.sol +++ /dev/null @@ -1,12 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -import {Script, console2} from "forge-std/Script.sol"; - -contract CounterScript is Script { - function setUp() public {} - - function run() public { - vm.broadcast(); - } -} diff --git a/contracts/src/Counter.sol b/contracts/src/Counter.sol deleted file mode 100644 index aded799..0000000 --- a/contracts/src/Counter.sol +++ /dev/null @@ -1,14 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -contract Counter { - uint256 public number; - - function setNumber(uint256 newNumber) public { - number = newNumber; - } - - function increment() public { - number++; - } -} diff --git a/contracts/src/test/utils/MerkleClaimERC20Test.sol b/contracts/src/test/utils/MerkleClaimERC20Test.sol index ce48b79..dbee823 100644 --- a/contracts/src/test/utils/MerkleClaimERC20Test.sol +++ b/contracts/src/test/utils/MerkleClaimERC20Test.sol @@ -4,8 +4,9 @@ pragma solidity >=0.8.0; /// ============ Imports ============ import {Test} from "forge-std/Test.sol"; -import {IERC20} from "@openzeppelin/token/ERC20/IERC20.sol"; + import {MerkleClaimERC20} from "../../MerkleClaimERC20.sol"; // MerkleClaimERC20 +import {IOverlayV1Token, TRANSFER_ROLE} from "../../interfaces/IOverlayV1Token.sol"; // /iOverlayV1Token import {OverlayV1Token} from "../../OverlayV1Token.sol"; // OverlayV1Token import {MerkleClaimERC20User} from "./MerkleClaimERC20User.sol"; // MerkleClaimERC20 user @@ -25,25 +26,20 @@ contract MerkleClaimERC20Test is Test { /// ============ Setup test suite ============ function setUp() public virtual { - address ovl = address(new OverlayV1Token()); + OverlayV1Token ovl = new OverlayV1Token(); // Create airdrop token TOKEN = new MerkleClaimERC20( - IERC20(ovl), + ovl, // Merkle root containing ALICE with 100e18 tokens but no BOB 0xd0aa6a4e5b4e13462921d7518eebdb7b297a7877d6cfe078b0c318827392fb55 ); - deal(ovl, address(TOKEN), 100e18); // total amount of tokens to be claimed - - // Setup airdrop users - ALICE = new MerkleClaimERC20User(TOKEN); // 0x185a4dc360ce69bdccee33b3784b0282f7961aea - BOB = new MerkleClaimERC20User(TOKEN); // 0xefc56627233b02ea95bae7e19f648d7dcd5bb132 - - // deployCodeTo("MerkleClaimERC20.sol", abi.encode(TOKEN), address(ALICE)); - // deployCodeTo("MerkleClaimERC20.sol", abi.encode(TOKEN), address(BOB)); + ovl.grantRole(TRANSFER_ROLE, address(TOKEN)); // allow contract to transfer OVL + deal(address(ovl), address(TOKEN), 100e18); // total amount of tokens to be claimed - emit log_named_address("Alice", address(ALICE)); - emit log_named_address("Bob", address(BOB)); + // Setup airdrop users with custom addresses (to match merkle tree) + deployCodeTo("MerkleClaimERC20User.sol", abi.encode(TOKEN), address(ALICE)); + deployCodeTo("MerkleClaimERC20User.sol", abi.encode(TOKEN), address(BOB)); } } diff --git a/contracts/test/Counter.t.sol b/contracts/test/Counter.t.sol deleted file mode 100644 index e9b9e6a..0000000 --- a/contracts/test/Counter.t.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -import {Test, console2} from "forge-std/Test.sol"; -import {Counter} from "../src/Counter.sol"; - -contract CounterTest is Test { - Counter public counter; - - function setUp() public { - counter = new Counter(); - counter.setNumber(0); - } - - function test_Increment() public { - counter.increment(); - assertEq(counter.number(), 1); - } - - function testFuzz_SetNumber(uint256 x) public { - counter.setNumber(x); - assertEq(counter.number(), x); - } -} From 2a3113789bbab45a2b90ad34826af648bf2dcbc6 Mon Sep 17 00:00:00 2001 From: Ezequiel Date: Thu, 26 Oct 2023 16:39:15 -0300 Subject: [PATCH 09/13] fix: README --- contracts/README.md | 74 ++++++++---------------------- contracts/src/MerkleClaimERC20.sol | 2 +- 2 files changed, 20 insertions(+), 56 deletions(-) diff --git a/contracts/README.md b/contracts/README.md index 9265b45..e45b05f 100644 --- a/contracts/README.md +++ b/contracts/README.md @@ -1,66 +1,30 @@ -## Foundry +# MerkleClaimERC20 -**Foundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.** +ERC20 token claimable by members of a [Merkle tree](https://en.wikipedia.org/wiki/Merkle_tree). Useful for conducting Airdrops. Utilizes [Solmate ERC20](https://github.com/Rari-Capital/solmate/blob/main/src/tokens/ERC20.sol) for modern ERC20 token implementation. -Foundry consists of: +## Test -- **Forge**: Ethereum testing framework (like Truffle, Hardhat and DappTools). -- **Cast**: Swiss army knife for interacting with EVM smart contracts, sending transactions and getting chain data. -- **Anvil**: Local Ethereum node, akin to Ganache, Hardhat Network. -- **Chisel**: Fast, utilitarian, and verbose solidity REPL. +Tests use [Foundry: Forge](https://github.com/gakonst/foundry). -## Documentation +Install Foundry using the installation steps in the README of the linked repo. -https://book.getfoundry.sh/ +### Run tests -## Usage +```bash +# Go to contracts directory, if not already there +cd contracts/ -### Build +# Get dependencies +forge update -```shell -$ forge build +# Run tests +forge test --root . +# Run tests with stack traces +forge test --root . -vvvv ``` -### Test +## Credits -```shell -$ forge test -``` - -### Format - -```shell -$ forge fmt -``` - -### Gas Snapshots - -```shell -$ forge snapshot -``` - -### Anvil - -```shell -$ anvil -``` - -### Deploy - -```shell -$ forge script script/Counter.s.sol:CounterScript --rpc-url --private-key -``` - -### Cast - -```shell -$ cast -``` - -### Help - -```shell -$ forge --help -$ anvil --help -$ cast --help -``` +- [@brockelmore](https://github.com/Anish-Agnihotri/merkle-airdrop-starter/issues?q=is%3Apr+author%3Abrockelmore) for [#1](https://github.com/Anish-Agnihotri/merkle-airdrop-starter/pull/1) +- [@transmissions11](https://github.com/Anish-Agnihotri/merkle-airdrop-starter/issues?q=is%3Apr+author%3Atransmissions11) for [#2](https://github.com/Anish-Agnihotri/merkle-airdrop-starter/pull/2) +- [@devanonon](https://github.com/Anish-Agnihotri/merkle-airdrop-starter/issues?q=is%3Apr+author%3Adevanonon) for [#3](https://github.com/Anish-Agnihotri/merkle-airdrop-starter/pull/8) diff --git a/contracts/src/MerkleClaimERC20.sol b/contracts/src/MerkleClaimERC20.sol index ac9fda2..b58f4f6 100644 --- a/contracts/src/MerkleClaimERC20.sol +++ b/contracts/src/MerkleClaimERC20.sol @@ -65,7 +65,7 @@ contract MerkleClaimERC20 { // Verify merkle proof, or revert if not in tree bytes32 leaf = keccak256(abi.encodePacked(to, amount)); - bool isValidLeaf = MerkleProof.verify(proof, merkleRoot, leaf); + bool isValidLeaf = MerkleProof.verifyCalldata(proof, merkleRoot, leaf); if (!isValidLeaf) revert NotInMerkle(); // Set address to claimed From 88e10df2bb4cd423cde40202320c799ab1023346 Mon Sep 17 00:00:00 2001 From: Ezequiel Date: Fri, 10 Nov 2023 21:04:41 -0300 Subject: [PATCH 10/13] chore: roll back ovl token changes --- contracts/src/OverlayV1Token.sol | 20 ------------------- contracts/src/interfaces/IOverlayV1Token.sol | 3 --- .../src/test/utils/MerkleClaimERC20Test.sol | 2 -- 3 files changed, 25 deletions(-) diff --git a/contracts/src/OverlayV1Token.sol b/contracts/src/OverlayV1Token.sol index 5f078c7..4e2664f 100644 --- a/contracts/src/OverlayV1Token.sol +++ b/contracts/src/OverlayV1Token.sol @@ -7,12 +7,7 @@ import "@openzeppelin/access/AccessControlEnumerable.sol"; import "./interfaces/IOverlayV1Token.sol"; contract OverlayV1Token is IOverlayV1Token, AccessControlEnumerable, ERC20("Overlay", "OVL") { - /// @notice indicates whether transfers are allowed for everyone or only whitelisted addresses - bool public transfersLocked; - constructor() { - // Only whitelisted addresses can transfer by default - transfersLocked = true; _grantRole(DEFAULT_ADMIN_ROLE, msg.sender); } @@ -33,19 +28,4 @@ contract OverlayV1Token is IOverlayV1Token, AccessControlEnumerable, ERC20("Over function burn(uint256 _amount) external onlyBurner { _burn(msg.sender, _amount); } - - function unlockTransfers() external onlyRole(DEFAULT_ADMIN_ROLE) { - transfersLocked = false; - } - - function _beforeTokenTransfer(address from, address to, uint256) - internal - view - override - { - require( - !transfersLocked || hasRole(TRANSFER_ROLE, from) || hasRole(TRANSFER_ROLE, to), - "ERC20: cannot transfer" - ); - } } diff --git a/contracts/src/interfaces/IOverlayV1Token.sol b/contracts/src/interfaces/IOverlayV1Token.sol index 5930d08..2e2ce52 100644 --- a/contracts/src/interfaces/IOverlayV1Token.sol +++ b/contracts/src/interfaces/IOverlayV1Token.sol @@ -8,13 +8,10 @@ bytes32 constant MINTER_ROLE = keccak256("MINTER"); bytes32 constant BURNER_ROLE = keccak256("BURNER"); bytes32 constant GOVERNOR_ROLE = keccak256("GOVERNOR"); bytes32 constant GUARDIAN_ROLE = keccak256("GUARDIAN"); -bytes32 constant TRANSFER_ROLE = keccak256("TRANSFER"); interface IOverlayV1Token is IAccessControlEnumerable, IERC20 { // mint/burn function mint(address _recipient, uint256 _amount) external; function burn(uint256 _amount) external; - - function unlockTransfers() external; } diff --git a/contracts/src/test/utils/MerkleClaimERC20Test.sol b/contracts/src/test/utils/MerkleClaimERC20Test.sol index dbee823..e10be7d 100644 --- a/contracts/src/test/utils/MerkleClaimERC20Test.sol +++ b/contracts/src/test/utils/MerkleClaimERC20Test.sol @@ -6,7 +6,6 @@ pragma solidity >=0.8.0; import {Test} from "forge-std/Test.sol"; import {MerkleClaimERC20} from "../../MerkleClaimERC20.sol"; // MerkleClaimERC20 -import {IOverlayV1Token, TRANSFER_ROLE} from "../../interfaces/IOverlayV1Token.sol"; // /iOverlayV1Token import {OverlayV1Token} from "../../OverlayV1Token.sol"; // OverlayV1Token import {MerkleClaimERC20User} from "./MerkleClaimERC20User.sol"; // MerkleClaimERC20 user @@ -35,7 +34,6 @@ contract MerkleClaimERC20Test is Test { 0xd0aa6a4e5b4e13462921d7518eebdb7b297a7877d6cfe078b0c318827392fb55 ); - ovl.grantRole(TRANSFER_ROLE, address(TOKEN)); // allow contract to transfer OVL deal(address(ovl), address(TOKEN), 100e18); // total amount of tokens to be claimed // Setup airdrop users with custom addresses (to match merkle tree) From 8a2df66cdf79348f799ca0cedde7939dd71ac077 Mon Sep 17 00:00:00 2001 From: Ezequiel Date: Fri, 10 Nov 2023 21:08:27 -0300 Subject: [PATCH 11/13] fix: do not deal directly to the merkle claim contract --- contracts/src/test/utils/MerkleClaimERC20Test.sol | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/contracts/src/test/utils/MerkleClaimERC20Test.sol b/contracts/src/test/utils/MerkleClaimERC20Test.sol index e10be7d..3acd061 100644 --- a/contracts/src/test/utils/MerkleClaimERC20Test.sol +++ b/contracts/src/test/utils/MerkleClaimERC20Test.sol @@ -16,7 +16,7 @@ contract MerkleClaimERC20Test is Test { /// ============ Storage ============ /// @dev MerkleClaimERC20 contract - MerkleClaimERC20 internal TOKEN; + MerkleClaimERC20 internal merkleClaim; /// @dev User: Alice (in merkle tree) MerkleClaimERC20User internal ALICE = MerkleClaimERC20User(0x185a4dc360CE69bDCceE33b3784B0282f7961aea); /// @dev User: Bob (not in merkle tree) @@ -28,16 +28,17 @@ contract MerkleClaimERC20Test is Test { OverlayV1Token ovl = new OverlayV1Token(); // Create airdrop token - TOKEN = new MerkleClaimERC20( + merkleClaim = new MerkleClaimERC20( ovl, // Merkle root containing ALICE with 100e18 tokens but no BOB 0xd0aa6a4e5b4e13462921d7518eebdb7b297a7877d6cfe078b0c318827392fb55 ); - deal(address(ovl), address(TOKEN), 100e18); // total amount of tokens to be claimed + deal(address(ovl), address(this), 100e18); // total amount of tokens to be claimed + ovl.transfer(address(merkleClaim), 100e18); // Setup airdrop users with custom addresses (to match merkle tree) - deployCodeTo("MerkleClaimERC20User.sol", abi.encode(TOKEN), address(ALICE)); - deployCodeTo("MerkleClaimERC20User.sol", abi.encode(TOKEN), address(BOB)); + deployCodeTo("MerkleClaimERC20User.sol", abi.encode(merkleClaim), address(ALICE)); + deployCodeTo("MerkleClaimERC20User.sol", abi.encode(merkleClaim), address(BOB)); } } From ebb7d9c11400bddf51701f9125094710ac9d0236 Mon Sep 17 00:00:00 2001 From: Ezequiel Date: Thu, 23 Nov 2023 13:45:42 -0300 Subject: [PATCH 12/13] feat: make MerkleClaim ownable and merkle root updatable --- contracts/src/MerkleClaimERC20.sol | 11 +++++++++-- contracts/src/test/MerkleClaimERC20.t.sol | 18 ++++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/contracts/src/MerkleClaimERC20.sol b/contracts/src/MerkleClaimERC20.sol index b58f4f6..beb4d1a 100644 --- a/contracts/src/MerkleClaimERC20.sol +++ b/contracts/src/MerkleClaimERC20.sol @@ -5,14 +5,15 @@ pragma solidity 0.8.19; import {IERC20} from "@openzeppelin/token/ERC20/IERC20.sol"; import {MerkleProof} from "@openzeppelin/utils/cryptography/MerkleProof.sol"; +import {Ownable} from "@openzeppelin/access/Ownable.sol"; /// @title MerkleClaimERC20 /// @author Anish Agnihotri -contract MerkleClaimERC20 { +contract MerkleClaimERC20 is Ownable { /// ============ Immutable storage ============ /// @notice ERC20-claimee inclusion root - bytes32 public immutable merkleRoot; + bytes32 public merkleRoot; /// @notice Contract address of airdropped token IERC20 public immutable token; @@ -77,4 +78,10 @@ contract MerkleClaimERC20 { // Emit claim event emit Claim(to, amount); } + + /// @notice Allows owner to update merkle root + /// @param _merkleRoot new merkle root + function updateMerkleRoot(bytes32 _merkleRoot) external onlyOwner { + merkleRoot = _merkleRoot; + } } diff --git a/contracts/src/test/MerkleClaimERC20.t.sol b/contracts/src/test/MerkleClaimERC20.t.sol index 0df0d2a..0b62578 100644 --- a/contracts/src/test/MerkleClaimERC20.t.sol +++ b/contracts/src/test/MerkleClaimERC20.t.sol @@ -138,4 +138,22 @@ contract Tests is MerkleClaimERC20Test { // Assert Alice balance before + 100 tokens = after balance assertEq(alicePostBalance, alicePreBalance + 100e18); } + + function testAdminUpdatesMerkleRoot() public { + bytes32 newMerkleRoot = bytes32(uint256(1)); + assertFalse(merkleClaim.merkleRoot() == newMerkleRoot); + + merkleClaim.updateMerkleRoot(newMerkleRoot); + + assertEq(merkleClaim.merkleRoot(), newMerkleRoot); + } + + function testUserUpdatesMerkleRoot() public { + bytes32 newMerkleRoot = bytes32(uint256(1)); + + vm.startPrank(address(BOB)); + + vm.expectRevert("Ownable: caller is not the owner"); + merkleClaim.updateMerkleRoot(newMerkleRoot); + } } From 21a9097c414d016de75e2dc3798ec347c2cb611f Mon Sep 17 00:00:00 2001 From: Ezequiel Date: Thu, 23 Nov 2023 16:08:11 -0300 Subject: [PATCH 13/13] feat: add deploy scripts --- contracts/.gitignore | 4 +-- contracts/foundry.toml | 6 ++++ contracts/script/DeployMerkleClaim.s.sol | 24 ++++++++++++++++ contracts/script/DeployOVL.s.sol | 36 ++++++++++++++++++++++++ 4 files changed, 67 insertions(+), 3 deletions(-) create mode 100644 contracts/script/DeployMerkleClaim.s.sol create mode 100644 contracts/script/DeployOVL.s.sol diff --git a/contracts/.gitignore b/contracts/.gitignore index 85198aa..7366ba4 100644 --- a/contracts/.gitignore +++ b/contracts/.gitignore @@ -3,9 +3,7 @@ cache/ out/ # Ignores development broadcast logs -!/broadcast -/broadcast/*/31337/ -/broadcast/**/dry-run/ +broadcast # Docs docs/ diff --git a/contracts/foundry.toml b/contracts/foundry.toml index 4ff40c4..e206aba 100644 --- a/contracts/foundry.toml +++ b/contracts/foundry.toml @@ -3,4 +3,10 @@ src = "src" out = "out" libs = ["lib"] +[rpc_endpoints] +sepolia = "${SEPOLIA_RPC_URL}" + +[etherscan] +sepolia = { key = "${ETHERSCAN_API_KEY}" } + # See more config options https://github.com/foundry-rs/foundry/tree/master/config \ No newline at end of file diff --git a/contracts/script/DeployMerkleClaim.s.sol b/contracts/script/DeployMerkleClaim.s.sol new file mode 100644 index 0000000..1878d19 --- /dev/null +++ b/contracts/script/DeployMerkleClaim.s.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +import "forge-std/Script.sol"; +import {IERC20} from "@openzeppelin/token/ERC20/IERC20.sol"; +import {MerkleClaimERC20} from "../src/MerkleClaimERC20.sol"; + +// Deploy with: +// source .env +// forge script script/DeployMerkleClaim.s.sol:DeployScript --rpc-url $SEPOLIA_RPC_URL --broadcast --verify -vvvv + +contract DeployScript is Script { + function run() external { + uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + vm.startBroadcast(deployerPrivateKey); + + IERC20 ovl = IERC20(0x088feB3063d118c037ecAc999AD53Ec532780614); + bytes32 merkleRoot = 0x0e4db3677f424b884357f6f95d4a5e2273e600875b8599841fcfe68539080562; + + MerkleClaimERC20 merkleClaim = new MerkleClaimERC20(ovl, merkleRoot); + + vm.stopBroadcast(); + } +} diff --git a/contracts/script/DeployOVL.s.sol b/contracts/script/DeployOVL.s.sol new file mode 100644 index 0000000..cca7779 --- /dev/null +++ b/contracts/script/DeployOVL.s.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +import "forge-std/Script.sol"; +import {OverlayV1Token} from "../src/OverlayV1Token.sol"; +import {MINTER_ROLE} from "../src/interfaces/IOverlayV1Token.sol"; + +// Deploy with: +// source .env +// forge script script/DeployOVL.s.sol:DeployOvlScript --rpc-url $SEPOLIA_RPC_URL --broadcast --verify -vvvv + +/* If it doesn't verify automatically, run: +forge verify-contract \ + --chain-id 421614 \ + --num-of-optimizations 200 \ + --watch \ + --etherscan-api-key \ + --compiler-version v0.8.19+commit.7dd6d404 \ + \ + src/OverlayV1Token.sol:OverlayV1Token +*/ + +contract DeployOvlScript is Script { + function run() external { + uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + vm.startBroadcast(deployerPrivateKey); + + OverlayV1Token ovl = new OverlayV1Token(); + + // Mint 100k OVL to deployer + ovl.grantRole(MINTER_ROLE, 0xaf7F476a8C72de272Fc9A4b6153BB1B8Caa843bF); + ovl.mint(0xaf7F476a8C72de272Fc9A4b6153BB1B8Caa843bF, 100_000 ether); + + vm.stopBroadcast(); + } +}