-
Notifications
You must be signed in to change notification settings - Fork 265
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
Changes from 23 commits
7fbd348
3697d7d
c1ed880
a4f98e2
ac5de2c
6c6fd85
eb9ba15
7327294
0f4388b
c75befd
7644214
a93acb3
0c49dfe
fdc7a7f
33b49ed
cdebe0f
11e0b32
e059449
4b8033b
921101d
ca937f7
3685944
63f4021
523dbbc
5b92cb7
73f1f9f
3968ad1
cad3ad9
b0b71a2
9330958
25ce3e2
4640c8a
1eb5657
384c99d
9bcaaeb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
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); | ||
_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; | ||
} | ||
} |
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)); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -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_); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+49
to
+59
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. 🛠️ Refactor suggestion Consider implementing reentrancy protection for external calls. The 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
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
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"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
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 | ||||||
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. 💡 Verification agent ❓ Verification inconclusiveImprove Directory Change Error Handling - cd upgrade-erc20
+ cd upgrade-erc20 || { echo "Failed to enter upgrade-erc20 directory"; exit 1; } Action: Enhance Directory Change Robustness
- cd upgrade-erc20
+ cd upgrade-erc20 || { echo "Failed to enter upgrade-erc20 directory"; exit 1; } 📝 Committable suggestion
Suggested change
🧰 Tools🪛 Shellcheck (0.10.0)[warning] 5-5: Use 'cd ... || exit' or 'cd ... || return' in case cd fails. (SC2164) |
||||||
|
||||||
PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 | ||||||
qbzzt marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
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 |
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.
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:📝 Committable suggestion