Skip to content

Commit

Permalink
feat(nfts): tbz s1 claim contracts (#18192)
Browse files Browse the repository at this point in the history
Co-authored-by: Daniel Wang <[email protected]>
  • Loading branch information
bearni95 and dantaik authored Oct 3, 2024
1 parent 237e3a3 commit 8348ca8
Show file tree
Hide file tree
Showing 9 changed files with 571 additions and 1 deletion.
99 changes: 99 additions & 0 deletions packages/nfts/contracts/trailblazers-airdrop/ERC20Airdrop.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/governance/utils/IVotes.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol";
import "@taiko/blacklist/IMinimalBlacklist.sol";

import "./MerkleClaimable.sol";

/// @title ERC20Airdrop
/// @notice Contract for managing Taiko token airdrop for eligible users.
/// @custom:security-contact [email protected]
contract ERC20Airdrop is MerkleClaimable, ReentrancyGuardUpgradeable, PausableUpgradeable {
using SafeERC20 for IERC20;

/// @notice The address of the Taiko token contract.
IERC20 public token;
/// @notice Blackist address
IMinimalBlacklist public blacklist;

/// @notice Event emitted when the blacklist is updated.
event BlacklistUpdated(address _blacklist);
/// @notice Errors

error ADDRESS_BLACKLISTED();

uint256[48] private __gap;

/// @notice Modifier to check if the address is not blacklisted.
/// @param _address The address to check.
modifier isNotBlacklisted(address _address) {
if (blacklist.isBlacklisted(_address)) revert ADDRESS_BLACKLISTED();
_;
}

/// @notice Initializes the contract.
/// @param _owner The owner of this contract.
/// @param _claimStart The start time of the claim period.
/// @param _claimEnd The end time of the claim period.
/// @param _merkleRoot The merkle root.
/// @param _token The address of the token contract.
function init(
address _owner,
uint64 _claimStart,
uint64 _claimEnd,
bytes32 _merkleRoot,
IERC20 _token,
address _blacklist
)
external
initializer
{
__ReentrancyGuard_init();
__Pausable_init();
__MerkleClaimable_init(_claimStart, _claimEnd, _merkleRoot);
_transferOwnership(_owner == address(0) ? _msgSender() : _owner);
blacklist = IMinimalBlacklist(_blacklist);
token = _token;
}

/// @notice Claims the airdrop for the user.
/// @param user The address of the user.
/// @param amount The amount of tokens to claim.
/// @param proof The merkle proof.
function claim(
address user,
uint256 amount,
bytes32[] calldata proof
)
external
nonReentrant
isNotBlacklisted(user)
{
// Check if this can be claimed
_verifyClaim(abi.encode(user, amount), proof);

// Transfer the tokens from contract
IERC20(token).transfer(user, amount);
}

/// @notice Withdraw ERC20 tokens from the Vault
/// @param _token The ERC20 token address to withdraw
/// @dev Only the owner can execute this function
function withdrawERC20(IERC20 _token) external onlyOwner {
// If token address is address(0), use token
if (address(_token) == address(0)) {
_token = token;
}
// Transfer the tokens to owner
_token.transfer(owner(), _token.balanceOf(address(this)));
}

/// @notice Internal method to authorize an upgrade
function _authorizeUpgrade(address) internal virtual override onlyOwner { }
}
138 changes: 138 additions & 0 deletions packages/nfts/contracts/trailblazers-airdrop/MerkleClaimable.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;

import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol";

import { UUPSUpgradeable } from
"@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import { Ownable2StepUpgradeable } from
"@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol";
import { MerkleProof } from "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol";
import { ContextUpgradeable } from
"@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol";

/// @title MerkleClaimable
/// @notice Contract for managing Taiko token airdrop for eligible users
/// @custom:security-contact [email protected]
abstract contract MerkleClaimable is
ContextUpgradeable,
UUPSUpgradeable,
Ownable2StepUpgradeable
{
/// @notice Mapping of hashes and their claim status
mapping(bytes32 hash => bool claimed) public isClaimed;

/// @notice Merkle root of the tree
bytes32 public merkleRoot;

/// @notice Unix timestamp for claim start
uint64 public claimStart;

/// @notice Unix timestamp for claim end
uint64 public claimEnd;

uint256[47] private __gap;

/// @notice Event emitted when a claim is made
/// @param hash Hash of the claim
event Claimed(bytes32 hash);

/// @notice Event emitted when config is changed
/// @param claimStart Unix timestamp for claim start
/// @param claimEnd Unix timestamp for claim end
/// @param merkleRoot Merkle root of the tree
event ConfigChanged(uint64 claimStart, uint64 claimEnd, bytes32 merkleRoot);

/// @notice Errors
error CLAIM_NOT_ONGOING();
error CLAIMED_ALREADY();
error INVALID_PARAMS();
error INVALID_PROOF();

/// @notice Modifier to check if the claim is ongoing
modifier ongoingClaim() {
if (
merkleRoot == 0x0 || claimStart == 0 || claimEnd == 0 || claimStart > block.timestamp
|| claimEnd < block.timestamp
) revert CLAIM_NOT_ONGOING();
_;
}

/// @notice Set config parameters
/// @param _claimStart Unix timestamp for claim start
/// @param _claimEnd Unix timestamp for claim end
/// @param _merkleRoot Merkle root of the tree
function setConfig(
uint64 _claimStart,
uint64 _claimEnd,
bytes32 _merkleRoot
)
external
onlyOwner
{
_setConfig(_claimStart, _claimEnd, _merkleRoot);
}

/// @notice Initialize the contract
/// @param _claimStart Unix timestamp for claim start
/// @param _claimEnd Unix timestamp for claim end
/// @param _merkleRoot Merkle root of the tree
function __MerkleClaimable_init(
uint64 _claimStart,
uint64 _claimEnd,
bytes32 _merkleRoot
)
internal
onlyInitializing
{
__Context_init();
_setConfig(_claimStart, _claimEnd, _merkleRoot);
}

/// @notice Verify an airdrop claim
/// @param data Data to be hashed
/// @param proof Merkle proof
function _verifyClaim(bytes memory data, bytes32[] calldata proof) internal ongoingClaim {
bytes32 hash = keccak256(abi.encode("CLAIM_TAIKO_AIRDROP", data));

if (isClaimed[hash]) revert CLAIMED_ALREADY();
if (!_verifyMerkleProof(proof, merkleRoot, hash)) revert INVALID_PROOF();

isClaimed[hash] = true;
emit Claimed(hash);
}

/// @notice Verify a Merkle proof
/// @param _proof Merkle proof
/// @param _merkleRoot Merkle root
/// @param _value Value to verify
/// @return Whether the proof is valid
function _verifyMerkleProof(
bytes32[] calldata _proof,
bytes32 _merkleRoot,
bytes32 _value
)
internal
pure
virtual
returns (bool)
{
return MerkleProof.verify(_proof, _merkleRoot, _value);
}

/// @notice Set config parameters
/// @param _claimStart Unix timestamp for claim start
/// @param _claimEnd Unix timestamp for claim end
/// @param _merkleRoot Merkle root of the tree
function _setConfig(uint64 _claimStart, uint64 _claimEnd, bytes32 _merkleRoot) private {
if (_claimStart > _claimEnd) revert INVALID_PARAMS();

claimStart = _claimStart;
claimEnd = _claimEnd;
merkleRoot = _merkleRoot;
emit ConfigChanged(_claimStart, _claimEnd, _merkleRoot);
}

/// @notice Internal method to authorize an upgrade
function _authorizeUpgrade(address) internal virtual override onlyOwner { }
}
4 changes: 4 additions & 0 deletions packages/nfts/deployments/trailblazers-airdrop/hekla.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"ERC20Airdrop": "0x42DB7bE491a8933FaADbca4891dA2632D45e5CfC",
"MerkleRoot": "0xea5b2299e76b4860965e9059388d021145269c96b816b07a808ff391cd80753e"
}
Empty file.
4 changes: 3 additions & 1 deletion packages/nfts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@
"kbw:deploy:mainnet": "forge clean && pnpm compile && forge script script/party-ticket/sol/Deploy.s.sol --rpc-url https://rpc.mainnet.taiko.xyz --broadcast --legacy --with-gas-price 30 ",
"kbw:upgradeV2:hekla": "forge clean && pnpm compile && forge script script/party-ticket/sol/UpgradeV2.s.sol --rpc-url https://rpc.hekla.taiko.xyz --broadcast --gas-estimate-multiplier 200",
"kbw:upgradeV2:mainnet": "forge clean && pnpm compile && forge script script/party-ticket/sol/UpgradeV2.s.sol --rpc-url https://rpc.mainnet.taiko.xyz --broadcast",
"pfp:deploy:hekla": "forge clean && pnpm compile && forge script script/profile/Deploy.s.sol --rpc-url https://rpc.hekla.taiko.xyz --broadcast --gas-estimate-multiplier 200"
"pfp:deploy:hekla": "forge clean && pnpm compile && forge script script/profile/Deploy.s.sol --rpc-url https://rpc.hekla.taiko.xyz --broadcast --gas-estimate-multiplier 200",
"pfp:deploy:mainnet": "forge clean && pnpm compile && forge script script/profile/Deploy.s.sol --rpc-url https://rpc.mainnet.taiko.xyz --broadcast --gas-estimate-multiplier 200",
"tbz:airdrop:hekla": "forge clean && pnpm compile && forge script script/trailblazers-airdrop/Deploy.s.sol --rpc-url https://rpc.hekla.taiko.xyz --broadcast --gas-estimate-multiplier 200"
},
"devDependencies": {
"@types/node": "^20.11.30",
Expand Down
93 changes: 93 additions & 0 deletions packages/nfts/script/trailblazers-airdrop/Deploy.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;

import { UtilsScript } from "./Utils.s.sol";
import { Script, console } from "forge-std/src/Script.sol";
import { Merkle } from "murky/Merkle.sol";
import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
import { TrailblazersBadges } from "../../contracts/trailblazers-badges/TrailblazersBadges.sol";
import { IMinimalBlacklist } from "@taiko/blacklist/IMinimalBlacklist.sol";
import { ERC20Airdrop } from "../../contracts/trailblazers-airdrop/ERC20Airdrop.sol";
import { ERC20Mock } from "../../test/util/MockTokens.sol";
import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol";
import { MockBlacklist } from "../../test/util/Blacklist.sol";

contract DeployScript is Script {
UtilsScript public utils;
string public jsonLocation;
uint256 public deployerPrivateKey;
address public deployerAddress;

// only used for production
IMinimalBlacklist blacklist = IMinimalBlacklist(0xe61E9034b5633977eC98E302b33e321e8140F105);

ERC20Airdrop public airdrop;
uint256 constant TOTAL_AVAILABLE_FUNDS = 1000 ether;

uint256 constant CLAIM_AMOUNT = 10 ether;

// hekla test root
bytes32 public merkleRoot = 0xea5b2299e76b4860965e9059388d021145269c96b816b07a808ff391cd80753e;

// rewards token
ERC20Upgradeable public erc20;
ERC20Mock public mockERC20;
// start and end times for the claim
uint64 constant CLAIM_DURATION = 1 days;
uint64 public CLAIM_START = uint64(block.timestamp);
uint64 public CLAIM_END = CLAIM_START + CLAIM_DURATION;

function setUp() public {
utils = new UtilsScript();
utils.setUp();

jsonLocation = utils.getContractJsonLocation();
deployerPrivateKey = utils.getPrivateKey();
deployerAddress = utils.getAddress();

vm.startBroadcast(deployerPrivateKey);

if (block.chainid != 167_000) {
// not mainnet, create mock contracts
blacklist = new MockBlacklist();
mockERC20 = new ERC20Mock();
// mint the necessary funds
erc20 = ERC20Upgradeable(address(mockERC20));
}

vm.stopBroadcast();
}

function run() public {
string memory jsonRoot = "root";

vm.startBroadcast(deployerPrivateKey);

// deploy token with empty root
address impl = address(new ERC20Airdrop());
address proxy = address(
new ERC1967Proxy(
impl,
abi.encodeCall(
ERC20Airdrop.init,
(deployerAddress, CLAIM_START, CLAIM_END, merkleRoot, erc20, address(blacklist))
)
)
);

airdrop = ERC20Airdrop(proxy);

// mint the necessary funds on hekla
if (block.chainid != 167_000) {
mockERC20.mint(address(airdrop), TOTAL_AVAILABLE_FUNDS);
}

console.log("Deployed ERC20Airdrop to:", address(airdrop));

vm.serializeBytes32(jsonRoot, "MerkleRoot", merkleRoot);
string memory finalJson = vm.serializeAddress(jsonRoot, "ERC20Airdrop", address(airdrop));
vm.writeJson(finalJson, jsonLocation);

vm.stopBroadcast();
}
}
Loading

0 comments on commit 8348ca8

Please sign in to comment.