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: add support for permit based tokens in erc20 gateway #329

Merged
merged 17 commits into from
Oct 22, 2024
Merged
Show file tree
Hide file tree
Changes from 8 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
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,
PermitToken,
} 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: PermitToken;
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
Loading