-
Notifications
You must be signed in to change notification settings - Fork 247
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
Crosschain Bridging of ETH tutorial #1372
base: main
Are you sure you want to change the base?
Changes from 3 commits
e58a81b
bb942bb
aa203bc
74cf432
b7dda56
b4e557a
43d6b61
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,53 +4,215 @@ lang: en-US | |
description: Learn how to bridge native cross-chain ETH transfers. | ||
--- | ||
|
||
import { Callout } from 'nextra/components' | ||
import { Steps } from 'nextra/components' | ||
import { Callout, Steps, Tabs } from 'nextra/components' | ||
import { InteropCallout } from '@/components/WipCallout' | ||
|
||
<InteropCallout /> | ||
|
||
# Bridging native cross-chain ETH transfers | ||
|
||
Crosschain ETH transfers in the Superchain are facilitated through the [SuperchainWETH](https://github.com/ethereum-optimism/optimism/blob/develop/packages/contracts-bedrock/src/L2/SuperchainWETH.sol) contract. | ||
This tutorial walks through how to send native ETH from chain 901 to 902. To simplify these steps, [supersim](/stack/interop/tools/supersim) will be run with the `--interop.autorelay` flag. The `--interop.autorelay` flag automatically triggers the relay message transaction once the initial send transaction is completed on the source chain, improving the developer experience by removing the need to manually send the relay message. | ||
|
||
<Callout> | ||
If the source chain uses native ETH as their gas token, but the destination chain uses a custom gas token, then the recipient will receive `SuperchainWETH` on the destination chain. | ||
This tutorial provides step-by-step instructions for bridging ETH from one Superchain Interop chain to another. | ||
For a conceptual overview, | ||
see the [interoperable ETH explainer](../superchain-weth). | ||
</Callout> | ||
|
||
## Overview | ||
|
||
Crosschain ETH transfers in the Superchain are facilitated through the [SuperchainWETH](https://github.com/ethereum-optimism/optimism/blob/develop/packages/contracts-bedrock/src/L2/SuperchainWETH.sol) contract. | ||
This tutorial walks through how to send native ETH from one chain to another. | ||
You can do this on [Supersim](../tools/supersim), [the Interop devnet](../tools/devnet), or production, once it is released. | ||
|
||
Note that if the source chain uses native ETH as their gas token, but the destination chain uses a custom gas token, then the recipient will receive `SuperchainWETH` on the destination chain. | ||
|
||
### What you'll build | ||
|
||
* A TypeScript application to transfer ETH chains | ||
|
||
### What you'll learn | ||
|
||
* How to send ETH on the blockchain and between blockchains | ||
* How to relay messages between chains | ||
|
||
## Prerequisites | ||
|
||
Before starting this tutorial, ensure your development environment meets the following requirements: | ||
|
||
### Technical knowledge | ||
|
||
* Intermediate TypeScript knowledge | ||
* Understanding of smart contract development | ||
* Familiarity with blockchain concepts | ||
|
||
### Development environment | ||
|
||
* Unix-like operating system (Linux, macOS, or WSL for Windows) | ||
* Node.js version 16 or higher | ||
* Git for version control | ||
|
||
### Required tools | ||
|
||
The tutorial uses these primary tools: | ||
|
||
* Foundry: For smart contract development | ||
* Supersim: For local blockchain simulation | ||
* TypeScript: For implementation | ||
* Viem: For blockchain interaction | ||
|
||
<Steps> | ||
### Start `supersim` with the autorelayer enabled | ||
### Configure the network | ||
|
||
1. Install [Foundry](https://book.getfoundry.sh/getting-started/installation). | ||
qbzzt marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
<Tabs items={['Supersim', 'Interop devnet']}> | ||
<Tabs.Tab> | ||
2. Follow the [Installation Guide](/app-developers/tutorials/supersim/getting-started/installation) to install [Supersim](../tools/supersim) for running blockchains with Interop. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is actually the first time I'm installing supersim and it looks like its not working:
I flagged it to the engineering team to see if I was doing something wrong, but I'll continue with the tutorial for now. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Try to run |
||
|
||
3. Supersim uses Foundry's `anvil` blockchains, which start with ten prefunded accounts. | ||
Set these environment variables to access one of those accounts on the L2 blockchains. | ||
|
||
```sh | ||
export PRIV_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 | ||
``` | ||
|
||
<details> | ||
<summary>Sanity check</summary> | ||
|
||
```sh | ||
cast balance --ether `cast wallet address $PRIV_KEY` --rpc-url http://localhost:9545 | ||
cast balance --ether `cast wallet address $PRIV_KEY` --rpc-url http://localhost:9546 | ||
``` | ||
</details> | ||
</Tabs.Tab> | ||
|
||
<Tabs.Tab> | ||
2. Set `PRIV_KEY` to the private key of an address that has [Sepolia ETH](https://cloud.google.com/application/web3/faucet/ethereum/sepolia). | ||
|
||
```sh | ||
export PRIV_KEY=0x<private key here> | ||
``` | ||
|
||
3. Send ETH to the two L2 blockchains. | ||
qbzzt marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
```sh | ||
cast send --rpc-url https://endpoints.omniatech.io/v1/eth/sepolia/public --private-key $PRIV_KEY --value 0.02ether 0x7385d89d38ab79984e7c84fab9ce5e6f4815468a | ||
cast send --rpc-url https://endpoints.omniatech.io/v1/eth/sepolia/public --private-key $PRIV_KEY --value 0.02ether 0x55f5c4653dbcde7d1254f9c690a5d761b315500c | ||
``` | ||
|
||
4. Wait a few minutes until you can see the ETH [on the block explorer](https://sid.testnet.routescan.io/) for your address. | ||
|
||
<details> | ||
<summary>Sanity check</summary> | ||
|
||
```sh | ||
cast balance --ether `cast wallet address $PRIV_KEY` --rpc-url https://interop-alpha-0.optimism.io/ | ||
cast balance --ether `cast wallet address $PRIV_KEY` --rpc-url https://interop-alpha-1.optimism.io/ | ||
``` | ||
</details> | ||
</Tabs.Tab> | ||
</Tabs> | ||
|
||
### Create the TypeScript project | ||
|
||
We need to create an executing message on the destination chain, and for that we use [the `@eth-optimism/viem` package](https://www.npmjs.com/package/@eth-optimism/viem). | ||
|
||
1. Create a new TypeScript project. | ||
|
||
```sh | ||
mkdir transfer-eth | ||
cd transfer-eth | ||
npm init -y | ||
npm install --save-dev -y viem tsx @types/node @eth-optimism/viem typescript | ||
mkdir src | ||
``` | ||
|
||
2. Download the ABI for `SuperchainWETH`. | ||
|
||
```sh | ||
wget https://raw.githubusercontent.com/ethereum-optimism/optimism/refs/heads/develop/packages/contracts-bedrock/snapshots/abi/SuperchainWETH.json | ||
qbzzt marked this conversation as resolved.
Show resolved
Hide resolved
|
||
mv SuperchainWETH.json src/SuperchainWETH.abi.json | ||
``` | ||
|
||
3. Place this in `src/transfer-eth.mts`: | ||
|
||
```typescript file=<rootDir>/public/tutorials/transfer-eth.mts hash=41c1d559ac010d3407a3c056f2f49405 | ||
``` | ||
|
||
<details> | ||
<summary>Explanation</summary> | ||
|
||
```typescript file=<rootDir>/public/tutorials/transfer-eth.mts#L13-L18 hash=5312f4634ac8762504935cd52d18c8ab | ||
``` | ||
|
||
Import all chain definitions from `@eth-optimism/viem`. | ||
|
||
```typescript file=<rootDir>/public/tutorials/transfer-eth.mts#L29-L32 hash=56873f8d2eebe5975a0c0b9ac7eecfe7 | ||
``` | ||
|
||
If the address we use is `0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266`, one of the prefunded addresses on `anvil`, assume we're using Supersim. | ||
Otherwise, use Interop devnet. | ||
|
||
```typescript file=<rootDir>/public/tutorials/transfer-eth.mts#L78-L80 hash=4933c70a9078c2369ef90bfe163f5fd7 | ||
``` | ||
|
||
To relay a message we need the information in the receipt. | ||
Also, we need to wait until the transaction with the relayed message is actually part of a block. | ||
|
||
```typescript file=<rootDir>/public/tutorials/transfer-eth.mts#L87-L89 hash=573f22b2b21415ff51c59c713fda07d1 | ||
``` | ||
|
||
A single transaction can send multiple messages. | ||
But here we know we sent just one, so we look for the first one in the list. | ||
|
||
```typescript file=<rootDir>/public/tutorials/transfer-eth.mts#L90-L96 hash=4b26775b46c116262af4c7299d6f1127 | ||
``` | ||
|
||
This is how you use `@eth-optimism/viem` to create an executing message. | ||
</details> | ||
|
||
### Run the example | ||
|
||
1. Run the example. | ||
|
||
```sh | ||
npx tsx src/transfer-eth.mts | ||
``` | ||
|
||
```sh | ||
supersim --interop.autorelay | ||
``` | ||
2. Read the results. | ||
|
||
### Initiate the send transaction on chain 901 | ||
``` | ||
Before transfer | ||
|
||
* In this step, you'll send ETH from Chain 901 to Chain 902 through `SuperchainWETH` contract deployed at `0x4200000000000000000000000000000000000024`. | ||
* Use the following command: | ||
Address: 0x7ED53BfaA58B79Dd655B2f229258C093b6C09A8C | ||
Balance on source chain: 0.020999799151902245 | ||
Balance on destination chain: 0.026999459226731331 | ||
``` | ||
|
||
```sh | ||
cast send 0x4200000000000000000000000000000000000024 "sendETH(address _to, uint256 _chainId)" 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 902 --value 10ether --rpc-url http://127.0.0.1:9545 --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 | ||
``` | ||
The initial state. | ||
|
||
### Wait for the relayed message to appear on chain 902 | ||
``` | ||
After transfer on source chain | ||
|
||
In a few seconds, you should see the relayed message on chain 902: | ||
Address: 0x7ED53BfaA58B79Dd655B2f229258C093b6C09A8C | ||
Balance on source chain: 0.019999732176717961 | ||
Balance on destination chain: 0.026999459226731331 | ||
``` | ||
|
||
```sh | ||
# example | ||
INFO [12-02|14:53:02.434] SuperchainWETH#RelayETH chain.id=902 from=0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 to=0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 amount=10,000,000,000,000,000,000 source=901 | ||
``` | ||
After the initiating message the balance on the source chain is immediately reduced. | ||
Notice that even though we are sending 0.001 ETH, the balance on the source chain is reduced by a bit more (here, approximately 67 gwei). | ||
This is the cost of the initiating transaction on the source chain. | ||
Of course, as there has been no transaction on the destination chain, that balance is unchanged. | ||
|
||
### Check the balance on chain 902 | ||
``` | ||
After relaying message to destination chain | ||
|
||
Verify that the balance of the ETH on chain 902 has increased: | ||
Address: 0x7ED53BfaA58B79Dd655B2f229258C093b6C09A8C | ||
Balance on source chain: 0.019999732176717961 | ||
Balance on destination chain: 0.027999278943880868 | ||
``` | ||
|
||
```sh | ||
cast balance 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 --rpc-url http://127.0.0.1:9546 | ||
``` | ||
Now the balance on the destination chain increases, by slightly less than 0.001 ETH. | ||
The executing message also has a transaction cost (in this case, about 180gwei). | ||
</Steps> | ||
|
||
## Next steps | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
import { | ||
createWalletClient, | ||
http, | ||
publicActions, | ||
getContract, | ||
Address, | ||
formatEther, | ||
parseEther, | ||
} from 'viem' | ||
|
||
import { privateKeyToAccount } from 'viem/accounts' | ||
|
||
import { | ||
supersimL2A, | ||
supersimL2B, | ||
interopAlpha0, | ||
interopAlpha1 | ||
} from '@eth-optimism/viem/chains' | ||
|
||
import { | ||
walletActionsL2, | ||
publicActionsL2, | ||
createInteropSentL2ToL2Messages, | ||
contracts as optimismContracts | ||
} from '@eth-optimism/viem' | ||
|
||
import superchainWethAbi from './SuperchainWETH.abi.json' | ||
|
||
const supersimAddress="0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" | ||
const account = privateKeyToAccount(process.env.PRIV_KEY as `0x${string}`) | ||
const sourceChain = account.address == supersimAddress ? supersimL2A : interopAlpha0 | ||
const destinationChain = account.address == supersimAddress ? supersimL2B : interopAlpha1 | ||
|
||
const sourceWallet = createWalletClient({ | ||
chain: sourceChain, | ||
transport: http(), | ||
account | ||
}).extend(publicActions) | ||
.extend(publicActionsL2()) | ||
.extend(walletActionsL2()) | ||
|
||
const destinationWallet = createWalletClient({ | ||
chain: destinationChain, | ||
transport: http(), | ||
account | ||
}).extend(publicActions) | ||
.extend(publicActionsL2()) | ||
.extend(walletActionsL2()) | ||
|
||
const wethOnSource = await getContract({ | ||
address: optimismContracts.superchainWETH.address, | ||
abi: superchainWethAbi, | ||
client: sourceWallet | ||
}) | ||
|
||
const reportBalance = async (address: string): Promise<void> => { | ||
const sourceBalance = await sourceWallet.getBalance({ | ||
address: address | ||
}); | ||
const destinationBalance = await destinationWallet.getBalance({ | ||
address: address | ||
}); | ||
|
||
console.log(` | ||
Address: ${address} | ||
Balance on source chain: ${formatEther(sourceBalance)} | ||
Balance on destination chain: ${formatEther(destinationBalance)} | ||
`); | ||
} | ||
|
||
console.log("Before transfer") | ||
await reportBalance(account.address) | ||
|
||
const sourceHash = await wethOnSource.write.sendETH({ | ||
value: parseEther('0.001'), | ||
args: [account.address, destinationChain.id] | ||
}) | ||
const sourceReceipt = await sourceWallet.waitForTransactionReceipt({ | ||
hash: sourceHash | ||
}) | ||
|
||
|
||
console.log("After transfer on source chain") | ||
await reportBalance(account.address) | ||
|
||
|
||
const sentMessage = | ||
(await createInteropSentL2ToL2Messages(sourceWallet, { receipt: sourceReceipt })) | ||
.sentMessages[0] | ||
const relayMsgTxnHash = await destinationWallet.interop.relayMessage({ | ||
sentMessageId: sentMessage.id, | ||
sentMessagePayload: sentMessage.payload, | ||
}) | ||
|
||
const receiptRelay = await destinationWallet.waitForTransactionReceipt( | ||
{hash: relayMsgTxnHash}) | ||
|
||
console.log("After relaying message to destination chain") | ||
await reportBalance(account.address) |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -299,6 +299,7 @@ Predeployed | |
predeployed | ||
Predeploys | ||
predeploys | ||
prefunded | ||
Preimage | ||
preimage | ||
PREIMAGES | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The tabular view on this step is throwing me off a little bit. I think it might be cleaner to just separate the tabs into their own steps
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this the option to run local or on the live devnet?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes. You can do either and it should work. Making them into separate steps would imply you need to do both, rather than either.