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

ERC20 rate limiter #239

Merged
merged 32 commits into from
Aug 28, 2024
Merged
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
806054c
refactor: add state vars to support rate limit
viraj124 Aug 20, 2024
8130f0d
feat: complete logic for handling rate limit for erc20's
viraj124 Aug 21, 2024
3e18057
fix: existing unit tests
viraj124 Aug 21, 2024
d4e16e9
chore: update var visibility
viraj124 Aug 23, 2024
f7bc3bc
refactor: add assertion checks to existing integration test
viraj124 Aug 23, 2024
2a75533
refactor: add -ve erc20 rate limit unit tests
viraj124 Aug 25, 2024
2bc3cf2
feat: add erc20 rate limit integration tests
viraj124 Aug 26, 2024
0ee8e14
chore: add changeset
viraj124 Aug 26, 2024
a7eb9c3
refactor: reduce codesize in integration tests
viraj124 Aug 26, 2024
467deb0
Merge branch 'main' into erc20-rate-limiter
viraj124 Aug 26, 2024
54709e7
chore: formatting
viraj124 Aug 26, 2024
4b538c3
chore: rebase main
viraj124 Aug 26, 2024
8dacfd1
chore: some more formatting
viraj124 Aug 26, 2024
bbd172f
fix: test compile time error
viraj124 Aug 26, 2024
e908ab0
chore; revert latest integration test updates
viraj124 Aug 26, 2024
0446863
chore: add events
viraj124 Aug 27, 2024
baae9e4
chore: update unit tests
viraj124 Aug 27, 2024
bd3e54a
refactor; make rate limit duration for erc20 dynammic
viraj124 Aug 27, 2024
53a75d3
refactor: update tests
viraj124 Aug 27, 2024
0ea7759
chore: small touchup
viraj124 Aug 27, 2024
7007dce
refactor: add missing unchecked
viraj124 Aug 27, 2024
f9b8006
Merge branch 'main' into erc20-rate-limiter
viraj124 Aug 27, 2024
4db4c3c
refactor: update events
viraj124 Aug 27, 2024
a3e4570
fix: compilation
viraj124 Aug 27, 2024
f078e30
Merge branch 'main' into erc20-rate-limiter
viraj124 Aug 27, 2024
d50ae01
refactor: add conditional rate limit check
viraj124 Aug 27, 2024
73e64d1
refactor: add test for conditional rate limit check
viraj124 Aug 27, 2024
0aeaef6
chore: revert some formatting changes
viraj124 Aug 27, 2024
0876f6c
chore; revert visibility change
viraj124 Aug 27, 2024
a59a2e2
refactor: remove redunddant initialize code for erc20 limiter
viraj124 Aug 28, 2024
ea95dff
refactor: update tests
viraj124 Aug 28, 2024
9330be3
Merge branch 'main' into erc20-rate-limiter
viraj124 Aug 28, 2024
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
5 changes: 5 additions & 0 deletions .changeset/moody-ghosts-lie.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@fuel-bridge/solidity-contracts': major
---

erc20 rate limit
313 changes: 246 additions & 67 deletions packages/integration-tests/tests/bridge_erc20.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { BridgeFungibleToken } from '@fuel-bridge/fungible-token';
import type { BridgeFungibleToken } from '@fuel-bridge/fungible-token';
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 { TestEnvironment } from '@fuel-bridge/test-utils';
import {
Expand All @@ -16,10 +20,11 @@ import {
getTokenId,
getBlock,
FUEL_CALL_TX_PARAMS,
hardhatSkipTime,
} from '@fuel-bridge/test-utils';
import chai from 'chai';
import { toBeHex } from 'ethers';
import type { Signer } from 'ethers';
import { toBeHex, parseEther } from 'ethers';
import type { JsonRpcProvider, Signer } from 'ethers';
import { Address, BN } from 'fuels';
import type {
AbstractAddress,
Expand Down Expand Up @@ -47,11 +52,95 @@ describe('Bridging ERC20 tokens', async function () {
// override the default test timeout from 2000ms
this.timeout(DEFAULT_TIMEOUT_MS);

async function generateWithdrawalMessageProof(
fuel_bridge: BridgeFungibleToken,
fuelTokenSender: FuelWallet,
ethereumTokenReceiverAddress: string,
NUM_TOKENS: bigint,
DECIMAL_DIFF: bigint
): Promise<MessageProof> {
// withdraw tokens back to the base chain
fuel_bridge.account = fuelTokenSender;
const paddedAddress =
'0x' + ethereumTokenReceiverAddress.slice(2).padStart(64, '0');
const fuelTokenSenderBalance = await fuelTokenSender.getBalance(
fuel_testAssetId
);
const transactionRequest = await fuel_bridge.functions
.withdraw(paddedAddress)
.addContracts([fuel_bridge, fuel_bridgeImpl])
.txParams({
tip: 0,
gasLimit: 1_000_000,
maxFee: 1,
})
.callParams({
forward: {
amount: new BN(NUM_TOKENS.toString()).div(
new BN(DECIMAL_DIFF.toString())
),
assetId: fuel_testAssetId,
},
})
.fundWithRequiredCoins();

const tx = await fuelTokenSender.sendTransaction(transactionRequest);
const fWithdrawTxResult = await tx.waitForResult();
expect(fWithdrawTxResult.status).to.equal('success');

// check that the sender balance has decreased by the expected amount
const newSenderBalance = await fuelTokenSender.getBalance(fuel_testAssetId);

expect(
newSenderBalance.eq(
fuelTokenSenderBalance.sub(toBeHex(NUM_TOKENS / DECIMAL_DIFF))
)
).to.be.true;

// Wait for the commited block
const withdrawBlock = await getBlock(
env.fuel.provider.url,
fWithdrawTxResult.blockId
);
const commitHashAtL1 = await waitForBlockCommit(
env,
withdrawBlock.header.height
);

const messageOutReceipt = getMessageOutReceipt(fWithdrawTxResult.receipts);
return await fuelTokenSender.provider.getMessageProof(
tx.id,
messageOutReceipt.nonce,
commitHashAtL1
);
}

async function relayMessage(
env: TestEnvironment,
withdrawMessageProof: MessageProof
) {
// wait for block finalization
await waitForBlockFinalization(env, withdrawMessageProof);

// construct relay message proof data
const relayMessageParams = createRelayMessageParams(withdrawMessageProof);

// relay message
await env.eth.fuelMessagePortal.relayMessage(
relayMessageParams.message,
relayMessageParams.rootBlockHeader,
relayMessageParams.blockHeader,
relayMessageParams.blockInHistoryProof,
relayMessageParams.messageInBlockProof
);
}

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

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

Expand All @@ -68,6 +157,15 @@ describe('Bridging ERC20 tokens', async function () {
await env.eth.fuelERC20Gateway.setAssetIssuerId(fuel_bridgeContractId);
fuel_testAssetId = getTokenId(fuel_bridge, eth_testTokenAddress);

// initializing rate limit params for the token
await env.eth.fuelERC20Gateway
.connect(env.eth.deployer)
.initializeRateLimit(
eth_testTokenAddress,
RATE_LIMIT_AMOUNT.toString(),
RATE_LIMIT_DURATION
);

const { value: expectedGatewayContractId } = await fuel_bridge.functions
.bridged_token_gateway()
.addContracts([fuel_bridge, fuel_bridgeImpl])
Expand Down Expand Up @@ -98,7 +196,7 @@ describe('Bridging ERC20 tokens', async function () {
});

describe('Bridge ERC20 to Fuel', async () => {
const NUM_TOKENS = 10_000_000_000n;
const NUM_TOKENS = 100000000000000000000n;
let ethereumTokenSender: Signer;
let ethereumTokenSenderAddress: string;
let ethereumTokenSenderBalance: bigint;
Expand Down Expand Up @@ -260,7 +358,7 @@ describe('Bridging ERC20 tokens', async function () {
});

describe('Bridge ERC20 from Fuel', async () => {
const NUM_TOKENS = 10_000_000_000n;
const NUM_TOKENS = 10000000000000000000n;
let fuelTokenSender: FuelWallet;
let ethereumTokenReceiver: Signer;
let ethereumTokenReceiverAddress: string;
Expand All @@ -278,89 +376,170 @@ describe('Bridging ERC20 tokens', async function () {

it('Bridge ERC20 via Fuel token contract', async () => {
// withdraw tokens back to the base chain
fuel_bridge.account = fuelTokenSender;
const paddedAddress =
'0x' + ethereumTokenReceiverAddress.slice(2).padStart(64, '0');
const fuelTokenSenderBalance = await fuelTokenSender.getBalance(
fuel_testAssetId
withdrawMessageProof = await generateWithdrawalMessageProof(
fuel_bridge,
fuelTokenSender,
ethereumTokenReceiverAddress,
NUM_TOKENS,
DECIMAL_DIFF
);
const transactionRequest = await fuel_bridge.functions
.withdraw(paddedAddress)
.addContracts([fuel_bridge, fuel_bridgeImpl])
.txParams({
tip: 0,
gasLimit: 1_000_000,
maxFee: 1,
})
.callParams({
forward: {
amount: fuelTokenSenderBalance,
assetId: fuel_testAssetId,
},
})
.fundWithRequiredCoins();

const tx = await fuelTokenSender.sendTransaction(transactionRequest);
const fWithdrawTxResult = await tx.waitForResult();
expect(fWithdrawTxResult.status).to.equal('success');
});

it('Relay Message from Fuel on Ethereum', async () => {
const withdrawnAmountBeforeRelay =
await env.eth.fuelERC20Gateway.currentPeriodAmount(
eth_testTokenAddress
);

const rateLimitEndDuratioBeforeRelay =
await env.eth.fuelERC20Gateway.currentPeriodEnd(eth_testTokenAddress);

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

// check rate limit params
const withdrawnAmountAfterRelay =
await env.eth.fuelERC20Gateway.currentPeriodAmount(
eth_testTokenAddress
);

const rateLimitEndDuratioAfterRelay =
await env.eth.fuelERC20Gateway.currentPeriodEnd(eth_testTokenAddress);

expect(rateLimitEndDuratioAfterRelay === rateLimitEndDuratioBeforeRelay)
.to.be.true;

// check that the sender balance has decreased by the expected amount
const newSenderBalance = await fuelTokenSender.getBalance(
fuel_testAssetId
);
expect(
newSenderBalance.eq(
fuelTokenSenderBalance.sub(toBeHex(NUM_TOKENS / DECIMAL_DIFF))
)
withdrawnAmountAfterRelay === NUM_TOKENS + withdrawnAmountBeforeRelay
).to.be.true;
});

// Wait for the commited block
const withdrawBlock = await getBlock(
env.fuel.provider.url,
fWithdrawTxResult.blockId
);
const commitHashAtL1 = await waitForBlockCommit(
env,
withdrawBlock.header.height
);
it('Rate limit parameters are updated when current withdrawn amount is more than the new limit', async () => {
const deployer = await env.eth.deployer;
const newRateLimit = '5';

await env.eth.fuelERC20Gateway
.connect(deployer)
.resetRateLimitAmount(
eth_testTokenAddress,
parseEther(newRateLimit),
RATE_LIMIT_DURATION
);

const messageOutReceipt = getMessageOutReceipt(
fWithdrawTxResult.receipts
const currentWithdrawnAmountAfterSettingLimit =
await env.eth.fuelERC20Gateway.currentPeriodAmount(
eth_testTokenAddress
);

expect(
currentWithdrawnAmountAfterSettingLimit === parseEther(newRateLimit)
).to.be.true;
});

it('Rate limit parameters are updated when the initial duration is over', async () => {
const deployer = await env.eth.deployer;
const newRateLimit = `30`;

const rateLimitDuration =
await env.eth.fuelERC20Gateway.rateLimitDuration(eth_testTokenAddress);

// fast forward time
await hardhatSkipTime(
env.eth.provider as JsonRpcProvider,
rateLimitDuration * 2n
);
withdrawMessageProof = await fuelTokenSender.provider.getMessageProof(
tx.id,
messageOutReceipt.nonce,
commitHashAtL1
const currentPeriodEndBeforeRelay =
await env.eth.fuelERC20Gateway.currentPeriodEnd(eth_testTokenAddress);

await env.eth.fuelERC20Gateway
.connect(deployer)
.resetRateLimitAmount(
eth_testTokenAddress,
parseEther(newRateLimit),
RATE_LIMIT_DURATION
);

// withdraw tokens back to the base chain
withdrawMessageProof = await generateWithdrawalMessageProof(
fuel_bridge,
fuelTokenSender,
ethereumTokenReceiverAddress,
NUM_TOKENS,
DECIMAL_DIFF
);

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

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

expect(currentPeriodEndAfterRelay > currentPeriodEndBeforeRelay).to.be
.true;

const currentPeriodAmount =
await env.eth.fuelERC20Gateway.currentPeriodAmount(
eth_testTokenAddress
);

expect(currentPeriodAmount === NUM_TOKENS).to.be.true;
});

it('Relay Message from Fuel on Ethereum', async () => {
// wait for block finalization
await waitForBlockFinalization(env, withdrawMessageProof);
it('Rate limit parameters are updated when new limit is set after the initial duration', async () => {
const rateLimitDuration =
await env.eth.fuelERC20Gateway.rateLimitDuration(eth_testTokenAddress);

// construct relay message proof data
const relayMessageParams = createRelayMessageParams(withdrawMessageProof);
const deployer = await env.eth.deployer;
const newRateLimit = `40`;

// relay message
// fast forward time
await hardhatSkipTime(
env.eth.provider as JsonRpcProvider,
rateLimitDuration * 2n
);

await env.eth.fuelMessagePortal
.connect(env.eth.signers[0])
.relayMessage(
relayMessageParams.message,
relayMessageParams.rootBlockHeader,
relayMessageParams.blockHeader,
relayMessageParams.blockInHistoryProof,
relayMessageParams.messageInBlockProof
const currentWithdrawnAmountBeforeSettingLimit =
await env.eth.fuelERC20Gateway.currentPeriodAmount(
eth_testTokenAddress
);
const currentPeriodEndBeforeSettingLimit =
await env.eth.fuelERC20Gateway.currentPeriodEnd(eth_testTokenAddress);

await env.eth.fuelERC20Gateway
.connect(deployer)
.resetRateLimitAmount(
eth_testTokenAddress,
parseEther(newRateLimit),
RATE_LIMIT_DURATION
);

const currentPeriodEndAfterSettingLimit =
await env.eth.fuelERC20Gateway.currentPeriodEnd(eth_testTokenAddress);
const currentWithdrawnAmountAfterSettingLimit =
await env.eth.fuelERC20Gateway.currentPeriodAmount(
eth_testTokenAddress
);

expect(
currentPeriodEndAfterSettingLimit > currentPeriodEndBeforeSettingLimit
).to.be.true;

expect(
currentWithdrawnAmountBeforeSettingLimit >
currentWithdrawnAmountAfterSettingLimit
).to.be.true;

expect(currentWithdrawnAmountAfterSettingLimit === 0n).to.be.true;
});

it('Check ERC20 arrived on Ethereum', async () => {
// check that the recipient balance has increased by the expected amount
const newReceiverBalance = await eth_testToken.balanceOf(
ethereumTokenReceiverAddress
);
expect(newReceiverBalance === ethereumTokenReceiverBalance + NUM_TOKENS)
.to.be.true;
expect(
newReceiverBalance === ethereumTokenReceiverBalance + NUM_TOKENS * 2n
).to.be.true;
});
});
});
Loading
Loading