From 8cea35148e12faae1b1919b7d2a19c2b81e48174 Mon Sep 17 00:00:00 2001 From: teddy Date: Thu, 8 Aug 2024 14:38:56 -0300 Subject: [PATCH] chore: configure medusa with basic supERC20 self-bridging (#19) - used --foundry-compile-all to ensure the test contract under `test/properties` is compiled (otherwise it is not compiled and medusa crashes when it can't find it's compiled representation) - set src,test,script to test/properties/medusa to not waste time compiling contracts that are not required for the medusa campaign - used an atomic bridge, which doesnt allow for testing of several of the proposed invariants fix: delete dead code test: give the fuzzer a head start docs: fix properties order test: document & implement assertions 22, 23 and 24 fix: fixes from self-review test: guide the fuzzer a little bit less previously: initial mint, bound on transfer amount: 146625 calls in 200s now: no initial mint, no bound on transfer amount: 176835 calls in 200s it doesn't seem to slow the fuzzer down fix: fixes after lovely feedback by disco docs: merge both documents and categorized properties by their milestone fix: fixes from parti's review fix: feedback from disco fix: feedback from doc refactor: separate state transitions from pure properties docs: update tested properties refactor: move all assertions into properties contract fix: move function without assertions back into handler test: only use assertion mode fix: improve justfile recipie for medusa --- packages/contracts-bedrock/.gitignore | 1 + packages/contracts-bedrock/foundry.toml | 5 + packages/contracts-bedrock/justfile | 3 + packages/contracts-bedrock/medusa.json | 82 ++++++++++++ .../test/properties/PROPERTIES.md | 125 ++++++++++++++++++ .../MockL2ToL2CrossDomainMessenger.t.sol | 53 ++++++++ .../medusa/Protocol.properties.t.sol | 93 +++++++++++++ .../medusa/handlers/Protocol.handler.t.sol | 122 +++++++++++++++++ 8 files changed, 484 insertions(+) create mode 100644 packages/contracts-bedrock/medusa.json create mode 100644 packages/contracts-bedrock/test/properties/PROPERTIES.md create mode 100644 packages/contracts-bedrock/test/properties/helpers/MockL2ToL2CrossDomainMessenger.t.sol create mode 100644 packages/contracts-bedrock/test/properties/medusa/Protocol.properties.t.sol create mode 100644 packages/contracts-bedrock/test/properties/medusa/handlers/Protocol.handler.t.sol diff --git a/packages/contracts-bedrock/.gitignore b/packages/contracts-bedrock/.gitignore index 96e09c8c7190..396c03d4458d 100644 --- a/packages/contracts-bedrock/.gitignore +++ b/packages/contracts-bedrock/.gitignore @@ -6,6 +6,7 @@ broadcast kout-deployment kout-proofs test/kontrol/logs +test/properties/medusa/corpus/ # Metrics coverage.out diff --git a/packages/contracts-bedrock/foundry.toml b/packages/contracts-bedrock/foundry.toml index cef9f85bbaeb..08ae8840dc49 100644 --- a/packages/contracts-bedrock/foundry.toml +++ b/packages/contracts-bedrock/foundry.toml @@ -97,3 +97,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/justfile b/packages/contracts-bedrock/justfile index bf96fd17cad1..654ad64cadee 100644 --- a/packages/contracts-bedrock/justfile +++ b/packages/contracts-bedrock/justfile @@ -26,6 +26,9 @@ test-kontrol: build-go-ffi build kontrol-summary-full test-kontrol-no-build test-kontrol-no-build: ./test/kontrol/scripts/run-kontrol.sh script +test-medusa timeout='100': + FOUNDRY_PROFILE=medusa medusa fuzz --timeout {{timeout}} + test-rerun: build-go-ffi forge test --rerun -vvv diff --git a/packages/contracts-bedrock/medusa.json b/packages/contracts-bedrock/medusa.json new file mode 100644 index 000000000000..cb4737956fea --- /dev/null +++ b/packages/contracts-bedrock/medusa.json @@ -0,0 +1,82 @@ +{ + "fuzzing": { + "workers": 10, + "workerResetLimit": 50, + "timeout": 0, + "testLimit": 500000, + "callSequenceLength": 100, + "corpusDirectory": "test/properties/medusa/corpus/", + "coverageEnabled": true, + "targetContracts": ["ProtocolProperties"], + "targetContractsBalances": [], + "constructorArgs": {}, + "deployerAddress": "0x30000", + "senderAddresses": [ + "0x10000", + "0x20000", + "0x30000" + ], + "blockNumberDelayMax": 60480, + "blockTimestampDelayMax": 604800, + "blockGasLimit": 30000000, + "transactionGasLimit": 12500000, + "testing": { + "stopOnFailedTest": true, + "stopOnFailedContractMatching": false, + "stopOnNoTests": true, + "testAllContracts": false, + "traceAll": true, + "assertionTesting": { + "enabled": true, + "testViewMethods": true, + "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/test/properties/PROPERTIES.md b/packages/contracts-bedrock/test/properties/PROPERTIES.md new file mode 100644 index 000000000000..36b7b330ab0b --- /dev/null +++ b/packages/contracts-bedrock/test/properties/PROPERTIES.md @@ -0,0 +1,125 @@ +# Supertoken advanced testing + +## Overview + +This document defines a set of properties global to the supertoken ecosystem, for which we will: + +- run a [Medusa](https://github.com/crytic/medusa) fuzzing campaign, trying to break system invariants +- formally prove with [Halmos](https://github.com/a16z/halmos) whenever possible + +## Milestones + +The supertoken ecosystem consists of not just the supertoken contract, but the required changes to other contracts for liquidity to reach the former. + +Considering only the supertoken contract is merged into the `develop` branch, and work for the other components is still in progress, we define three milestones for the testing campaign: + +- SupERC20: concerned with only the supertoken contract, the first one to be implemented +- Factories: covers the above + the development of `OptimismSuperchainERC20Factory` and required changes to `OptimismMintableERC20Factory` +- Liquidity Migration: includes the `convert` function on the `L2StandardBridgeInterop` to migrate liquidity from legacy tokens into supertokens + +## Where to place the testing campaign + +Given the [OP monorepo](https://github.com/ethereum-optimism/optimism) already has invariant testing provided by foundry, it's not a trivial matter where to place this advanced testing campaign. Two alternatives are proposed: + +- including it in the mainline OP monorepo, in a subdirectory of the existing test contracts such as `test/invariants/medusa/superc20/` +- keep the campaign in wonderland's fork of the repository, in its own feature branch, in which case the deliverable would consist primarily of: + - a summary of the results, extending this document + - PRs with extra unit tests replicating found issues to the main repo where applicable + +## Contracts in scope + +- [ ] [OptimismMintableERC20Factory](https://github.com/defi-wonderland/optimism/blob/develop/packages/contracts-bedrock/src/universal/OptimismMintableERC20Factory.sol) (modifications to enable `convert` not yet merged) +- [ ] [OptimismSuperchainERC20](https://github.com/defi-wonderland/optimism/blob/develop/packages/contracts-bedrock/src/L2/OptimismSuperchainERC20.sol1) +- [ ] [OptimismSuperchainERC20Factory](https://github.com/defi-wonderland/optimism/pull/8/files#diff-09838f5703c353d0f7c5ff395acc04c1768ef58becac67404bc17e1fb0018517) (not yet merged) +- [ ] [L2StandardBridgeInterop](https://github.com/defi-wonderland/optimism/pull/10/files#diff-56cf869412631eac0a04a03f7d026596f64a1e00fcffa713bc770d67c6856c2f) (not yet merged) + +## Behavior assumed correct + +- [ ] inclusion of relay transactions +- [ ] sequencer implementation +- [ ] [OptimismMintableERC20](https://github.com/defi-wonderland/optimism/blob/develop/packages/contracts-bedrock/src/universal/OptimismMintableERC20.sol) +- [ ] [L2ToL2CrossDomainMessenger](https://github.com/defi-wonderland/optimism/blob/develop/packages/contracts-bedrock/src/L2/L2CrossDomainMessenger.sol) +- [ ] [CrossL2Inbox](https://github.com/defi-wonderland/src/L2/CrossL2Inbox.sol) + +## Pain points + +- existing fuzzing tools use the same EVM to run the tested contracts as they do for asserting invariants, tracking ghost variables and everything else necessary to provision a fuzzing campaign. While this is usually very convenient, it means that we can’t assert on the behaviour/state of *different* chains from within a fuzzing campaign. This means we will have to walk around the requirement of supertokens having the same address across all chains, and implement a way to mock tokens existing in different chains. We will strive to formally prove it in a unitary fashion to mitigate this in properties 0 and 1 +- a buffer to represent 'in transit' messages should be implemented to assert on invariants relating to the non-atomicity of bridging from one chain to another. It is yet to be determined if it’ll be a FIFO queue (assuming ideal message ordering by sequencers) or it’ll have random-access capability to simulate messages arriving out of order + +## Definitions + +- *legacy token:* an OptimismMintableERC20 or L2StandardERC20 token on the suprechain that has either been deployed by the factory after the liquidity migration upgrade to the latter, or has been deployed before it **but** added to factory’s `deployments` mapping as part of the upgrade. This testing campaign is not concerned with tokens on L1 or not listed in the factory’s `deployments` mapping. +- *supertoken:* a SuperchainERC20 contract deployed by the `OptimismSuperchainERC20Factory` + +# Ecosystem properties + +legend: +- `[ ]`: property not yet tested +- `**[ ]**`: property not yet tested, dev/research team has asked for extra focus on it +- `[X]`: tested/proven property +- `[~]`: partially tested/proven property +- `:(`: property won't be tested due to some limitation + +## Unit test + +| id | milestone | description | halmos | medusa | +| --- | --- | --- | --- | --- | +| 0 | Factories | supertoken token address does not depend on the executing chain’s chainID | [ ] | [ ] | +| 1 | Factories | supertoken token address depends on remote token, name, symbol and decimals | [ ] | [ ] | +| 2 | Liquidity Migration | convert() should only allow converting legacy tokens to supertoken and viceversa | [ ] | [ ] | +| 3 | Liquidity Migration | convert() only allows migrations between tokens representing the same remote asset | [ ] | [ ] | +| 4 | Liquidity Migration | convert() only allows migrations from tokens with the same decimals | [ ] | [ ] | +| 5 | Liquidity Migration | convert() burns the same amount of legacy token that it mints of supertoken, and viceversa | [ ] | [ ] | + +## Valid state + +| id | milestone | description | halmos | medusa | +| --- | --- | --- | --- | --- | +| 6 | SupERC20 | calls to sendERC20 succeed as long as caller has enough balance | [ ] | [ ] | +| 7 | SupERC20 | calls to relayERC20 always succeed as long as the cross-domain caller is valid | **[ ]** | [ ] | + +## Variable transition + +| id | milestone | description | halmos | medusa | +| --- | --- | --- | --- | --- | +| 8 | SupERC20 | sendERC20 with a value of zero does not modify accounting | [ ] | [ ] | +| 9 | SupERC20 | relayERC20 with a value of zero does not modify accounting | [ ] | [ ] | +| 10 | SupERC20 | sendERC20 decreases the token's totalSupply in the source chain exactly by the input amount | [ ] | [ ] | +| 11 | SupERC20 | relayERC20 increases the token's totalSupply in the destination chain exactly by the input amount | [ ] | [ ] | +| 12 | Liquidity Migration | supertoken total supply only increases on calls to mint() by the L2toL2StandardBridge | [ ] | [~] | +| 13 | Liquidity Migration | supertoken total supply only decreases on calls to burn() by the L2toL2StandardBridge | [ ] | [ ] | +| 14 | SupERC20 | supertoken total supply starts at zero | [ ] | [x] | +| 15 | Factories | deploying a supertoken registers its remote token in the factory | [ ] | [ ] | +| 16 | Factories | deploying an OptimismMintableERC20 registers its remote token in the factory | [ ] | [ ] | + +## High level + +| id | milestone | description | halmos | medusa | +| --- | --- | --- | --- | --- | +| 17 | Liquidity Migration | only calls to convert(legacy, super) can increase a supertoken’s total supply across chains | [ ] | [ ] | +| 18 | Liquidity Migration | only calls to convert(super, legacy) can decrease a supertoken’s total supply across chains | [ ] | [ ] | +| 19 | Liquidity Migration | sum of supertoken total supply across all chains is always <= to convert(legacy, super)- convert(super, legacy) | [ ] | [ ] | +| 20 | SupERC20 | tokens sendERC20-ed on a source chain to a destination chain can be relayERC20-ed on it as long as the source chain is in the dependency set of the destination chain | [ ] | [ ] | +| 21 | Liquidity Migration | sum of supertoken total supply across all chains is = to convert(legacy, super)- convert(super, legacy) when all cross-chain messages are processed | [ ] | [ ] | + +## Atomic bridging pseudo-properties + +As another layer of defense, the following properties are defined which assume bridging operations to be atomic (that is, the sequencer and L2Inbox and CrossDomainMessenger contracts are fully abstracted away, `sendERC20` triggering the `relayERC20` call on the same transaction) +It’s worth noting that these properties will not hold for a live system + +| id | milestone | description | halmos | medusa | +| --- | --- | --- | --- | --- | +| 22 | SupERC20 | sendERC20 decreases sender balance in source chain and increases receiver balance in destination chain exactly by the input amount | [ ] | [x] | +| 23 | SupERC20 | sendERC20 decreases total supply in source chain and increases it in destination chain exactly by the input amount | [ ] | [x] | +| 24 | Liquidity Migration | sum of supertoken total supply across all chains is always equal to convert(legacy, super)- convert(super, legacy) | [ ] | [~] | + +# Expected external interactions + +- regular ERC20 operations between any accounts on the same chain, provided by [crytic ERC20 properties](https://github.com/crytic/properties?tab=readme-ov-file#erc20-tests) + +# Invariant-breaking candidates (brain dump) + +here we’ll list possible interactions that we intend the fuzzing campaign to support in order to help break invariants + +- [ ] changing the decimals of tokens after deployment +- [ ] `convert()` ing between multiple (3+) representations of the same remote token, by having different names/symbols diff --git a/packages/contracts-bedrock/test/properties/helpers/MockL2ToL2CrossDomainMessenger.t.sol b/packages/contracts-bedrock/test/properties/helpers/MockL2ToL2CrossDomainMessenger.t.sol new file mode 100644 index 000000000000..0e3f819a6f06 --- /dev/null +++ b/packages/contracts-bedrock/test/properties/helpers/MockL2ToL2CrossDomainMessenger.t.sol @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.25; + +import { OptimismSuperchainERC20 } from "src/L2/OptimismSuperchainERC20.sol"; +import { SafeCall } from "src/libraries/SafeCall.sol"; + +contract MockL2ToL2CrossDomainMessenger { + ///////////////////////////////////////////////////////// + // State vars mocking the L2toL2CrossDomainMessenger // + ///////////////////////////////////////////////////////// + address public crossDomainMessageSender; + address public crossDomainMessageSource; + + /////////////////////////////////////////////////// + // Helpers for cross-chain interaction mocking // + /////////////////////////////////////////////////// + mapping(address supertoken => bytes32 deploySalt) public superTokenInitDeploySalts; + mapping(uint256 chainId => mapping(bytes32 deploySalt => address supertoken)) public superTokenAddresses; + + function crossChainMessageReceiver( + address sender, + uint256 destinationChainId + ) + external + view + returns (OptimismSuperchainERC20) + { + return OptimismSuperchainERC20(superTokenAddresses[destinationChainId][superTokenInitDeploySalts[sender]]); + } + + function registerSupertoken(bytes32 deploySalt, uint256 chainId, address token) external { + superTokenAddresses[chainId][deploySalt] = token; + superTokenInitDeploySalts[token] = deploySalt; + } + + //////////////////////////////////////////////////////// + // Functions mocking the L2toL2CrossDomainMessenger // + //////////////////////////////////////////////////////// + + /// @notice recipient will not be used since in normal execution it's the same + /// address on a different chain, but here we have to compute it to mock + /// cross-chain messaging + function sendMessage(uint256 chainId, address, /*recipient*/ bytes memory message) external { + address crossChainRecipient = superTokenAddresses[chainId][superTokenInitDeploySalts[msg.sender]]; + if (crossChainRecipient == msg.sender) { + require(false, "same chain"); + } + crossDomainMessageSender = crossChainRecipient; + crossDomainMessageSource = msg.sender; + SafeCall.call(crossDomainMessageSender, 0, message); + crossDomainMessageSender = address(0); + } +} diff --git a/packages/contracts-bedrock/test/properties/medusa/Protocol.properties.t.sol b/packages/contracts-bedrock/test/properties/medusa/Protocol.properties.t.sol new file mode 100644 index 000000000000..dd169dc5748d --- /dev/null +++ b/packages/contracts-bedrock/test/properties/medusa/Protocol.properties.t.sol @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.25; + +import { ProtocolHandler } from "./handlers/Protocol.handler.t.sol"; +import { OptimismSuperchainERC20 } from "src/L2/OptimismSuperchainERC20.sol"; +import { EnumerableMap } from "@openzeppelin/contracts/utils/structs/EnumerableMap.sol"; + +contract ProtocolProperties is ProtocolHandler { + using EnumerableMap for EnumerableMap.Bytes32ToUintMap; + + // TODO: will need rework after + // - non-atomic bridge + // - `convert` + /// @custom:property-id 24 + /// @custom:property sum of supertoken total supply across all chains is always equal to convert(legacy, super)- + /// convert(super, legacy) + function property_totalSupplyAcrossChainsEqualsMints() external view { + // iterate over unique deploy salts aka supertokens that are supposed to be compatible with each other + for (uint256 deploySaltIndex = 0; deploySaltIndex < ghost_totalSupplyAcrossChains.length(); deploySaltIndex++) { + uint256 totalSupply = 0; + (bytes32 currentSalt, uint256 trackedSupply) = ghost_totalSupplyAcrossChains.at(deploySaltIndex); + // and then over all the (mocked) chain ids where that supertoken could be deployed + for (uint256 validChainId = 0; validChainId < MAX_CHAINS; validChainId++) { + address supertoken = MESSENGER.superTokenAddresses(validChainId, currentSalt); + if (supertoken != address(0)) { + totalSupply += OptimismSuperchainERC20(supertoken).totalSupply(); + } + } + assert(trackedSupply == totalSupply); + } + } + + /// @notice deploy a new supertoken with deploy salt determined by params, to the given (of course mocked) chainId + /// @custom:property-id 14 + /// @custom:property supertoken total supply starts at zero + function property_DeployNewSupertoken( + TokenDeployParams memory params, + uint256 chainId + ) + external + validateTokenDeployParams(params) + { + chainId = bound(chainId, 0, MAX_CHAINS - 1); + OptimismSuperchainERC20 supertoken = _deploySupertoken( + remoteTokens[params.remoteTokenIndex], + WORDS[params.nameIndex], + WORDS[params.symbolIndex], + DECIMALS[params.decimalsIndex], + chainId + ); + // 14 + assert(supertoken.totalSupply() == 0); + } + + /// @custom:property-id 22 + /// @custom:property sendERC20 decreases sender balance in source chain and increases receiver balance in + /// destination chain exactly by the input amount + /// @custom:property-id 23 + /// @custom:property sendERC20 decreases total supply in source chain and increases it in destination chain exactly + /// by the input amount + function property_SelfBridgeSupertoken(uint256 fromIndex, uint256 destinationChainId, uint256 amount) external { + destinationChainId = bound(destinationChainId, 0, MAX_CHAINS - 1); + fromIndex = bound(fromIndex, 0, allSuperTokens.length - 1); + OptimismSuperchainERC20 sourceToken = OptimismSuperchainERC20(allSuperTokens[fromIndex]); + OptimismSuperchainERC20 destinationToken = + MESSENGER.crossChainMessageReceiver(address(sourceToken), destinationChainId); + // TODO: when implementing non-atomic bridging, allow for the token to + // not yet be deployed and funds be recovered afterwards. + require(address(destinationToken) != address(0)); + uint256 sourceBalanceBefore = sourceToken.balanceOf(msg.sender); + uint256 sourceSupplyBefore = sourceToken.totalSupply(); + uint256 destinationBalanceBefore = destinationToken.balanceOf(msg.sender); + uint256 destinationSupplyBefore = destinationToken.totalSupply(); + + vm.prank(msg.sender); + try sourceToken.sendERC20(msg.sender, amount, destinationChainId) { + uint256 sourceBalanceAfter = sourceToken.balanceOf(msg.sender); + uint256 destinationBalanceAfter = destinationToken.balanceOf(msg.sender); + // no free mint + assert(sourceBalanceBefore + destinationBalanceBefore == sourceBalanceAfter + destinationBalanceAfter); + // 22 + assert(sourceBalanceBefore - amount == sourceBalanceAfter); + assert(destinationBalanceBefore + amount == destinationBalanceAfter); + uint256 sourceSupplyAfter = sourceToken.totalSupply(); + uint256 destinationSupplyAfter = destinationToken.totalSupply(); + // 23 + assert(sourceSupplyBefore - amount == sourceSupplyAfter); + assert(destinationSupplyBefore + amount == destinationSupplyAfter); + } catch { + assert(address(destinationToken) == address(sourceToken) || sourceBalanceBefore < amount); + } + } +} diff --git a/packages/contracts-bedrock/test/properties/medusa/handlers/Protocol.handler.t.sol b/packages/contracts-bedrock/test/properties/medusa/handlers/Protocol.handler.t.sol new file mode 100644 index 000000000000..266650486e34 --- /dev/null +++ b/packages/contracts-bedrock/test/properties/medusa/handlers/Protocol.handler.t.sol @@ -0,0 +1,122 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.25; + +import { TestBase } from "forge-std/Base.sol"; +import { StdUtils } from "forge-std/StdUtils.sol"; + +import { ERC1967Proxy } from "@openzeppelin/contracts-v5/proxy/ERC1967/ERC1967Proxy.sol"; +import { EnumerableMap } from "@openzeppelin/contracts/utils/structs/EnumerableMap.sol"; +import { OptimismSuperchainERC20 } from "src/L2/OptimismSuperchainERC20.sol"; +import { Predeploys } from "src/libraries/Predeploys.sol"; +import { MockL2ToL2CrossDomainMessenger } from "../../helpers/MockL2ToL2CrossDomainMessenger.t.sol"; + +contract ProtocolHandler is TestBase, StdUtils { + using EnumerableMap for EnumerableMap.Bytes32ToUintMap; + + uint8 internal constant MAX_CHAINS = 4; + uint8 internal constant INITIAL_TOKENS = 1; + uint8 internal constant INITIAL_SUPERTOKENS = 1; + uint8 internal constant SUPERTOKEN_INITIAL_MINT = 100; + address internal constant BRIDGE = Predeploys.L2_STANDARD_BRIDGE; + MockL2ToL2CrossDomainMessenger internal constant MESSENGER = + MockL2ToL2CrossDomainMessenger(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER); + OptimismSuperchainERC20 internal superchainERC20Impl; + // NOTE: having more options for this enables the fuzzer to configure + // different supertokens for the same remote token + string[] internal WORDS = ["TOKENS"]; + uint8[] internal DECIMALS = [6, 18]; + + struct TokenDeployParams { + uint8 remoteTokenIndex; + uint8 nameIndex; + uint8 symbolIndex; + uint8 decimalsIndex; + } + + address[] internal remoteTokens; + address[] internal allSuperTokens; + + //@notice 'real' deploy salt => total supply sum across chains + EnumerableMap.Bytes32ToUintMap internal ghost_totalSupplyAcrossChains; + + constructor() { + vm.etch(address(MESSENGER), address(new MockL2ToL2CrossDomainMessenger()).code); + superchainERC20Impl = new OptimismSuperchainERC20(); + for (uint256 remoteTokenIndex = 0; remoteTokenIndex < INITIAL_TOKENS; remoteTokenIndex++) { + _deployRemoteToken(); + for (uint256 supertokenChainId = 0; supertokenChainId < INITIAL_SUPERTOKENS; supertokenChainId++) { + _deploySupertoken(remoteTokens[remoteTokenIndex], WORDS[0], WORDS[0], DECIMALS[0], supertokenChainId); + } + } + } + + /// @notice the deploy params are _indexes_ to pick from a pre-defined array of options and limit + /// the amount of supertokens for a given remoteAsset that are incompatible between them, as + /// two supertokens have to share decimals, name, symbol and remoteAsset to be considered + /// the same asset, and therefore bridgable. + modifier validateTokenDeployParams(TokenDeployParams memory params) { + params.remoteTokenIndex = uint8(bound(params.remoteTokenIndex, 0, remoteTokens.length - 1)); + params.nameIndex = uint8(bound(params.nameIndex, 0, WORDS.length - 1)); + params.symbolIndex = uint8(bound(params.symbolIndex, 0, WORDS.length - 1)); + params.decimalsIndex = uint8(bound(params.decimalsIndex, 0, DECIMALS.length - 1)); + _; + } + + function handler_MockNewRemoteToken() external { + _deployRemoteToken(); + } + + /// @notice pick one already-deployed supertoken and mint an arbitrary amount of it + /// necessary so there is something to be bridged :D + /// TODO: will be replaced when testing the factories and `convert()` + function handler_MintSupertoken(uint256 index, uint96 amount) external { + index = bound(index, 0, allSuperTokens.length - 1); + address addr = allSuperTokens[index]; + vm.prank(BRIDGE); + // medusa calls with different senders by default + OptimismSuperchainERC20(addr).mint(msg.sender, amount); + // currentValue will be zero if key is not present + (, uint256 currentValue) = ghost_totalSupplyAcrossChains.tryGet(MESSENGER.superTokenInitDeploySalts(addr)); + ghost_totalSupplyAcrossChains.set(MESSENGER.superTokenInitDeploySalts(addr), currentValue + amount); + } + + /// @notice deploy a remote token, that supertokens will be a representation of. They are never called, so there + /// is no need to actually deploy a contract for them + function _deployRemoteToken() internal { + // make sure they don't conflict with predeploys/preinstalls/precompiles/other tokens + remoteTokens.push(address(uint160(1000 + remoteTokens.length))); + } + + /// @notice deploy a new supertoken representing remoteToken + /// remoteToken, name, symbol and decimals determine the 'real' deploy salt + /// and supertokens sharing it are interoperable between them + /// we however use the chainId as part of the deploy salt to mock the ability of + /// supertokens to exist on different chains on a single EVM. + function _deploySupertoken( + address remoteToken, + string memory name, + string memory symbol, + uint8 decimals, + uint256 chainId + ) + internal + returns (OptimismSuperchainERC20 supertoken) + { + // this salt would be used in production. Tokens sharing it will be bridgable with each other + bytes32 realSalt = keccak256(abi.encode(remoteToken, name, symbol, decimals)); + // what we use in the tests to walk around two contracts needing two different addresses + // tbf we could be using CREATE1, but this feels more verbose + bytes32 hackySalt = keccak256(abi.encode(remoteToken, name, symbol, decimals, chainId)); + supertoken = OptimismSuperchainERC20( + address( + // TODO: Use the SuperchainERC20 Beacon Proxy + new ERC1967Proxy{ salt: hackySalt }( + address(superchainERC20Impl), + abi.encodeCall(OptimismSuperchainERC20.initialize, (remoteToken, name, symbol, decimals)) + ) + ) + ); + MESSENGER.registerSupertoken(realSalt, chainId, address(supertoken)); + allSuperTokens.push(address(supertoken)); + } +}