Skip to content

Commit

Permalink
feat(distribution): support ERC-20 token
Browse files Browse the repository at this point in the history
  • Loading branch information
robertu7 committed Dec 21, 2023
1 parent 4afbd8f commit 0daad33
Show file tree
Hide file tree
Showing 8 changed files with 128 additions and 51 deletions.
9 changes: 5 additions & 4 deletions .gas-snapshot
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,11 @@ CurationTest:testCannotCurateNativeTokenZeroAddress() (gas: 16488)
CurationTest:testERC20Curation() (gas: 59908)
CurationTest:testNativeTokenCuration() (gas: 60085)
CurationTest:testNativeTokenCurationToContractAcceptor() (gas: 37466)
DistributionTest:testCannotSetAdminByAdmin() (gas: 17320)
DistributionTest:testCannotSetAdminByAttacker() (gas: 11066)
DistributionTest:testDrop() (gas: 146404)
DistributionTest:testSetAdmin() (gas: 15416)
DistributionTest:testCannotSetAdminByAdmin() (gas: 17342)
DistributionTest:testCannotSetAdminByAttacker() (gas: 11089)
DistributionTest:testClaim() (gas: 404806)
DistributionTest:testDrop() (gas: 356360)
DistributionTest:testSetAdmin() (gas: 15395)
LogbookNFTSVGTest:testTokenURI(uint8,uint8,uint16) (runs: 256, μ: 2019505, ~: 1310779)
LogbookTest:testClaim() (gas: 135608)
LogbookTest:testDonate(uint96) (runs: 256, μ: 155485, ~: 156936)
Expand Down
16 changes: 8 additions & 8 deletions scripts/billboard/generate-merkle-tree.js
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
import { StandardMerkleTree } from "@openzeppelin/merkle-tree";
import fs from "fs";
const { StandardMerkleTree } = require("@openzeppelin/merkle-tree");
const fs = require("fs");

// (1)
// [cid, address, amount]
const values = [
[
"Qmf5z5DKcwNWYUP9udvnSCTN2Se4A8kpZJY7JuUVFEqdGU",
"0x0000000000000000000000000000000000000102",
"1000000000000000000", // 1 ethers
"0x0000000000000000000000000000000000000066",
"1000000000000000000", // 1 USDT
],
[
"QmSAwncsWGXeqwrL5USBzQXvjqfH1nFfARLGM91sfd4NZe",
"0x0000000000000000000000000000000000000102",
"500000000000000000", // 0.5 ethers
"0x0000000000000000000000000000000000000067",
"500000000000000000", // 0.5 USDT
],
[
"QmUQQSeWxcqoNLKroGtz137c7QBWpzbNr9RcqDtVzZxJ3x",
"0x0000000000000000000000000000000000000103",
"10000000000000000", // 0.01 ethers
"0x0000000000000000000000000000000000000068",
"10000000000000000", // 0.01 USDT
],
];

Expand Down
50 changes: 30 additions & 20 deletions src/Billboard/Distribution.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ pragma solidity ^0.8.20;
import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/utils/Counters.sol";

import "./IDistribution.sol";
Expand All @@ -13,8 +14,8 @@ contract Distribution is IDistribution, Ownable {
using Counters for Counters.Counter;
Counters.Counter public lastTreeId;

// access control
address public admin;
address public token;

// treeId_ => merkleRoot_
mapping(uint256 => bytes32) public merkleRoots;
Expand All @@ -23,10 +24,13 @@ contract Distribution is IDistribution, Ownable {
mapping(uint256 => uint256) public balances;

// treeId_ => cid_ => account_
mapping(uint256 => mapping(bytes32 => mapping(address => bool))) public hasClaimed;
mapping(uint256 => mapping(string => mapping(address => bool))) public hasClaimed;

constructor() {
admin = msg.sender;
constructor(address token_, address admin_) {
require(token_ != address(0), "zero address");

admin = admin_;
token = token_;
}

//////////////////////////////
Expand All @@ -49,45 +53,48 @@ contract Distribution is IDistribution, Ownable {
/// Drop & claim
//////////////////////////////

/// @inheritdoc IDistribution
function setToken(address token_) external isFromAdmin {
token = token_;
}

/// @inheritdoc IDistribution
function drop(bytes32 merkleRoot_, uint256 amount_) external payable isFromAdmin returns (uint256 treeId_) {
require(msg.value > 0, "no value");
require(amount_ == msg.value, "invalid amount");
// Transfer
SafeERC20.safeTransferFrom(IERC20(token), msg.sender, address(this), amount_);

// Set the merkle root
lastTreeId.increment();
treeId_ = lastTreeId.current();

// Set the merkle root
merkleRoots[treeId_] = merkleRoot_;

// Set the balance
balances[treeId_] = msg.value;
// Set the balance for the tree
balances[treeId_] = amount_;

emit Drop(treeId_, msg.value);
emit Drop(treeId_, amount_);
}

/// @inheritdoc IDistribution
function claim(
uint256 treeId_,
bytes32 cid_,
string calldata cid_,
address account_,
uint256 amount_,
bytes32[] calldata merkleProof_
) external {
require(!hasClaimed[treeId_][cid_][account_], "already claimed.");

// Verify the merkle proof
bytes32 _leaf = keccak256(abi.encodePacked(cid_, account_, amount_));
require(MerkleProof.verify(merkleProof_, merkleRoots[treeId_], _leaf), "invalid proof.");
bytes32 _leaf = keccak256(bytes.concat(keccak256(abi.encode(cid_, account_, amount_))));
require(MerkleProof.verify(merkleProof_, merkleRoots[treeId_], _leaf), "invalid proof");

// Mark it as claimed
// Mark it as claimed first for to prevent reentrancy
hasClaimed[treeId_][cid_][account_] = true;

// Transfer
(bool _success, ) = account_.call{value: amount_}("");
require(_success, "transfer failed");
require(IERC20(token).transfer(account_, amount_), "failed token transfer");

// Update the balance
// Update the balance for the tree
balances[treeId_] -= amount_;

emit Claim(cid_, account_, amount_);
Expand All @@ -97,7 +104,10 @@ contract Distribution is IDistribution, Ownable {
function sweep(uint256 treeId_, address target_) external isFromAdmin {
uint256 _balance = balances[treeId_];

(bool _success, ) = target_.call{value: _balance}("");
require(_success, "transfer failed");
// Transfer
require(IERC20(token).transfer(target_, _balance), "failed token transfer");

// Update the balance for the tree
balances[treeId_] = 0;
}
}
12 changes: 10 additions & 2 deletions src/Billboard/IDistribution.sol
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ interface IDistribution {
* @param account_ Address of claim
* @param amount_ Amount of claim
*/
event Claim(bytes32 cid_, address indexed account_, uint256 amount_);
event Claim(string cid_, address indexed account_, uint256 amount_);

/**
* @dev Emitted when admin is changed.
Expand All @@ -36,6 +36,14 @@ interface IDistribution {
*/
function setAdmin(address account_) external;

/**
* @notice Set ERC-20 token
*
* @param token_ Address of ERC-20 token
*
*/
function setToken(address token_) external;

/**
* @notice Create a new drop
*
Expand All @@ -59,7 +67,7 @@ interface IDistribution {
*/
function claim(
uint256 treeId_,
bytes32 cid_,
string calldata cid_,
address account_,
uint256 amount_,
bytes32[] calldata proof_
Expand Down
43 changes: 41 additions & 2 deletions src/test/Billboard/DistributionTest.t.sol
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
//SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.20;

import "./DistributionTestBase.t.sol";
import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol";
import "@openzeppelin/contracts/token/ERC721/IERC721.sol";

import "./DistributionTestBase.t.sol";

contract DistributionTest is DistributionTestBase {
function testSetAdmin() public {
vm.prank(OWNER);
Expand Down Expand Up @@ -34,12 +36,49 @@ contract DistributionTest is DistributionTestBase {
assertEq(distribution.lastTreeId(), 1);
assertEq(distribution.merkleRoots(1), TREE_1_ROOT);
assertEq(distribution.balances(1), _amount);
assertEq(usdt.balanceOf(address(distribution)), _amount);

// drop#2
drop(_amount);
assertEq(distribution.lastTreeId(), 2);
assertEq(distribution.merkleRoots(2), TREE_1_ROOT);
assertEq(distribution.balances(2), _amount);
assertEq(address(distribution).balance, _amount * 2);
assertEq(usdt.balanceOf(address(distribution)), _amount * 2);
}

function testClaim() public {
// drop#1
uint256 _amount = 1510000000000000000;
drop(_amount);

// claim#Alice
uint256 balanceAlce = address(USER_ALICE).balance;
distribution.claim(
1,
TREE_1_CIDS[USER_ALICE],
USER_ALICE,
TREE_1_AMOUNTS[USER_ALICE],
TREE_1_PROOFS[USER_ALICE]
);
assertEq(usdt.balanceOf(address(USER_ALICE)), balanceAlce + TREE_1_AMOUNTS[USER_ALICE]);

// claim#Bob
uint256 balanceBob = address(USER_BOB).balance;
distribution.claim(1, TREE_1_CIDS[USER_BOB], USER_BOB, TREE_1_AMOUNTS[USER_BOB], TREE_1_PROOFS[USER_BOB]);
assertEq(usdt.balanceOf(address(USER_BOB)), balanceBob + TREE_1_AMOUNTS[USER_BOB]);

// claim#Charlie
uint256 balanceCharlie = address(USER_CHARLIE).balance;
distribution.claim(
1,
TREE_1_CIDS[USER_CHARLIE],
USER_CHARLIE,
TREE_1_AMOUNTS[USER_CHARLIE],
TREE_1_PROOFS[USER_CHARLIE]
);
assertEq(usdt.balanceOf(address(USER_CHARLIE)), balanceCharlie + TREE_1_AMOUNTS[USER_CHARLIE]);

// check balance
assertEq(address(distribution).balance, 0);
}
}
47 changes: 33 additions & 14 deletions src/test/Billboard/DistributionTestBase.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@ import "forge-std/Vm.sol";

import {Distribution} from "../../Billboard/Distribution.sol";
import {IDistribution} from "../../Billboard/IDistribution.sol";
import {USDT} from "../utils/USDT.sol";

contract DistributionTestBase is Test {
Distribution internal distribution;
USDT internal usdt;

address constant ZERO_ADDRESS = address(0);
address constant FAKE_CONTRACT = address(1);
Expand All @@ -21,28 +23,35 @@ contract DistributionTestBase is Test {
address constant USER_CHARLIE = address(104);
address constant ATTACKER = address(200);

bytes32 constant TREE_1_ROOT = 0x12c628670e93b44e4305c14af8efd4989e78cd5e9cbfa7f8b792326d08271b89;
bytes32 constant TREE_1_ROOT = 0xf2e79881fa5ed7db88877ca21ec885996d6176cf455504472b68f5517203e314;
mapping(address => bytes32[]) public TREE_1_PROOFS;
mapping(address => string) public TREE_1_CIDS;
mapping(address => uint256) public TREE_1_AMOUNTS;

function setUp() public {
vm.startPrank(OWNER);

// label addresses
vm.label(OWNER, "OWNER");
vm.label(ADMIN, "ADMIN");
vm.label(USER_ALICE, "USER_ALICE");
vm.label(USER_BOB, "USER_BOB");
vm.label(USER_CHARLIE, "USER_CHARLIE");

// init proofs
bytes32[] memory proofAlice = new bytes32[](1);
proofAlice[0] = 0xf9b6aef995735ac234dd8f82c58b902f63846b2e900756d78ac93f5cab9acdd5;
proofAlice[0] = 0x884512338d5de33ee9c6e0a1c2a47ff1c8ca788bbb8b34552e39cb98aaaa5c08;
TREE_1_PROOFS[USER_ALICE] = proofAlice;

bytes32[] memory proofBob = new bytes32[](2);
proofBob[0] = 0xe3908a942209f327ea24b807c861c449d5994b50d152e447c1282fc6190d742d;
proofBob[1] = 0xf23837c66d3ebf279f5f0a5ea3ee937887d05da50cb7f57d2009ce5058d4695c;
proofBob[0] = 0x685ad0f74cc48ff99f8fa41d3f8d2e3c7672e7afe48a680c9418b7268626fc89;
proofBob[1] = 0xfa171588c56e80a41d8e67e9c9a8dc6b25dbdf1e16699c612981ebdf04045c3f;
TREE_1_PROOFS[USER_BOB] = proofBob;

bytes32[] memory proofCharlie = new bytes32[](2);
proofCharlie[0] = 0xa88b11df30bc3ccfee7a51d2e7d0a65ca0a7c5a2272ce4a65c27b65141916fc6;
proofCharlie[1] = 0xf23837c66d3ebf279f5f0a5ea3ee937887d05da50cb7f57d2009ce5058d4695c;
TREE_1_PROOFS[USER_BOB] = proofCharlie;
proofCharlie[0] = 0x27349dbdeb528d38831624696ac843c93d915cbf47db44f6087b3e431152c4de;
proofCharlie[1] = 0xfa171588c56e80a41d8e67e9c9a8dc6b25dbdf1e16699c612981ebdf04045c3f;
TREE_1_PROOFS[USER_CHARLIE] = proofCharlie;

// init cids
TREE_1_CIDS[USER_ALICE] = "Qmf5z5DKcwNWYUP9udvnSCTN2Se4A8kpZJY7JuUVFEqdGU";
Expand All @@ -54,19 +63,29 @@ contract DistributionTestBase is Test {
TREE_1_AMOUNTS[USER_BOB] = 500000000000000000;
TREE_1_AMOUNTS[USER_CHARLIE] = 10000000000000000;

// deploy
distribution = new Distribution();
assertEq(distribution.admin(), OWNER);
// deploy USDT
usdt = new USDT(OWNER, 0);

// set admin
distribution.setAdmin(ADMIN);
// deploy Distribution contract
distribution = new Distribution(address(usdt), ADMIN);
assertEq(distribution.admin(), ADMIN);

vm.stopPrank();

uint256 MAX_ALLOWANCE = type(uint256).max;
vm.prank(ADMIN);
usdt.approve(address(distribution), MAX_ALLOWANCE);
vm.prank(USER_ALICE);
usdt.approve(address(distribution), MAX_ALLOWANCE);
vm.prank(USER_BOB);
usdt.approve(address(distribution), MAX_ALLOWANCE);
vm.prank(USER_CHARLIE);
usdt.approve(address(distribution), MAX_ALLOWANCE);
}

function drop(uint256 amount_) public {
deal(address(usdt), ADMIN, amount_);
vm.prank(ADMIN);
vm.deal(ADMIN, amount_);
distribution.drop{value: amount_}(TREE_1_ROOT, amount_);
distribution.drop(TREE_1_ROOT, amount_);
}
}
2 changes: 1 addition & 1 deletion src/test/Curation/Curation.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import "forge-std/console2.sol";

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

import {USDT} from "./USDT.sol";
import {USDT} from "../utils/USDT.sol";
import {Acceptor, Rejector} from "./Receivers.sol";
import {Curation as CurationContract} from "../../Curation/Curation.sol";

Expand Down
File renamed without changes.

0 comments on commit 0daad33

Please sign in to comment.