diff --git a/pages/interop/tutorials/_meta.json b/pages/interop/tutorials/_meta.json index 67816f137..7407e8e14 100644 --- a/pages/interop/tutorials/_meta.json +++ b/pages/interop/tutorials/_meta.json @@ -3,6 +3,7 @@ "deploy-superchain-erc20": "Deploying a SuperchainERC20", "transfer-superchainERC20": "Transferring a SuperchainERC20", "custom-superchain-erc20": "Custom SuperchainERC20 tokens", + "upgrade-to-superchain-erc20": "Upgrade ERC20 for Superchain support", "bridge-crosschain-eth": "Bridging native cross-chain ETH transfers", "relay-messages-cast": "Relaying interop messages using `cast`", "relay-messages-viem": "Relaying interop messages using `viem`", diff --git a/pages/interop/tutorials/message-passing.mdx b/pages/interop/tutorials/message-passing.mdx index 4ab7f54ce..5214c4597 100644 --- a/pages/interop/tutorials/message-passing.mdx +++ b/pages/interop/tutorials/message-passing.mdx @@ -238,7 +238,7 @@ For development purposes, we'll first use autorelay mode to handle message execu 6. Create `src/GreetingSender.sol`. - ```solidity file=/public/tutorials/GreetingSender.sol#L1-L28 hash=75d197d1e1da112421785c2160f6a55a + ```solidity file=/public/tutorials/GreetingSender.sol#L1-L28 hash=9ed77001810caf52bbaa94da8b0dc5c6 ```
diff --git a/pages/interop/tutorials/upgrade-to-superchain-erc20.mdx b/pages/interop/tutorials/upgrade-to-superchain-erc20.mdx new file mode 100644 index 000000000..89232c9cd --- /dev/null +++ b/pages/interop/tutorials/upgrade-to-superchain-erc20.mdx @@ -0,0 +1,58 @@ +--- +title: Upgrading ERC20 to SuperchainERC20 +lang: en-US +description: Tutorial on how to take an existing ERC20 and upgrade it to SuperchainERC20. +topic: Interoperability +personas: [Developer] +categories: [Tutorial, Interop] +content_type: article +--- + +import { Callout, Steps, Card, Cards } from 'nextra/components' + + + The SuperchainERC20 standard is ready for production deployments. + Please note that the OP Stack interoperability upgrade, required for crosschain messaging, is currently still in active development. + + +# Upgrading ERC20 to SuperchainERC20 + +## Overview + +This guide explains how to upgrade an ERC20 to a [`SuperchainERC20`](https://github.com/ethereum-optimism/optimism/blob/develop/packages/contracts-bedrock/src/L2/SuperchainERC20.sol) that can teleport across the [Superchain interop cluster](/interop/explainer#superchain-interop-cluster) using the [`SuperchainTokenBridge`](https://github.com/ethereum-optimism/optimism/blob/develop/packages/contracts-bedrock/src/L2/SuperchainTokenBridge.sol) contract. For more information on how it works, [see the explainer](/interop/superchain-erc20). + +{/* + +I put this warning here when we don't have it on most pages because this tutorial +has, IMHO, code that is a lot more likely to be used in production. It doesn't just +show what is possible, it does the exact job needed. + +*/} + +There are several ways to upgrade an existing ERC20 for interop, depending on your circumstances: + +{/* + +* If you can upgrade the existing contract, but the address is not available on other chains? In that case, use a custom bridge + +upgrade-to-superchain-erc20/custom-bridge. + +*/} + +| When To Use | Action | +| ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | +| You can install a new ERC20 contract | [**Deploy New SuperchainERC20 contracts** directly](/interop/tutorials/deploy-superchain-erc20) | +| Existing ERC20 contract cannot be upgraded | [**Implement Lockbox Solution** to bridge between tokens](/interop/tutorials/upgrade-to-superchain-erc20/lockbox) | +| You can deploy to other chains using the same proxy address | [**Perform Contract Upgrade** while maintaining address](/interop/tutorials/upgrade-to-superchain-erc20/contract-upgrade) | + + + } /> + + } /> + + +## Next steps + +* Deploy a [SuperchainERC20](/interop/tutorials/deploy-superchain-erc20) to the Superchain +* [Learn more about SuperchainERC20](/interop/superchain-erc20) +* Build a [revolutionary app](/app-developers/get-started) that uses multiple blockchains within the Superchain diff --git a/pages/interop/tutorials/upgrade-to-superchain-erc20/_meta.json b/pages/interop/tutorials/upgrade-to-superchain-erc20/_meta.json new file mode 100644 index 000000000..29d8c190c --- /dev/null +++ b/pages/interop/tutorials/upgrade-to-superchain-erc20/_meta.json @@ -0,0 +1,4 @@ +{ + "contract-upgrade": "Contract upgrade", + "lockbox": "Lockboxes for permissionless interop" +} diff --git a/pages/interop/tutorials/upgrade-to-superchain-erc20/contract-upgrade.mdx b/pages/interop/tutorials/upgrade-to-superchain-erc20/contract-upgrade.mdx new file mode 100644 index 000000000..8d998d537 --- /dev/null +++ b/pages/interop/tutorials/upgrade-to-superchain-erc20/contract-upgrade.mdx @@ -0,0 +1,328 @@ +--- +title: Contract upgrade +lang: en-US +description: Tutorial on how to upgrade a proxied ERC20 contract for use with Superchain interop. +topic: Interoperability +personas: [Developer] +categories: [Tutorial, Interop] +content_type: article +--- + +import { Callout, Steps, Tabs } from 'nextra/components' + + + The SuperchainERC20 standard is ready for production deployments. + Please note that the OP Stack interoperability upgrade, required for crosschain messaging, is currently still in active development. + + +# Contract upgrade + +## Overview + +This guide explains how to upgrade an ERC20 to a [`SuperchainERC20`](https://github.com/ethereum-optimism/optimism/blob/develop/packages/contracts-bedrock/src/L2/SuperchainERC20.sol) that can teleport across the [Superchain interop cluster](/interop/explainer#superchain-interop-cluster) when the original ERC20 contract was placed behind a proxy to enable future upgrades. + +
+ About this tutorial + + **What you'll learn** + + * How to upgrade an ERC20 token to enable Superchain interoperability when it was deployed with a proxy. + + **Prerequisite knowledge** + + * You should already know how to [deploy SuperchainERC20 tokens with custom code](/interop/tutorials/). +
+ + + The code on the documentation site is sample code, *not* production code. + This means that we ran it, and it works as advertised. + However, it did not pass through the rigorous audit process that most Optimism code undergoes. + You're welcome to use it, but if you need it for production purposes you should get it audited first. + + +{/* + +I put this warning here, when we don't have it on most pages, because this tutorial +has code that is a lot more likely to be used in production. It doesn't just +show what is possible, it does the exact job needed. + +*/} + +### What you'll do + +* Upgrade an existing ERC20 that uses [the proxy pattern](https://docs.openzeppelin.com/upgrades-plugins/proxies) to comply with interop requirements (with the proper authority). + +## How beacon proxies work + +```mermaid +sequenceDiagram + Actor User + User->>BeaconProxy: transfer(
, ) + BeaconProxy->>UpgradeableBeacon: What is the implementation address? + UpgradeableBeacon->>BeaconProxy: It is 0xBAD0...60A7 + BeaconProxy->>0xBAD0...60A7: transfer(
, ) +``` + +A [beacon proxy](https://docs.openzeppelin.com/contracts/3.x/api/proxy#BeaconProxy) uses two contracts. +The [`UpgradeableBeacon`](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/proxy/beacon/UpgradeableBeacon.sol) contract holds the address of the implementation contract. +The [`BeaconProxy`](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/proxy/beacon/BeaconProxy.sol) contract is the one called for the functionality, the one that holds the storage. +When a user (or another contract) calls `BeaconProxy`, it asks `UpgradeableBeacon` for the implementation address and then uses [`delegatecall`](https://www.evm.codes/?fork=cancun#f4) to call that contract. + +```mermaid +sequenceDiagram + Actor User + Actor Owner + Participant BeaconProxy + Participant 0x600D...60A7 + Owner->>UpgradeableBeacon: Your new implementation address is 0x600D...60A7 + User->>BeaconProxy: transfer(
, ) + BeaconProxy->>UpgradeableBeacon: What is the implementation address? + UpgradeableBeacon->>BeaconProxy: It is 0x600D...60A7 + BeaconProxy->>0x600D...60A7: transfer(
, ) +``` + +To upgrade the contract, an authorized address (typically the `Owner`) calls `UpgradeableBeacon` directly to specify the new implementation contract address. +After that happens, all new calls are sent to the new implementation. + +## Instructions + +Some steps depend on whether you want to deploy on [Supersim](/interop/tools/supersim) or on the [development network](/interop/tools/devnet). + + + ### Install and run Supersim + + If you are going to use Supersim, [follow these instructions](/app-developers/tutorials/supersim/getting-started/installation) to install and run Supersim. + + + Make sure to run Supersim with autorelay on. + + ```sh + ./supersim --interop.autorelay true + ``` + + + ### Setup the ERC20 token on chain A + + Download and run the setup script. + + ```sh + curl https://docs.optimism.io/tutorials/setup-for-erc20-upgrade.sh > setup-for-erc20-upgrade.sh + chmod +x setup-for-erc20-upgrade.sh + ./setup-for-erc20-upgrade.sh + ``` + + If you want to deploy to the [development networks](/interop/tools/devnet), provide `setup-for-erc20-upgrade.sh` with the private key of an address with ETH on both devnets. + + ```sh + ./setup-for-erc20-upgrade.sh + ``` + + ### Store the addresses + + Execute the bottom two lines of the setup script output to store the ERC20 address and the address of the beacon contract. + + ```sh + BEACON_ADDRESS=0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512 + export ERC20_ADDRESS=0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0 + ``` + + ### Specify environment variables + + Specify these variables, which we use later: + + + + Set these parameters for Supersim. + + ```sh + PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 + USER_ADDRESS=0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 + URL_CHAIN_A=http://127.0.0.1:9545 + URL_CHAIN_B=http://127.0.0.1:9546 + INTEROP_BRIDGE=0x4200000000000000000000000000000000000028 + ``` + + + + For Devnet, specify in `PRIVATE_KEY` the private key you used for the setup script and then these parameters. + + ```sh + USER_ADDRESS=`cast wallet address --private-key $PRIVATE_KEY` + URL_CHAIN_A=https://interop-alpha-0.optimism.io + URL_CHAIN_B=https://interop-alpha-1.optimism.io + INTEROP_BRIDGE=0x4200000000000000000000000000000000000028 + ``` + + + + ### Create a Foundry project + + We create a [Foundry](https://book.getfoundry.sh/) project and import the [OpenZeppelin](https://www.openzeppelin.com/solidity-contracts) contracts, which were used for the original ERC20 and proxy deployment. + + ```sh + mkdir proxy-upgrade + cd proxy-upgrade + forge init + forge install OpenZeppelin/openzeppelin-contracts + forge install OpenZeppelin/openzeppelin-contracts-upgradeable + forge install ethereum-optimism/interop-lib + ``` + + ### Create and run the deployment script + + 1. Create an `script/LabSetup.s.sol` file with this content: + + ```solidity file=/public/tutorials/setup-for-erc20-upgrade.sh#L26-L66 hash=83e951ca27872311bd7ae734c24cdbc2 filename="script/LabSetup.s.sol" + ``` + + This is the same deployment script used for the original deployment on chain A. + + 2. Run this command to deploy the same contracts on chain B. + + ```sh + forge script script/LabSetup.s.sol --rpc-url $URL_CHAIN_B --broadcast --private-key $PRIVATE_KEY --tc LabSetup + ``` + + Scroll up and see the Logs section of the output: + + ``` + == Logs == + Token address: 0x5FbDB2315678afecb367f032d93F642f64180aa3 + msg.sender: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 + UpgradeableBeacon: 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512 + Proxy: 0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0 + ``` + + Verify that the proxy address is the same as `$ERC20_ADDRESS`, and that the beacon address is the same as `$BEACON_ADDRESS`. + +
+ What to do when the values are not the same + + This can happen when the nonce values of `0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266` (or your address in the case of using devnet) on chain A and chain B are different. + + You can see the nonce values using these commands: + + ```sh + cast nonce $USER_ADDRESS --rpc-url $URL_CHAIN_A + cast nonce $USER_ADDRESS --rpc-url $URL_CHAIN_B + ``` + + The easiest solution is to send transactions to the chain with the lower nonce until the nonces are equal, and then deploy to both chains. + + ```sh + forge script script/LabSetup.s.sol --rpc-url $URL_CHAIN_A --broadcast --private-key $PRIVATE_KEY --tc LabSetup + forge script script/LabSetup.s.sol --rpc-url $URL_CHAIN_B --broadcast --private-key $PRIVATE_KEY --tc LabSetup + ``` + + If you do this, remember to update `$ERC20_ADDRESS` and `$BEACON_ADDRESS`. + + If the nonce on chain B is already higher than the nonce was on chain A when the original proxy contract was deployed this method is not available and you have to either create a special bridge or [use a lockbox](/interop/tutorials/upgrade-to-superchain-erc20/lockbox). +
+ + ### Deploy ERC7802 contracts + + We need to replace the ERC20 contracts with contracts that: + + * Support [ERC7802](https://eips.ethereum.org/EIPS/eip-7802) and [ERC165](https://eips.ethereum.org/EIPS/eip-165). + * Have the same storage layout as the ERC20 contracts they replace. + + + These contracts do *not* need to be deployed to the same address. + The address that needs to be the same is not the address of the ERC20 contract itself, but of the proxy. + + + 1. Create a file, `src/InteropToken.sol`: + + ```solidity file=/public/tutorials/InteropToken.sol hash=007791836635608fdeb9c70c1b368f25 filename="src/InteropToken.sol" + ``` + +
+ Detailed explanation + + ```solidity file=/public/tutorials/InteropToken.sol#L1-L5 hash=36b9b9d0fb1ff680dc0eaa1c48b7c56b + ``` + + Most of the code is identical to the original `MyToken`. + + ```solidity file=/public/tutorials/InteropToken.sol#L6-L7 hash=f06f3bd72be73dbd754008da7dd00d48 + ``` + + These are the imports needed for ERC7802 support. + We need `IERC165` for documentation purposes, and `IERC7802` for the ERC7802 events. + + ```solidity file=/public/tutorials/InteropToken.sol#L9 hash=ca402292e7551621669ef1a59b85d7ce + ``` + + We also implement [ERC165](https://eips.ethereum.org/EIPS/eip-165), but we don't need to import anything from there. + + ```solidity file=/public/tutorials/InteropToken.sol#L10-L14 hash=37e9b49f50a8b70971ce5d0112bd934e + ``` + + This function is identical to the one in `MyToken`. + + ```solidity file=/public/tutorials/InteropToken.sol#L16-L36 hash=448a7e21e094b3fd961f2b8ee15bc6c7 + ``` + + Standard [ERC7802](https://eips.ethereum.org/EIPS/eip-7802) behavior. + + ```solidity file=/public/tutorials/InteropToken.sol#L38-L42 hash=abb2093e9681984f25afa6f9d8b237a3 + ``` + + Standard [ERC165](https://eips.ethereum.org/EIPS/eip-165) behavior. +
+ + + Copying the original ERC20 token code with minimal differences is one method to keep the storage layout identical. + Alternatively, if you want to use a different contract, such as `SuperchainERC20`, you can modify the storage layout to match the old one using [the Solidity docs](https://docs.soliditylang.org/en/latest/internals/layout_in_storage.html). + + + 2. Deploy this contract on both chains, and store the addresses (which may or may not be the same). + + ```sh + ERC7802_A=`forge create InteropToken --private-key $PRIVATE_KEY --rpc-url $URL_CHAIN_A --broadcast | awk '/Deployed to:/ {print $3}'` + ERC7802_B=`forge create InteropToken --private-key $PRIVATE_KEY --rpc-url $URL_CHAIN_B --broadcast | awk '/Deployed to:/ {print $3}'` + ``` + + ### Update proxies + + Notify the beacon contracts of the new implementation contracts. + + ```sh + cast send $BEACON_ADDRESS --private-key $PRIVATE_KEY "upgradeTo(address)" $ERC7802_A --rpc-url $URL_CHAIN_A + cast send $BEACON_ADDRESS --private-key $PRIVATE_KEY "upgradeTo(address)" $ERC7802_B --rpc-url $URL_CHAIN_B + ``` + + ### Verification + + 1. See your balance on chain A. + + ```sh + cast call $ERC20_ADDRESS "balanceOf(address)" $USER_ADDRESS --rpc-url $URL_CHAIN_A | cast from-wei + ``` + + 2. See your balance on chain B. + + ```sh + cast call $ERC20_ADDRESS "balanceOf(address)" $USER_ADDRESS --rpc-url $URL_CHAIN_B | cast from-wei + ``` + + 3. Transfer 0.1 token. + + ```sh + AMOUNT=`echo 0.1 | cast to-wei` + cast send $INTEROP_BRIDGE --rpc-url $URL_CHAIN_A --private-key $PRIVATE_KEY "sendERC20(address,address,uint256,uint256)" $ERC20_ADDRESS $USER_ADDRESS $AMOUNT `cast chain-id --rpc-url $URL_CHAIN_B` + ``` + + 4. See the new balances. The A chain should have 0.9 tokens, and the B chain should have 0.1 tokens. + + ```sh + cast call $ERC20_ADDRESS "balanceOf(address)" $USER_ADDRESS --rpc-url $URL_CHAIN_A | cast from-wei + cast call $ERC20_ADDRESS "balanceOf(address)" $USER_ADDRESS --rpc-url $URL_CHAIN_B | cast from-wei + ``` +
+ +## Next steps + +* Deploy a [SuperchainERC20](/interop/tutorials/deploy-superchain-erc20) to the Superchain +* [Learn more about SuperchainERC20](/interop/superchain-erc20) +* Build a [revolutionary app](/app-developers/get-started) that uses multiple blockchains within the Superchain diff --git a/pages/interop/tutorials/upgrade-to-superchain-erc20/lockbox.mdx b/pages/interop/tutorials/upgrade-to-superchain-erc20/lockbox.mdx new file mode 100644 index 000000000..c99468504 --- /dev/null +++ b/pages/interop/tutorials/upgrade-to-superchain-erc20/lockbox.mdx @@ -0,0 +1,369 @@ +--- +title: Lockboxes for permissionless interop +lang: en-US +description: Tutorial on how to take permissionlessly create a lockbox contract to enable Superchain interoperability. +topic: Interoperability +personas: [Developer] +categories: [Tutorial, Interop] +content_type: article +--- + +import { Steps, Callout, Tabs } from 'nextra/components' + + + The SuperchainERC20 standard is ready for production deployments. + Please note that the OP Stack interoperability upgrade, required for crosschain messaging, is currently still in active development. + + +# Lockboxes for permissionless interop + +## Overview + +The lockbox is a smart contract that accepts deposits of the original ERC-20 and issues an equivalent amount of tokens that are Superchain interop compatible. +Users can unwrap their Superchain interop token at any time by returning it to the contract, which burns the Superchain interop tokens and releases the corresponding original ERC-20 from the lockbox. + +
+ About this tutorial + + **What you'll learn** + + * How to permissionlessly create a lockbox contract to enable Superchain interoperability. + + **Prerequisite knowledge** + + * You should already know how to [deploy SuperchainERC20 tokens with custom code](/interop/tutorials/custom-superchain-erc20). +
+ + + The code on the documentation site is sample code, *not* production code. + This means that we ran it, and it works as advertised. + However, it did not pass through the rigorous audit process that most Optimism code undergoes. + You're welcome to use it, but if you need it for production purposes you should get it audited first. + + +{/* + +I put this warning here, when we don't have it on most pages, because this tutorial +has code that is a lot more likely to be used in production. It doesn't just +show what is possible, it does the exact job needed. + +*/} + +### What you'll do + +Create a lockbox `SuperchainERC20` contract to enable interoperability for an ERC20 contract without permission from the original ERC20 deployer. + +## Instructions + +Some steps depend on whether you want to deploy on [Supersim](/interop/tools/supersim) or on the [development network](/interop/tools/devnet). + + + ### Install and run Supersim + + If you are going to use Supersim, [follow these instructions](/app-developers/tutorials/supersim/getting-started/installation) to install and run Supersim. + + + Make sure to run Supersim with autorelay on. + + ```sh + ./supersim --interop.autorelay true + ``` + + + ### Setup the ERC-20 token on chain A + + Download and run the setup script. + + ```sh + curl https://docs.optimism.io/tutorials/setup-for-erc20-upgrade.sh > setup-for-erc20-upgrade.sh + chmod +x setup-for-erc20-upgrade.sh + ./setup-for-erc20-upgrade.sh + ``` + + If you want to deploy to the [development networks](/interop/tools/devnet), provide `setup-for-erc20-upgrade.sh` with the private key of an address with ETH on both devnets. + + ```sh + ./setup-for-erc20-upgrade.sh + ``` + + ### Store the addresses + + Execute the bottom two lines of the setup script output to store the ERC-20 address and the address of the beacon contract. + + ```sh + BEACON_ADDRESS=0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512 + export ERC20_ADDRESS=0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0 + ``` + + ### Specify environment variables + + 1. Specify these variables, which we use later: + + + + Set these parameters for Supersim. + + ```sh + PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 + USER_ADDRESS=0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 + URL_CHAIN_A=http://127.0.0.1:9545 + URL_CHAIN_B=http://127.0.0.1:9546 + ``` + + + + For Devnet, specify in `PRIVATE_KEY` the private key you used for the setup script and then these parameters. + + ```sh + USER_ADDRESS=`cast wallet address --private-key $PRIVATE_KEY` + URL_CHAIN_A=https://interop-alpha-0.optimism.io + URL_CHAIN_B=https://interop-alpha-1.optimism.io + ``` + + + + 2. Regardless of whether you use Supersim or Devnet, specify these variables. + + ```sh + INTEROP_BRIDGE=0x4200000000000000000000000000000000000028 + export ERC20_CHAINID=`cast chain-id --rpc-url $URL_CHAIN_A` + ORIGINAL_TOKEN_NAME=`cast call $ERC20_ADDRESS "name()" --rpc-url $URL_CHAIN_A | cast to-ascii` + export NEW_TOKEN_NAME="$ORIGINAL_TOKEN_NAME Lockbox" + ORIGINAL_TOKEN_SYMBOL=`cast call $ERC20_ADDRESS "symbol()" --rpc-url $URL_CHAIN_A | cast to-ascii` + export NEW_TOKEN_SYMBOL="$ORIGINAL_TOKEN_SYMBOL-L" + export TOKEN_DECIMALS=`cast call $ERC20_ADDRESS "decimals()" --rpc-url $URL_CHAIN_A | cast to-dec` + ``` + + ### Update the deployment utilities + + The new `SuperchainERC20` variant is called `LockboxSuperchainERC20`, and it requires different constructor parameters. + To be able to deploy it, we need to modify some of the deployment utilities. + + 1. Download [the SuperchainERC20 starter kit](/interop/tutorials/deploy-superchain-erc20), and install libraries, etc. + + ```sh + git clone https://github.com/ethereum-optimism/superchainerc20-starter.git + cd superchainerc20-starter + pnpm install + pnpm init:env + ``` + + 2. Replace `packages/contracts/package.json` with this code: + + ```json filename="packages/contracts/package.json" + { + "name": "@superchainerc20-starter/contracts", + "main": "index.js", + "scripts": { + "deploy:dev": "env-cmd -f .env cross-env-shell 'wait-port http://:8420/ready && forge script scripts/SuperchainERC20Deployer.s.sol --broadcast --private-key $DEPLOYER_PRIVATE_KEY'", + "deploy:token": "env-cmd -f .env cross-env-shell 'forge script scripts/LockboxDeployer.s.sol --broadcast --private-key $DEPLOYER_PRIVATE_KEY'", + "update:rpcs": "cd ../.. && ./scripts/fetch-superchain-rpc-urls.sh", + "install": "forge install", + "build": "forge build", + "test": "forge test", + "init:env": "cp .env.example .env" + }, + "dependencies": { + "viem": "^2.21.37" + } + } + ``` + + 3. Create a new file, `packages/contracts/scripts/LockboxDeployer.s.sol`: + + ```solidity filename="packages/contracts/scripts/LockboxDeployer.s.sol" file=/public/tutorials/LockboxDeployer.s.sol hash=534b543709be173d87508a53322d8c59 + ``` + +
+ Explanation of the modified functions + + For the most part, this is the standard `SuperchainERC20Deployer.s.sol` that comes with the SuperchainERC20 starter kit. + Some functions are modified, as explained below. + + ```solidity file=/public/tutorials/LockboxDeployer.s.sol#L46-L52 hash=302d02c3895f109e5e64d265b0473e6a + ``` + + Get the majority of the configuration from the environment. + Mostly of it is derived from the configuration of the original ERC-20 token. + + Note that there is no `owner` here. + This `SuperchainERC20` contract does not need an owner, because minting and burning are handled by the users themselves (by locking and unlocking the original tokens). + + ```solidity file=/public/tutorials/LockboxDeployer.s.sol#L54-L69 hash=c45855080dc554cece35ed87e2d68f68 + ``` + + "Manually" calculate the address that [`CREATE2`](https://www.evm.codes/?fork=cancun#f5) will give us.\ + If there is already a contract there, we have a problem. + Otherwise, deploy `LockboxSuperchainERC20`. + + ```solidity file=/public/tutorials/LockboxDeployer.s.sol#L80-L84 hash=5d1f71b16a6f02d52a79b1a9e7588f87 + ``` + + I modified this salt function to include a timestamp (obtained using `vm.unixTime()` in the constructor). + This is not necessary, but I consider it a developer experience improvement. + During development you redeploy slightly modified code a lot of times. + It is easier if you don't need to manually change the salt every time. + + + Remove this before deploying to production. + Otherwise, as new blockchains join the Interop cluster, you may not be able to deploy your contract at the same address. + +
+ + ### Create and deploy the new contract + + 1. Create this file in `packages/contracts/src/LockboxSuperchainERC20.sol`: + + ```solidity filename="packages/contracts/src/LockboxSuperchainERC20.sol" file=/public/tutorials/LockboxSuperchainERC20.sol hash=d326f0e1c26904b844263274914951cf + ``` + +
+ Explanation + + ```solidity file=/public/tutorials/LockboxSuperchainERC20.sol#L11-L12 hash=45d211a19533f9b0dee310743b25459f + ``` + + The lockbox contract needs to know the contract for which it is a lockbox. + This requires not just the address, but also to know what chain has it. + + ```solidity file=/public/tutorials/LockboxSuperchainERC20.sol#L47-L57 hash=20f6aa15d113dcaf992875184173cb47 + ``` + + Users call this function to transfer original tokens to the contract and mint themselves an equivalent number of lockbox tokens. + This function has several tests to make sure it can be called. + + * Check the chain ID. + Locking and redeeming tokens can only be done on the original token's chain. + * Use [`transferFrom`](https://ethereum.org/en/developers/tutorials/erc20-annotated-code/#transferFrom) to transfer the tokens to ourselves. + This call typically reverts when it fails, but it can also return `false`. + In that case, we revert. + There are two reasons it may fail. + * The user (in this case, the `LockboxSuperchainERC20` contract) does not have [the allowance](https://ethereum.org/en/developers/tutorials/erc20-annotated-code/#_approve) to spend that amount of tokens from the original owner (`msg.sender`). + * The original owner (`msg.sender`) does not have enough tokens to transfer. + + If the tests are successful, mint the requested amount for `msg.sender`. + + ```solidity file=/public/tutorials/LockboxSuperchainERC20.sol#L59-L67 hash=2e63a9cd1ac1114c3fb2110e28b60924 + ``` + + Users call this function to redeem their existing lockbox tokens and replace them with the original tokens. + It also has multiple tests. + + * Again, check chain ID. + * Try to `_burn` the amount of lockbox tokens. + [The solady `_burn` function](https://github.com/Vectorized/solady/blob/main/src/tokens/ERC20.sol#L539-L542), the one we inherit from `SuperchainERC20`, reverts if the user does not have enough tokens to burn. + * Transfer the amount of the original ERC-20 redeemed to + the caller. + This should never fail, because lockbox ERC-20 tokens are supposed to always be backed by an equal number of the original tokens. + However, if it does fail for some reason, revert. +
+ + 2. Deploy the contract. + + + + ```sh + pnpm contracts:deploy:token + ``` + + + + To deploy to the [development networks](/interop/tools/devnet), follow the steps [in the tutorial](/interop/tutorials/deploy-superchain-erc20#prepare-for-deployment) before you deploy the contract. + + Then, update `packages/contracts/.env` and deploy the token. + + ```sh + echo DEPLOYER_PRIVATE_KEY=$PRIVATE_KEY > packages/contracts/.env + pnpm contracts:deploy:token + ``` + + + + 3. Get the new token address and store it in an environment variable. + + ```sh + NEW_TOKEN_ADDRESS=`cat packages/contracts/broadcast/multi/LockboxDeployer.s.sol-latest/run.json | awk '/contractAddress/ {print $2}' | head -1 | sed 's/[",]//g'` + ``` + + ### Verification + + 1. Check that the user has a single token of the original ERC-20. + + ```sh + cast call $ERC20_ADDRESS "balanceOf(address)" $USER_ADDRESS --rpc-url $URL_CHAIN_A | cast from-wei + ``` + + 2. Lock a quarter token in the lockbox ERC-20 contract. + To do this we first need to give the lockbox ERC-20 contract an allowance and then call it. + + ```sh + QUARTER_TOKEN=`echo 0.25 | cast to-wei` + cast send $ERC20_ADDRESS "approve(address,uint256)" $NEW_TOKEN_ADDRESS $QUARTER_TOKEN --private-key $PRIVATE_KEY --rpc-url $URL_CHAIN_A + cast send $NEW_TOKEN_ADDRESS "lockAndMint(uint256)" $QUARTER_TOKEN --private-key $PRIVATE_KEY --rpc-url $URL_CHAIN_A + ``` + + 3. See the balances of the user, both original and lockbox, and the balance of the lockbox contract itself. + + ```sh + cast call $ERC20_ADDRESS "balanceOf(address)" $USER_ADDRESS --rpc-url $URL_CHAIN_A | cast from-wei + cast call $NEW_TOKEN_ADDRESS "balanceOf(address)" $USER_ADDRESS --rpc-url $URL_CHAIN_A | cast from-wei + cast call $ERC20_ADDRESS "balanceOf(address)" $NEW_TOKEN_ADDRESS --rpc-url $URL_CHAIN_A | cast from-wei + ``` + + 4. Transfer 0.1 token to chain B. + + ```sh + TENTH_TOKEN=`echo 0.1 | cast to-wei` + cast send $INTEROP_BRIDGE --rpc-url $URL_CHAIN_A --private-key $PRIVATE_KEY "sendERC20(address,address,uint256,uint256)" $NEW_TOKEN_ADDRESS $USER_ADDRESS $TENTH_TOKEN `cast chain-id --rpc-url $URL_CHAIN_B` + ``` + + 5. Check the user's balances on both chains. + + ```sh + cast call $NEW_TOKEN_ADDRESS "balanceOf(address)" $USER_ADDRESS --rpc-url $URL_CHAIN_A | cast from-wei + cast call $NEW_TOKEN_ADDRESS "balanceOf(address)" $USER_ADDRESS --rpc-url $URL_CHAIN_B | cast from-wei + ``` + + 6. Specify the configuration for another user. + + + + ```sh + USER_ADDRESS_2=0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC + PRIVATE_KEY_2=0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a + ``` + + + + Specify the private key (`PRIVATE_KEY_2`) and user address (`USER_ADDRESS_2`) of another user that has ETH on both devnets. + + + + 7. Transfer new tokens to the new user (on chain B) and see that they were actually transferred. + + ```sh + cast send $NEW_TOKEN_ADDRESS "transfer(address,uint256)" $USER_ADDRESS_2 $TENTH_TOKEN --private-key $PRIVATE_KEY --rpc-url $URL_CHAIN_B + cast call $NEW_TOKEN_ADDRESS "balanceOf(address)" $USER_ADDRESS --rpc-url $URL_CHAIN_B | cast from-wei + cast call $NEW_TOKEN_ADDRESS "balanceOf(address)" $USER_ADDRESS_2 --rpc-url $URL_CHAIN_B | cast from-wei + ``` + + 8. As the new user, transfer tokens back to chain A and redeem them. + + ```sh + cast send $INTEROP_BRIDGE --rpc-url $URL_CHAIN_B --private-key $PRIVATE_KEY_2 "sendERC20(address,address,uint256,uint256)" $NEW_TOKEN_ADDRESS $USER_ADDRESS_2 $TENTH_TOKEN `cast chain-id --rpc-url $URL_CHAIN_A` + cast send $NEW_TOKEN_ADDRESS --rpc-url $URL_CHAIN_A --private-key $PRIVATE_KEY_2 "redeemAndBurn(uint256)" $TENTH_TOKEN + ``` + + 9. Check that the second user does not have any more of the new tokens, but does have the original token. + + ```sh + cast call $NEW_TOKEN_ADDRESS "balanceOf(address)" $USER_ADDRESS_2 --rpc-url $URL_CHAIN_A | cast from-wei + cast call $ERC20_ADDRESS "balanceOf(address)" $USER_ADDRESS_2 --rpc-url $URL_CHAIN_A | cast from-wei + ``` +
+ +## Next steps + +* Deploy a [SuperchainERC20](/interop/tutorials/deploy-superchain-erc20) to the Superchain +* [Learn more about SuperchainERC20](/interop/superchain-erc20) +* Build a [revolutionary app](/app-developers/get-started) that uses multiple blockchains within the Superchain diff --git a/public/tutorials/InteropToken.sol b/public/tutorials/InteropToken.sol new file mode 100644 index 000000000..5d34ae636 --- /dev/null +++ b/public/tutorials/InteropToken.sol @@ -0,0 +1,43 @@ +pragma solidity ^0.8.28; + +import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import {IERC7802, IERC165} from "lib/interop-lib/src/interfaces/IERC7802.sol"; +import {PredeployAddresses} from "lib/interop-lib/src/libraries/PredeployAddresses.sol"; + +contract InteropToken is Initializable, ERC20Upgradeable, OwnableUpgradeable, IERC7802 { + function initialize(string memory name, string memory symbol, uint256 initialSupply) public initializer { + __ERC20_init(name, symbol); + __Ownable_init(msg.sender); + _mint(msg.sender, initialSupply); + } + + /// @notice Allows the SuperchainTokenBridge to mint tokens. + /// @param _to Address to mint tokens to. + /// @param _amount Amount of tokens to mint. + function crosschainMint(address _to, uint256 _amount) external { + require(msg.sender == PredeployAddresses.SUPERCHAIN_TOKEN_BRIDGE, "Unauthorized"); + + _mint(_to, _amount); + + emit CrosschainMint(_to, _amount, msg.sender); + } + + /// @notice Allows the SuperchainTokenBridge to burn tokens. + /// @param _from Address to burn tokens from. + /// @param _amount Amount of tokens to burn. + function crosschainBurn(address _from, uint256 _amount) external { + require(msg.sender == PredeployAddresses.SUPERCHAIN_TOKEN_BRIDGE, "Unauthorized"); + + _burn(_from, _amount); + + emit CrosschainBurn(_from, _amount, msg.sender); + } + + /// @inheritdoc IERC165 + function supportsInterface(bytes4 _interfaceId) public view virtual returns (bool) { + return _interfaceId == type(IERC7802).interfaceId || _interfaceId == type(IERC20).interfaceId + || _interfaceId == type(IERC165).interfaceId; + } +} diff --git a/public/tutorials/LockboxDeployer.s.sol b/public/tutorials/LockboxDeployer.s.sol new file mode 100644 index 000000000..1e16f6770 --- /dev/null +++ b/public/tutorials/LockboxDeployer.s.sol @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.25; + +import {Script, console} from "forge-std/Script.sol"; +import {Vm} from "forge-std/Vm.sol"; +import {LockboxSuperchainERC20} from "../src/LockboxSuperchainERC20.sol"; + +contract LockboxDeployer is Script { + string deployConfig; + uint256 timestamp; + + constructor() { + string memory deployConfigPath = vm.envOr("DEPLOY_CONFIG_PATH", string("/configs/deploy-config.toml")); + string memory filePath = string.concat(vm.projectRoot(), deployConfigPath); + deployConfig = vm.readFile(filePath); + timestamp = vm.unixTime(); + } + + /// @notice Modifier that wraps a function in broadcasting. + modifier broadcast() { + vm.startBroadcast(msg.sender); + _; + vm.stopBroadcast(); + } + + function setUp() public {} + + function run() public { + string[] memory chainsToDeployTo = vm.parseTomlStringArray(deployConfig, ".deploy_config.chains"); + + address deployedAddress; + + for (uint256 i = 0; i < chainsToDeployTo.length; i++) { + string memory chainToDeployTo = chainsToDeployTo[i]; + + console.log("Deploying to chain: ", chainToDeployTo); + + vm.createSelectFork(chainToDeployTo); + address _deployedAddress = deployLockboxSuperchainERC20(); + deployedAddress = _deployedAddress; + } + + outputDeploymentResult(deployedAddress); + } + + function deployLockboxSuperchainERC20() public broadcast returns (address addr_) { + string memory name = vm.envString("NEW_TOKEN_NAME"); + string memory symbol = vm.envString("NEW_TOKEN_SYMBOL"); + uint256 decimals = vm.envUint("TOKEN_DECIMALS"); + require(decimals <= type(uint8).max, "decimals exceeds uint8 range"); + address originalTokenAddress = vm.envAddress("ERC20_ADDRESS"); + uint256 originalChainId = vm.envUint("ERC20_CHAINID"); + + bytes memory initCode = abi.encodePacked( + type(LockboxSuperchainERC20).creationCode, + abi.encode(name, symbol, uint8(decimals), originalTokenAddress, originalChainId) + ); + address preComputedAddress = vm.computeCreate2Address(_implSalt(), keccak256(initCode)); + if (preComputedAddress.code.length > 0) { + console.log( + "There is already a contract at %s", preComputedAddress, "on chain id: ", block.chainid + ); + addr_ = preComputedAddress; + } else { + addr_ = address(new LockboxSuperchainERC20{salt: _implSalt()}( + name, symbol, uint8(decimals), originalTokenAddress, originalChainId)); + console.log("Deployed LockboxSuperchainERC20 at address: ", addr_, "on chain id: ", block.chainid); + } + } + + function outputDeploymentResult(address deployedAddress) public { + console.log("Outputting deployment result"); + + string memory obj = "result"; + string memory jsonOutput = vm.serializeAddress(obj, "deployedAddress", deployedAddress); + + vm.writeJson(jsonOutput, "deployment.json"); + } + + /// @notice The CREATE2 salt to be used when deploying the token. + function _implSalt() internal view returns (bytes32) { + string memory salt = vm.parseTomlString(deployConfig, ".deploy_config.salt"); + return keccak256(abi.encodePacked(salt, timestamp)); + } +} diff --git a/public/tutorials/LockboxSuperchainERC20.sol b/public/tutorials/LockboxSuperchainERC20.sol new file mode 100644 index 000000000..2bc96d825 --- /dev/null +++ b/public/tutorials/LockboxSuperchainERC20.sol @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.25; + +import {SuperchainERC20} from "./SuperchainERC20.sol"; +import {IERC20} from "@openzeppelin/contracts/interfaces/IERC20.sol"; + +contract LockboxSuperchainERC20 is SuperchainERC20 { + string private _name; + string private _symbol; + uint8 private immutable _decimals; + address immutable _originalTokenAddress; + uint256 immutable _originalChainId; + + constructor( + string memory name_, + string memory symbol_, + uint8 decimals_, + address originalTokenAddress_, + uint256 originalChainId_) { + require(originalTokenAddress_ != address(0), "Invalid token address"); + require(originalChainId_ != 0, "Invalid chain ID"); + _name = name_; + _symbol = symbol_; + _decimals = decimals_; + _originalTokenAddress = originalTokenAddress_; + _originalChainId = originalChainId_; + } + + function name() public view virtual override returns (string memory) { + return _name; + } + + function symbol() public view virtual override returns (string memory) { + return _symbol; + } + + function decimals() public view override returns (uint8) { + return _decimals; + } + + function originalTokenAddress() public view returns (address) { + return _originalTokenAddress; + } + + function originalChainId() public view returns (uint256) { + return _originalChainId; + } + + function lockAndMint(uint256 amount_) external { + IERC20 originalToken = IERC20(_originalTokenAddress); + + require(block.chainid == _originalChainId, "Wrong chain"); + bool success = originalToken.transferFrom(msg.sender, address(this), amount_); + + // Not necessariy if the ERC-20 contract reverts rather than reverting. + // However, the standard allows the ERC-20 contract to return false instead. + require(success, "No tokens to lock, no mint either"); + _mint(msg.sender, amount_); + } + + function redeemAndBurn(uint256 amount_) external { + IERC20 originalToken = IERC20(_originalTokenAddress); + + require(block.chainid == _originalChainId, "Wrong chain"); + _burn(msg.sender, amount_); + + bool success = originalToken.transfer(msg.sender, amount_); + require(success, "Transfer failed, this should not happen"); + } +} + diff --git a/public/tutorials/setup-for-erc20-upgrade.sh b/public/tutorials/setup-for-erc20-upgrade.sh new file mode 100644 index 000000000..6748d6c5e --- /dev/null +++ b/public/tutorials/setup-for-erc20-upgrade.sh @@ -0,0 +1,76 @@ +#! /bin/sh + +rm -rf upgrade-erc20 +mkdir upgrade-erc20 +cd upgrade-erc20 + +if [ -z $1 ] +then + echo Supersim + PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 + URL_CHAIN_A=http://localhost:9545 + URL_CHAIN_B=http://localhost:9546 +else + echo Devnet + PRIVATE_KEY=$1 + URL_CHAIN_A=https://interop-alpha-0.optimism.io + URL_CHAIN_B=https://interop-alpha-1.optimism.io +fi + +USER_ADDRESS=`cast wallet address --private-key $PRIVATE_KEY` + +forge init +forge install OpenZeppelin/openzeppelin-contracts-upgradeable + +cat > script/LabSetup.s.sol <