Skip to content

Tutorial: Upgrade ERC20 to support interop #1525

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

Closed
Closed
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
7fbd348
WIP
qbzzt Mar 22, 2025
3697d7d
WIP
qbzzt Mar 22, 2025
c1ed880
Auto-fix: Update breadcrumbs, spelling dictionary and other automated…
qbzzt Mar 22, 2025
a4f98e2
Proxy method works
qbzzt Mar 24, 2025
ac5de2c
Auto-fix: Update breadcrumbs, spelling dictionary and other automated…
qbzzt Mar 24, 2025
6c6fd85
Added explanation of InteropToken.sol
qbzzt Mar 24, 2025
eb9ba15
Auto-fix: Update breadcrumbs, spelling dictionary and other automated…
qbzzt Mar 24, 2025
7327294
fixes
qbzzt Mar 24, 2025
0f4388b
WIP
qbzzt Mar 29, 2025
c75befd
Auto-fix: Update breadcrumbs, spelling dictionary and other automated…
qbzzt Mar 29, 2025
7644214
Need to polish
qbzzt Mar 30, 2025
a93acb3
Auto-fix: Update breadcrumbs, spelling dictionary and other automated…
qbzzt Mar 30, 2025
0c49dfe
WIP
qbzzt Apr 1, 2025
fdc7a7f
Ready for review
qbzzt Apr 1, 2025
33b49ed
Auto-fix: Update breadcrumbs, spelling dictionary and other automated…
qbzzt Apr 1, 2025
cdebe0f
Lint
qbzzt Apr 2, 2025
11e0b32
Code rabbit suggestions
qbzzt Apr 2, 2025
e059449
WIP, some @zainbacchu comments
qbzzt Apr 2, 2025
4b8033b
WIP
qbzzt Apr 2, 2025
921101d
WIP
qbzzt Apr 3, 2025
ca937f7
Auto-fix: Update breadcrumbs, spelling dictionary and other automated…
qbzzt Apr 3, 2025
3685944
lint
qbzzt Apr 3, 2025
63f4021
Merge branch 'main' into 250320-upgrade-erc20-to-superchain
qbzzt Apr 3, 2025
523dbbc
Multiple changes
qbzzt Apr 4, 2025
5b92cb7
WIP
qbzzt Apr 4, 2025
73f1f9f
WIP
qbzzt Apr 4, 2025
3968ad1
Almost ready
qbzzt Apr 4, 2025
cad3ad9
Comments addressed
qbzzt Apr 4, 2025
b0b71a2
Lint
qbzzt Apr 4, 2025
9330958
breadcrumbs
qbzzt Apr 4, 2025
25ce3e2
Update pages/stack/interop/tutorials/upgrade-to-superchain-erc20/lock…
qbzzt Apr 4, 2025
4640c8a
Update proxy-upgrade.mdx
qbzzt Apr 4, 2025
1eb5657
Import Card(s)
qbzzt Apr 4, 2025
384c99d
Update pages/stack/interop/tutorials/upgrade-to-superchain-erc20.mdx
qbzzt Apr 4, 2025
9bcaaeb
Code rabbit
qbzzt Apr 4, 2025
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
2 changes: 2 additions & 0 deletions pages/stack/interop/tutorials.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,6 @@ Documentation covering Interop related tutorials.

<Card title="Creating custom SuperchainERC20 tokens" href="/stack/interop/tutorials/custom-superchain-erc20" icon={<img src="/img/icons/shapes.svg" />} />

<Card title="Upgrading ERC-20 tokens for interop support" href="/stack/interop/tutorials/upgrade-to-superchain-erc20" icon={<img src="/img/icons/shapes.svg" />} />

</Cards>
3 changes: 2 additions & 1 deletion pages/stack/interop/tutorials/_meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
"message-passing": "Interop message passing",
"deploy-superchain-erc20": "Deploying a SuperchainERC20",
"transfer-superchainERC20": "Transferring a SuperchainERC20",
"custom-superchain-erc20": "Custom SuperchainERC20 tokens",
"custom-superchain-erc20": "Custom SuperchainERC20 tokens",
"upgrade-to-superchain-erc20": "Upgrading ERC-20 tokens for interop 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`",
Expand Down
531 changes: 531 additions & 0 deletions pages/stack/interop/tutorials/upgrade-to-superchain-erc20.mdx

Large diffs are not rendered by default.

43 changes: 43 additions & 0 deletions public/tutorials/InteropToken.sol
Original file line number Diff line number Diff line change
@@ -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);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Potential compile-time error with __Ownable_init(msg.sender).

By default, OpenZeppelin’s __Ownable_init() does not accept any parameters, causing a mismatch. If there is no custom override, consider applying this fix:

-__Ownable_init(msg.sender);
+__Ownable_init();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
__Ownable_init(msg.sender);
__Ownable_init();

_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;
}
}
85 changes: 85 additions & 0 deletions public/tutorials/LockboxDeployer.s.sol
Original file line number Diff line number Diff line change
@@ -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));
}
}
69 changes: 69 additions & 0 deletions public/tutorials/LockboxSuperchainERC20.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// 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_) {
_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_);
}
Comment on lines +49 to +59
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Consider implementing reentrancy protection for external calls.

The lockAndMint function makes an external call (transferFrom) before updating the contract state (_mint). This ordering is susceptible to reentrancy attacks if the original token contract is malicious or compromised.

Apply this diff to implement checks-effects-interactions pattern:

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_);
+   // Mint first (effects)
+   _mint(msg.sender, amount_);
+   
+   // Then transfer tokens (interactions)
+   bool success = originalToken.transferFrom(msg.sender, address(this), amount_);
+
+   // Not necessary if the ERC-20 contract reverts rather than returning false.
+   // However, the standard allows the ERC-20 contract to return false instead.
+   require(success, "Transfer failed, reverting mint");
}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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 lockAndMint(uint256 amount_) external {
IERC20 originalToken = IERC20(_originalTokenAddress);
require(block.chainid == _originalChainId, "Wrong chain");
// Mint first (effects)
_mint(msg.sender, amount_);
// Then transfer tokens (interactions)
bool success = originalToken.transferFrom(msg.sender, address(this), amount_);
// Not necessary if the ERC-20 contract reverts rather than returning false.
// However, the standard allows the ERC-20 contract to return false instead.
require(success, "Transfer failed, reverting mint");
}


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");
}
}

66 changes: 66 additions & 0 deletions public/tutorials/setup-for-erc20-upgrade.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
#! /bin/sh

rm -rf upgrade-erc20
mkdir upgrade-erc20
cd upgrade-erc20
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

❓ Verification inconclusive

Improve Directory Change Error Handling
The command cd upgrade-erc20 does not check whether the directory change was successful. Consider adding error handling to exit if the directory doesn't exist or the change fails. For example:

- cd upgrade-erc20
+ cd upgrade-erc20 || { echo "Failed to enter upgrade-erc20 directory"; exit 1; }

Action: Enhance Directory Change Robustness

  • In public/tutorials/setup-for-erc20-upgrade.sh (line 5), ensure that changing into the upgrade-erc20 directory is verified.
  • Update the command as follows to abort execution if the directory change fails:
- cd upgrade-erc20
+ cd upgrade-erc20 || { echo "Failed to enter upgrade-erc20 directory"; exit 1; }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
cd upgrade-erc20
cd upgrade-erc20 || { echo "Failed to enter upgrade-erc20 directory"; exit 1; }
🧰 Tools
🪛 Shellcheck (0.10.0)

[warning] 5-5: Use 'cd ... || exit' or 'cd ... || return' in case cd fails.

(SC2164)


PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
USER_ADDRESS=0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
URL_CHAIN_A=http://127.0.0.1:9545
URL_CHAIN_B=http://127.0.0.1:9546


forge init
forge install OpenZeppelin/openzeppelin-contracts-upgradeable

cat > script/LabSetup.s.sol <<EOF
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.20;

import {Script, console} from "forge-std/Script.sol";
import {UpgradeableBeacon} from "../lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/proxy/beacon/UpgradeableBeacon.sol";
import {BeaconProxy} from "../lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/proxy/beacon/BeaconProxy.sol";

import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol";
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";

contract MyToken is Initializable, ERC20Upgradeable, OwnableUpgradeable {
function initialize(string memory name, string memory symbol, uint256 initialSupply) public initializer {
__ERC20_init(name, symbol);
__Ownable_init(msg.sender);
_mint(msg.sender, initialSupply);
}
}

contract LabSetup is Script {
function setUp() public {}

function run() public {
vm.startBroadcast();

MyToken token = new MyToken();
console.log("Token address:", address(token));
console.log("msg.sender:", msg.sender);

UpgradeableBeacon beacon = new UpgradeableBeacon(address(token), msg.sender);
console.log("UpgradeableBeacon:", address(beacon));

BeaconProxy proxy = new BeaconProxy(address(beacon),
abi.encodeCall(MyToken.initialize, ("Test", "TST", block.chainid == 901 ? 10**18 : 0))
);
console.log("Proxy:", address(proxy));

vm.stopBroadcast();
}
}
EOF

forge script script/LabSetup.s.sol --rpc-url $URL_CHAIN_A --broadcast --private-key $PRIVATE_KEY --tc LabSetup | tee setup_output

BEACON_ADDRESS=`cat setup_output | awk '/Beacon:/ {print $2}'`
ERC20_ADDRESS=`cat setup_output | awk '/Proxy:/ {print $2}'`

echo Run these commands to store the configuration:
echo BEACON_ADDRESS=$BEACON_ADDRESS
echo ERC20_ADDRESS=$ERC20_ADDRESS
3 changes: 3 additions & 0 deletions words.txt
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ JSPATH
jspath
jwtsecret
Keccak
knowlege
leveldb
lightkdf
Lisk
Expand Down Expand Up @@ -318,6 +319,7 @@ productionize
productionized
Protip
Proxied
proxied
Proxyd
proxyd
Pyth
Expand Down Expand Up @@ -371,6 +373,7 @@ smartcard
snapshotlog
Snapsync
snapsync
solady
Solana
Soneium
soyboy
Expand Down