diff --git a/src/Billboard/Distribution.sol b/src/Billboard/Distribution.sol new file mode 100644 index 0000000..0139f78 --- /dev/null +++ b/src/Billboard/Distribution.sol @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: Apache-2.0 +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/utils/Counters.sol"; + +import "./IDistribution.sol"; + +// https://github.com/Uniswap/merkle-distributor +contract Distribution is IDistribution, Ownable { + using Counters for Counters.Counter; + Counters.Counter public lastTreeId; + + // treeId_ => merkleRoot_ + mapping(uint256 => bytes32) public merkleRoots; + + // treeId_ => balance_ + mapping(uint256 => uint256) public balances; + + // treeId_ => cid_ => account_ + mapping(uint256 => mapping(bytes32 => mapping(address => bool))) public hasClaimed; + + /// @inheritdoc IDistribution + function drop(bytes32 merkleRoot_) external payable onlyOwner returns (uint256 treeId_) { + require(msg.value > 0, "no value"); + + lastTreeId.increment(); + treeId_ = lastTreeId.current(); + + // Set the merkle root + merkleRoots[treeId_] = merkleRoot_; + + // Set the balance + balances[treeId_] = msg.value; + + emit Drop(treeId_, msg.value); + } + + /// @inheritdoc IDistribution + function claim( + uint256 treeId_, + bytes32 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."); + + // Mark it as claimed + hasClaimed[treeId_][cid_][account_] = true; + + // Transfer + (bool _success, ) = account_.call{value: amount_}(""); + require(_success, "transfer failed"); + + // Update the balance + balances[treeId_] -= amount_; + + emit Claim(cid_, account_, amount_); + } + + /// @inheritdoc IDistribution + function sweep(uint256 treeId_, address target_) external onlyOwner { + uint256 _balance = balances[treeId_]; + + (bool _success, ) = target_.call{value: _balance}(""); + require(_success, "transfer failed"); + } +} diff --git a/src/Billboard/IDistribution.sol b/src/Billboard/IDistribution.sol new file mode 100644 index 0000000..8b4ec61 --- /dev/null +++ b/src/Billboard/IDistribution.sol @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.20; + +interface IDistribution { + /** + * @dev Emitted when an new drop is created. + * + * @param treeId_ Tree ID of the drop + * @param amount_ Total amount of the drop + */ + event Drop(uint256 indexed treeId_, uint256 amount_); + + /** + * @dev Emitted when an claim is made. + * + * @param cid_ Content ID of claim + * @param account_ Address of claim + * @param amount_ Amount of claim + */ + event Claim(bytes32 cid_, address indexed account_, uint256 amount_); + + /** + * @notice Create a new drop + * + * @param merkleRoot_ Merkle root of new drop + * + * Emits a {Drop} event on success. + */ + function drop(bytes32 merkleRoot_) external payable returns (uint256 treeId_); + + /** + * @notice Claim and transfer tokens + * + * @param treeId_ Tree ID + * @param cid_ Content ID + * @param account_ Address of claim + * @param amount_ Amount of claim + * @param proof_ Merkle proof for (treeId_, cid_, account_, amount_) + * + * Emits a {Claim} event on success. + */ + function claim( + uint256 treeId_, + bytes32 cid_, + address account_, + uint256 amount_, + bytes32[] calldata proof_ + ) external; + + /** + * @notice Sweep any unclaimed funds + * + * Transfers the full tokenbalance from the distributor contract to `target_` address. + * + * @param treeId_ Tree ID + * @param target_ Address that should receive the unclaimed funds + */ + function sweep(uint256 treeId_, address target_) external; +} diff --git a/src/Distribution/IDistribution.sol b/src/Distribution/IDistribution.sol deleted file mode 100644 index 1414106..0000000 --- a/src/Distribution/IDistribution.sol +++ /dev/null @@ -1,61 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.20; - -interface IDistribution { - /** - * @dev Emitted when an airdrop is claimed for an `account_`. - * in the merkle tree, `amount_` is the amount of tokens claimed and transferred. - * @param treeId_ Tree ID - * @param account_ Address of claim - * @param amount_ Amount of claim - */ - event Claimed(uint256 treeId_, address indexed account_, uint256 amount_); - - /** - * Returns the address of the token distributed by this contract. - */ - function token() external view returns (address); - - /** - * Returns the merkle root of a given `treeId_` merkle tree containing - * account balances available to claim. - * - * @param treeId_ Tree ID - * - */ - function merkleRoot(uint256 treeId_) external view returns (bytes32); - - /** - * @notice Claim and transfer tokens - * - * Verifies the provided proof and params - * and transfers 'amount_' of tokens to 'account_'. - * - * @param treeId_ Tree ID - * @param account_ Address of claim - * @param amount_ Amount of claim - * @param proof_ Merkle proof for (treeId_, account_, amount_) - * - * Emits a {Claimed} event on success. - */ - function claim(uint256 treeId_, address account_, uint256 amount_, bytes32[] calldata proof_) external; - - /** - * @notice Sweep any unclaimed funds - * - * Transfers the full tokenbalance from the distributor contract to `target_` address. - * - * @param treeId_ Tree ID - * @param target_ Address that should receive the unclaimed funds - */ - function sweep(uint256 treeId_, address target_) external; - - /** - * @notice Sweep any unclaimed funds to owner address - * - * Transfers the full tokenbalance from the distributor contract to owner of contract. - * - * @param treeId_ Tree ID - */ - function sweepToOwner(uint256 treeId_) external; -}