Skip to content

Commit

Permalink
don't update current withdrawn amount when rate limit is reset when d…
Browse files Browse the repository at this point in the history
…uration hasn't passed (#290)

- closes #288

---------

Co-authored-by: Mad <[email protected]>
  • Loading branch information
viraj124 and DefiCake authored Sep 22, 2024
1 parent 6030a40 commit de18552
Show file tree
Hide file tree
Showing 16 changed files with 477 additions and 75 deletions.
5 changes: 5 additions & 0 deletions .changeset/green-carrots-rule.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@fuel-bridge/solidity-contracts': minor
---

don't update current withdrawn amount when rate limit is reset
2 changes: 1 addition & 1 deletion .github/actions/setup-rust/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ inputs:
rust-version:
default: 1.80.1
forc-components:
default: '[email protected], fuel-core@0.33.0'
default: '[email protected], fuel-core@0.36.0'

runs:
using: 'composite'
Expand Down
11 changes: 7 additions & 4 deletions docker/fuel-core/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
# https://github.com/FuelLabs/chain-configuration/tree/master/upgradelog/ignition-testnet
# and apply the latest state_transition_function and consensus_parameter
# when upgrading fuel-core
FROM ghcr.io/fuellabs/fuel-core:v0.33.0
FROM ghcr.io/fuellabs/fuel-core:v0.36.0

ARG FUEL_IP=0.0.0.0
ARG FUEL_PORT=4001
Expand All @@ -22,13 +22,16 @@ RUN git clone \
https://github.com/FuelLabs/chain-configuration.git \
/chain-configuration

# Anchor the chain configuration to a specific commit to avoid CI breaking
RUN cd /chain-configuration && git checkout c1c4d3bca57f64118a8d157310e0a839ae5c180a

# Copy the base local configuration
RUN cp -R /chain-configuration/local/* ./

# Copy the testnet consensus parameters and state transition bytecode
RUN cp /chain-configuration/upgradelog/ignition-testnet/consensus_parameters/5.json \
# Copy the devnet consensus parameters and state transition bytecode
RUN cp /chain-configuration/upgradelog/ignition-devnet/consensus_parameters/9.json \
./latest_consensus_parameters.json
RUN cp /chain-configuration/upgradelog/ignition-testnet/state_transition_function/6.wasm \
RUN cp /chain-configuration/upgradelog/ignition-devnet/state_transition_function/9.wasm \
./state_transition_bytecode.wasm

# update local state_config with custom genesis coins config
Expand Down
2 changes: 1 addition & 1 deletion docker/full-env/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# This image is built for external projects that want to have
# an e2e test environment
FROM ghcr.io/fuellabs/fuel-core:v0.33.0 as fuel-core
FROM ghcr.io/fuellabs/fuel-core:v0.36.0 as fuel-core
FROM ghcr.io/fuellabs/fuel-block-committer:v0.4.0 as fuel-committer

FROM node:20-slim as base
Expand Down
96 changes: 90 additions & 6 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Before delving into the details, let's understand a few key concepts that will b

- Layer 1 (L1): An EVM blockchain, namely Ethereum.
- Layer 2 (L2): The Fuel blockchain will sometimes be referred as the Layer 2 or L2.
- Fuel block: a block of the Fuel blockchain, generated by sequencers. It contains transactions, but also messages, originating from L1 with L2 destination and viceversa.
- Fuel block: a block of the Fuel blockchain, generated by sequencers. It contains transactions, but also messages, originating from L1 with L2 destination and vice-versa.
- Fuel root block: the last block of a Fuel epoch.
- Fuel epoch: A group of Fuel blocks, packed together and identified by the last block of the epoch.
- (Incoming / Outgoing) messages: depending on the context, messages will be referred as "incoming" or "outgoing". A L1 -> L2 message is an outgoing message from the L1 perspective, and an incoming message from the L2 perspective. A L2 -> L1 message inverts those tags.
Expand All @@ -20,7 +20,7 @@ Before delving into the details, let's understand a few key concepts that will b
- `FuelChainState` contract: L1 Smart contract that holds epoch information.
- `FuelMessagePortal` contract: L1 Smart contract that is able to validate relay messages from Fuel Blocks that have been committed to the state contract, and relays those messages to the corresponding entities in L1
- Layer 1 Bridge smart contract: L1 smart contract that holds L1 tokens. Upon user deposits, it generates an message for the L2 counterpart to mint the associated L2 token.
- Layer 2 Bridge smart contract: L2 (Fuel) smart contract that mints L2 tokens when receiving messsages from the L1 counterpart. Similarly, burns L2 tokens and generates a withdrawal message for the L1 contract to release L1 tokens to their rightful owner.
- Layer 2 Bridge smart contract: L2 (Fuel) smart contract that mints L2 tokens when receiving messages from the L1 counterpart. Similarly, burns L2 tokens and generates a withdrawal message for the L1 contract to release L1 tokens to their rightful owner.
- Deposit: User action by which some L1 tokens are locked in the L1 bridge smart contracts, minting the same amount in the L2 chain.
- Withdrawal: User action by which some L2 tokens are burnt in the L2 bridge smart contracts, releasing the burnt amount in the L1 chain.

Expand All @@ -30,18 +30,18 @@ Before delving into the details, let's understand a few key concepts that will b

## Bridge flow

Fuel 's bridge system is built on a message protocol that allows the sending (and receiving) of messages between entities located in two different blockchains, namely the L1 (Ethereum) and L2 (Fuel blockchain). The system features sending messages in both directions (L1 to L2, and L2 to L1), though the mechanisms involved for each direction are different and almost independent:
Fuel's bridge system is built on a message protocol that allows the sending (and receiving) of messages between entities located in two different blockchains, namely the L1 (Ethereum) and L2 (Fuel blockchain). The system features sending messages in both directions (L1 to L2, and L2 to L1), though the mechanisms involved for each direction are different and almost independent:

- A message that goes from L1 to L2 originates with an Ethereum event, to which Fuel sequencers will be listening. The event that creates this message will be parsed and included as an UTXO in the Fuel chain.
- A message that goes from L2 to L1 originates with a Fuel receipt. Receipts are bound with Fuel block headers. The block committer will push witnesses (Merkle roots) to Ethereum, so that these messages can be trustlessly unwrapped and sent via Merkle proofs to their recipients.

It can be derived that; if the entities receiving these messages are capable of interpreting them, some actions can be executed.

From here on, you will read first the logic involved in the L1 to L2 message passing, and viceversa after.
From here on, you will read first the logic involved in the L1 to L2 message passing, and vice-versa after.

### Message passing from L1 to L2

The [Message Portal](../packages/solidity-contracts/contracts/fuelchain/FuelMessagePortal.sol) contains a `sendMessage` function that can be called by any entity on the L1 blockchain. This function will emit an event `MessageSent` to be picked up by Fuel's sequencers, optionally containing an ETH value that will be depositted in the contract, and a data payload. The sequencers will include said message in the following blocks of the L2 blockchain, by adding an UTXO that reflects the original message. Messages will be reflected in the block header's inbox.
The [Message Portal](../packages/solidity-contracts/contracts/fuelchain/FuelMessagePortal.sol) contains a `sendMessage` function that can be called by any entity on the L1 blockchain. This function will emit an event `MessageSent` to be picked up by Fuel's sequencers, optionally containing an ETH value that will be deposited in the contract, and a data payload. The sequencers will include said message in the following blocks of the L2 blockchain, by adding an UTXO that reflects the original message. Messages will be reflected in the block header's inbox.

The `MessageSent` event emitted on the Ethereum chain and its counterpart UTXO `MessageCoin` on the Fuel chain hold, among other fields, a `value` (amount of ETH that is deposited), a payload `data` and an ID `recipient` that can spend this message in the L2.

Expand Down Expand Up @@ -110,7 +110,7 @@ sequenceDiagram

#### L1 (Ethereum) ERC20 Deposit

A deposit operation is an use case of the message passing system from L1 entities to L2 entities. The user will send a transaction to deposit an asset in a L1 bridge contract `FuelERC20Gateway`, which will in turn call `sendMessage` and emit a `MessageSent` event through the `FuelMessagePortal` to be picked up by the Fuel blockchain 's sequencer. The recipient of this message shall be the `bridge predicate` (a L2 entity that posseses an ID), whose role will be explained further ahead. The payload of the message will just reflect the fact that the `FuelERC20Gateway` has received a deposit of a L1 token, with a given amount and a desired recipient. Sequencers will include this message as part of an UTXO that can be spent by the `bridge predicate`. The `predicate` holds these UTXOs until any other entity (which can be the users themselves, or another "relayer" entity) executes the predicate 's logic, which has the responsibility for running validation logic before sending the data to the bridge contract on the L2, `erc20bridge.sw`, by calling `process_message`. Once the UTXO is "spent" (delivered), this last contract will have the capability of processing the payload inside the message, validating that their sender is the L1 bridge contract `FuelERC20Gateway`, and then proceeding to mint an equivalent asset and amount in the Fuel blockchain to the one originally deposited in the L1. Then, the minted assets become available to the user in the L2.
A deposit operation is a use case of the message passing system from L1 entities to L2 entities. The user will send a transaction to deposit an asset in a L1 bridge contract `FuelERC20Gateway`, which will in turn call `sendMessage` and emit a `MessageSent` event through the `FuelMessagePortal` to be picked up by the Fuel blockchain's sequencer. The recipient of this message shall be the `bridge predicate` (a L2 entity that possesses an ID), whose role will be explained further ahead. The payload of the message will just reflect the fact that the `FuelERC20Gateway` has received a deposit of a L1 token, with a given amount and a desired recipient. Sequencers will include this message as part of an UTXO that can be spent by the `bridge predicate`. The `predicate` holds these UTXOs until any other entity (which can be the users themselves, or another "relayer" entity) executes the predicate's logic, which has the responsibility for running validation logic before sending the data to the bridge contract on the L2, `erc20bridge.sw`, by calling `process_message`. Once the UTXO is "spent" (delivered), this last contract will have the capability of processing the payload inside the message, validating that their sender is the L1 bridge contract `FuelERC20Gateway`, and then proceeding to mint an equivalent asset and amount in the Fuel blockchain to the one originally deposited in the L1. Then, the minted assets become available to the user in the L2.

```mermaid
sequenceDiagram
Expand Down Expand Up @@ -343,6 +343,90 @@ You can follow the implementation of this flow via:
- [Message Portal](../packages/solidity-contracts/contracts/fuelchain/FuelMessagePortal/v3/FuelMessagePortalV3.sol) 's `relayMessage` function
- [FuelERC20Gateway.sol](../packages/solidity-contracts/contracts/messaging/gateway/FuelERC20Gateway/FuelERC20GatewayV4.sol) 's `finalizeWithdrawal`

## Rate Limit Mechanism

The Fuel Bridge uses a rate limiting mechanism as a protection for withdrawals which ensures that until a specific time period passes by only a certain maximum amount of ETH/ERC20 tokens can be withdrawn & the withdrawn limit is reset after each time interval. Fuel's implementation is inspired by the [Linea Bridge](https://github.com/Consensys/linea-contracts/blob/main/contracts/messageService/lib/RateLimiter.sol)

There is a small difference between the implementation done by Fuel from Linea's discussed with examples below

```
solidity
/**
Linea Implementation
* @notice Resets the rate limit amount.
* @dev If the used amount is higher, it is set to the limit to avoid confusion/issues.
* @dev Only the RATE_LIMIT_SETTER_ROLE is allowed to execute this function.
* @dev Emits the LimitAmountChanged event.
* @dev usedLimitAmountToSet will use the default value of zero if period has expired
* @param _amount The amount to reset the limit to.
*/
function resetRateLimitAmount(uint256 _amount) external onlyRole(RATE_LIMIT_SETTER_ROLE) {
uint256 usedLimitAmountToSet;
bool amountUsedLoweredToLimit;
bool usedAmountResetToZero;
if (currentPeriodEnd < block.timestamp) {
currentPeriodEnd = block.timestamp + periodInSeconds;
usedAmountResetToZero = true;
} else {
if (_amount < currentPeriodAmountInWei) {
usedLimitAmountToSet = _amount;
amountUsedLoweredToLimit = true;
}
}
limitInWei = _amount;
if (usedAmountResetToZero || amountUsedLoweredToLimit) {
currentPeriodAmountInWei = usedLimitAmountToSet;
}
emit LimitAmountChanged(_msgSender(), _amount, amountUsedLoweredToLimit, usedAmountResetToZero);
}
/**
* Fuel Implementation
* @notice Resets the rate limit amount.
* @param _amount The amount to reset the limit to.
* The difference from the linea implementation is that when currentPeriodEnd >= block.timestamp then if the new rate limit amount is less than the currentPeriodAmount, then currentPeriodAmount is not updated this makes sure that if rate limit is first reduced & then increased within the rate limit duration then any extra amount can't be withdrawn
*/
function resetRateLimitAmount(uint256 _amount) external onlyRole(SET_RATE_LIMITER_ROLE) {
// if period has expired then currentPeriodAmount is zero
if (currentPeriodEnd < block.timestamp) {
unchecked {
currentPeriodEnd = block.timestamp + rateLimitDuration;
}
currentPeriodAmount = 0;
}
limitAmount = _amount;
emit ResetRateLimit(_amount);
}
```

### Example showcasing the difference in implementation

- Linea Implementation

| Rate Limit Action | Current Withdrawal Amount | Current Rate Limit |
| ----------------- | :-----------------------: | -----------------: |
| No Update | 9 ether | 10 ether |
| Reduced | 5 ether | 5 ether |
| Increased | 5 ether | 10 ether |

- Fuel Implementation

| Rate Limit Action | Current Withdrawal Amount | Current Rate Limit |
| ----------------- | :-----------------------: | -----------------: |
| No Update | 9 ether | 10 ether |
| Reduced | 9 ether | 5 ether |
| Increased | 9 ether | 10 ether |

## Decimals adjustment

One of Fuel 's key design principles is to minimize execution costs and minimize state growth. One of the design decisions furthering that goal is the use of 64 bits (`u64`) to code the balances that are bridged from L1 to L2. The most popular token standard, `ERC20`, codes the amounts with `256 bits`. If the amount transferred from L1 to L2 surpasses the capacity of Fuel to mint the L2 counterpart, a `refund` message will be generated, that will allow the user to retrieve the originally deposited amount in L1. We foresee this to be an extreme case, as there are currently no widely used tokens that surpass `2^64` units of supply.
Expand Down
2 changes: 1 addition & 1 deletion fuel-toolchain.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@
channel = "testnet"

[components]
fuel-core = "0.33.0"
fuel-core = "0.36.0"
forc = "0.63.4"
2 changes: 1 addition & 1 deletion packages/base-asset/fuel-toolchain.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ channel = "nightly-2024-08-20"

[components]
forc = "0.63.1"
fuel-core = "0.33.0"
fuel-core = "0.36.0"
50 changes: 42 additions & 8 deletions packages/integration-tests/tests/bridge_erc20.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,6 @@ describe('Bridging ERC20 tokens', async function () {
.addContracts([fuel_bridge, fuel_bridgeImpl])
.txParams({
tip: 0,
gasLimit: 1_000_000,
maxFee: 1,
})
.callParams({
Expand Down Expand Up @@ -273,7 +272,6 @@ describe('Bridging ERC20 tokens', async function () {
expect(message).to.not.be.null;

const tx = await relayCommonMessage(env.fuel.deployer, message, {
gasLimit: 30000000,
maturity: undefined,
contractIds: [fuel_bridgeImpl.id.toHexString()],
});
Expand Down Expand Up @@ -359,6 +357,7 @@ describe('Bridging ERC20 tokens', async function () {

describe('Bridge ERC20 from Fuel', async () => {
const NUM_TOKENS = 10000000000000000000n;
const largeRateLimit = `30`;
let fuelTokenSender: FuelWallet;
let ethereumTokenReceiver: Signer;
let ethereumTokenReceiverAddress: string;
Expand Down Expand Up @@ -414,9 +413,14 @@ describe('Bridging ERC20 tokens', async function () {
).to.be.true;
});

it('Rate limit parameters are updated when current withdrawn amount is more than the new limit', async () => {
it('Rate limit parameters are updated when current withdrawn amount is more than the new limit & set a new higher limit', async () => {
const deployer = await env.eth.deployer;
const newRateLimit = '5';
let newRateLimit = '5';

let withdrawnAmountBeforeReset =
await env.eth.fuelERC20Gateway.currentPeriodAmount(
eth_testTokenAddress
);

await env.eth.fuelERC20Gateway
.connect(deployer)
Expand All @@ -426,19 +430,42 @@ describe('Bridging ERC20 tokens', async function () {
RATE_LIMIT_DURATION
);

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

// current withdrawn amount doesn't change when rate limit is updated

expect(
currentWithdrawnAmountAfterSettingLimit === withdrawnAmountBeforeReset
).to.be.true;

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

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

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

expect(
currentWithdrawnAmountAfterSettingLimit === parseEther(newRateLimit)
currentWithdrawnAmountAfterSettingLimit === withdrawnAmountBeforeReset
).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);
Expand All @@ -455,10 +482,17 @@ describe('Bridging ERC20 tokens', async function () {
.connect(deployer)
.resetRateLimitAmount(
eth_testTokenAddress,
parseEther(newRateLimit),
parseEther(largeRateLimit),
RATE_LIMIT_DURATION
);

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

expect(currentWitdrawnAmountAfterReset == 0n).to.be.true;

// withdraw tokens back to the base chain
withdrawMessageProof = await generateWithdrawalMessageProof(
fuel_bridge,
Expand Down
Loading

0 comments on commit de18552

Please sign in to comment.