-
Notifications
You must be signed in to change notification settings - Fork 2.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(nfts): tbz s1 claim contracts (#18192)
Co-authored-by: Daniel Wang <[email protected]>
- Loading branch information
Showing
9 changed files
with
571 additions
and
1 deletion.
There are no files selected for viewing
99 changes: 99 additions & 0 deletions
99
packages/nfts/contracts/trailblazers-airdrop/ERC20Airdrop.sol
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
138
packages/nfts/contracts/trailblazers-airdrop/MerkleClaimable.sol
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 { } | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
{ | ||
"ERC20Airdrop": "0x42DB7bE491a8933FaADbca4891dA2632D45e5CfC", | ||
"MerkleRoot": "0xea5b2299e76b4860965e9059388d021145269c96b816b07a808ff391cd80753e" | ||
} |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
} |
Oops, something went wrong.