diff --git a/.gitmodules b/.gitmodules index f2b84783c409..f68c4748c7f4 100644 --- a/.gitmodules +++ b/.gitmodules @@ -26,3 +26,15 @@ [submodule "packages/contracts-bedrock/lib/automate"] path = packages/contracts-bedrock/lib/automate url = https://github.com/gelatodigital/automate +[submodule "packages/contracts-bedrock/lib/halmos-cheatcodes"] + path = packages/contracts-bedrock/lib/halmos-cheatcodes + url = https://github.com/a16z/halmos-cheatcodes +[submodule "lib/halmos-cheatcodes"] + path = lib/halmos-cheatcodes + url = https://github.com/a16z/halmos-cheatcodes +[submodule "halmos-cheatcodes"] + path = halmos-cheatcodes + url = https://github.com/a16z/halmos-cheatcodes +[submodule "packages/contracts-bedrock/halmos-cheatcodes"] + path = packages/contracts-bedrock/halmos-cheatcodes + url = https://github.com/a16z/halmos-cheatcodes diff --git a/lib/halmos-cheatcodes b/lib/halmos-cheatcodes new file mode 160000 index 000000000000..c0d865508c0f --- /dev/null +++ b/lib/halmos-cheatcodes @@ -0,0 +1 @@ +Subproject commit c0d865508c0fee0a11b97732c5e90f9cad6b65a5 diff --git a/packages/contracts-bedrock/foundry.toml b/packages/contracts-bedrock/foundry.toml index 4b1dbdeba780..92c14c2de264 100644 --- a/packages/contracts-bedrock/foundry.toml +++ b/packages/contracts-bedrock/foundry.toml @@ -88,3 +88,8 @@ src = 'test/kontrol/proofs' out = 'kout-proofs' test = 'test/kontrol/proofs' script = 'test/kontrol/proofs' + +[profile.medusa] +src = 'test/properties/medusa/' +test = 'test/properties/medusa/' +script = 'test/properties/medusa/' diff --git a/packages/contracts-bedrock/medusa.json b/packages/contracts-bedrock/medusa.json new file mode 100644 index 000000000000..a0425784ebdb --- /dev/null +++ b/packages/contracts-bedrock/medusa.json @@ -0,0 +1,82 @@ +{ + "fuzzing": { + "workers": 10, + "workerResetLimit": 50, + "timeout": 0, + "testLimit": 500, + "callSequenceLength": 100, + "corpusDirectory": "", + "coverageEnabled": true, + "targetContracts": ["SuperchainERC20FactoryFuzz"], + "targetContractsBalances": [], + "constructorArgs": {}, + "deployerAddress": "0x30000", + "senderAddresses": [ + "0x10000", + "0x20000", + "0x30000" + ], + "blockNumberDelayMax": 60480, + "blockTimestampDelayMax": 604800, + "blockGasLimit": 125000000, + "transactionGasLimit": 12500000, + "testing": { + "stopOnFailedTest": true, + "stopOnFailedContractMatching": false, + "stopOnNoTests": true, + "testAllContracts": false, + "traceAll": false, + "assertionTesting": { + "enabled": true, + "testViewMethods": false, + "panicCodeConfig": { + "failOnCompilerInsertedPanic": false, + "failOnAssertion": true, + "failOnArithmeticUnderflow": false, + "failOnDivideByZero": false, + "failOnEnumTypeConversionOutOfBounds": false, + "failOnIncorrectStorageAccess": false, + "failOnPopEmptyArray": false, + "failOnOutOfBoundsArrayAccess": false, + "failOnAllocateTooMuchMemory": false, + "failOnCallUninitializedVariable": false + } + }, + "propertyTesting": { + "enabled": false, + "testPrefixes": [ + "property_" + ] + }, + "optimizationTesting": { + "enabled": false, + "testPrefixes": [ + "optimize_" + ] + }, + "targetFunctionSignatures": [], + "excludeFunctionSignatures": [] + }, + "chainConfig": { + "codeSizeCheckDisabled": true, + "cheatCodes": { + "cheatCodesEnabled": true, + "enableFFI": false + } + } + }, + "compilation": { + "platform": "crytic-compile", + "platformConfig": { + "target": ".", + "solcVersion": "", + "exportDirectory": "", + "args": ["--foundry-out-directory", "artifacts","--foundry-compile-all"] + } + }, + "logging": { + "level": "info", + "logDirectory": "", + "noColor": false + } +} diff --git a/packages/contracts-bedrock/package.json b/packages/contracts-bedrock/package.json index 19ca43cde30a..a4c6bbbcf833 100644 --- a/packages/contracts-bedrock/package.json +++ b/packages/contracts-bedrock/package.json @@ -29,6 +29,7 @@ "gas-snapshot": "pnpm build:go-ffi && pnpm gas-snapshot:no-build", "kontrol-summary": "./test/kontrol/scripts/make-summary-deployment.sh", "kontrol-summary-fp": "KONTROL_FP_DEPLOYMENT=true pnpm kontrol-summary", + "medusa": "FOUNDRY_PROFILE=medusa medusa fuzz", "snapshots": "forge build && npx tsx scripts/autogen/generate-snapshots.ts && pnpm kontrol-summary-fp && pnpm kontrol-summary", "snapshots:check": "./scripts/checks/check-snapshots.sh", "semver-lock": "forge script scripts/SemverLock.s.sol", @@ -61,6 +62,7 @@ "eslint-plugin-jsdoc": "^48.8.3", "eslint-plugin-prefer-arrow": "^1.2.3", "eslint-plugin-prettier": "^4.0.0", + "halmos-cheatcodes": "github:a16z/halmos-cheatcodes#c0d8655", "prettier": "^2.8.0", "tsx": "^4.16.2", "typescript": "^5.5.4" diff --git a/packages/contracts-bedrock/src/L2/L2StandardBridgeInterop.sol b/packages/contracts-bedrock/src/L2/L2StandardBridgeInterop.sol new file mode 100644 index 000000000000..ad4a378a5fda --- /dev/null +++ b/packages/contracts-bedrock/src/L2/L2StandardBridgeInterop.sol @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +import { Predeploys } from "src/libraries/Predeploys.sol"; +import { L2StandardBridge } from "./L2StandardBridge.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; + +/// @notice Thrown when the decimals of the tokens are not the same. +error InvalidDecimals(); + +/// @notice Thrown when the legacy address is not found in the OptimismMintableERC20Factory. +error InvalidLegacyAddress(); + +/// @notice Thrown when the SuperchainERC20 address is not found in the SuperchainERC20Factory. +error InvalidSuperchainAddress(); + +/// @notice Thrown when the remote addresses of the tokens are not the same. +error InvalidTokenPair(); + +// TODO: Use OptimismMintableERC20Factory contract instead of interface +interface IOptimismMintableERC20Factory { + function deployments(address) external view returns (address); +} + +// TODO: Move to a separate file +interface ISuperchainERC20Factory { + function deployments(address) external view returns (address); +} + +// TODO: Use an existing interface with `mint` and `burn`? +interface MintableAndBurnable is IERC20 { + function mint(address, uint256) external; + function burn(address, uint256) external; +} + +/// @custom:proxied +/// @custom:predeploy 0x4200000000000000000000000000000000000010 +/// @title L2StandardBridgeInterop +/// @notice The L2StandardBridgeInterop is an extension of the L2StandardBridge that allows for +/// the conversion of tokens between legacy tokens (OptimismMintableERC20 or StandardL2ERC20) +/// and SuperchainERC20 tokens. +contract L2StandardBridgeInterop is L2StandardBridge { + /// @notice Emitted when a conversion is made. + /// @param from The token being converted from. + /// @param to The token being converted to. + /// @param caller The caller of the conversion. + /// @param amount The amount of tokens being converted. + event Converted(address indexed from, address indexed to, address indexed caller, uint256 amount); + + /// @notice Converts `amount` of `from` token to `to` token. + /// @param _from The token being converted from. + /// @param _to The token being converted to. + /// @param _amount The amount of tokens being converted. + function convert(address _from, address _to, uint256 _amount) external { + _validatePair(_from, _to); + + MintableAndBurnable(_from).burn(msg.sender, _amount); + MintableAndBurnable(_to).mint(msg.sender, _amount); + + emit Converted(_from, _to, msg.sender, _amount); + } + + /// @notice Validates the pair of tokens. + /// @param _from The token being converted from. + /// @param _to The token being converted to. + function _validatePair(address _from, address _to) internal view { + // 1. Decimals check + if (IERC20Metadata(_from).decimals() != IERC20Metadata(_to).decimals()) revert InvalidDecimals(); + + // Order tokens for factory validation + if (_isOptimismMintableERC20(_from)) { + _validateFactories(_from, _to); + } else { + _validateFactories(_to, _from); + } + } + + /// @notice Validates that the tokens are deployed by the correct factory. + /// @param _legacyAddr The legacy token address (OptimismMintableERC20 or StandardL2ERC20). + /// @param _superAddr The SuperchainERC20 address. + function _validateFactories(address _legacyAddr, address _superAddr) internal view { + // 2. Valid legacy check + address _legacyRemoteToken = + IOptimismMintableERC20Factory(Predeploys.OPTIMISM_MINTABLE_ERC20_FACTORY).deployments(_legacyAddr); + if (_legacyRemoteToken == address(0)) revert InvalidLegacyAddress(); + + // 3. Valid SuperchainERC20 check + address _superRemoteToken = + ISuperchainERC20Factory(Predeploys.OPTIMISM_SUPERCHAIN_ERC20_FACTORY).deployments(_superAddr); + if (_superRemoteToken == address(0)) revert InvalidSuperchainAddress(); + + // 4. Same remote address check + if (_legacyRemoteToken != _superRemoteToken) revert InvalidTokenPair(); + } +} diff --git a/packages/contracts-bedrock/src/L2/L2ToL2CrossDomainMessenger.sol b/packages/contracts-bedrock/src/L2/L2ToL2CrossDomainMessenger.sol index cafcf0d0ccd7..46a11b93c7e8 100644 --- a/packages/contracts-bedrock/src/L2/L2ToL2CrossDomainMessenger.sol +++ b/packages/contracts-bedrock/src/L2/L2ToL2CrossDomainMessenger.sol @@ -7,6 +7,7 @@ import { CrossL2Inbox } from "src/L2/CrossL2Inbox.sol"; import { IL2ToL2CrossDomainMessenger } from "src/L2/IL2ToL2CrossDomainMessenger.sol"; import { ISemver } from "src/universal/ISemver.sol"; import { SafeCall } from "src/libraries/SafeCall.sol"; +import "forge-std/Test.sol"; /// @notice Thrown when a non-written slot in transient storage is attempted to be read from. error NotEntered(); @@ -123,6 +124,7 @@ contract L2ToL2CrossDomainMessenger is IL2ToL2CrossDomainMessenger, ISemver { /// @param _message Message payload to call target with. function sendMessage(uint256 _destination, address _target, bytes calldata _message) external payable { if (_destination == block.chainid) revert MessageDestinationSameChain(); + console.log("block.chainid", block.chainid); if (_target == Predeploys.CROSS_L2_INBOX) revert MessageTargetCrossL2Inbox(); if (_target == Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER) revert MessageTargetL2ToL2CrossDomainMessenger(); diff --git a/packages/contracts-bedrock/src/L2/SuperchainERC20.sol b/packages/contracts-bedrock/src/L2/SuperchainERC20.sol new file mode 100644 index 000000000000..cacccde97ec8 --- /dev/null +++ b/packages/contracts-bedrock/src/L2/SuperchainERC20.sol @@ -0,0 +1,140 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.25; + +import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import { ISuperchainERC20 } from "src/L2/ISuperchainERC20.sol"; +import { IL2ToL2CrossDomainMessenger } from "src/L2/IL2ToL2CrossDomainMessenger.sol"; +import { ISemver } from "src/universal/ISemver.sol"; +import { SafeCall } from "src/libraries/SafeCall.sol"; +import { Predeploys } from "src/libraries/Predeploys.sol"; + +/// @notice Thrown when attempting to relay a message and the function caller (msg.sender) is not +/// L2ToL2CrossDomainMessenger. +error RelayMessageCallerNotL2ToL2CrossDomainMessenger(); + +/// @notice Thrown when attempting to relay a message and the cross domain message sender is not this SuperchainERC20. +error MessageSenderNotThisSuperchainERC20(); + +/// @notice Thrown when attempting to mint or burn tokens and the function caller is not the StandardBridge. +error CallerNotBridge(); + +/// @custom:proxied +/// @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. This construction builds on top of the L2ToL2CrossDomainMessenger +/// for both replay protection and domain binding. +contract SuperchainERC20 is ISuperchainERC20, ERC20, ISemver { + /// @notice Address of the L2ToL2CrossDomainMessenger Predeploy. + address internal constant MESSENGER = Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER; + + /// @notice Address of the StandardBridge Predeploy. + address internal constant BRIDGE = Predeploys.L2_STANDARD_BRIDGE; + + /// @notice Decimals of the token + uint8 private immutable DECIMALS; + + /// @notice Address of the corresponding version of this token on the remote chain. + address public immutable REMOTE_TOKEN; + + /// @notice Emitted whenever tokens are minted for an account. + /// @param account Address of the account tokens are being minted for. + /// @param amount Amount of tokens minted. + event Mint(address indexed account, uint256 amount); + + /// @notice Emitted whenever tokens are burned from an account. + /// @param account Address of the account tokens are being burned from. + /// @param amount Amount of tokens burned. + event Burn(address indexed account, uint256 amount); + + /// @notice Emitted whenever tokens are sent to another chain. + /// @param from Address of the sender. + /// @param to Address of the recipient. + /// @param amount Amount of tokens sent. + /// @param chainId Chain ID of the destination chain. + event SentERC20(address indexed from, address indexed to, uint256 amount, uint256 chainId); + + /// @notice Emitted whenever tokens are successfully relayed on this chain. + /// @param to Address of the recipient. + /// @param amount Amount of tokens relayed. + event RelayedERC20(address indexed to, uint256 amount); + + /// @notice A modifier that only allows the bridge to call + modifier onlyBridge() { + if (msg.sender != BRIDGE) revert CallerNotBridge(); + _; + } + + /// @notice Semantic version. + /// @custom:semver 1.0.0 + string public constant version = "1.0.0"; + + /// @param _remoteToken Address of the corresponding remote token. + /// @param _name ERC20 name. + /// @param _symbol ERC20 symbol. + /// @param _decimals ERC20 decimals. + constructor( + address _remoteToken, + string memory _name, + string memory _symbol, + uint8 _decimals + ) + ERC20(_name, _symbol) + { + REMOTE_TOKEN = _remoteToken; + DECIMALS = _decimals; + } + + /// @notice Allows the StandardBridge to mint tokens. + /// @param _to Address to mint tokens to. + /// @param _amount Amount of tokens to mint. + function mint(address _to, uint256 _amount) external virtual onlyBridge { + _mint(_to, _amount); + emit Mint(_to, _amount); + } + + /// @notice Allows the StandardBridge to burn tokens. + /// @param _from Address to burn tokens from. + /// @param _amount Amount of tokens to burn. + function burn(address _from, uint256 _amount) external virtual onlyBridge { + _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 { + _burn(msg.sender, _amount); + + bytes memory _message = abi.encodeCall(this.relayERC20, (_to, _amount)); + IL2ToL2CrossDomainMessenger(MESSENGER).sendMessage(_chainId, address(this), _message); + + emit SentERC20(msg.sender, _to, _amount, _chainId); + } + + /// @notice Relays tokens received from another chain. + /// @param _to Address to relay tokens to. + /// @param _amount Amount of tokens to relay. + function relayERC20(address _to, uint256 _amount) external { + if (msg.sender != MESSENGER) revert RelayMessageCallerNotL2ToL2CrossDomainMessenger(); + + if (IL2ToL2CrossDomainMessenger(MESSENGER).crossDomainMessageSender() != address(this)) { + revert MessageSenderNotThisSuperchainERC20(); + } + + _mint(_to, _amount); + + emit RelayedERC20(_to, _amount); + } + + /// @notice Returns the number of decimals used to get its user representation. + /// For example, if `decimals` equals `2`, a balance of `505` tokens should + /// be displayed to a user as `5.05` (`505 / 10 ** 2`). + /// NOTE: This information is only used for _display_ purposes: it in + /// no way affects any of the arithmetic of the contract, including + /// {IERC20-balanceOf} and {IERC20-transfer}. + function decimals() public view override returns (uint8) { + return DECIMALS; + } +} diff --git a/packages/contracts-bedrock/src/L2/SuperchainERC20Beacon.sol b/packages/contracts-bedrock/src/L2/SuperchainERC20Beacon.sol new file mode 100644 index 000000000000..850d98c70dfd --- /dev/null +++ b/packages/contracts-bedrock/src/L2/SuperchainERC20Beacon.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.25; + +import { IBeacon } from "@openzeppelin/contracts/proxy/beacon/IBeacon.sol"; + +/// @title SuperchainERC20Beacon +/// @notice SuperchainERC20Beacon is the beacon proxy for the SuperchainERC20 implementation. +contract SuperchainERC20Beacon is IBeacon { + /// TODO: Replace with real implementation address + /// @notice Address of the SuperchainERC20 implementation. + address internal constant IMPLEMENTATION_ADDRESS = 0x4200000000000000000000000000000000000042; + + /// @inheritdoc IBeacon + function implementation() external pure override returns (address) { + return IMPLEMENTATION_ADDRESS; + } +} diff --git a/packages/contracts-bedrock/src/L2/SuperchainERC20Factory.sol b/packages/contracts-bedrock/src/L2/SuperchainERC20Factory.sol new file mode 100644 index 000000000000..4e6e2785376e --- /dev/null +++ b/packages/contracts-bedrock/src/L2/SuperchainERC20Factory.sol @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.25; + +import { ISemver } from "src/universal/ISemver.sol"; +import { Predeploys } from "src/libraries/Predeploys.sol"; +import { BeaconProxy } from "@openzeppelin/contracts/proxy/beacon/BeaconProxy.sol"; +import { CREATE3 } from "@rari-capital/solmate/src/utils/CREATE3.sol"; + +/// @custom:proxied +/// @title SuperchainERC20Factory +/// @notice SuperchainERC20Factory is a factory contract that deploys SuperchainERC20 Beacon Proxies using CREATE3. +contract SuperchainERC20Factory is ISemver { + /// @notice Mapping of the deployed SuperchainERC20 to the remote token address. + mapping(address superchainToken => address remoteToken) public deployments; + + /// @notice Emitted when a SuperchainERC20 is deployed. + /// @param superchainERC20 Address of the SuperchainERC20 deployment. + /// @param remoteToken Address of the remote token. + /// @param name Name of the SuperchainERC20. + /// @param symbol Symbol of the SuperchainERC20. + /// @param decimals Decimals of the SuperchainERC20. + event SuperchainERC20Deployed( + address indexed superchainERC20, address indexed remoteToken, string name, string symbol, uint8 decimals + ); + + /// @notice Semantic version. + /// @custom:semver 1.0.0 + string public constant version = "1.0.0"; + + /// @notice Deploys a SuperchainERC20 Beacon Proxy using CREATE3. + /// @param _remoteToken Address of the remote token. + /// @param _name Name of the SuperchainERC20. + /// @param _symbol Symbol of the SuperchainERC20. + /// @param _decimals Decimals of the SuperchainERC20. + /// @return _superchainERC20 Address of the SuperchainERC20 deployment. + function deploy( + address _remoteToken, + string memory _name, + string memory _symbol, + uint8 _decimals + ) + external + returns (address _superchainERC20) + { + // Encode the BeaconProxy creation code with the beacon contract address and metadata + bytes memory _creationCode = bytes.concat( + type(BeaconProxy).creationCode, + abi.encode(Predeploys.SUPERCHAIN_ERC20_BEACON, abi.encode(_remoteToken, _name, _symbol, _decimals)) + ); + + // Use CREATE3 for deterministic deployment + // bytes32 _salt = keccak256(abi.encode(_remoteToken, _name, _symbol, _decimals)); + bytes32 _salt = bytes32(abi.encode(_remoteToken, _name, _symbol, _decimals)); + _superchainERC20 = CREATE3.deploy({ salt: _salt, creationCode: _creationCode, value: 0 }); + + // Store SuperchainERC20 and remote token addresses + deployments[_superchainERC20] = _remoteToken; + + emit SuperchainERC20Deployed(_superchainERC20, _remoteToken, _name, _symbol, _decimals); + } +} diff --git a/packages/contracts-bedrock/src/libraries/Predeploys.sol b/packages/contracts-bedrock/src/libraries/Predeploys.sol index 0aece54898d3..4f27be8a80a9 100644 --- a/packages/contracts-bedrock/src/libraries/Predeploys.sol +++ b/packages/contracts-bedrock/src/libraries/Predeploys.sol @@ -95,6 +95,13 @@ library Predeploys { /// @notice Address of the ETHLiquidity predeploy. address internal constant ETH_LIQUIDITY = 0x4200000000000000000000000000000000000025; + /// @notice Address of the OptimismSuperchainERC20Factory predeploy. + address internal constant OPTIMISM_SUPERCHAIN_ERC20_FACTORY = 0x4200000000000000000000000000000000000026; + + /// TODO: Replace with real predeploy address + /// @notice Address of the SuperchainERC20Beacon predeploy. + address internal constant SUPERCHAIN_ERC20_BEACON = 0x4200000000000000000000000000000000000027; + /// @notice Returns the name of the predeploy at the given address. function getName(address _addr) internal pure returns (string memory out_) { require(isPredeployNamespace(_addr), "Predeploys: address must be a predeploy"); @@ -121,8 +128,10 @@ library Predeploys { if (_addr == LEGACY_ERC20_ETH) return "LegacyERC20ETH"; if (_addr == CROSS_L2_INBOX) return "CrossL2Inbox"; if (_addr == L2_TO_L2_CROSS_DOMAIN_MESSENGER) return "L2ToL2CrossDomainMessenger"; + if (_addr == SUPERCHAIN_ERC20_BEACON) return "SuperchainERC20Beacon"; if (_addr == SUPERCHAIN_WETH) return "SuperchainWETH"; if (_addr == ETH_LIQUIDITY) return "ETHLiquidity"; + if (_addr == OPTIMISM_SUPERCHAIN_ERC20_FACTORY) return "OptimismSuperchainERC20Factory"; revert("Predeploys: unnamed predeploy"); } @@ -140,7 +149,9 @@ library Predeploys { || _addr == OPTIMISM_MINTABLE_ERC721_FACTORY || _addr == PROXY_ADMIN || _addr == BASE_FEE_VAULT || _addr == L1_FEE_VAULT || _addr == SCHEMA_REGISTRY || _addr == EAS || _addr == GOVERNANCE_TOKEN || (_useInterop && _addr == CROSS_L2_INBOX) || (_useInterop && _addr == L2_TO_L2_CROSS_DOMAIN_MESSENGER) - || (_useInterop && _addr == SUPERCHAIN_WETH) || (_useInterop && _addr == ETH_LIQUIDITY); + || (_useInterop && _addr == SUPERCHAIN_ERC20_BEACON) + || (_useInterop && _addr == SUPERCHAIN_WETH) || (_useInterop && _addr == ETH_LIQUIDITY) + || (_useInterop && _addr == OPTIMISM_SUPERCHAIN_ERC20_FACTORY); } function isPredeployNamespace(address _addr) internal pure returns (bool) { diff --git a/packages/contracts-bedrock/test/L2/SuperchainERC20.t.sol b/packages/contracts-bedrock/test/L2/SuperchainERC20.t.sol new file mode 100644 index 000000000000..e5e4f8828ae1 --- /dev/null +++ b/packages/contracts-bedrock/test/L2/SuperchainERC20.t.sol @@ -0,0 +1,298 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.25; + +// Testing utilities +import { Test } from "forge-std/Test.sol"; +import { Vm } from "forge-std/Vm.sol"; + +// Libraries +import { Predeploys } from "src/libraries/Predeploys.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { stdStorage, StdStorage } from "forge-std/Test.sol"; +import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import { IL2ToL2CrossDomainMessenger } from "src/L2/IL2ToL2CrossDomainMessenger.sol"; + +// Target contract +import { + SuperchainERC20, + CallerNotBridge, + RelayMessageCallerNotL2ToL2CrossDomainMessenger, + MessageSenderNotThisSuperchainERC20, + CallerNotBridge +} from "src/L2/SuperchainERC20.sol"; +import { ISuperchainERC20 } from "src/L2/ISuperchainERC20.sol"; + +/// @title SuperchainERC20Test +/// @dev Contract for testing the SuperchainERC20 contract. +contract SuperchainERC20Test is Test { + address internal constant ZERO_ADDRESS = address(0); + address internal constant REMOTE_TOKEN = address(0x123); + string internal constant NAME = "SuperchainERC20"; + string internal constant SYMBOL = "SCE"; + uint8 internal constant DECIMALS = 18; + address internal constant BRIDGE = Predeploys.L2_STANDARD_BRIDGE; + address internal constant MESSENGER = Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER; + + SuperchainERC20 public superchainERC20; + + /// @dev Sets up the test suite. + function setUp() public { + superchainERC20 = new SuperchainERC20(REMOTE_TOKEN, NAME, SYMBOL, DECIMALS); + } + + /// @dev 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); + } + + /// @dev Test that the bridge's constructor sets the correct values. + function test_constructor_succeeds() public view { + assertEq(superchainERC20.name(), NAME); + assertEq(superchainERC20.symbol(), SYMBOL); + assertEq(superchainERC20.decimals(), DECIMALS); + assertEq(superchainERC20.REMOTE_TOKEN(), REMOTE_TOKEN); + } + + /// @dev Tests the `mint` function reverts when the caller is not the bridge. + function testFuzz_mint_callerNotBridge_reverts(address _caller, address _to, uint256 _amount) public { + // Ensure the caller is not the bridge + vm.assume(_caller != BRIDGE); + + // Expect the revert with `CallerNotBridge` selector + vm.expectRevert(CallerNotBridge.selector); + + // Call the `mint` function with the non-bridge caller + vm.prank(_caller); + superchainERC20.mint(_to, _amount); + } + + /// @dev Tests the `mint` function reverts when the amount is zero. + function testFuzz_mint_zeroAddressTo_reverts(uint256 _amount) public { + // Expect the revert reason "ERC20: mint to the zero address" + vm.expectRevert("ERC20: mint to the zero address"); + + // Call the `mint` function with the zero address + vm.prank(BRIDGE); + superchainERC20.mint({ _to: ZERO_ADDRESS, _amount: _amount }); + } + + /// @dev Tests the `mint` succeeds and emits the `Mint` event. + function testFuzz_mint_succeeds(address _to, uint256 _amount) public { + // Ensure `_to` is not the zero address + vm.assume(_to != ZERO_ADDRESS); + + // Get the total supply and balance of `_to` before the mint 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(true, true, true, true, address(superchainERC20)); + emit IERC20.Transfer(ZERO_ADDRESS, _to, _amount); + + // Look for the emit of the `Mint` event + vm.expectEmit(true, true, true, true, address(superchainERC20)); + emit SuperchainERC20.Mint(_to, _amount); + + // Call the `mint` function with the bridge caller + vm.prank(BRIDGE); + superchainERC20.mint(_to, _amount); + + // Check the total supply and balance of `_to` after the mint were updated correctly + assertEq(superchainERC20.totalSupply(), _totalSupplyBefore + _amount); + assertEq(superchainERC20.balanceOf(_to), _toBalanceBefore + _amount); + } + + /// @dev Tests the `burn` function reverts when the caller is not the bridge. + function testFuzz_burn_callerNotBridge_reverts(address _caller, address _from, uint256 _amount) public { + // Ensure the caller is not the bridge + vm.assume(_caller != BRIDGE); + + // Expect the revert with `CallerNotBridge` selector + vm.expectRevert(CallerNotBridge.selector); + + // Call the `burn` function with the non-bridge caller + vm.prank(_caller); + superchainERC20.burn(_from, _amount); + } + + /// @dev Tests the `burn` function reverts when the amount is zero. + function testFuzz_burn_zeroAddressFrom_reverts(uint256 _amount) public { + // Expect the revert reason "ERC20: burn from the zero address" + vm.expectRevert("ERC20: burn from the zero address"); + + // Call the `burn` function with the zero address + vm.prank(BRIDGE); + superchainERC20.burn({ _from: ZERO_ADDRESS, _amount: _amount }); + } + + /// @dev Tests the `burn` burns the amount and emits the `Burn` event. + function testFuzz_burn_succeeds(address _from, uint256 _amount) public { + // Ensure `_from` is not the zero address + vm.assume(_from != ZERO_ADDRESS); + + // Mint some tokens to `_from` so then they can be burned + vm.prank(BRIDGE); + superchainERC20.mint(_from, _amount); + + // Get the total supply and balance of `_from` before the burn to compare later on the assertions + uint256 _totalSupplyBefore = superchainERC20.totalSupply(); + uint256 _fromBalanceBefore = superchainERC20.balanceOf(_from); + + // Look for the emit of the `Transfer` event + vm.expectEmit(true, true, true, true, address(superchainERC20)); + emit IERC20.Transfer(_from, ZERO_ADDRESS, _amount); + + // Look for the emit of the `Burn` event + vm.expectEmit(true, true, true, true, address(superchainERC20)); + emit SuperchainERC20.Burn(_from, _amount); + + // Call the `burn` function with the bridge caller + vm.prank(BRIDGE); + superchainERC20.burn(_from, _amount); + + // Check the total supply and balance of `_from` after the burn were updated correctly + assertEq(superchainERC20.totalSupply(), _totalSupplyBefore - _amount); + assertEq(superchainERC20.balanceOf(_from), _fromBalanceBefore - _amount); + } + + /// @dev Tests the `sendERC20` function burns the sender tokens, sends the message, and emits the `SentERC20` 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); + + // Mint some tokens to the sender so then they can be sent + vm.prank(BRIDGE); + 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(true, true, true, true, address(superchainERC20)); + emit IERC20.Transfer(_sender, ZERO_ADDRESS, _amount); + + // Look for the emit of the `SentERC20` event + vm.expectEmit(true, true, true, true, address(superchainERC20)); + emit SuperchainERC20.SentERC20(_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, (_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); + } + + /// @dev 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); + + // Expect the revert with `RelayMessageCallerNotL2ToL2CrossDomainMessenger` selector + vm.expectRevert(RelayMessageCallerNotL2ToL2CrossDomainMessenger.selector); + + // Call the `relayERC20` function with the non-messenger caller + vm.prank(_caller); + superchainERC20.relayERC20(_to, _amount); + } + + /// @dev 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(_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 `MessageSenderNotThisSuperchainERC20` selector + vm.expectRevert(MessageSenderNotThisSuperchainERC20.selector); + + // Call the `relayERC20` function with the sender caller + vm.prank(MESSENGER); + superchainERC20.relayERC20(_to, _amount); + } + + /// @dev Tests the `relayERC20` function reverts when the `_to` address is the zero address. + function testFuzz_relayERC20_zeroAddressTo_reverts(uint256 _amount) public { + // Expect the revert reason "ERC20: mint to the zero address" + vm.expectRevert("ERC20: mint to the zero address"); + + // 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({ _to: ZERO_ADDRESS, _amount: _amount }); + } + + /// @dev Tests the `relayERC20` mints the proper amount and emits the `RelayedERC20` event. + function testFuzz_relayERC20_succeeds(address _to, uint256 _amount) public { + 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)) + ); + + // 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(true, true, true, true, address(superchainERC20)); + emit IERC20.Transfer(ZERO_ADDRESS, _to, _amount); + + // Look for the emit of the `RelayedERC20` event + vm.expectEmit(true, true, true, true, address(superchainERC20)); + emit SuperchainERC20.RelayedERC20(_to, _amount); + + // Call the `relayERC20` function with the messenger caller + vm.prank(MESSENGER); + superchainERC20.relayERC20(_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); + } + + /// @dev Tests the `decimals` function always returns the correct value. + function testFuzz_decimals_succeeds(uint8 _decimals) public { + SuperchainERC20 _newSuperchainERC20 = new SuperchainERC20(REMOTE_TOKEN, NAME, SYMBOL, _decimals); + assertEq(_newSuperchainERC20.decimals(), _decimals); + } + + /// @dev Tests the `REMOTE_TOKEN` function always returns the correct value. + function testFuzz_remoteToken_succeeds(address _remoteToken) public { + SuperchainERC20 _newSuperchainERC20 = new SuperchainERC20(_remoteToken, NAME, SYMBOL, DECIMALS); + assertEq(_newSuperchainERC20.REMOTE_TOKEN(), _remoteToken); + } +} diff --git a/packages/contracts-bedrock/test/L2/SuperchainERC20Factory.t.sol b/packages/contracts-bedrock/test/L2/SuperchainERC20Factory.t.sol new file mode 100644 index 000000000000..65709c10774c --- /dev/null +++ b/packages/contracts-bedrock/test/L2/SuperchainERC20Factory.t.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.25; + +import { Test } from "forge-std/Test.sol"; +import { SuperchainERC20Factory } from "src/L2/SuperchainERC20Factory.sol"; + +contract SuperchainERC20FactoryTest is Test { + struct DeployParams { + address remoteToken; + string name; + string symbol; + uint8 decimals; + } + + SuperchainERC20Factory internal factory; + + constructor() { + factory = new SuperchainERC20Factory(); + } + + // this is a stateless check, so halmos will probably supersede it + function test_ContractAddressDependsOnParams(DeployParams memory left, DeployParams memory right) external { + vm.assume( + left.remoteToken != right.remoteToken || keccak256(bytes(left.name)) != keccak256(bytes(right.name)) + || keccak256(bytes(left.symbol)) != keccak256(bytes(right.symbol)) || left.decimals != right.decimals + ); + address superc20Left = factory.deploy(left.remoteToken, left.name, left.symbol, left.decimals); + address superc20Right = factory.deploy(right.remoteToken, right.name, right.symbol, right.decimals); + assert(superc20Left != superc20Right); + } + + function test_ContractAddressDoesNotDependOnChainId( + DeployParams memory params, + uint256 chainIdLeft, + uint256 chainIdRight + ) + external + { + vm.chainId(chainIdLeft); + address superc20Left = factory.deploy(params.remoteToken, params.name, params.symbol, params.decimals); + vm.chainId(chainIdRight); + address superc20Right = factory.deploy(params.remoteToken, params.name, params.symbol, params.decimals); + assert(superc20Left == superc20Right); + } +} diff --git a/packages/contracts-bedrock/test/properties/halmos/SuperchainERC20.t.sol b/packages/contracts-bedrock/test/properties/halmos/SuperchainERC20.t.sol new file mode 100644 index 000000000000..1aa2f7312454 --- /dev/null +++ b/packages/contracts-bedrock/test/properties/halmos/SuperchainERC20.t.sol @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.25; + +import { Test } from "forge-std/Test.sol"; +import "forge-std/Test.sol"; + +import { SuperchainERC20 } from "src/L2/SuperchainERC20.sol"; +import { SymTest } from "halmos-cheatcodes/src/SymTest.sol"; +import { L2ToL2CrossDomainMessenger } from "src/L2/L2ToL2CrossDomainMessenger.sol"; +import { Predeploys } from "src/libraries/Predeploys.sol"; + +interface IHevm { + function chaind(uint256) external; + + function etch(address addr, bytes calldata code) external; + + function prank(address addr) external; +} + +contract HalmosTest is SymTest, Test { } + +contract SuperchainERC20_SymTest is HalmosTest { + IHevm hevm = IHevm(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); + + SuperchainERC20 internal superchainERC20; + address internal remoteToken = address(bytes20(keccak256("remoteToken"))); + uint8 internal decimals = 18; + address internal user = address(bytes20(keccak256("user"))); + + constructor() { + address _l2ToL2CrossDomainMessenger = address(new L2ToL2CrossDomainMessenger()); + hevm.etch(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER, _l2ToL2CrossDomainMessenger.code); + + superchainERC20 = new SuperchainERC20(remoteToken, "SuperchainERC20", "SUPER", decimals); + } + + function check_setup() public view { + assert(superchainERC20.REMOTE_TOKEN() == remoteToken); + assert(superchainERC20.decimals() == decimals); + } + + // Works + function check_mint(address _to, uint256 _amount) public { + vm.assume(_to != address(0)); + + uint256 _totalSupplyBef = superchainERC20.totalSupply(); + uint256 _balanceBef = superchainERC20.balanceOf(_to); + + vm.startPrank(Predeploys.L2_STANDARD_BRIDGE); + superchainERC20.mint(_to, _amount); + + assert(superchainERC20.totalSupply() == _totalSupplyBef + _amount); + assert(superchainERC20.balanceOf(_to) == _balanceBef + _amount); + } + + // Doesn't work :( + function check_sendERC20ZeroCall(address _user, address _to, uint256 _chainId) public { + console.log(1); + vm.assume(_chainId != 1); + vm.assume(_user != address(0)); + vm.assume( + _to != address(Predeploys.CROSS_L2_INBOX) && _to != address(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER) + ); + + uint256 _totalSupplyBef = superchainERC20.totalSupply(); + + vm.startPrank(_user); + console.log(_user); + console.log(_to); + console.log(_chainId); + superchainERC20.sendERC20(_to, 0, _chainId); + + uint256 _totalSupplyAft = superchainERC20.totalSupply(); + + assert(_totalSupplyBef == _totalSupplyAft); + } +} diff --git a/packages/contracts-bedrock/test/properties/halmos/SuperchainERC20Factory.t.sol b/packages/contracts-bedrock/test/properties/halmos/SuperchainERC20Factory.t.sol new file mode 100644 index 000000000000..bf9c6bd3120d --- /dev/null +++ b/packages/contracts-bedrock/test/properties/halmos/SuperchainERC20Factory.t.sol @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.25; + +import { Test } from "forge-std/Test.sol"; + +import { SuperchainERC20Factory } from "src/L2/SuperchainERC20Factory.sol"; +import { SymTest } from "halmos-cheatcodes/src/SymTest.sol"; +import { Predeploys } from "src/libraries/Predeploys.sol"; +import { BeaconProxy } from "@openzeppelin/contracts/proxy/beacon/BeaconProxy.sol"; +import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import { SuperchainERC20Beacon } from "src/L2/SuperchainERC20Beacon.sol"; +import { SuperchainERC20 } from "src/L2/SuperchainERC20.sol"; + +interface IHevm { + function chainid() external view returns (uint256); + + function etch(address addr, bytes calldata code) external; +} + +contract HalmosTest is SymTest, Test { } + +contract SuperchainERC20Factory_SymbTest is HalmosTest { + struct DeployParams { + address remoteToken; + uint8 decimals; + } + + SuperchainERC20Factory internal factory; + IHevm hevm = IHevm(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); + + constructor() { + // new BeaconProxy(Predeploys.SUPERCHAIN_ERC20_BEACON, ''); + + // assert(address(Predeploys.SUPERCHAIN_ERC20_BEACON).code.length == 0); + + // vm.etch( + // Predeploys.SUPERCHAIN_ERC20_BEACON, + // hex"60806040526004361061005e5760003560e01c80635c60da1b116100435780635c60da1b146100be5780638f283970146100f8578063f851a440146101185761006d565b80633659cfe6146100755780634f1ef286146100955761006d565b3661006d5761006b61012d565b005b61006b61012d565b34801561008157600080fd5b5061006b6100903660046106d9565b610224565b6100a86100a33660046106f4565b610296565b6040516100b59190610777565b60405180910390f35b3480156100ca57600080fd5b506100d3610419565b60405173ffffffffffffffffffffffffffffffffffffffff90911681526020016100b5565b34801561010457600080fd5b5061006b6101133660046106d9565b6104b0565b34801561012457600080fd5b506100d3610517565b60006101577f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc5490565b905073ffffffffffffffffffffffffffffffffffffffff8116610201576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602560248201527f50726f78793a20696d706c656d656e746174696f6e206e6f7420696e6974696160448201527f6c697a656400000000000000000000000000000000000000000000000000000060648201526084015b60405180910390fd5b3660008037600080366000845af43d6000803e8061021e573d6000fd5b503d6000f35b7fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035473ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16148061027d575033155b1561028e5761028b816105a3565b50565b61028b61012d565b60606102c07fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035490565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614806102f7575033155b1561040a57610305846105a3565b6000808573ffffffffffffffffffffffffffffffffffffffff16858560405161032f9291906107ea565b600060405180830381855af49150503d806000811461036a576040519150601f19603f3d011682016040523d82523d6000602084013e61036f565b606091505b509150915081610401576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152603960248201527f50726f78793a2064656c656761746563616c6c20746f206e657720696d706c6560448201527f6d656e746174696f6e20636f6e7472616374206661696c65640000000000000060648201526084016101f8565b91506104129050565b61041261012d565b9392505050565b60006104437fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035490565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16148061047a575033155b156104a557507f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc5490565b6104ad61012d565b90565b7fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035473ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161480610509575033155b1561028e5761028b8161060b565b60006105417fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035490565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161480610578575033155b156104a557507fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035490565b7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc81905560405173ffffffffffffffffffffffffffffffffffffffff8216907fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b90600090a250565b60006106357fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035490565b7fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61038390556040805173ffffffffffffffffffffffffffffffffffffffff8084168252851660208201529192507f7e644d79422f17c01e4894b5f4f588d331ebfa28653d42ae832dc59e38c9798f910160405180910390a15050565b803573ffffffffffffffffffffffffffffffffffffffff811681146106d457600080fd5b919050565b6000602082840312156106eb57600080fd5b610412826106b0565b60008060006040848603121561070957600080fd5b610712846106b0565b9250602084013567ffffffffffffffff8082111561072f57600080fd5b818601915086601f83011261074357600080fd5b81358181111561075257600080fd5b87602082850101111561076457600080fd5b6020830194508093505050509250925092565b600060208083528351808285015260005b818110156107a457858101830151858201604001528201610788565b818111156107b6576000604083870101525b50601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe016929092016040019392505050565b818382376000910190815291905056fea164736f6c634300080f000a" + // ); + + // vm.store( + // Predeploys.SUPERCHAIN_ERC20_BEACON, + + // ) + + // assert(address(Predeploys.SUPERCHAIN_ERC20_BEACON).code.length > 0); + + address _superchainERC20Beacon = address(new SuperchainERC20Beacon()); + hevm.etch(Predeploys.SUPERCHAIN_ERC20_BEACON, _superchainERC20Beacon.code); + + address _token = address(new SuperchainERC20(address(0), "SuperchainERC20", "SUPER", 22)); + vm.etch(0x4200000000000000000000000000000000000042, _token.code); + factory = new SuperchainERC20Factory(); + } + + function check_Setup() public { + assert(address(Predeploys.SUPERCHAIN_ERC20_BEACON).code.length > 0); + assert(address(0x4200000000000000000000000000000000000042).code.length > 0); + } + + // this is a stateless check, so halmos will probably supersede it + function check_contractAddressDependsOnParams(DeployParams memory left, DeployParams memory right) external { + string memory _leftName = svm.createString(96, "leftName"); + string memory _rightName = svm.createString(96, "rightName"); + + string memory _leftSymbol = svm.createString(96, "leftSymbol"); + string memory _rightSymbol = svm.createString(96, "rightSymbol"); + + require( + left.remoteToken != right.remoteToken || keccak256(bytes(_leftName)) != keccak256(bytes(_rightName)) + || keccak256(bytes(_leftSymbol)) != keccak256(bytes(_rightSymbol)) || left.decimals != right.decimals + ); + + address superc20Left = factory.deploy(left.remoteToken, _leftName, _leftSymbol, left.decimals); + address superc20Right = factory.deploy(right.remoteToken, _rightName, _rightSymbol, right.decimals); + assert(superc20Left != superc20Right); + } + + // function check_contractAddressDoesNotDependOnChainId( + // DeployParams memory params, + // uint256 chainIdLeft, + // uint256 chainIdRight + // ) + // external + // { + // vm.chainId(chainIdLeft); + // address superc20Left = factory.deploy(params.remoteToken, params.name, params.symbol, params.decimals); + // vm.chainId(chainIdRight); + // address superc20Right = factory.deploy(params.remoteToken, params.name, params.symbol, params.decimals); + // assert(superc20Left == superc20Right); + // } +} diff --git a/packages/contracts-bedrock/test/properties/medusa/SuperchainERC20Factory.t.sol b/packages/contracts-bedrock/test/properties/medusa/SuperchainERC20Factory.t.sol new file mode 100644 index 000000000000..4d572517cf24 --- /dev/null +++ b/packages/contracts-bedrock/test/properties/medusa/SuperchainERC20Factory.t.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.25; + +import { Test } from "forge-std/Test.sol"; + +import { SuperchainERC20Factory } from "src/L2/SuperchainERC20Factory.sol"; + +contract SuperchainERC20FactoryFuzz is Test { + struct DeployParams { + address remoteToken; + string name; + string symbol; + uint8 decimals; + } + + SuperchainERC20Factory internal factory; + + constructor() { + factory = new SuperchainERC20Factory(); + } + + // this is a stateless check, so halmos will probably supersede it + function testContractAddressDependsOnParams(DeployParams memory left, DeployParams memory right) external { + vm.assume( + left.remoteToken != right.remoteToken || keccak256(bytes(left.name)) != keccak256(bytes(right.name)) + || keccak256(bytes(left.symbol)) != keccak256(bytes(right.symbol)) || left.decimals != right.decimals + ); + address superc20Left = factory.deploy(left.remoteToken, left.name, left.symbol, left.decimals); + address superc20Right = factory.deploy(right.remoteToken, right.name, right.symbol, right.decimals); + assert(superc20Left != superc20Right); + } + + function testContractAddressDoesNotDependOnChainId( + DeployParams memory params, + uint256 chainIdLeft, + uint256 chainIdRight + ) + external + { + vm.chainId(chainIdLeft); + address superc20Left = factory.deploy(params.remoteToken, params.name, params.symbol, params.decimals); + vm.chainId(chainIdRight); + address superc20Right = factory.deploy(params.remoteToken, params.name, params.symbol, params.decimals); + assert(superc20Left == superc20Right); + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cf74605d4482..c1be6939f310 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -46,6 +46,12 @@ importers: eslint-plugin-prettier: specifier: ^4.0.0 version: 4.2.1(eslint-config-prettier@9.1.0(eslint@8.56.0))(eslint@8.56.0)(prettier@2.8.8) + halmos: + specifier: github:a16z/halmos#337adbac61881b32386beb9245e928309ed0f1b7 + version: halmos#337adbac61881b32386beb9245e928309ed0f1b7@https://codeload.github.com/a16z/halmos/tar.gz/337adbac61881b32386beb9245e928309ed0f1b7 + halmos-cheatcodes: + specifier: github:a16z/halmos-cheatcodes#c0d8655 + version: halmos-cheatcodes#c0d8655@https://codeload.github.com/a16z/halmos-cheatcodes/tar.gz/c0d8655 prettier: specifier: ^2.8.0 version: 2.8.8 @@ -999,6 +1005,14 @@ packages: graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + halmos#337adbac61881b32386beb9245e928309ed0f1b7@https://codeload.github.com/a16z/halmos/tar.gz/337adbac61881b32386beb9245e928309ed0f1b7: + resolution: {tarball: https://codeload.github.com/a16z/halmos/tar.gz/337adbac61881b32386beb9245e928309ed0f1b7} + version: 0.0.0 + + halmos-cheatcodes#c0d8655@https://codeload.github.com/a16z/halmos-cheatcodes/tar.gz/c0d8655: + resolution: {tarball: https://codeload.github.com/a16z/halmos-cheatcodes/tar.gz/c0d8655} + version: 0.0.0 + has-bigints@1.0.2: resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==} @@ -2773,6 +2787,10 @@ snapshots: graphemer@1.4.0: {} + halmos#337adbac61881b32386beb9245e928309ed0f1b7@https://codeload.github.com/a16z/halmos/tar.gz/337adbac61881b32386beb9245e928309ed0f1b7: {} + + halmos-cheatcodes#c0d8655@https://codeload.github.com/a16z/halmos-cheatcodes/tar.gz/c0d8655: {} + has-bigints@1.0.2: {} has-flag@3.0.0: {}