Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(protocol): add ERC20Airdrop test and deployment script #15752

Merged
merged 4 commits into from
Feb 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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: [email protected]
// 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)
)
);
}
}
Loading