Skip to content

Commit

Permalink
feat: add support for permit based tokens in erc20 gateway (#329)
Browse files Browse the repository at this point in the history
- closes #156 
- implements [EIP 2612](https://eips.ethereum.org/EIPS/eip-2612) in the
gateway
  • Loading branch information
viraj124 authored Oct 22, 2024
1 parent d3d819b commit f77346c
Show file tree
Hide file tree
Showing 8 changed files with 581 additions and 30 deletions.
6 changes: 6 additions & 0 deletions .changeset/tricky-avocados-dream.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@fuel-bridge/solidity-contracts': minor
'@fuel-bridge/test-utils': minor
---

add support for permit tokens in the erc20 gateway
243 changes: 216 additions & 27 deletions packages/integration-tests/tests/bridge_erc20.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,18 @@ import {
RATE_LIMIT_AMOUNT,
RATE_LIMIT_DURATION,
} from '@fuel-bridge/solidity-contracts/protocol/constants';
import type { Token } from '@fuel-bridge/solidity-contracts/typechain';
import type {
Token,
MockPermitToken,
} from '@fuel-bridge/solidity-contracts/typechain';
import type { TestEnvironment } from '@fuel-bridge/test-utils';
import {
setupEnvironment,
relayCommonMessage,
waitForMessage,
createRelayMessageParams,
getOrDeployECR20Contract,
getOrDeployERC20PermitContract,
getOrDeployL2Bridge,
FUEL_TX_PARAMS,
getMessageOutReceipt,
Expand Down Expand Up @@ -42,12 +46,15 @@ describe('Bridging ERC20 tokens', async function () {

let env: TestEnvironment;
let eth_testToken: Token;
let eth_permitTestToken: MockPermitToken;
let eth_testTokenAddress: string;
let eth_permitTestTokenAddress: string;
let eth_erc20GatewayAddress: string;
let fuel_bridge: BridgeFungibleToken;
let fuel_bridgeImpl: BridgeFungibleToken;
let fuel_bridgeContractId: string;
let fuel_testAssetId: string;
let fuel_test_permit_token_AssetId: string;

// override the default test timeout from 2000ms
this.timeout(DEFAULT_TIMEOUT_MS);
Expand Down Expand Up @@ -114,7 +121,7 @@ describe('Bridging ERC20 tokens', async function () {
);
}

async function relayMessage(
async function relayMessageFromFuel(
env: TestEnvironment,
withdrawMessageProof: MessageProof
) {
Expand All @@ -134,14 +141,108 @@ describe('Bridging ERC20 tokens', async function () {
);
}

async function relayMessageFromEthereum(
env: TestEnvironment,
fuelTokenMessageReceiver: AbstractAddress,
fuelTokenMessageNonce: BN,
fuel_AssetId: string,
amount: bigint
) {
// relay the message ourselves
const message = await waitForMessage(
env.fuel.provider,
fuelTokenMessageReceiver,
fuelTokenMessageNonce,
FUEL_MESSAGE_TIMEOUT_MS
);
expect(message).to.not.be.null;

const tx = await relayCommonMessage(env.fuel.deployer, message, {
maturity: undefined,
contractIds: [fuel_bridgeImpl.id.toHexString()],
});

const txResult = await tx.waitForResult();

expect(txResult.status).to.equal('success');
expect(txResult.mintedAssets.length).to.equal(1);

const [mintedAsset] = txResult.mintedAssets;

expect(mintedAsset.assetId).to.equal(fuel_AssetId);
expect(mintedAsset.amount.toString()).to.equal(
(amount / DECIMAL_DIFF).toString()
);
}

async function buildPermitParams(
name: string,
tokenAddress: string,
gatewayAddress: string,
amount: bigint,
nonce: bigint,
deadline: number,
deployer: Signer
) {
const domain: any = {
name: name,
version: '1',
chainId: env.eth.provider._network.chainId,
verifyingContract: tokenAddress,
};

const types: any = {
Permit: [
{ name: 'owner', type: 'address' },
{ name: 'spender', type: 'address' },
{ name: 'value', type: 'uint256' },
{ name: 'nonce', type: 'uint256' },
{ name: 'deadline', type: 'uint256' },
],
};

const values: any = {
owner: await deployer.getAddress(),
spender: gatewayAddress,
value: amount.toString(),
nonce: nonce.toString(),
deadline: deadline.toString(),
};

return { domain, types, values };
}

function parseSignature(signature: string) {
signature = signature.startsWith('0x') ? signature.slice(2) : signature;

// Ensure the signature length is correct
if (signature.length !== 130) {
throw new Error('Invalid signature length!');
}
// Extract R, S, V
const r = '0x' + signature.slice(0, 64);
const s = '0x' + signature.slice(64, 128);
const v = parseInt(signature.slice(128, 130), 16);
// Return formatted values
return {
r,
s,
v,
};
}

before(async () => {
env = await setupEnvironment({});
eth_erc20GatewayAddress = (
await env.eth.fuelERC20Gateway.getAddress()
).toLowerCase();

eth_testToken = await getOrDeployECR20Contract(env);
eth_permitTestToken = await getOrDeployERC20PermitContract(env);
eth_testTokenAddress = (await eth_testToken.getAddress()).toLowerCase();
eth_permitTestTokenAddress = (
await eth_permitTestToken.getAddress()
).toLowerCase();

const { contract, implementation } = await getOrDeployL2Bridge(
env,
Expand All @@ -156,6 +257,11 @@ describe('Bridging ERC20 tokens', async function () {
await env.eth.fuelERC20Gateway.setAssetIssuerId(fuel_bridgeContractId);
fuel_testAssetId = getTokenId(fuel_bridge, eth_testTokenAddress);

fuel_test_permit_token_AssetId = getTokenId(
fuel_bridge,
eth_permitTestTokenAddress
);

// initializing rate limit params for the token
await env.eth.fuelERC20Gateway
.connect(env.eth.deployer)
Expand Down Expand Up @@ -200,13 +306,17 @@ describe('Bridging ERC20 tokens', async function () {

describe('Bridge ERC20 to Fuel', async () => {
const NUM_TOKENS = 100000000000000000000n;
const DEADLINE = Math.floor(Date.now() / 1000) + 600; // 10 mins from current timestamp
let ethereumTokenSender: Signer;
let ethereumTokenSenderAddress: string;
let ethereumTokenSenderBalance: bigint;
let ethereumPermitTokenSenderBalance: bigint;
let fuelTokenReceiver: FuelWallet;
let fuelTokenReceiverAddress: string;
let fuelTokenReceiverBalance: BN;
let fuelPermitTokenReceiverBalance: BN;
let fuelTokenMessageNonce: BN;
let fuelTokenMessageNonceForPermitToken: BN;
let fuelTokenMessageReceiver: AbstractAddress;

before(async () => {
Expand All @@ -217,14 +327,88 @@ describe('Bridging ERC20 tokens', async function () {
.mint(ethereumTokenSenderAddress, NUM_TOKENS)
.then((tx) => tx.wait());

await eth_permitTestToken
.mint(ethereumTokenSenderAddress, NUM_TOKENS)
.then((tx) => tx.wait());

ethereumTokenSenderBalance = await eth_testToken.balanceOf(
ethereumTokenSenderAddress
);
ethereumPermitTokenSenderBalance = await eth_permitTestToken.balanceOf(
ethereumTokenSenderAddress
);
fuelTokenReceiver = env.fuel.signers[0];
fuelTokenReceiverAddress = fuelTokenReceiver.address.toHexString();
fuelTokenReceiverBalance = await fuelTokenReceiver.getBalance(
fuel_testAssetId
);
fuelPermitTokenReceiverBalance = await fuelTokenReceiver.getBalance(
fuel_test_permit_token_AssetId
);
});

it('Bridge ERC20 token with permit via FuelERC20Gateway', async () => {
const tokenName = await eth_permitTestToken.name();
const tokenAddress = await eth_permitTestToken.getAddress();
const gatewayAddress = await env.eth.fuelERC20Gateway.getAddress();
const deployerNonce = await eth_permitTestToken.nonces(
ethereumTokenSender
);

const signatureParams = await buildPermitParams(
tokenName,
tokenAddress,
gatewayAddress,
NUM_TOKENS,
deployerNonce,
DEADLINE,
ethereumTokenSender
);

const signature = await ethereumTokenSender.signTypedData(
signatureParams.domain,
signatureParams.types,
signatureParams.values
);

const { r, s, v } = parseSignature(signature);
const permitData = {
deadline: DEADLINE,
v,
r,
s,
};

// use the FuelERC20Gateway to deposit test tokens and receive equivalent tokens on Fuel
const receipt = await env.eth.fuelERC20Gateway
.connect(ethereumTokenSender)
.depositWithPermit(
fuelTokenReceiverAddress,
eth_permitTestTokenAddress,
NUM_TOKENS,
permitData
)
.then((tx) => tx.wait());

expect(receipt.status).to.equal(1);

// parse events from logs
const [event, ...restOfEvents] =
await env.eth.fuelMessagePortal.queryFilter(
env.eth.fuelMessagePortal.filters.MessageSent,
receipt.blockNumber,
receipt.blockNumber
);
expect(restOfEvents.length).to.be.eq(0); // Should be only 1 event

fuelTokenMessageNonceForPermitToken = new BN(event.args.nonce.toString());

// check that the sender balance has decreased by the expected amount
const newSenderBalance = await eth_permitTestToken.balanceOf(
ethereumTokenSenderAddress
);
expect(newSenderBalance === ethereumPermitTokenSenderBalance - NUM_TOKENS)
.to.be.true;
});

it('Bridge ERC20 via FuelERC20Gateway', async () => {
Expand All @@ -241,7 +425,6 @@ describe('Bridging ERC20 tokens', async function () {
.then((tx) => tx.wait());

expect(receipt.status).to.equal(1);

// parse events from logs
const [event, ...restOfEvents] =
await env.eth.fuelMessagePortal.queryFilter(
Expand All @@ -262,34 +445,27 @@ describe('Bridging ERC20 tokens', async function () {
.true;
});

it('Relay message from Ethereum on Fuel', async () => {
it('Relay messages from Ethereum on Fuel', async () => {
// override the default test timeout from 2000ms
this.timeout(FUEL_MESSAGE_TIMEOUT_MS);

// relay the message ourselves
const message = await waitForMessage(
env.fuel.provider,
// relay the standard erc20 deposit
await relayMessageFromEthereum(
env,
fuelTokenMessageReceiver,
fuelTokenMessageNonce,
FUEL_MESSAGE_TIMEOUT_MS
fuel_testAssetId,
NUM_TOKENS
);
expect(message).to.not.be.null;

const tx = await relayCommonMessage(env.fuel.deployer, message, {
maturity: undefined,
contractIds: [fuel_bridgeImpl.id.toHexString()],
});

const txResult = await tx.waitForResult();

expect(txResult.status).to.equal('success');
expect(txResult.mintedAssets.length).to.equal(1);

const [mintedAsset] = txResult.mintedAssets;

expect(mintedAsset.assetId).to.equal(fuel_testAssetId);
expect(mintedAsset.amount.toString()).to.equal(
(NUM_TOKENS / DECIMAL_DIFF).toString()
// override the default test timeout from 2000ms
this.timeout(FUEL_MESSAGE_TIMEOUT_MS);
// relay the erc20 permit token deposit
await relayMessageFromEthereum(
env,
fuelTokenMessageReceiver,
fuelTokenMessageNonceForPermitToken,
fuel_test_permit_token_AssetId,
NUM_TOKENS
);
});

Expand Down Expand Up @@ -320,6 +496,19 @@ describe('Bridging ERC20 tokens', async function () {
).to.be.true;
});

it('Check ERC20 permit token arrived on Fuel', async () => {
// check that the recipient balance has increased by the expected amount
const newReceiverPermitBalance = await fuelTokenReceiver.getBalance(
fuel_test_permit_token_AssetId
);

expect(
newReceiverPermitBalance.eq(
fuelPermitTokenReceiverBalance.add(toBeHex(NUM_TOKENS / DECIMAL_DIFF))
)
).to.be.true;
});

it('Bridge metadata', async () => {
// use the FuelERC20Gateway to deposit test tokens and receive equivalent tokens on Fuel
const receipt = await env.eth.fuelERC20Gateway
Expand Down Expand Up @@ -416,7 +605,7 @@ describe('Bridging ERC20 tokens', async function () {
await env.eth.fuelERC20Gateway.currentPeriodEnd(eth_testTokenAddress);

// relay message
await relayMessage(env, withdrawMessageProof);
await relayMessageFromFuel(env, withdrawMessageProof);

// check rate limit params
const withdrawnAmountAfterRelay =
Expand Down Expand Up @@ -541,7 +730,7 @@ describe('Bridging ERC20 tokens', async function () {
);

// relay message
await relayMessage(env, withdrawMessageProof);
await relayMessageFromFuel(env, withdrawMessageProof);

const currentPeriodEndAfterRelay =
await env.eth.fuelERC20Gateway.currentPeriodEnd(eth_testTokenAddress);
Expand Down
Loading

0 comments on commit f77346c

Please sign in to comment.