Skip to content

feat(l2): implement ERC20 bridge #3241

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

Open
wants to merge 43 commits into
base: main
Choose a base branch
from
Open

feat(l2): implement ERC20 bridge #3241

wants to merge 43 commits into from

Conversation

iovoid
Copy link
Contributor

@iovoid iovoid commented Jun 19, 2025

Motivation

We want to be able to bridge ERC20 tokens.

Description

The inner workings are explained on #3223

Copy link

github-actions bot commented Jun 19, 2025

Lines of code report

Total lines added: 250
Total lines removed: 158
Total lines changed: 408

Detailed view
+--------------------------------------------------+-------+------+
| File                                             | Lines | Diff |
+--------------------------------------------------+-------+------+
| ethrex/crates/l2/contracts/bin/deployer/error.rs | 27    | -2   |
+--------------------------------------------------+-------+------+
| ethrex/crates/l2/contracts/bin/deployer/main.rs  | 623   | -156 |
+--------------------------------------------------+-------+------+
| ethrex/crates/l2/sdk/src/sdk.rs                  | 762   | +250 |
+--------------------------------------------------+-------+------+

@iovoid iovoid changed the title feat(l2): ERC20 bridge feat(l2): implement ERC20 bridge Jun 19, 2025
@iovoid iovoid requested review from ManuelBilbao and a team as code owners June 23, 2025 21:01
iovoid and others added 2 commits June 24, 2025 09:19
@ManuelBilbao ManuelBilbao added the L2 Rollup client label Jun 24, 2025
@iovoid iovoid force-pushed the feat/erc20-bridge branch from d2de067 to 18013bc Compare June 24, 2025 18:04
Comment on lines +54 to +57
/// @notice How much of each L1 token was deposited to each L2 token.
/// @dev Stored as L1 -> L2 -> amount
/// @dev Prevents L2 tokens from faking their L1 address and stealing tokens
mapping (address => mapping (address => uint256)) public depositsERC20;
Copy link
Collaborator

@MegaRedHand MegaRedHand Jun 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should consider moving this to the L2 for reduced gas costs. Opened an issue for that: #3299

use genesis_tool::genesis::write_genesis_as_json;
mod cli;
mod error;

fn main() -> Result<(), SystemContractsUpdaterError> {
let opts = SystemContractsUpdaterOptions::parse();
compile_contract(&opts.contracts_path, "src/l2/CommonBridgeL2.sol", true)?;
compile_contract(&opts.contracts_path, "src/l2/L1Messenger.sol", true)?;
compile_contract(&opts.contracts_path, "src/l2/L2ToL1Messenger.sol", true)?;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this contract change in the future? Consider this scenario:

  1. You update the code, this yields a new bytecode.
  2. You then add the bytecode to the genesis file like what happens below, on lines 42-45.

If the contract was already deployed, this would result in a different genesis file from the original one, right?

Of course, feel free to correct me.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would result in a different genesis, but that is intended. The purpose of system_contracts_updater is to update the genesis file with the new bytecode for the system contracts.

As for the contract, it could change albeit very rarely. If you wanted, for example, to allow submitting signed messages on behalf of other users.

@MegaRedHand MegaRedHand requested a review from ManuelBilbao June 26, 2025 15:58
}
}

pub fn download_contract_deps(contracts_path: &Path) -> Result<(), GitError> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not quite agree putting this in the SDK. @ilitteri what do you think?

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "../l2/interfaces/IERC20L2.sol";

/// @title OnChainProposer contract.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is incorrect

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 06232b5.

/// @author LambdaClass
contract TestTokenL2 is ERC20, IERC20L2 {
address public L1_TOKEN = address(0);
address public constant BRIDGE = 0x000000000000000000000000000000000000FFff;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we put the bridge address on the interface so token operators don't mess up?

Comment on lines +147 to +149
IERC20(tokenL1).safeTransferFrom(msg.sender, address(this), amount);

depositsERC20[tokenL1][tokenL2] += amount;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we take care of reentrancy here?

Copy link
Collaborator

@MegaRedHand MegaRedHand Jun 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's remove that by swapping these two lines (the external call and storage access), just to be sure.

I don't think any attacks are possible, though, since here we're not sending any value out of the bridge. Any token transfers that result in recursive deposits will only further increase this amount, checking that the transfer doesn't revert any of those times.

Comment on lines +39 to +42
try this._mintERC20(tokenL1, tokenL2, destination, amount) {
} catch {
_withdrawERC20(tokenL1, tokenL2, destination, amount);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use if (!success) instead of try-catch?

_withdrawERC20(tokenL1, tokenL2, destination, amount);
}

function _withdrawERC20(address tokenL1, address tokenL2, address destination, uint256 amount) private {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would use a better name here. Something like sendERC20WithdrawToL1 (there should be better ones)

IERC20(tokenL1).safeTransfer(msg.sender, claimedAmount);
}

function _claimWithdrawal(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's find better names for this functions, we have claimWithdrawal, _claimWithdrawal, claimWithdrawalERC20, _claimWithdrawalProof and _verifyWithdrawProof (if I'm not missing anyone else). It's hard to understand what does each one do

function _deposit(DepositValues memory depositValues) private {
require(msg.value > 0, "CommonBridge: amount to deposit is zero");
function _deposit(address from, DepositValues memory depositValues) private {
depositsERC20[ETH_TOKEN][ETH_TOKEN] += msg.value;
Copy link
Collaborator

@MegaRedHand MegaRedHand Jun 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line is confusing since the function is also being called on non-ETH deposits. We should move it to the caller's side

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
L2 Rollup client
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants