Commit 9968aa3b authored by AgusDuha's avatar AgusDuha Committed by GitHub

feat: add SuperchainERC20 baseline (#11675)

* feat: add superchain erc20 baseline (#37)

* feat: add superchain erc20 baseline

* feat: make superchain ERC20 simpler

* fix: small version fix and tests

* test: fix test name

* test: remove unused import

* feat: making baseline abstract

* fix: interfaces to comply with the new interface checker

* fix: import paths and empty line

* fix: lint line

---------
Co-authored-by: default avatar0xng <ng@defi.sucks>
Co-authored-by: default avatar0xng <87835144+0xng@users.noreply.github.com>
parent 7efa4e7c
...@@ -116,8 +116,8 @@ ...@@ -116,8 +116,8 @@
"sourceCodeHash": "0x4b806cc85cead74c8df34ab08f4b6c6a95a1a387a335ec8a7cb2de4ea4e1cf41" "sourceCodeHash": "0x4b806cc85cead74c8df34ab08f4b6c6a95a1a387a335ec8a7cb2de4ea4e1cf41"
}, },
"src/L2/OptimismSuperchainERC20.sol": { "src/L2/OptimismSuperchainERC20.sol": {
"initCodeHash": "0xdd16dbc0ccbbac53ec2d4273f06334960a907a9f20b7c40300833227ee31d0de", "initCodeHash": "0x4fd71b5352b78d51d39625b6defa77a75be53067b32f3cba86bd17a46917adf9",
"sourceCodeHash": "0x910d43a17800df64dbc104f69ef1f900ca761cec4949c01d1c1126fde5268349" "sourceCodeHash": "0xad3934ea533544b3c130c80be26201354af85f9166cb2ce54d96e5e383ebb5c1"
}, },
"src/L2/OptimismSuperchainERC20Beacon.sol": { "src/L2/OptimismSuperchainERC20Beacon.sol": {
"initCodeHash": "0x99ce8095b23c124850d866cbc144fee6cee05dbc6bb5d83acadfe00b90cf42c7", "initCodeHash": "0x99ce8095b23c124850d866cbc144fee6cee05dbc6bb5d83acadfe00b90cf42c7",
......
...@@ -2,27 +2,17 @@ ...@@ -2,27 +2,17 @@
pragma solidity 0.8.25; pragma solidity 0.8.25;
import { IOptimismSuperchainERC20Extension } from "src/L2/interfaces/IOptimismSuperchainERC20.sol"; import { IOptimismSuperchainERC20Extension } from "src/L2/interfaces/IOptimismSuperchainERC20.sol";
import { ERC20 } from "@solady/tokens/ERC20.sol";
import { IL2ToL2CrossDomainMessenger } from "src/L2/interfaces/IL2ToL2CrossDomainMessenger.sol"; import { IL2ToL2CrossDomainMessenger } from "src/L2/interfaces/IL2ToL2CrossDomainMessenger.sol";
import { ISemver } from "src/universal/interfaces/ISemver.sol"; import { ISemver } from "src/universal/interfaces/ISemver.sol";
import { Predeploys } from "src/libraries/Predeploys.sol"; import { Predeploys } from "src/libraries/Predeploys.sol";
import { ERC20 } from "@solady/tokens/ERC20.sol";
import { SuperchainERC20 } from "src/L2/SuperchainERC20.sol";
import { Initializable } from "@openzeppelin/contracts-v5/proxy/utils/Initializable.sol"; import { Initializable } from "@openzeppelin/contracts-v5/proxy/utils/Initializable.sol";
import { ERC165 } from "@openzeppelin/contracts-v5/utils/introspection/ERC165.sol"; import { ERC165 } from "@openzeppelin/contracts-v5/utils/introspection/ERC165.sol";
/// @notice Thrown when attempting to relay a message and the function caller (msg.sender) is not
/// L2ToL2CrossDomainMessenger.
error CallerNotL2ToL2CrossDomainMessenger();
/// @notice Thrown when attempting to relay a message and the cross domain message sender is not this
/// OptimismSuperchainERC20.
error InvalidCrossDomainSender();
/// @notice Thrown when attempting to mint or burn tokens and the function caller is not the StandardBridge. /// @notice Thrown when attempting to mint or burn tokens and the function caller is not the StandardBridge.
error OnlyBridge(); error OnlyBridge();
/// @notice Thrown when attempting to mint or burn tokens and the account is the zero address.
error ZeroAddress();
/// @custom:proxied true /// @custom:proxied true
/// @title OptimismSuperchainERC20 /// @title OptimismSuperchainERC20
/// @notice OptimismSuperchainERC20 is a standard extension of the base ERC20 token contract that unifies ERC20 token /// @notice OptimismSuperchainERC20 is a standard extension of the base ERC20 token contract that unifies ERC20 token
...@@ -31,10 +21,13 @@ error ZeroAddress(); ...@@ -31,10 +21,13 @@ error ZeroAddress();
/// token, turning it fungible and interoperable across the superchain. Likewise, it also enables the inverse /// token, turning it fungible and interoperable across the superchain. Likewise, it also enables the inverse
/// conversion path. /// conversion path.
/// Moreover, it builds on top of the L2ToL2CrossDomainMessenger for both replay protection and domain binding. /// Moreover, it builds on top of the L2ToL2CrossDomainMessenger for both replay protection and domain binding.
contract OptimismSuperchainERC20 is IOptimismSuperchainERC20Extension, ERC20, ISemver, Initializable, ERC165 { contract OptimismSuperchainERC20 is
/// @notice Address of the L2ToL2CrossDomainMessenger Predeploy. IOptimismSuperchainERC20Extension,
address internal constant MESSENGER = Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER; SuperchainERC20,
ISemver,
Initializable,
ERC165
{
/// @notice Address of the StandardBridge Predeploy. /// @notice Address of the StandardBridge Predeploy.
address internal constant BRIDGE = Predeploys.L2_STANDARD_BRIDGE; address internal constant BRIDGE = Predeploys.L2_STANDARD_BRIDGE;
...@@ -57,7 +50,7 @@ contract OptimismSuperchainERC20 is IOptimismSuperchainERC20Extension, ERC20, IS ...@@ -57,7 +50,7 @@ contract OptimismSuperchainERC20 is IOptimismSuperchainERC20Extension, ERC20, IS
} }
/// @notice Returns the storage for the OptimismSuperchainERC20Metadata. /// @notice Returns the storage for the OptimismSuperchainERC20Metadata.
function _getMetadataStorage() private pure returns (OptimismSuperchainERC20Metadata storage _storage) { function _getStorage() private pure returns (OptimismSuperchainERC20Metadata storage _storage) {
assembly { assembly {
_storage.slot := OPTIMISM_SUPERCHAIN_ERC20_METADATA_SLOT _storage.slot := OPTIMISM_SUPERCHAIN_ERC20_METADATA_SLOT
} }
...@@ -92,7 +85,7 @@ contract OptimismSuperchainERC20 is IOptimismSuperchainERC20Extension, ERC20, IS ...@@ -92,7 +85,7 @@ contract OptimismSuperchainERC20 is IOptimismSuperchainERC20Extension, ERC20, IS
external external
initializer initializer
{ {
OptimismSuperchainERC20Metadata storage _storage = _getMetadataStorage(); OptimismSuperchainERC20Metadata storage _storage = _getStorage();
_storage.remoteToken = _remoteToken; _storage.remoteToken = _remoteToken;
_storage.name = _name; _storage.name = _name;
_storage.symbol = _symbol; _storage.symbol = _symbol;
...@@ -121,54 +114,19 @@ contract OptimismSuperchainERC20 is IOptimismSuperchainERC20Extension, ERC20, IS ...@@ -121,54 +114,19 @@ contract OptimismSuperchainERC20 is IOptimismSuperchainERC20Extension, ERC20, IS
emit Burn(_from, _amount); emit Burn(_from, _amount);
} }
/// @notice Sends tokens to some target address on another chain.
/// @param _to Address to send tokens to.
/// @param _amount Amount of tokens to send.
/// @param _chainId Chain ID of the destination chain.
function sendERC20(address _to, uint256 _amount, uint256 _chainId) external {
if (_to == address(0)) revert ZeroAddress();
_burn(msg.sender, _amount);
bytes memory _message = abi.encodeCall(this.relayERC20, (msg.sender, _to, _amount));
IL2ToL2CrossDomainMessenger(MESSENGER).sendMessage(_chainId, address(this), _message);
emit SendERC20(msg.sender, _to, _amount, _chainId);
}
/// @notice Relays tokens received from another chain.
/// @param _from Address of the msg.sender of sendERC20 on the source chain.
/// @param _to Address to relay tokens to.
/// @param _amount Amount of tokens to relay.
function relayERC20(address _from, address _to, uint256 _amount) external {
if (_to == address(0)) revert ZeroAddress();
if (msg.sender != MESSENGER) revert CallerNotL2ToL2CrossDomainMessenger();
if (IL2ToL2CrossDomainMessenger(MESSENGER).crossDomainMessageSender() != address(this)) {
revert InvalidCrossDomainSender();
}
uint256 source = IL2ToL2CrossDomainMessenger(MESSENGER).crossDomainMessageSource();
_mint(_to, _amount);
emit RelayERC20(_from, _to, _amount, source);
}
/// @notice Returns the address of the corresponding version of this token on the remote chain. /// @notice Returns the address of the corresponding version of this token on the remote chain.
function remoteToken() public view override returns (address) { function remoteToken() public view override returns (address) {
return _getMetadataStorage().remoteToken; return _getStorage().remoteToken;
} }
/// @notice Returns the name of the token. /// @notice Returns the name of the token.
function name() public view virtual override returns (string memory) { function name() public view virtual override returns (string memory) {
return _getMetadataStorage().name; return _getStorage().name;
} }
/// @notice Returns the symbol of the token. /// @notice Returns the symbol of the token.
function symbol() public view virtual override returns (string memory) { function symbol() public view virtual override returns (string memory) {
return _getMetadataStorage().symbol; return _getStorage().symbol;
} }
/// @notice Returns the number of decimals used to get its user representation. /// @notice Returns the number of decimals used to get its user representation.
...@@ -178,7 +136,7 @@ contract OptimismSuperchainERC20 is IOptimismSuperchainERC20Extension, ERC20, IS ...@@ -178,7 +136,7 @@ contract OptimismSuperchainERC20 is IOptimismSuperchainERC20Extension, ERC20, IS
/// no way affects any of the arithmetic of the contract, including /// no way affects any of the arithmetic of the contract, including
/// {IERC20-balanceOf} and {IERC20-transfer}. /// {IERC20-balanceOf} and {IERC20-transfer}.
function decimals() public view override returns (uint8) { function decimals() public view override returns (uint8) {
return _getMetadataStorage().decimals; return _getStorage().decimals;
} }
/// @notice ERC165 interface check function. /// @notice ERC165 interface check function.
......
// SPDX-License-Identifier: MIT
pragma solidity 0.8.25;
import { ISuperchainERC20Extensions, ISuperchainERC20Errors } from "src/L2/interfaces/ISuperchainERC20.sol";
import { ERC20 } from "@solady/tokens/ERC20.sol";
import { IL2ToL2CrossDomainMessenger } from "src/L2/interfaces/IL2ToL2CrossDomainMessenger.sol";
import { Predeploys } from "src/libraries/Predeploys.sol";
/// @title SuperchainERC20
/// @notice SuperchainERC20 is a standard extension of the base ERC20 token contract that unifies ERC20 token
/// bridging to make it fungible across the Superchain. It builds on top of the L2ToL2CrossDomainMessenger for
/// both replay protection and domain binding.
abstract contract SuperchainERC20 is ISuperchainERC20Extensions, ISuperchainERC20Errors, ERC20 {
/// @notice Address of the L2ToL2CrossDomainMessenger Predeploy.
address internal constant MESSENGER = Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER;
/// @notice Sends tokens to some target address on another chain.
/// @param _to Address to send tokens to.
/// @param _amount Amount of tokens to send.
/// @param _chainId Chain ID of the destination chain.
function sendERC20(address _to, uint256 _amount, uint256 _chainId) external virtual {
if (_to == address(0)) revert ZeroAddress();
_burn(msg.sender, _amount);
bytes memory _message = abi.encodeCall(this.relayERC20, (msg.sender, _to, _amount));
IL2ToL2CrossDomainMessenger(MESSENGER).sendMessage(_chainId, address(this), _message);
emit SendERC20(msg.sender, _to, _amount, _chainId);
}
/// @notice Relays tokens received from another chain.
/// @param _from Address of the msg.sender of sendERC20 on the source chain.
/// @param _to Address to relay tokens to.
/// @param _amount Amount of tokens to relay.
function relayERC20(address _from, address _to, uint256 _amount) external virtual {
if (msg.sender != MESSENGER) revert CallerNotL2ToL2CrossDomainMessenger();
if (IL2ToL2CrossDomainMessenger(MESSENGER).crossDomainMessageSender() != address(this)) {
revert InvalidCrossDomainSender();
}
uint256 source = IL2ToL2CrossDomainMessenger(MESSENGER).crossDomainMessageSource();
_mint(_to, _amount);
emit RelayERC20(_from, _to, _amount, source);
}
}
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
pragma solidity ^0.8.0; pragma solidity ^0.8.0;
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { IERC20Solady } from "src/dependency/interfaces/IERC20Solady.sol";
import { ISuperchainERC20Extensions } from "./ISuperchainERC20.sol"; import { ISuperchainERC20Extensions, ISuperchainERC20Errors } from "./ISuperchainERC20.sol";
/// @title IOptimismSuperchainERC20Extension /// @title IOptimismSuperchainERC20Extension
/// @notice This interface is available on the OptimismSuperchainERC20 contract. /// @notice This interface is available on the OptimismSuperchainERC20 contract.
/// We declare it as a separate interface so that it can be used in /// We declare it as a separate interface so that it can be used in
/// custom implementations of SuperchainERC20. /// custom implementations of SuperchainERC20.
interface IOptimismSuperchainERC20Extension is ISuperchainERC20Extensions { interface IOptimismSuperchainERC20Extension is ISuperchainERC20Extensions, ISuperchainERC20Errors {
/// @notice Emitted whenever tokens are minted for an account. /// @notice Emitted whenever tokens are minted for an account.
/// @param account Address of the account tokens are being minted for. /// @param account Address of the account tokens are being minted for.
/// @param amount Amount of tokens minted. /// @param amount Amount of tokens minted.
...@@ -34,5 +34,5 @@ interface IOptimismSuperchainERC20Extension is ISuperchainERC20Extensions { ...@@ -34,5 +34,5 @@ interface IOptimismSuperchainERC20Extension is ISuperchainERC20Extensions {
} }
/// @title IOptimismSuperchainERC20 /// @title IOptimismSuperchainERC20
/// @notice Combines the ERC20 interface with the OptimismSuperchainERC20Extension interface. /// @notice Combines Solady's ERC20 interface with the OptimismSuperchainERC20Extension interface.
interface IOptimismSuperchainERC20 is IERC20, IOptimismSuperchainERC20Extension { } interface IOptimismSuperchainERC20 is IERC20Solady, IOptimismSuperchainERC20Extension { }
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
pragma solidity ^0.8.0; pragma solidity ^0.8.0;
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { IERC20Solady } from "src/dependency/interfaces/IERC20Solady.sol";
/// @title ISuperchainERC20Extensions /// @title ISuperchainERC20Extensions
/// @notice Interface for the extensions to the ERC20 standard that are used by SuperchainERC20. /// @notice Interface for the extensions to the ERC20 standard that are used by SuperchainERC20.
...@@ -35,6 +35,21 @@ interface ISuperchainERC20Extensions { ...@@ -35,6 +35,21 @@ interface ISuperchainERC20Extensions {
function relayERC20(address _from, address _to, uint256 _amount) external; function relayERC20(address _from, address _to, uint256 _amount) external;
} }
/// @title ISuperchainERC20Errors
/// @notice Interface containing the errors added in the SuperchainERC20 implementation.
interface ISuperchainERC20Errors {
/// @notice Thrown when attempting to relay a message and the function caller (msg.sender) is not
/// L2ToL2CrossDomainMessenger.
error CallerNotL2ToL2CrossDomainMessenger();
/// @notice Thrown when attempting to relay a message and the cross domain message sender is not this
/// SuperchainERC20.
error InvalidCrossDomainSender();
/// @notice Thrown when attempting to perform an operation and the account is the zero address.
error ZeroAddress();
}
/// @title ISuperchainERC20 /// @title ISuperchainERC20
/// @notice Combines the ERC20 interface with the SuperchainERC20Extensions interface. /// @notice Combines Solady's ERC20 interface with the SuperchainERC20Extensions interface.
interface ISuperchainERC20 is IERC20, ISuperchainERC20Extensions { } interface ISuperchainERC20 is IERC20Solady, ISuperchainERC20Extensions, ISuperchainERC20Errors { }
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
interface IERC20Solady {
/// @dev The total supply has overflowed.
error TotalSupplyOverflow();
/// @dev The allowance has overflowed.
error AllowanceOverflow();
/// @dev The allowance has underflowed.
error AllowanceUnderflow();
/// @dev Insufficient balance.
error InsufficientBalance();
/// @dev Insufficient allowance.
error InsufficientAllowance();
/// @dev The permit is invalid.
error InvalidPermit();
/// @dev The permit has expired.
error PermitExpired();
/// @dev Emitted when `amount` tokens is transferred from `from` to `to`.
event Transfer(address indexed from, address indexed to, uint256 amount);
/// @dev Emitted when `amount` tokens is approved by `owner` to be used by `spender`.
event Approval(address indexed owner, address indexed spender, uint256 amount);
/// @dev Returns the name of the token.
function name() external view returns (string memory);
/// @dev Returns the symbol of the token.
function symbol() external view returns (string memory);
/// @dev Returns the decimals places of the token.
function decimals() external view returns (uint8);
/// @dev Returns the amount of tokens in existence.
function totalSupply() external view returns (uint256 result);
/// @dev Returns the amount of tokens owned by `owner`
function balanceOf(address owner) external view returns (uint256 result);
/// @dev Returns the amount of tokens that `spender` can spend on behalf of `owner`.
function allowance(address owner, address spender) external view returns (uint256 result);
/// @dev Sets `amount` as the allowance of `spender` over the caller's tokens.
///
/// Emits a {Approval} event.
function approve(address spender, uint256 amount) external returns (bool);
/// @dev Transfer `amount` tokens from the caller to `to`.
///
/// Requirements:
/// - `from` must at least have `amount`.
///
/// Emits a {Transfer} event.
function transfer(address to, uint256 amount) external returns (bool);
/// @dev Transfers `amount` tokens from `from` to `to`.
///
/// Note: Does not update the allowance if it is the maximum uint256 value.
///
/// Requirements:
/// - `from` must at least have `amount`.
/// - The caller must have at least `amount` of allowance to transfer the tokens of `from`.
///
/// Emits a {Transfer} event.
function transferFrom(address from, address to, uint256 amount) external returns (bool);
/// @dev Returns the current nonce for `owner`.
/// This value is used to compute the signature for EIP-2612 permit.
function nonces(address owner) external view returns (uint256 result);
/// @dev Sets `value` as the allowance of `spender` over the tokens of `owner`,
/// authorized by a signed approval by `owner`.
///
/// Emits a {Approval} event.
function permit(
address owner,
address spender,
uint256 value,
uint256 deadline,
uint8 v,
bytes32 r,
bytes32 s
)
external;
/// @dev Returns the EIP-712 domain separator for the EIP-2612 permit.
function DOMAIN_SEPARATOR() external view returns (bytes32 result);
}
...@@ -17,14 +17,11 @@ import { BeaconProxy } from "@openzeppelin/contracts-v5/proxy/beacon/BeaconProxy ...@@ -17,14 +17,11 @@ import { BeaconProxy } from "@openzeppelin/contracts-v5/proxy/beacon/BeaconProxy
// Target contract // Target contract
import { import {
OptimismSuperchainERC20, OptimismSuperchainERC20, IOptimismSuperchainERC20Extension, OnlyBridge
IOptimismSuperchainERC20Extension,
CallerNotL2ToL2CrossDomainMessenger,
InvalidCrossDomainSender,
OnlyBridge,
ZeroAddress
} from "src/L2/OptimismSuperchainERC20.sol"; } from "src/L2/OptimismSuperchainERC20.sol";
import { ISuperchainERC20Extensions } from "src/L2/interfaces/ISuperchainERC20.sol";
// SuperchainERC20 Interfaces
import { ISuperchainERC20Extensions, ISuperchainERC20Errors } from "src/L2/interfaces/ISuperchainERC20.sol";
/// @title OptimismSuperchainERC20Test /// @title OptimismSuperchainERC20Test
/// @notice Contract for testing the OptimismSuperchainERC20 contract. /// @notice Contract for testing the OptimismSuperchainERC20 contract.
...@@ -135,7 +132,7 @@ contract OptimismSuperchainERC20Test is Test { ...@@ -135,7 +132,7 @@ contract OptimismSuperchainERC20Test is Test {
/// @notice Tests the `mint` function reverts when the amount is zero. /// @notice Tests the `mint` function reverts when the amount is zero.
function testFuzz_mint_zeroAddressTo_reverts(uint256 _amount) public { function testFuzz_mint_zeroAddressTo_reverts(uint256 _amount) public {
// Expect the revert with `ZeroAddress` selector // Expect the revert with `ZeroAddress` selector
vm.expectRevert(ZeroAddress.selector); vm.expectRevert(ISuperchainERC20Errors.ZeroAddress.selector);
// Call the `mint` function with the zero address // Call the `mint` function with the zero address
vm.prank(BRIDGE); vm.prank(BRIDGE);
...@@ -152,11 +149,11 @@ contract OptimismSuperchainERC20Test is Test { ...@@ -152,11 +149,11 @@ contract OptimismSuperchainERC20Test is Test {
uint256 _toBalanceBefore = superchainERC20.balanceOf(_to); uint256 _toBalanceBefore = superchainERC20.balanceOf(_to);
// Look for the emit of the `Transfer` event // Look for the emit of the `Transfer` event
vm.expectEmit(true, true, true, true, address(superchainERC20)); vm.expectEmit(address(superchainERC20));
emit IERC20.Transfer(ZERO_ADDRESS, _to, _amount); emit IERC20.Transfer(ZERO_ADDRESS, _to, _amount);
// Look for the emit of the `Mint` event // Look for the emit of the `Mint` event
vm.expectEmit(true, true, true, true, address(superchainERC20)); vm.expectEmit(address(superchainERC20));
emit IOptimismSuperchainERC20Extension.Mint(_to, _amount); emit IOptimismSuperchainERC20Extension.Mint(_to, _amount);
// Call the `mint` function with the bridge caller // Call the `mint` function with the bridge caller
...@@ -184,7 +181,7 @@ contract OptimismSuperchainERC20Test is Test { ...@@ -184,7 +181,7 @@ contract OptimismSuperchainERC20Test is Test {
/// @notice Tests the `burn` function reverts when the amount is zero. /// @notice Tests the `burn` function reverts when the amount is zero.
function testFuzz_burn_zeroAddressFrom_reverts(uint256 _amount) public { function testFuzz_burn_zeroAddressFrom_reverts(uint256 _amount) public {
// Expect the revert with `ZeroAddress` selector // Expect the revert with `ZeroAddress` selector
vm.expectRevert(ZeroAddress.selector); vm.expectRevert(ISuperchainERC20Errors.ZeroAddress.selector);
// Call the `burn` function with the zero address // Call the `burn` function with the zero address
vm.prank(BRIDGE); vm.prank(BRIDGE);
...@@ -205,11 +202,11 @@ contract OptimismSuperchainERC20Test is Test { ...@@ -205,11 +202,11 @@ contract OptimismSuperchainERC20Test is Test {
uint256 _fromBalanceBefore = superchainERC20.balanceOf(_from); uint256 _fromBalanceBefore = superchainERC20.balanceOf(_from);
// Look for the emit of the `Transfer` event // Look for the emit of the `Transfer` event
vm.expectEmit(true, true, true, true, address(superchainERC20)); vm.expectEmit(address(superchainERC20));
emit IERC20.Transfer(_from, ZERO_ADDRESS, _amount); emit IERC20.Transfer(_from, ZERO_ADDRESS, _amount);
// Look for the emit of the `Burn` event // Look for the emit of the `Burn` event
vm.expectEmit(true, true, true, true, address(superchainERC20)); vm.expectEmit(address(superchainERC20));
emit IOptimismSuperchainERC20Extension.Burn(_from, _amount); emit IOptimismSuperchainERC20Extension.Burn(_from, _amount);
// Call the `burn` function with the bridge caller // Call the `burn` function with the bridge caller
...@@ -224,7 +221,7 @@ contract OptimismSuperchainERC20Test is Test { ...@@ -224,7 +221,7 @@ contract OptimismSuperchainERC20Test is Test {
/// @notice Tests the `sendERC20` function reverts when the `_to` address is the zero address. /// @notice Tests the `sendERC20` function reverts when the `_to` address is the zero address.
function testFuzz_sendERC20_zeroAddressTo_reverts(uint256 _amount, uint256 _chainId) public { function testFuzz_sendERC20_zeroAddressTo_reverts(uint256 _amount, uint256 _chainId) public {
// Expect the revert with `ZeroAddress` selector // Expect the revert with `ZeroAddress` selector
vm.expectRevert(ZeroAddress.selector); vm.expectRevert(ISuperchainERC20Errors.ZeroAddress.selector);
// Call the `sendERC20` function with the zero address // Call the `sendERC20` function with the zero address
vm.prank(BRIDGE); vm.prank(BRIDGE);
...@@ -247,11 +244,11 @@ contract OptimismSuperchainERC20Test is Test { ...@@ -247,11 +244,11 @@ contract OptimismSuperchainERC20Test is Test {
uint256 _senderBalanceBefore = superchainERC20.balanceOf(_sender); uint256 _senderBalanceBefore = superchainERC20.balanceOf(_sender);
// Look for the emit of the `Transfer` event // Look for the emit of the `Transfer` event
vm.expectEmit(true, true, true, true, address(superchainERC20)); vm.expectEmit(address(superchainERC20));
emit IERC20.Transfer(_sender, ZERO_ADDRESS, _amount); emit IERC20.Transfer(_sender, ZERO_ADDRESS, _amount);
// Look for the emit of the `SendERC20` event // Look for the emit of the `SendERC20` event
vm.expectEmit(true, true, true, true, address(superchainERC20)); vm.expectEmit(address(superchainERC20));
emit ISuperchainERC20Extensions.SendERC20(_sender, _to, _amount, _chainId); emit ISuperchainERC20Extensions.SendERC20(_sender, _to, _amount, _chainId);
// Mock the call over the `sendMessage` function and expect it to be called properly // Mock the call over the `sendMessage` function and expect it to be called properly
...@@ -280,7 +277,7 @@ contract OptimismSuperchainERC20Test is Test { ...@@ -280,7 +277,7 @@ contract OptimismSuperchainERC20Test is Test {
vm.assume(_to != ZERO_ADDRESS); vm.assume(_to != ZERO_ADDRESS);
// Expect the revert with `CallerNotL2ToL2CrossDomainMessenger` selector // Expect the revert with `CallerNotL2ToL2CrossDomainMessenger` selector
vm.expectRevert(CallerNotL2ToL2CrossDomainMessenger.selector); vm.expectRevert(ISuperchainERC20Errors.CallerNotL2ToL2CrossDomainMessenger.selector);
// Call the `relayERC20` function with the non-messenger caller // Call the `relayERC20` function with the non-messenger caller
vm.prank(_caller); vm.prank(_caller);
...@@ -307,30 +304,13 @@ contract OptimismSuperchainERC20Test is Test { ...@@ -307,30 +304,13 @@ contract OptimismSuperchainERC20Test is Test {
); );
// Expect the revert with `InvalidCrossDomainSender` selector // Expect the revert with `InvalidCrossDomainSender` selector
vm.expectRevert(InvalidCrossDomainSender.selector); vm.expectRevert(ISuperchainERC20Errors.InvalidCrossDomainSender.selector);
// Call the `relayERC20` function with the sender caller // Call the `relayERC20` function with the sender caller
vm.prank(MESSENGER); vm.prank(MESSENGER);
superchainERC20.relayERC20(_crossDomainMessageSender, _to, _amount); superchainERC20.relayERC20(_crossDomainMessageSender, _to, _amount);
} }
/// @notice Tests the `relayERC20` function reverts when the `_to` address is the zero address.
function testFuzz_relayERC20_zeroAddressTo_reverts(uint256 _amount) public {
// Expect the revert with `ZeroAddress` selector
vm.expectRevert(ZeroAddress.selector);
// Mock the call over the `crossDomainMessageSender` function setting the same address as value
vm.mockCall(
MESSENGER,
abi.encodeWithSelector(IL2ToL2CrossDomainMessenger.crossDomainMessageSender.selector),
abi.encode(address(superchainERC20))
);
// Call the `relayERC20` function with the zero address
vm.prank(MESSENGER);
superchainERC20.relayERC20({ _from: ZERO_ADDRESS, _to: ZERO_ADDRESS, _amount: _amount });
}
/// @notice Tests the `relayERC20` mints the proper amount and emits the `RelayERC20` event. /// @notice Tests the `relayERC20` mints the proper amount and emits the `RelayERC20` event.
function testFuzz_relayERC20_succeeds(address _from, address _to, uint256 _amount, uint256 _source) public { function testFuzz_relayERC20_succeeds(address _from, address _to, uint256 _amount, uint256 _source) public {
vm.assume(_from != ZERO_ADDRESS); vm.assume(_from != ZERO_ADDRESS);
...@@ -355,11 +335,11 @@ contract OptimismSuperchainERC20Test is Test { ...@@ -355,11 +335,11 @@ contract OptimismSuperchainERC20Test is Test {
uint256 _toBalanceBefore = superchainERC20.balanceOf(_to); uint256 _toBalanceBefore = superchainERC20.balanceOf(_to);
// Look for the emit of the `Transfer` event // Look for the emit of the `Transfer` event
vm.expectEmit(true, true, true, true, address(superchainERC20)); vm.expectEmit(address(superchainERC20));
emit IERC20.Transfer(ZERO_ADDRESS, _to, _amount); emit IERC20.Transfer(ZERO_ADDRESS, _to, _amount);
// Look for the emit of the `RelayERC20` event // Look for the emit of the `RelayERC20` event
vm.expectEmit(true, true, true, true, address(superchainERC20)); vm.expectEmit(address(superchainERC20));
emit ISuperchainERC20Extensions.RelayERC20(_from, _to, _amount, _source); emit ISuperchainERC20Extensions.RelayERC20(_from, _to, _amount, _source);
// Call the `relayERC20` function with the messenger caller // Call the `relayERC20` function with the messenger caller
......
// SPDX-License-Identifier: MIT
pragma solidity 0.8.25;
// Testing utilities
import { Test } from "forge-std/Test.sol";
// Libraries
import { Predeploys } from "src/libraries/Predeploys.sol";
import { IERC20 } from "@openzeppelin/contracts-v5/token/ERC20/IERC20.sol";
import { IL2ToL2CrossDomainMessenger } from "src/L2/interfaces/IL2ToL2CrossDomainMessenger.sol";
// Target contract
import { SuperchainERC20 } from "src/L2/SuperchainERC20.sol";
import { ISuperchainERC20Extensions, ISuperchainERC20Errors } from "src/L2/interfaces/ISuperchainERC20.sol";
/// @notice Mock contract for the SuperchainERC20 contract so tests can mint tokens.
contract SuperchainERC20Mock is SuperchainERC20 {
string private _name;
string private _symbol;
uint8 private _decimals;
constructor(string memory __name, string memory __symbol, uint8 __decimals) {
_name = __name;
_symbol = __symbol;
_decimals = __decimals;
}
function mint(address _account, uint256 _amount) public {
_mint(_account, _amount);
}
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 virtual override returns (uint8) {
return _decimals;
}
}
/// @title SuperchainERC20Test
/// @notice Contract for testing the SuperchainERC20 contract.
contract SuperchainERC20Test is Test {
address internal constant ZERO_ADDRESS = address(0);
string internal constant NAME = "SuperchainERC20";
string internal constant SYMBOL = "SCE";
uint8 internal constant DECIMALS = 18;
address internal constant MESSENGER = Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER;
SuperchainERC20 public superchainERC20Impl;
SuperchainERC20Mock public superchainERC20;
/// @notice Sets up the test suite.
function setUp() public {
superchainERC20 = new SuperchainERC20Mock(NAME, SYMBOL, DECIMALS);
}
/// @notice Helper function to setup a mock and expect a call to it.
function _mockAndExpect(address _receiver, bytes memory _calldata, bytes memory _returned) internal {
vm.mockCall(_receiver, _calldata, _returned);
vm.expectCall(_receiver, _calldata);
}
/// @notice Test that the contract's `constructor` sets the correct values.
function test_constructor_succeeds() public view {
assertEq(superchainERC20.name(), NAME);
assertEq(superchainERC20.symbol(), SYMBOL);
assertEq(superchainERC20.decimals(), DECIMALS);
}
/// @notice Tests the `sendERC20` function reverts when the `_to` address is the zero address.
function testFuzz_sendERC20_zeroAddressTo_reverts(uint256 _amount, uint256 _chainId) public {
// Expect the revert with `ZeroAddress` selector
vm.expectRevert(ISuperchainERC20Errors.ZeroAddress.selector);
// Call the `sendERC20` function with the zero address
superchainERC20.sendERC20({ _to: ZERO_ADDRESS, _amount: _amount, _chainId: _chainId });
}
/// @notice Tests the `sendERC20` function burns the sender tokens, sends the message, and emits the `SendERC20`
/// event.
function testFuzz_sendERC20_succeeds(address _sender, address _to, uint256 _amount, uint256 _chainId) external {
// Ensure `_sender` is not the zero address
vm.assume(_sender != ZERO_ADDRESS);
vm.assume(_to != ZERO_ADDRESS);
// Mint some tokens to the sender so then they can be sent
superchainERC20.mint(_sender, _amount);
// Get the total supply and balance of `_sender` before the send to compare later on the assertions
uint256 _totalSupplyBefore = superchainERC20.totalSupply();
uint256 _senderBalanceBefore = superchainERC20.balanceOf(_sender);
// Look for the emit of the `Transfer` event
vm.expectEmit(address(superchainERC20));
emit IERC20.Transfer(_sender, ZERO_ADDRESS, _amount);
// Look for the emit of the `SendERC20` event
vm.expectEmit(address(superchainERC20));
emit ISuperchainERC20Extensions.SendERC20(_sender, _to, _amount, _chainId);
// Mock the call over the `sendMessage` function and expect it to be called properly
bytes memory _message = abi.encodeCall(superchainERC20.relayERC20, (_sender, _to, _amount));
_mockAndExpect(
MESSENGER,
abi.encodeWithSelector(
IL2ToL2CrossDomainMessenger.sendMessage.selector, _chainId, address(superchainERC20), _message
),
abi.encode("")
);
// Call the `sendERC20` function
vm.prank(_sender);
superchainERC20.sendERC20(_to, _amount, _chainId);
// Check the total supply and balance of `_sender` after the send were updated correctly
assertEq(superchainERC20.totalSupply(), _totalSupplyBefore - _amount);
assertEq(superchainERC20.balanceOf(_sender), _senderBalanceBefore - _amount);
}
/// @notice Tests the `relayERC20` function reverts when the caller is not the L2ToL2CrossDomainMessenger.
function testFuzz_relayERC20_notMessenger_reverts(address _caller, address _to, uint256 _amount) public {
// Ensure the caller is not the messenger
vm.assume(_caller != MESSENGER);
vm.assume(_to != ZERO_ADDRESS);
// Expect the revert with `CallerNotL2ToL2CrossDomainMessenger` selector
vm.expectRevert(ISuperchainERC20Errors.CallerNotL2ToL2CrossDomainMessenger.selector);
// Call the `relayERC20` function with the non-messenger caller
vm.prank(_caller);
superchainERC20.relayERC20(_caller, _to, _amount);
}
/// @notice Tests the `relayERC20` function reverts when the `crossDomainMessageSender` that sent the message is not
/// the same SuperchainERC20 address.
function testFuzz_relayERC20_notCrossDomainSender_reverts(
address _crossDomainMessageSender,
address _to,
uint256 _amount
)
public
{
vm.assume(_to != ZERO_ADDRESS);
vm.assume(_crossDomainMessageSender != address(superchainERC20));
// Mock the call over the `crossDomainMessageSender` function setting a wrong sender
vm.mockCall(
MESSENGER,
abi.encodeWithSelector(IL2ToL2CrossDomainMessenger.crossDomainMessageSender.selector),
abi.encode(_crossDomainMessageSender)
);
// Expect the revert with `InvalidCrossDomainSender` selector
vm.expectRevert(ISuperchainERC20Errors.InvalidCrossDomainSender.selector);
// Call the `relayERC20` function with the sender caller
vm.prank(MESSENGER);
superchainERC20.relayERC20(_crossDomainMessageSender, _to, _amount);
}
/// @notice Tests the `relayERC20` mints the proper amount and emits the `RelayERC20` event.
function testFuzz_relayERC20_succeeds(address _from, address _to, uint256 _amount, uint256 _source) public {
vm.assume(_from != ZERO_ADDRESS);
vm.assume(_to != ZERO_ADDRESS);
// Mock the call over the `crossDomainMessageSender` function setting the same address as value
_mockAndExpect(
MESSENGER,
abi.encodeWithSelector(IL2ToL2CrossDomainMessenger.crossDomainMessageSender.selector),
abi.encode(address(superchainERC20))
);
// Mock the call over the `crossDomainMessageSource` function setting the source chain ID as value
_mockAndExpect(
MESSENGER,
abi.encodeWithSelector(IL2ToL2CrossDomainMessenger.crossDomainMessageSource.selector),
abi.encode(_source)
);
// Get the total supply and balance of `_to` before the relay to compare later on the assertions
uint256 _totalSupplyBefore = superchainERC20.totalSupply();
uint256 _toBalanceBefore = superchainERC20.balanceOf(_to);
// Look for the emit of the `Transfer` event
vm.expectEmit(address(superchainERC20));
emit IERC20.Transfer(ZERO_ADDRESS, _to, _amount);
// Look for the emit of the `RelayERC20` event
vm.expectEmit(address(superchainERC20));
emit ISuperchainERC20Extensions.RelayERC20(_from, _to, _amount, _source);
// Call the `relayERC20` function with the messenger caller
vm.prank(MESSENGER);
superchainERC20.relayERC20(_from, _to, _amount);
// Check the total supply and balance of `_to` after the relay were updated correctly
assertEq(superchainERC20.totalSupply(), _totalSupplyBefore + _amount);
assertEq(superchainERC20.balanceOf(_to), _toBalanceBefore + _amount);
}
/// @notice Tests the `name` function always returns the correct value.
function testFuzz_name_succeeds(string memory _name) public {
SuperchainERC20 _newSuperchainERC20 = new SuperchainERC20Mock(_name, SYMBOL, DECIMALS);
assertEq(_newSuperchainERC20.name(), _name);
}
/// @notice Tests the `symbol` function always returns the correct value.
function testFuzz_symbol_succeeds(string memory _symbol) public {
SuperchainERC20 _newSuperchainERC20 = new SuperchainERC20Mock(NAME, _symbol, DECIMALS);
assertEq(_newSuperchainERC20.symbol(), _symbol);
}
/// @notice Tests the `decimals` function always returns the correct value.
function testFuzz_decimals_succeeds(uint8 _decimals) public {
SuperchainERC20 _newSuperchainERC20 = new SuperchainERC20Mock(NAME, SYMBOL, _decimals);
assertEq(_newSuperchainERC20.decimals(), _decimals);
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment