Skip to content

Commit

Permalink
feat(protocol): allow one-tx claim and delegation for bridged ERC20 t…
Browse files Browse the repository at this point in the history
…okens (#15727)

Co-authored-by: Brecht Devos <Brechtp.Devos@gmail.com>
Co-authored-by: D <51912515+adaki2004@users.noreply.github.com>
  • Loading branch information
3 people committed Feb 11, 2024
1 parent 8099bd1 commit 603f24b
Show file tree
Hide file tree
Showing 7 changed files with 382 additions and 254 deletions.
30 changes: 25 additions & 5 deletions packages/protocol/contracts/team/airdrop/ERC20Airdrop.sol
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
pragma solidity 0.8.24;

import "lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
import "lib/openzeppelin-contracts/contracts/governance/utils/IVotes.sol";
import "./MerkleClaimable.sol";

/// @title ERC20Airdrop
Expand All @@ -25,8 +26,8 @@ contract ERC20Airdrop is MerkleClaimable {
uint256[48] private __gap;

function init(
uint64 _claimStarts,
uint64 _claimEnds,
uint64 _claimStart,
uint64 _claimEnd,
bytes32 _merkleRoot,
address _token,
address _vault
Expand All @@ -35,14 +36,33 @@ contract ERC20Airdrop is MerkleClaimable {
initializer
{
__Essential_init();
_setConfig(_claimStarts, _claimEnds, _merkleRoot);
__MerkleClaimable_init(_claimStart, _claimEnd, _merkleRoot);

token = _token;
vault = _vault;
}

function _claimWithData(bytes calldata data) internal override {
(address user, uint256 amount) = abi.decode(data, (address, uint256));
function claimAndDelegate(
address user,
uint256 amount,
bytes32[] calldata proof,
bytes calldata delegationData
)
external
nonReentrant
{
// Check if this can be claimed
_verifyClaim(abi.encode(user, amount), proof);

// Transfer the tokens
IERC20(token).transferFrom(vault, user, amount);

// Delegate the voting power to delegatee.
// Note that the signature (v,r,s) may not correspond to the user address,
// but since the data is provided by Taiko backend, it's not an issue even if
// client can change the data to call delegateBySig for another user.
(address delegatee, uint256 nonce, uint256 expiry, uint8 v, bytes32 r, bytes32 s) =
abi.decode(delegationData, (address, uint256, uint256, uint8, bytes32, bytes32));
IVotes(token).delegateBySig(delegatee, nonce, expiry, v, r, s);
}
}
20 changes: 11 additions & 9 deletions packages/protocol/contracts/team/airdrop/ERC20Airdrop2.sol
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@ contract ERC20Airdrop2 is MerkleClaimable {
}

function init(
uint64 _claimStarts,
uint64 _claimEnds,
uint64 _claimStart,
uint64 _claimEnd,
bytes32 _merkleRoot,
address _token,
address _vault,
Expand All @@ -58,14 +58,21 @@ contract ERC20Airdrop2 is MerkleClaimable {
initializer
{
__Essential_init();
// Unix timestamp=_claimEnds+1 marks the first timestamp the users are able to withdraw.
_setConfig(_claimStarts, _claimEnds, _merkleRoot);
__MerkleClaimable_init(_claimStart, _claimEnd, _merkleRoot);

token = _token;
vault = _vault;
withdrawalWindow = _withdrawalWindow;
}

function claim(address user, uint256 amount, bytes32[] calldata proof) external nonReentrant {
// Check if this can be claimed
_verifyClaim(abi.encode(user, amount), proof);

// Assign the tokens
claimedAmount[user] += amount;
}

/// @notice External withdraw function
/// @param user User address
function withdraw(address user) external ongoingWithdrawals {
Expand Down Expand Up @@ -102,9 +109,4 @@ contract ERC20Airdrop2 is MerkleClaimable {

withdrawableAmount = timeBasedAllowance - withdrawnAmount[user];
}

function _claimWithData(bytes calldata data) internal override {
(address user, uint256 amount) = abi.decode(data, (address, uint256));
claimedAmount[user] += amount;
}
}
19 changes: 14 additions & 5 deletions packages/protocol/contracts/team/airdrop/ERC721Airdrop.sol
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ contract ERC721Airdrop is MerkleClaimable {
uint256[48] private __gap;

function init(
uint64 _claimStarts,
uint64 _claimEnds,
uint64 _claimStart,
uint64 _claimEnd,
bytes32 _merkleRoot,
address _token,
address _vault
Expand All @@ -34,15 +34,24 @@ contract ERC721Airdrop is MerkleClaimable {
initializer
{
__Essential_init();
_setConfig(_claimStarts, _claimEnds, _merkleRoot);
__MerkleClaimable_init(_claimStart, _claimEnd, _merkleRoot);

token = _token;
vault = _vault;
}

function _claimWithData(bytes calldata data) internal override {
(address user, uint256[] memory tokenIds) = abi.decode(data, (address, uint256[]));
function claim(
address user,
uint256[] calldata tokenIds,
bytes32[] calldata proof
)
external
nonReentrant
{
// Check if this can be claimed
_verifyClaim(abi.encode(user, tokenIds), proof);

// Transfer the tokens
for (uint256 i; i < tokenIds.length; ++i) {
IERC721Upgradeable(token).safeTransferFrom(vault, user, tokenIds[i]);
}
Expand Down
63 changes: 36 additions & 27 deletions packages/protocol/contracts/team/airdrop/MerkleClaimable.sol
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,7 @@

pragma solidity 0.8.24;

import { MerkleProofUpgradeable } from
"lib/openzeppelin-contracts-upgradeable/contracts/utils/cryptography/MerkleProofUpgradeable.sol";
import "lib/openzeppelin-contracts/contracts/utils/cryptography/MerkleProof.sol";
import "../../common/EssentialContract.sol";

/// @title MerkleClaimable
Expand All @@ -42,27 +41,6 @@ abstract contract MerkleClaimable is EssentialContract {
_;
}

function claim(
bytes calldata data,
bytes32[] calldata proof
)
external
nonReentrant
ongoingClaim
{
bytes32 hash = keccak256(abi.encode("CLAIM_TAIKO_AIRDROP", data));

if (isClaimed[hash]) revert CLAIMED_ALREADY();

if (!MerkleProofUpgradeable.verify(proof, merkleRoot, hash)) {
revert INVALID_PROOF();
}

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

/// @notice Set config parameters
/// @param _claimStart Unix timestamp for claim start
/// @param _claimEnd Unix timestamp for claim end
Expand All @@ -78,12 +56,43 @@ abstract contract MerkleClaimable is EssentialContract {
_setConfig(_claimStart, _claimEnd, _merkleRoot);
}

function _setConfig(uint64 _claimStart, uint64 _claimEnd, bytes32 _merkleRoot) internal {
// solhint-disable-next-line func-name-mixedcase
function __MerkleClaimable_init(
uint64 _claimStart,
uint64 _claimEnd,
bytes32 _merkleRoot
)
internal
{
_setConfig(_claimStart, _claimEnd, _merkleRoot);
}

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);
}

function _verifyMerkleProof(
bytes32[] calldata _proof,
bytes32 _merkleRoot,
bytes32 _value
)
internal
pure
virtual
returns (bool)
{
return MerkleProof.verify(_proof, _merkleRoot, _value);
}

function _setConfig(uint64 _claimStart, uint64 _claimEnd, bytes32 _merkleRoot) private {
claimStart = _claimStart;
claimEnd = _claimEnd;
merkleRoot = _merkleRoot;
}

/// @dev Must revert in case of errors.
function _claimWithData(bytes calldata data) internal virtual;
}
90 changes: 90 additions & 0 deletions packages/protocol/test/team/airdrop/ERC20Airdrop.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;

import "../../TaikoTest.sol";

contract MockERC20Airdrop is ERC20Airdrop {
function _verifyMerkleProof(
bytes32[] calldata, /*proof*/
bytes32, /*merkleRoot*/
bytes32 /*value*/
)
internal
pure
override
returns (bool)
{
return true;
}
}

contract TestERC20Airdrop is TaikoTest {
address public owner = randAddress();

// Private Key: 0x1dc880d28041a41132437eae90c9e09c3b9e13438c2d0f6207804ceece623395
address public Lily = 0x3447b15c1b0a27D339C812b98881eC64051068b3;

bytes32 public constant merkleRoot = bytes32(uint256(1));
bytes32[] public merkleProof;
uint64 public claimStart;
uint64 public claimEnd;

TaikoToken token;
ERC20Airdrop airdrop;

function setUp() public {
claimStart = uint64(block.timestamp + 10);
claimEnd = uint64(block.timestamp + 10_000);
merkleProof = new bytes32[](3);

token = TaikoToken( deployProxy({
name: "taiko_token",
impl: address(new TaikoToken()),
data: abi.encodeCall(TaikoToken.init, ("Taiko Token", "TKO", owner)) }));


airdrop = ERC20Airdrop(
deployProxy({
name: "MockERC20Airdrop",
impl: address(new MockERC20Airdrop()),
data: abi.encodeCall(
ERC20Airdrop.init, (claimStart, claimEnd, merkleRoot, address(token), owner)
)
})
);

vm.roll(block.number + 1);
}

function test_claimAndDelegate_with_wrong_delegation_data() public {
vm.warp(claimStart);

bytes memory delegation = bytes("");

vm.expectRevert("ERC20: insufficient allowance"); // no allowance
vm.prank(Lily, Lily);
airdrop.claimAndDelegate(Lily, 100, merkleProof, delegation);

vm.prank(owner, owner);
token.approve(address(airdrop), 1_000_000_000e18);

vm.expectRevert(); // cannot decode the delegation data
vm.prank(Lily, Lily);
airdrop.claimAndDelegate(Lily, 100, merkleProof, delegation);

address delegatee = randAddress();
uint256 nonce = 1;
uint256 expiry = block.timestamp + 10_000;
uint8 v;
bytes32 r;
bytes32 s;

delegation = abi.encode(delegatee, nonce, expiry, v, r, s);

vm.expectRevert(); // signature invalid
vm.prank(Lily, Lily);
airdrop.claimAndDelegate(Lily, 100, merkleProof, delegation);

// TODO(daniel): add a new test by initializing the right value for the above 6 variables.
}
}
Loading

0 comments on commit 603f24b

Please sign in to comment.