Skip to content

Commit

Permalink
feat(protocol): add ERC20Airdrop test and deployment script (#15752)
Browse files Browse the repository at this point in the history
Co-authored-by: Keszey Dániel <keszeyd@MacBook-Pro.local>
  • Loading branch information
adaki2004 and Keszey Dániel authored Feb 13, 2024
1 parent 0282133 commit e60588c
Show file tree
Hide file tree
Showing 3 changed files with 272 additions and 14 deletions.
67 changes: 67 additions & 0 deletions packages/protocol/script/DeployERC20Airdrop.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// SPDX-License-Identifier: MIT
// _____ _ _ _ _
// |_ _|_ _(_) |_____ | | __ _| |__ ___
// | |/ _` | | / / _ \ | |__/ _` | '_ (_-<
// |_|\__,_|_|_\_\___/ |____\__,_|_.__/__/
//
// Email: security@taiko.xyz
// Website: https://taiko.xyz
// GitHub: https://github.com/taikoxyz
// Discord: https://discord.gg/taikoxyz
// Twitter: https://twitter.com/taikoxyz
// Blog: https://mirror.xyz/labs.taiko.eth
// Youtube: https://www.youtube.com/@taikoxyz

pragma solidity 0.8.24;

import "../test/DeployCapability.sol";
import "forge-std/console2.sol";

import "../contracts/team/airdrop/ERC20Airdrop.sol";

// @KorbinianK , @2manslkh
// As written also in the tests the workflow shall be the following (checklist):
// 1. Is Vault - which will store the tokens - deployed ?
// 2. Is (bridged) TKO token existing ?
// 3. Is ERC20Airdrop contract is 'approved operator' on the TKO token ?
// 4. Proof (merkle root) and minting window related variabes (start, end) set ?
// If YES the answer to all above, we can go live with airdrop, which is like:
// 1. User go to website. -> For sake of simplicity he is eligible
// 2. User wants to mint, but first site established the delegateHash (user sets a delegatee) which
// the user signs
// 3. Backend retrieves the proof and together with signature in the input params, user fires away
// the claimAndDelegate() transaction.
contract DeployERC20Airdrop is DeployCapability {
uint256 public deployerPrivKey = vm.envUint("PRIVATE_KEY"); // Owner of the ERC20 airdrop
// contract
address public bridgedTko = vm.envAddress("BRIDGED_TKO_ADDRESS");
address public vaultAddress = vm.envAddress("VAULT_ADDRESS");

function setUp() external { }

function run() external {
require(deployerPrivKey != 0, "invalid deployer priv key");
require(vaultAddress != address(0), "invalid vault address");
require(bridgedTko != address(0), "invalid bridged tko address");

vm.startBroadcast(deployerPrivKey);

ERC20Airdrop(
deployProxy({
name: "ERC20Airdrop",
impl: address(new ERC20Airdrop()),
data: abi.encodeCall(ERC20Airdrop.init, (0, 0, bytes32(0), bridgedTko, vaultAddress))
})
);

/// @dev Once the Vault is done, we need to have a contract in that vault through which we
/// authorize the airdrop contract to be a spender of the vault.
// example:
//
// SOME_VAULT_CONTRACT(vaultAddress).approveAirdropContractAsSpender(
// bridgedTko, address(ERC20Airdrop), 50_000_000_000e18
// );

vm.stopBroadcast();
}
}
152 changes: 138 additions & 14 deletions packages/protocol/test/team/airdrop/ERC20Airdrop.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
pragma solidity 0.8.24;

import "../../TaikoTest.sol";
import "./LibDelegationSigUtil.sol";
import "lib/openzeppelin-contracts-upgradeable/contracts/access/OwnableUpgradeable.sol";

contract MockERC20Airdrop is ERC20Airdrop {
function _verifyMerkleProof(
Expand All @@ -18,6 +20,47 @@ contract MockERC20Airdrop is ERC20Airdrop {
}
}

// Simple mock - so that we do not need to deploy AddressManager (for these tests). With this
// contract we mock an ERC20Vault which mints tokens into a Vault (which holds the TKO).
contract MockAddressManager {
address mockERC20Vault;

constructor(address _mockERC20Vault) {
mockERC20Vault = _mockERC20Vault;
}

function getAddress(uint64, /*chainId*/ bytes32 /*name*/ ) public view returns (address) {
return mockERC20Vault;
}
}

// It does nothing but:
// - stores the tokens for the airdrop
// - owner can call approve() on token, and approving the AirdropERC20.sol contract so it acts on
// behalf
// - funds can later be withdrawn by the user
contract SimpleERC20Vault is OwnableUpgradeable {
/// @notice Initializes the vault.
function init() external initializer {
__Ownable_init();
}

function approveAirdropContract(
address token,
address approvedActor,
uint256 amount
)
public
onlyOwner
{
BridgedERC20(token).approve(approvedActor, amount);
}

function withdrawFunds(address token, address to) public onlyOwner {
BridgedERC20(token).transfer(to, BridgedERC20(token).balanceOf(address(this)));
}
}

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

Expand All @@ -29,41 +72,124 @@ contract TestERC20Airdrop is TaikoTest {
uint64 public claimStart;
uint64 public claimEnd;

TaikoToken token;
ERC20Airdrop airdrop;
BridgedERC20 token;
MockERC20Airdrop airdrop;
MockAddressManager addressManager;
SimpleERC20Vault vault;

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

// 1. We need to have a vault
vault = SimpleERC20Vault(
deployProxy({
name: "vault",
impl: address(new SimpleERC20Vault()),
data: abi.encodeCall(SimpleERC20Vault.init, ())
})
);

// 2. Need to add it to the AddressManager (below here i'm just mocking it) so that we can
// mint TKO. Basically this step only required in this test. Only thing we need to be sure
// on testnet/mainnet. Vault (which Aridrop transfers from) HAVE tokens.
addressManager = new MockAddressManager(address(vault));

token = TaikoToken(
// 3. Deploy a bridged TKO token (but on mainnet it will be just a bridged token from L1 to
// L2) - not necessary step on mainnet.
token = BridgedERC20(
deployProxy({
name: "taiko_token",
impl: address(new TaikoToken()),
data: abi.encodeCall(TaikoToken.init, ("Taiko Token", "TKO", owner))
name: "tko",
impl: address(new BridgedERC20()),
data: abi.encodeCall(
BridgedERC20.init,
(address(addressManager), randAddress(), 100, 18, "TKO", "Taiko Token")
)
})
);

airdrop = ERC20Airdrop(
vm.stopPrank();

// 5. Mint (AKA transfer) to the vault. This step on mainnet will be done by Taiko Labs. For
// testing on A6 the imporatnt thing is: HAVE tokens in this vault!
vm.prank(address(vault), owner);
BridgedERC20(token).mint(address(vault), 1_000_000_000e18);

// 6. Deploy the airdrop contract, and set the claimStart, claimEnd and merkleRoot -> On
// mainnet it will be separated into 2 tasks obviously, because first we deploy, then we set
// those variables. On testnet (e.g. A6) it shall also be 2 steps easily. Deploy a contract,
// then set merkle.
claimStart = uint64(block.timestamp + 10);
claimEnd = uint64(block.timestamp + 10_000);
merkleProof = new bytes32[](3);

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

vm.stopPrank();

// 7. Approval (Vault approves Airdrop contract to be the spender!) Has to be done on
// testnet and mainnet too, obviously.
vm.prank(address(vault), owner);
BridgedERC20(token).approve(address(airdrop), 1_000_000_000e18);

// Vault shall have the balance
assertEq(BridgedERC20(token).balanceOf(address(vault)), 1_000_000_000e18);

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

function getAliceDelegatesToBobSignature()
public
view
returns (address delegatee, uint256 nonce, uint256 expiry, uint8 v, bytes32 r, bytes32 s)
{
// Query user's nonce
nonce = BridgedERC20(token).nonces(Bob);
expiry = block.timestamp + 1_000_000;
delegatee = Bob;

LibDelegationSigUtil.Delegate memory delegate;
delegate.delegatee = delegatee;
delegate.nonce = nonce;
delegate.expiry = expiry;
bytes32 hash = LibDelegationSigUtil.getTypedDataHash(delegate, address(token));

// 0x2 is Alice's private key
(v, r, s) = vm.sign(0x1, hash);
}

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

// 1. Alice puts together the HASH (for delegating BOB) and signs it too.
(address delegatee, uint256 nonce, uint256 expiry, uint8 v, bytes32 r, bytes32 s) =
getAliceDelegatesToBobSignature();
// 2. Encode data
bytes memory delegationData = abi.encode(delegatee, nonce, expiry, v, r, s);
vm.prank(Alice, Alice);
airdrop.claimAndDelegate(Alice, 100, merkleProof, delegationData);

// Check Alice balance
assertEq(token.balanceOf(Alice), 100);
// Check who is delegatee, shall be Bob
assertEq(token.delegates(Alice), Bob);
}

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

bytes memory delegation = bytes("");

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

Expand All @@ -86,7 +212,5 @@ contract TestERC20Airdrop is TaikoTest {
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.
}
}
67 changes: 67 additions & 0 deletions packages/protocol/test/team/airdrop/LibDelegationSigUtil.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;

/// @notice Creating the TypedDataV4 hash
// NOTE: This contract implements the version of the encoding known as "v4", as implemented by the
// JSON RPC method
// https://docs.metamask.io/guide/signing-data.html[`eth_signTypedDataV4` in MetaMask].
/// @dev IMPORTANT!! This is for testing, but we need this in the UI for recreating the same hash
/// (to be signed by the user).
// A good resource on how-to:
// link:
// https://medium.com/@javaidea/how-to-sign-and-verify-eip-712-signatures-with-solidity-and-typescript-part-1-5118fdda1fe7

library LibDelegationSigUtil {
// EIP712 TYPES_HASH.
bytes32 private constant _TYPE_HASH = keccak256(
"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"
);

// For delegation - this TYPES_HASH is fixed.
bytes32 private constant _DELEGATION_TYPEHASH =
keccak256("Delegation(address delegatee,uint256 nonce,uint256 expiry)");

function getDomainSeparator(address verifierContract) public view returns (bytes32) {
// This is how we create a contract level domain separator!
// todo (@KorbinianK , @2manslkh): Do it off-chain, in the UI
return keccak256(
abi.encode(
_TYPE_HASH,
keccak256(bytes("Taiko Token")),
keccak256(bytes("1")),
block.chainid,
verifierContract
)
);
}

struct Delegate {
address delegatee;
uint256 nonce;
uint256 expiry;
}

// computes the hash of a delegation
function getStructHash(Delegate memory _delegate) internal pure returns (bytes32) {
return keccak256(
abi.encode(_DELEGATION_TYPEHASH, _delegate.delegatee, _delegate.nonce, _delegate.expiry)
);
}

// computes the hash of the fully encoded EIP-712 message for the domain, which can be used to
// recover the signer
function getTypedDataHash(
Delegate memory _permit,
address verifierContract
)
public
view
returns (bytes32)
{
return keccak256(
abi.encodePacked(
"\x19\x01", getDomainSeparator(verifierContract), getStructHash(_permit)
)
);
}
}

0 comments on commit e60588c

Please sign in to comment.