Skip to content

Commit f77346c

Browse files
authored
feat: add support for permit based tokens in erc20 gateway (#329)
- closes #156 - implements [EIP 2612](https://eips.ethereum.org/EIPS/eip-2612) in the gateway
1 parent d3d819b commit f77346c

File tree

8 files changed

+581
-30
lines changed

8 files changed

+581
-30
lines changed

.changeset/tricky-avocados-dream.md

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@fuel-bridge/solidity-contracts': minor
3+
'@fuel-bridge/test-utils': minor
4+
---
5+
6+
add support for permit tokens in the erc20 gateway

packages/integration-tests/tests/bridge_erc20.ts

+216-27
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,18 @@ import {
33
RATE_LIMIT_AMOUNT,
44
RATE_LIMIT_DURATION,
55
} from '@fuel-bridge/solidity-contracts/protocol/constants';
6-
import type { Token } from '@fuel-bridge/solidity-contracts/typechain';
6+
import type {
7+
Token,
8+
MockPermitToken,
9+
} from '@fuel-bridge/solidity-contracts/typechain';
710
import type { TestEnvironment } from '@fuel-bridge/test-utils';
811
import {
912
setupEnvironment,
1013
relayCommonMessage,
1114
waitForMessage,
1215
createRelayMessageParams,
1316
getOrDeployECR20Contract,
17+
getOrDeployERC20PermitContract,
1418
getOrDeployL2Bridge,
1519
FUEL_TX_PARAMS,
1620
getMessageOutReceipt,
@@ -42,12 +46,15 @@ describe('Bridging ERC20 tokens', async function () {
4246

4347
let env: TestEnvironment;
4448
let eth_testToken: Token;
49+
let eth_permitTestToken: MockPermitToken;
4550
let eth_testTokenAddress: string;
51+
let eth_permitTestTokenAddress: string;
4652
let eth_erc20GatewayAddress: string;
4753
let fuel_bridge: BridgeFungibleToken;
4854
let fuel_bridgeImpl: BridgeFungibleToken;
4955
let fuel_bridgeContractId: string;
5056
let fuel_testAssetId: string;
57+
let fuel_test_permit_token_AssetId: string;
5158

5259
// override the default test timeout from 2000ms
5360
this.timeout(DEFAULT_TIMEOUT_MS);
@@ -114,7 +121,7 @@ describe('Bridging ERC20 tokens', async function () {
114121
);
115122
}
116123

117-
async function relayMessage(
124+
async function relayMessageFromFuel(
118125
env: TestEnvironment,
119126
withdrawMessageProof: MessageProof
120127
) {
@@ -134,14 +141,108 @@ describe('Bridging ERC20 tokens', async function () {
134141
);
135142
}
136143

144+
async function relayMessageFromEthereum(
145+
env: TestEnvironment,
146+
fuelTokenMessageReceiver: AbstractAddress,
147+
fuelTokenMessageNonce: BN,
148+
fuel_AssetId: string,
149+
amount: bigint
150+
) {
151+
// relay the message ourselves
152+
const message = await waitForMessage(
153+
env.fuel.provider,
154+
fuelTokenMessageReceiver,
155+
fuelTokenMessageNonce,
156+
FUEL_MESSAGE_TIMEOUT_MS
157+
);
158+
expect(message).to.not.be.null;
159+
160+
const tx = await relayCommonMessage(env.fuel.deployer, message, {
161+
maturity: undefined,
162+
contractIds: [fuel_bridgeImpl.id.toHexString()],
163+
});
164+
165+
const txResult = await tx.waitForResult();
166+
167+
expect(txResult.status).to.equal('success');
168+
expect(txResult.mintedAssets.length).to.equal(1);
169+
170+
const [mintedAsset] = txResult.mintedAssets;
171+
172+
expect(mintedAsset.assetId).to.equal(fuel_AssetId);
173+
expect(mintedAsset.amount.toString()).to.equal(
174+
(amount / DECIMAL_DIFF).toString()
175+
);
176+
}
177+
178+
async function buildPermitParams(
179+
name: string,
180+
tokenAddress: string,
181+
gatewayAddress: string,
182+
amount: bigint,
183+
nonce: bigint,
184+
deadline: number,
185+
deployer: Signer
186+
) {
187+
const domain: any = {
188+
name: name,
189+
version: '1',
190+
chainId: env.eth.provider._network.chainId,
191+
verifyingContract: tokenAddress,
192+
};
193+
194+
const types: any = {
195+
Permit: [
196+
{ name: 'owner', type: 'address' },
197+
{ name: 'spender', type: 'address' },
198+
{ name: 'value', type: 'uint256' },
199+
{ name: 'nonce', type: 'uint256' },
200+
{ name: 'deadline', type: 'uint256' },
201+
],
202+
};
203+
204+
const values: any = {
205+
owner: await deployer.getAddress(),
206+
spender: gatewayAddress,
207+
value: amount.toString(),
208+
nonce: nonce.toString(),
209+
deadline: deadline.toString(),
210+
};
211+
212+
return { domain, types, values };
213+
}
214+
215+
function parseSignature(signature: string) {
216+
signature = signature.startsWith('0x') ? signature.slice(2) : signature;
217+
218+
// Ensure the signature length is correct
219+
if (signature.length !== 130) {
220+
throw new Error('Invalid signature length!');
221+
}
222+
// Extract R, S, V
223+
const r = '0x' + signature.slice(0, 64);
224+
const s = '0x' + signature.slice(64, 128);
225+
const v = parseInt(signature.slice(128, 130), 16);
226+
// Return formatted values
227+
return {
228+
r,
229+
s,
230+
v,
231+
};
232+
}
233+
137234
before(async () => {
138235
env = await setupEnvironment({});
139236
eth_erc20GatewayAddress = (
140237
await env.eth.fuelERC20Gateway.getAddress()
141238
).toLowerCase();
142239

143240
eth_testToken = await getOrDeployECR20Contract(env);
241+
eth_permitTestToken = await getOrDeployERC20PermitContract(env);
144242
eth_testTokenAddress = (await eth_testToken.getAddress()).toLowerCase();
243+
eth_permitTestTokenAddress = (
244+
await eth_permitTestToken.getAddress()
245+
).toLowerCase();
145246

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

260+
fuel_test_permit_token_AssetId = getTokenId(
261+
fuel_bridge,
262+
eth_permitTestTokenAddress
263+
);
264+
159265
// initializing rate limit params for the token
160266
await env.eth.fuelERC20Gateway
161267
.connect(env.eth.deployer)
@@ -200,13 +306,17 @@ describe('Bridging ERC20 tokens', async function () {
200306

201307
describe('Bridge ERC20 to Fuel', async () => {
202308
const NUM_TOKENS = 100000000000000000000n;
309+
const DEADLINE = Math.floor(Date.now() / 1000) + 600; // 10 mins from current timestamp
203310
let ethereumTokenSender: Signer;
204311
let ethereumTokenSenderAddress: string;
205312
let ethereumTokenSenderBalance: bigint;
313+
let ethereumPermitTokenSenderBalance: bigint;
206314
let fuelTokenReceiver: FuelWallet;
207315
let fuelTokenReceiverAddress: string;
208316
let fuelTokenReceiverBalance: BN;
317+
let fuelPermitTokenReceiverBalance: BN;
209318
let fuelTokenMessageNonce: BN;
319+
let fuelTokenMessageNonceForPermitToken: BN;
210320
let fuelTokenMessageReceiver: AbstractAddress;
211321

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

330+
await eth_permitTestToken
331+
.mint(ethereumTokenSenderAddress, NUM_TOKENS)
332+
.then((tx) => tx.wait());
333+
220334
ethereumTokenSenderBalance = await eth_testToken.balanceOf(
221335
ethereumTokenSenderAddress
222336
);
337+
ethereumPermitTokenSenderBalance = await eth_permitTestToken.balanceOf(
338+
ethereumTokenSenderAddress
339+
);
223340
fuelTokenReceiver = env.fuel.signers[0];
224341
fuelTokenReceiverAddress = fuelTokenReceiver.address.toHexString();
225342
fuelTokenReceiverBalance = await fuelTokenReceiver.getBalance(
226343
fuel_testAssetId
227344
);
345+
fuelPermitTokenReceiverBalance = await fuelTokenReceiver.getBalance(
346+
fuel_test_permit_token_AssetId
347+
);
348+
});
349+
350+
it('Bridge ERC20 token with permit via FuelERC20Gateway', async () => {
351+
const tokenName = await eth_permitTestToken.name();
352+
const tokenAddress = await eth_permitTestToken.getAddress();
353+
const gatewayAddress = await env.eth.fuelERC20Gateway.getAddress();
354+
const deployerNonce = await eth_permitTestToken.nonces(
355+
ethereumTokenSender
356+
);
357+
358+
const signatureParams = await buildPermitParams(
359+
tokenName,
360+
tokenAddress,
361+
gatewayAddress,
362+
NUM_TOKENS,
363+
deployerNonce,
364+
DEADLINE,
365+
ethereumTokenSender
366+
);
367+
368+
const signature = await ethereumTokenSender.signTypedData(
369+
signatureParams.domain,
370+
signatureParams.types,
371+
signatureParams.values
372+
);
373+
374+
const { r, s, v } = parseSignature(signature);
375+
const permitData = {
376+
deadline: DEADLINE,
377+
v,
378+
r,
379+
s,
380+
};
381+
382+
// use the FuelERC20Gateway to deposit test tokens and receive equivalent tokens on Fuel
383+
const receipt = await env.eth.fuelERC20Gateway
384+
.connect(ethereumTokenSender)
385+
.depositWithPermit(
386+
fuelTokenReceiverAddress,
387+
eth_permitTestTokenAddress,
388+
NUM_TOKENS,
389+
permitData
390+
)
391+
.then((tx) => tx.wait());
392+
393+
expect(receipt.status).to.equal(1);
394+
395+
// parse events from logs
396+
const [event, ...restOfEvents] =
397+
await env.eth.fuelMessagePortal.queryFilter(
398+
env.eth.fuelMessagePortal.filters.MessageSent,
399+
receipt.blockNumber,
400+
receipt.blockNumber
401+
);
402+
expect(restOfEvents.length).to.be.eq(0); // Should be only 1 event
403+
404+
fuelTokenMessageNonceForPermitToken = new BN(event.args.nonce.toString());
405+
406+
// check that the sender balance has decreased by the expected amount
407+
const newSenderBalance = await eth_permitTestToken.balanceOf(
408+
ethereumTokenSenderAddress
409+
);
410+
expect(newSenderBalance === ethereumPermitTokenSenderBalance - NUM_TOKENS)
411+
.to.be.true;
228412
});
229413

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

243427
expect(receipt.status).to.equal(1);
244-
245428
// parse events from logs
246429
const [event, ...restOfEvents] =
247430
await env.eth.fuelMessagePortal.queryFilter(
@@ -262,34 +445,27 @@ describe('Bridging ERC20 tokens', async function () {
262445
.true;
263446
});
264447

265-
it('Relay message from Ethereum on Fuel', async () => {
448+
it('Relay messages from Ethereum on Fuel', async () => {
266449
// override the default test timeout from 2000ms
267450
this.timeout(FUEL_MESSAGE_TIMEOUT_MS);
268-
269-
// relay the message ourselves
270-
const message = await waitForMessage(
271-
env.fuel.provider,
451+
// relay the standard erc20 deposit
452+
await relayMessageFromEthereum(
453+
env,
272454
fuelTokenMessageReceiver,
273455
fuelTokenMessageNonce,
274-
FUEL_MESSAGE_TIMEOUT_MS
456+
fuel_testAssetId,
457+
NUM_TOKENS
275458
);
276-
expect(message).to.not.be.null;
277459

278-
const tx = await relayCommonMessage(env.fuel.deployer, message, {
279-
maturity: undefined,
280-
contractIds: [fuel_bridgeImpl.id.toHexString()],
281-
});
282-
283-
const txResult = await tx.waitForResult();
284-
285-
expect(txResult.status).to.equal('success');
286-
expect(txResult.mintedAssets.length).to.equal(1);
287-
288-
const [mintedAsset] = txResult.mintedAssets;
289-
290-
expect(mintedAsset.assetId).to.equal(fuel_testAssetId);
291-
expect(mintedAsset.amount.toString()).to.equal(
292-
(NUM_TOKENS / DECIMAL_DIFF).toString()
460+
// override the default test timeout from 2000ms
461+
this.timeout(FUEL_MESSAGE_TIMEOUT_MS);
462+
// relay the erc20 permit token deposit
463+
await relayMessageFromEthereum(
464+
env,
465+
fuelTokenMessageReceiver,
466+
fuelTokenMessageNonceForPermitToken,
467+
fuel_test_permit_token_AssetId,
468+
NUM_TOKENS
293469
);
294470
});
295471

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

499+
it('Check ERC20 permit token arrived on Fuel', async () => {
500+
// check that the recipient balance has increased by the expected amount
501+
const newReceiverPermitBalance = await fuelTokenReceiver.getBalance(
502+
fuel_test_permit_token_AssetId
503+
);
504+
505+
expect(
506+
newReceiverPermitBalance.eq(
507+
fuelPermitTokenReceiverBalance.add(toBeHex(NUM_TOKENS / DECIMAL_DIFF))
508+
)
509+
).to.be.true;
510+
});
511+
323512
it('Bridge metadata', async () => {
324513
// use the FuelERC20Gateway to deposit test tokens and receive equivalent tokens on Fuel
325514
const receipt = await env.eth.fuelERC20Gateway
@@ -416,7 +605,7 @@ describe('Bridging ERC20 tokens', async function () {
416605
await env.eth.fuelERC20Gateway.currentPeriodEnd(eth_testTokenAddress);
417606

418607
// relay message
419-
await relayMessage(env, withdrawMessageProof);
608+
await relayMessageFromFuel(env, withdrawMessageProof);
420609

421610
// check rate limit params
422611
const withdrawnAmountAfterRelay =
@@ -541,7 +730,7 @@ describe('Bridging ERC20 tokens', async function () {
541730
);
542731

543732
// relay message
544-
await relayMessage(env, withdrawMessageProof);
733+
await relayMessageFromFuel(env, withdrawMessageProof);
545734

546735
const currentPeriodEndAfterRelay =
547736
await env.eth.fuelERC20Gateway.currentPeriodEnd(eth_testTokenAddress);

0 commit comments

Comments
 (0)