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

Custom USDC bridge #1

Open
wants to merge 26 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
cefdeb1
chore: modify L1SharedBridge.sol and L2SharedBridge.sol contracts
fedealconada Sep 13, 2024
b4169aa
chore: adjust forked L1 tests
fedealconada Sep 13, 2024
bc05068
chore: add deployment scripts
fedealconada Sep 18, 2024
5ed6c5f
chore: add other scripts
fedealconada Sep 18, 2024
42ecd88
chore: fix imports and remove Counter
fedealconada Sep 13, 2024
a2f2ae5
chore: update gitignore
fedealconada Sep 13, 2024
0d38cd6
chore: improve scripts
fedealconada Sep 18, 2024
a5b035b
chore: add README
fedealconada Sep 17, 2024
a169202
chore: fix burn mechanism on L2SharedBridge
fedealconada Sep 17, 2024
1f46975
chore: clean up and fixes
fedealconada Sep 17, 2024
0838cc2
chore: update README
fedealconada Sep 18, 2024
a3e68ba
chore: fix build
fedealconada Sep 18, 2024
a7919a4
chore: remove unused tests
fedealconada Sep 18, 2024
a9b771c
chore: polishes
fedealconada Sep 18, 2024
60854d8
chore: polish contracts and tests
fedealconada Sep 18, 2024
caeb069
chore: update addresses
fedealconada Sep 19, 2024
9840995
chore: fix finalize withdrawal script
fedealconada Sep 18, 2024
a6c88ec
chore: fix finalize withdrawal script II
fedealconada Sep 18, 2024
bb00ffc
chore: add unit tests for l2 bridge
fedealconada Sep 19, 2024
dc65224
chore: add fixes from reviews: rmv legacy code, modify interfaces
fedealconada Oct 2, 2024
76f6c9c
chore: update README
fedealconada Oct 2, 2024
80c6669
chore: add utils to save deployed addresses
fedealconada Sep 19, 2024
6e68ca5
Merge pull request #2 from sophon-org/save-addresses
fedealconada Oct 2, 2024
4e06bbb
chore: rmv receiveEth function
fedealconada Oct 2, 2024
a3355b4
chore: update DeployL1SharedBridge script and upgrade
fedealconada Oct 8, 2024
425dc35
chore: update DeployL2SharedBridge script
fedealconada Oct 8, 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
14 changes: 14 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# general
PRIVATE_KEY=YOUR_PRIVATE_KEY
ETHERSCAN_API_KEY="YOUR_ETHERSCAN_API_KEY"
PROXY_ADMIN=WHOEVER_IS_THE_PROXY_ADMIN

# ethereum sepolia
SEPOLIA_RPC_URL=https://eth-sepolia.g.alchemy.com/v2/sp2BM_VFMURcKWMWg8HDVSBgvnm75_NS
SEPOLIA_VERIFIER_URL=https://api.etherscan.io/api
SEPOLIA_CHAIN_ID="11155111"

# sophon sepolia
SOPHON_RPC_URL=https://rpc.testnet.sophon.xyz
SOPHON_SEPOLIA_VERIFIER_URL=https://block-explorer-api.testnet.sophon.xyz/api
SOPHON_SEPOLIA_CHAIN_ID="531050104"
4 changes: 4 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ jobs:
with:
version: nightly

- name: Install NPM dependencies
run: |
yarn install

- name: Show Forge version
run: |
forge --version
Expand Down
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,8 @@ docs/
node_modules/

# Libraries
lib/
lib/

# Coverage
report/
lcov.info
71 changes: 40 additions & 31 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,66 +1,75 @@
## Foundry
## Custom bridge for Native USDC

**Foundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.**
We want to use the canonical zkSync Bridge with a [custom bridge](https://docs.zksync.io/build/developer-reference/bridging-assets#custom-bridges-on-l1-and-l2) implementation that would work only for USDC (since we want to use the [native USDC](https://github.com/circlefin/stablecoin-evm/blob/master/doc/bridged_USDC_standard.md) and not the ERC20 that the zksync bridge deploys by default).

Foundry consists of:
The repo implements the [src/L1SharedBridge.sol](https://github.com/sophon-org/custom-usdc-bridge/pull/1/files#diff-1698a2f52c7225fb2a4d7cf5241c28ce85cb4514ffac8a9ec30ab728c4065f6e) and [src/L2SharedBridge.sol](https://github.com/sophon-org/custom-usdc-bridge/pull/1/files#diff-ad529a25299727c85e9b20798cb94a45aaec2709bc7208400fea76e4c2cdb4be) which are custom bridge contracts based on the ones found on [MatterLabs era-contracts](https://github.com/matter-labs/era-contracts) (we've forked from [this](https://github.com/matter-labs/era-contracts.git#bce4b2d0f34bd87f1aaadd291772935afb1c3bd6) commit)

- **Forge**: Ethereum testing framework (like Truffle, Hardhat and DappTools).
- **Cast**: Swiss army knife for interacting with EVM smart contracts, sending transactions and getting chain data.
- **Anvil**: Local Ethereum node, akin to Ganache, Hardhat Network.
- **Chisel**: Fast, utilitarian, and verbose solidity REPL.
**Bridge/Withdrawal flow**

## Documentation
The flow to bridge and withdraw using custom bridges is the same as when bridging with any token except for the fact that when `Bridgehub` is called, you need specify the address of the custom L1 bridge.

https://book.getfoundry.sh/
**To bridge USDC from L1 (Ethereum) -> L2 (Sophon):**
- Call `bridgehub.requestL2TransactionTwoBridges` (same as normally) but you set the `secondBridgeAddress` with the custom shared bridge deployed on L1. This way, the bridgehub contracts knows which contract to ping to.
- `requestL2TransactionTwoBridges` makes a call to the custom shared bridge `customBridgeL1.bridgehubDeposit` function and transfers `USDC` from the user to this contract and emits an event.
- sequencers will pick this event and automatically make a call to `customL2Bridge.finalizeDeposit` on the custom bridge deployed on L2 (on Sophon).
- `finalizeDeposit` is the one that calls `usdc.mint()` to mint `USDC` on L2 (**note** this custom bridge must have `MINTER` role on the `USDC` contract).

**To withdraw USDC from L2 (Sophon) -> L1 (Etheruem):**
- User makes a call to `customBridgeL2.withdraw` (same as normally except for the fact that you're calling the custom bridge contract)
- Once the batch is sealed, user needs to call `customL1Bridge.finalizeWithdrawal` to finalise the withdrawal

## Usage

### Build

```shell
$ forge build

# zkSync build
$ forge build --zksync
```

### Test
### Scripts

```shell
$ forge test
```
# Deploy L1 Shared Bridge
$ source .env && forge script ./script/DeployL1SharedBridge.s.sol --rpc-url sepoliaTestnet --private-key $PRIVATE_KEY --verify --broadcast

### Format
# Deploy L2 Shared Bridge
$ source .env && forge script ./script/DeployL2SharedBridge.s.sol --rpc-url sophonTestnet --private-key $PRIVATE_KEY --zksync --broadcast --verify --slow

```shell
$ forge fmt
```
# Initialise L1 Shared Bridge
$ source .env && forge script ./script/InitialiseL1SharedBridge.s.sol --rpc-url sepoliaTestnet --private-key $PRIVATE_KEY --broadcast

### Gas Snapshots
# Bridge from Sophon to Ethereum (L1 -> L2)
$ source .env && forge script ./script/Bridge.s.sol --rpc-url sepoliaTestnet --private-key $PRIVATE_KEY --ffi --broadcast

```shell
$ forge snapshot
# Withdraw from Sophon to Ethereum (L2 -> L1)
$ source .env && forge script ./script/Withdraw.s.sol --rpc-url sophonTestnet --private-key $PRIVATE_KEY --zksync --slow -vvvv --broadcast

# Finalise withdrawal on Ethereum
$ source .env && export L2_WITHDRAWAL_HASH="YOUR_TX_HASH" && forge script ./script/FinalizeWithdrawal.s.sol --rpc-url sepoliaTestnet --private-key $PRIVATE_KEY --ffi --broadcast
```

### Anvil
### Test

```shell
$ anvil
$ forge test
```

### Deploy

### Coverage
```shell
$ forge script script/Counter.s.sol:CounterScript --rpc-url <your_rpc_url> --private-key <your_private_key>
$ forge coverage --report lcov --no-match-coverage '^.*(node_modules|test|script)/.*$' && genhtml ./lcov.info --branch-coverage --rc derive_function_end_line=0 --output-directory report
```

### Cast
### Format

```shell
$ cast <subcommand>
$ forge fmt
```

### Help
### Gas Snapshots

```shell
$ forge --help
$ anvil --help
$ cast --help
```
$ forge snapshot
```
12 changes: 12 additions & 0 deletions codecov.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# interfaces are automatically ignored by forge coverage
ignore:
- "test" # ignore test/ folder
- "script" # ignore scripts/ folder
- "libs" # ignore libs/ folder

comment:
layout: " diff, flags, files"
behavior: default
require_changes: false # if true: only post the comment if coverage changes
require_base: false # [true :: must have a base report to post]
require_head: true # [true :: must have a head report to post]
9 changes: 9 additions & 0 deletions deployments/11155111/addresses.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"Bridgehub": "0x35A54c8C757806eB6820629bc82d90E056394C92",
"EraDiamongProxy": "0x9A6DE0f62Aa270A8bCB1e2610078650D539B1Ef9",
"L1SharedBridge": "0x3f842b5FaD08Bac49D0517C975d393f5f466Fd3b",
"L1SharedBridge-impl": "0x0B106df49f3A5010b89a463ceCDBD0e66A64a62A",
"SOPH": "0x06c03F9319EBbd84065336240dcc243bda9D8896",
"SharedBridge": "0x3E8b2fe58675126ed30d0d12dea2A9bda72D18Ae",
"USDC": "0xBF4FdF7BF4014EA78C0A07259FBc4315Cb10d94E"
}
4 changes: 4 additions & 0 deletions deployments/531050104/addresses.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"L2SharedBridge": "0x72591d4135B712861d8d4513a2f6860Ac30A684D",
"USDC": "0x27553b610304b6AB77855a963f8208443D773E60"
}
10 changes: 10 additions & 0 deletions foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,15 @@
src = "contracts"
out = "out"
libs = ["lib"]
auto_detect_solc = true
fs_permissions = [{ access = "read-write", path = "./deployments" }]

# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options

[etherscan]
sepoliaTestnet = { key = "${ETHERSCAN_API_KEY}", url = "${SEPOLIA_VERIFIER_URL}", chain = 11155111 }
sophonTestnet = { key = "${ETHERSCAN_API_KEY}", url = "${SOPHON_SEPOLIA_VERIFIER_URL}", chain = 531050104 }

[rpc_endpoints]
sepoliaTestnet = "${SEPOLIA_RPC_URL}"
sophonTestnet = "${SOPHON_RPC_URL}"
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@
"@openzeppelin/contracts": "4.9.5",
"@openzeppelin/contracts-upgradeable": "4.9.5",
"era-contracts": "https://github.com/matter-labs/era-contracts.git"

},
"dependencies": {
"dotenv": "^16.4.5",
"ethers": "^6.13.2",
"zksync-ethers": "^6.12.1"
}
}
97 changes: 97 additions & 0 deletions script/Bridge.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import {Script, console} from "forge-std/Script.sol";
import {IERC20} from "forge-std/interfaces/IERC20.sol";
import {
L2TransactionRequestTwoBridgesOuter,
IBridgehub
} from "@era-contracts/l1-contracts/contracts/bridgehub/IBridgehub.sol";
import {DeploymentUtils} from "../utils/DeploymentUtils.sol";

interface MockUSDC {
function approve(address usr, uint256 wad) external;
}

contract BridgeScript is Script, DeploymentUtils {
function setUp() public {}

function run() public {
vm.startBroadcast();
uint256 amountToBridge = 1 * 10 ** 6; // 1 USDC

approve(getDeployedContract("SOPH"), type(uint256).max);
approve(getDeployedContract("USDC"), amountToBridge);
bridge(amountToBridge);

vm.stopBroadcast();
}

function bridge(uint256 amountToBridge) public {
uint256 L2_GAS_LIMIT = 435293;
uint256 TX_GAS_PER_PUBDATA_BYTE_LIMIT = 800;
uint256 CHAIN_ID = vm.envUint("SOPHON_SEPOLIA_CHAIN_ID"); // Sophon Sepolia

// Prepare data for the bridgehubDeposit call
bytes memory depositData = abi.encode(
getDeployedContract("USDC"),
amountToBridge,
msg.sender // sender is the recipient of the tokens on L2
);

// TODO: gasPrice() call sometimes fails sometimes not, why?
// uint256 l2GasPrice = gasPrice();
// console.log("Gas price:", l2GasPrice);
// uint256 baseCost = IBridgehub(SEPOLIA_L1_BRIDGEHUB).l2TransactionBaseCost(
// CHAIN_ID, l2GasPrice, L2_GAS_LIMIT, TX_GAS_PER_PUBDATA_BYTE_LIMIT
// );
// // TODO: why baseCost is returning a very high number?
// console.log("Base cost:", baseCost);

IBridgehub(getDeployedContract("Bridgehub")).requestL2TransactionTwoBridges( // No vale needed if base token is not ETH (e.g SOPH)
L2TransactionRequestTwoBridgesOuter({
chainId: CHAIN_ID,
mintValue: 10e18, // base tokens (SOPH for Sophon Sepolia, ETH for Sepolia)
// mintValue: baseCost,
l2Value: 0,
l2GasLimit: L2_GAS_LIMIT, // TODO: it should take ~300'000
l2GasPerPubdataByteLimit: TX_GAS_PER_PUBDATA_BYTE_LIMIT, // TODO: how to calculate?
refundRecipient: address(0),
secondBridgeAddress: getDeployedContract("L1SharedBridge"),
secondBridgeValue: 0,
secondBridgeCalldata: depositData
})
);
}

function approve(address token, uint256 amount) public {
IERC20 token = IERC20(token);
address l1Bridge = getDeployedContract("L1SharedBridge");

// approve shared bridge to spend base tokens
console.log("Checking %s allowance...", token.symbol());
uint256 allowance = token.allowance(msg.sender, l1Bridge);
if (allowance < amount) {
console.log("Approving...");
if (address(token) == getDeployedContract("USDC")) {
MockUSDC(address(token)).approve(l1Bridge, amount);
} else {
token.approve(l1Bridge, amount);
}
console.log("New allowance:", token.allowance(msg.sender, l1Bridge));
}
console.log("%s allowance OK", token.symbol());
}

function gasPrice() public returns (uint256 price) {
// cast gas-price --rpc-url https://rpc.testnet.sophon.xyz/
string[] memory args = new string[](4);
args[0] = "cast";
args[1] = "gas-price";
args[2] = "--rpc-url";
args[3] = "https://rpc.testnet.sophon.xyz/"; // TODO: get from ENV
string memory result = string(vm.ffi(args));

return vm.parseUint(result);
}
}
74 changes: 74 additions & 0 deletions script/DeployL1SharedBridge.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import {Script, console} from "forge-std/Script.sol";
import {L1SharedBridge} from "../src/L1SharedBridge.sol";
import {IBridgehub} from "@era-contracts/l1-contracts/contracts/bridgehub/IBridgehub.sol";
import {
ITransparentUpgradeableProxy,
TransparentUpgradeableProxy
} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";
import {DeploymentUtils} from "../utils/DeploymentUtils.sol";

contract DeployL1SharedBridge is Script, DeploymentUtils {
function run() public {
// TODO: fix and send corresponding network and config names
_run("", "");
}

function run(string memory networkName, string memory configName) public {
_run(networkName, configName);
}

// call from tests -- avoiding the return when calling from the script results in cleaner logs
function runAndReturnResults(string memory networkName, string memory configName)
public
returns (address sharedBridgeProxy, address sharedBridgeImpl)
{
return _run(networkName, configName);
}

function _run(string memory networkName, string memory configName)
internal
returns (address sharedBridgeProxy, address sharedBridgeImpl)
{
// TODO: set proper addresses, maybe read from env
address deployerAddress = msg.sender;
address proxyAdmin = vm.envAddress("PROXY_ADMIN");

vm.startBroadcast();

sharedBridgeProxy = getDeployedContract("L1SharedBridge");

// deploy implementation
sharedBridgeImpl =
address(new L1SharedBridge(getDeployedContract("USDC"), IBridgehub(getDeployedContract("Bridgehub"))));

// if proxy exists, upgrade proxy with new implementation
if (sharedBridgeProxy != address(0)) {
console.log("Upgrading L1SharedBridge");
if (msg.sender != proxyAdmin) revert("Only proxy admin can upgrade the implementation");
ITransparentUpgradeableProxy(payable(sharedBridgeProxy)).upgradeTo(sharedBridgeImpl);
console.log("L1SharedBridge implementation upgraded @", sharedBridgeImpl);
saveDeployedContract("L1SharedBridge-impl", sharedBridgeImpl);
return (sharedBridgeProxy, sharedBridgeImpl);
}

// deploy proxy
sharedBridgeProxy = address(
new TransparentUpgradeableProxy(
sharedBridgeImpl,
proxyAdmin,
abi.encodeWithSelector(L1SharedBridge.initialize.selector, deployerAddress)
)
);

console.log("L1SharedBridge implementation deployed @", address(sharedBridgeImpl));
console.log("L1SharedBridge proxy deployed @", address(sharedBridgeProxy));
saveDeployedContract("L1SharedBridge", address(sharedBridgeProxy));
saveDeployedContract("L1SharedBridge-impl", address(sharedBridgeImpl));

vm.stopBroadcast();
return (address(sharedBridgeProxy), address(sharedBridgeImpl));
}
}
Loading