diff --git a/.github/workflows/medusa.yml b/.github/workflows/medusa.yml new file mode 100644 index 0000000000000..d6f166144c72b --- /dev/null +++ b/.github/workflows/medusa.yml @@ -0,0 +1,23 @@ +name: CI + +on: [push] + +jobs: + medusa-tests: + name: Medusa Test + runs-on: ubuntu-latest + container: ghcr.io/defi-wonderland/eth-security-toolbox-ci:dev + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install dependencies + working-directory: ./packages/contracts-bedrock + run: yarn --frozen-lockfile --network-concurrency 1 + + - name: Run Medusa + working-directory: ./packages/contracts-bedrock + run: medusa fuzz --test-limit 200000 diff --git a/packages/contracts-bedrock/contracts/test/invariants/PROPERTIES.md b/packages/contracts-bedrock/contracts/test/invariants/PROPERTIES.md index 60afa2aab1c69..d5af93a2715a3 100644 --- a/packages/contracts-bedrock/contracts/test/invariants/PROPERTIES.md +++ b/packages/contracts-bedrock/contracts/test/invariants/PROPERTIES.md @@ -22,7 +22,7 @@ The fact that the state root is not writeable in the lifetime of the contract is ## mutate the state root Idea for this is to initialize the BalanceClaimer in the campaign constructor with either -- [ ] an empty state root (for ...purity? ie allowing the fuzzer choose the inputs with the greatest variability) +- [ ] an empty state root (for ...purity? ie allowing the fuzzer choose the inputs with the greatest variability) - [ ] pre-filled state root (to cover code faster) and set of valid claims, with the downside of calls creating and have handlers to _add_ valid claims to the set, overwriting the state root @@ -36,6 +36,8 @@ this would involve - [ ] have a modifier (and an extra param of fuzzed input in every handler/property check) which will be used to initialize the state root on the first call - [ ] have all handlers afterwards only process claims (valid or not, obviously) and not create new ones +This has the upside of being identical to the production setup, but would yield uglier code and potentially have worse pseudorandom input since we would be having all the state as fields of structs in arrays + # nice to haves - [ ] use tokens' actual bytecode in the fuzzing campaign diff --git a/packages/contracts-bedrock/contracts/test/invariants/balance-claimer/FuzzTest.t.sol b/packages/contracts-bedrock/contracts/test/invariants/balance-claimer/FuzzTest.t.sol new file mode 100644 index 0000000000000..89503bff13fea --- /dev/null +++ b/packages/contracts-bedrock/contracts/test/invariants/balance-claimer/FuzzTest.t.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.15; + +import {BalanceClaimerGuidedHandlers} from './handlers/guided/BalanceClaimer.t.sol'; +import {BalanceClaimerUnguidedHandlers} from './handlers/unguided/BalanceClaimer.t.sol'; +import {BalanceClaimerProperties} from './properties/BalanceClaimer.t.sol'; + +contract FuzzTest is BalanceClaimerGuidedHandlers, BalanceClaimerUnguidedHandlers, BalanceClaimerProperties {} diff --git a/packages/contracts-bedrock/contracts/test/invariants/balance-claimer/handlers/guided/BalanceClaimer.t.sol b/packages/contracts-bedrock/contracts/test/invariants/balance-claimer/handlers/guided/BalanceClaimer.t.sol new file mode 100644 index 0000000000000..bf1fd9773fa49 --- /dev/null +++ b/packages/contracts-bedrock/contracts/test/invariants/balance-claimer/handlers/guided/BalanceClaimer.t.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.15; + +import {BalanceClaimerSetup} from '../../setup/BalanceClaimer.t.sol'; + +contract BalanceClaimerGuidedHandlers is BalanceClaimerSetup { + function handler_fooGuided() external { + } +} diff --git a/packages/contracts-bedrock/contracts/test/invariants/balance-claimer/handlers/unguided/BalanceClaimer.t.sol b/packages/contracts-bedrock/contracts/test/invariants/balance-claimer/handlers/unguided/BalanceClaimer.t.sol new file mode 100644 index 0000000000000..9aa23b6117a6a --- /dev/null +++ b/packages/contracts-bedrock/contracts/test/invariants/balance-claimer/handlers/unguided/BalanceClaimer.t.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.15; + +import {BalanceClaimerSetup} from '../../setup/BalanceClaimer.t.sol'; + +contract BalanceClaimerUnguidedHandlers is BalanceClaimerSetup { + /// @custom:property-id + /// @custom:property + function handler_fooUnguided(address _caller, string memory _newGreeting) external { + } +} diff --git a/packages/contracts-bedrock/contracts/test/invariants/balance-claimer/properties/BalanceClaimer.t.sol b/packages/contracts-bedrock/contracts/test/invariants/balance-claimer/properties/BalanceClaimer.t.sol new file mode 100644 index 0000000000000..283438a9f027b --- /dev/null +++ b/packages/contracts-bedrock/contracts/test/invariants/balance-claimer/properties/BalanceClaimer.t.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.15; + +import {BalanceClaimerSetup} from '../setup/BalanceClaimer.t.sol'; + +contract BalanceClaimerProperties is BalanceClaimerSetup { + /// @custom:property-id 1 + /// @custom:property foo + function property_foo() external view { + } +} diff --git a/packages/contracts-bedrock/contracts/test/invariants/balance-claimer/setup/BalanceClaimer.t.sol b/packages/contracts-bedrock/contracts/test/invariants/balance-claimer/setup/BalanceClaimer.t.sol new file mode 100644 index 0000000000000..9ed978aeebb13 --- /dev/null +++ b/packages/contracts-bedrock/contracts/test/invariants/balance-claimer/setup/BalanceClaimer.t.sol @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.15; + +import "forge-std/console.sol"; + +import {BalanceClaimer} from "contracts/L1/winddown/BalanceClaimer.sol"; +import {OptimismPortal} from "contracts/L1/OptimismPortal.sol"; +import {L1StandardBridge} from "contracts/L1/L1StandardBridge.sol"; +import {L1ChugSplashProxy} from "contracts/legacy/L1ChugSplashProxy.sol"; +import {L2OutputOracle} from "contracts/L1/L2OutputOracle.sol"; +import {SystemConfig} from "contracts/L1/SystemConfig.sol"; +import {Proxy} from "contracts/universal/Proxy.sol"; +import {WinddownConstants} from "scripts/winddown-upgrade/WinddownConstants.sol"; + +import {CommonBase} from "forge-std/Base.sol"; + +contract BalanceClaimerSetup is CommonBase { + L1StandardBridge internal l1StandardBridge; + OptimismPortal internal optimismPortal; + BalanceClaimer internal balanceClaimer; + + constructor() { + // _targetContract = new BalanceClaimer(); + // optimismPortal = new OptimimismPortal({ + // _l2Oracle: L2OutputOracle(address(0)), + // _guardian: address(0), + // _config: SystemConfig(address(0))}, + // balanceClaimer: balanceClaimer); + // l1StandardBridge = new L1StandardBridge(address(0), address(balanceClaimer)); + // Get the proxies for L1StandardBridge and OptimismPortal + L1ChugSplashProxy l1StandardBridgeProxy = + L1ChugSplashProxy(payable(address(WinddownConstants.L1_STANDARD_BRIDGE_PROXY))); + Proxy optimismPortalProxy = Proxy(payable(address(WinddownConstants.OPTIMISM_PORTAL_PROXY))); + + // Deploy BalanceClaimer proxy + Proxy balanceClaimerProxy = new Proxy(address(this)); + + // Deploy BalanceClaimer implementation + BalanceClaimer balanceClaimerImpl = new BalanceClaimer(); + + // Set BalanceClaimer implementation + balanceClaimerProxy.upgradeToAndCall( + address(balanceClaimerImpl), + abi.encodeWithSelector( + balanceClaimerImpl.initialize.selector, + address(optimismPortalProxy), + address(l1StandardBridgeProxy), + WinddownConstants.MERKLE_ROOT + ) + ); + + optimismPortal = OptimismPortal(payable(optimismPortalProxy)); + l1StandardBridge = L1StandardBridge(payable(l1StandardBridgeProxy)); + balanceClaimer = BalanceClaimer(address(balanceClaimerProxy)); + } + + /// @custom:prop-id 0 + /// @custom:prop sanity checks for setup + function property_setup() external { + assert(address(l1StandardBridge.BALANCE_CLAIMER()) == address(balanceClaimer)); + assert(address(optimismPortal.BALANCE_CLAIMER()) == address(balanceClaimer)); + assert(address(balanceClaimer.ethBalanceWithdrawer()) == address(l1StandardBridge)); + assert(address(balanceClaimer.erc20BalanceWithdrawer()) == address(optimismPortal)); + assert(balanceClaimer.root() == bytes32(0)); + } +} diff --git a/packages/contracts-bedrock/foundry.toml b/packages/contracts-bedrock/foundry.toml index 18e9f3361ed87..3b04c12b8cd4c 100644 --- a/packages/contracts-bedrock/foundry.toml +++ b/packages/contracts-bedrock/foundry.toml @@ -24,6 +24,11 @@ fs_permissions = [ { 'access'='read-write', 'path'='./.resource-metering.csv' }, ] +[profile.medusa] +src='contracts/test/invariants/balance-claimer' +test='contracts/test/invariants/balance-claimer' +script='contracts/test/invariants/balance-claimer' + [profile.ci] fuzz_runs = 512 diff --git a/packages/contracts-bedrock/medusa.json b/packages/contracts-bedrock/medusa.json new file mode 100644 index 0000000000000..412e674d4c997 --- /dev/null +++ b/packages/contracts-bedrock/medusa.json @@ -0,0 +1,89 @@ +{ + "fuzzing": { + "workers": 10, + "workerResetLimit": 50, + "timeout": 0, + "testLimit": 0, + "shrinkLimit": 5000, + "callSequenceLength": 100, + "corpusDirectory": "", + "coverageEnabled": true, + "coverageFormats": [ + "html", + "lcov" + ], + "targetContracts": ["FuzzTest"], + "predeployedContracts": {}, + "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": 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 + }, + "skipAccountChecks": true + } + }, + "compilation": { + "platform": "crytic-compile", + "platformConfig": { + "target": ".", + "solcVersion": "", + "exportDirectory": "", + "args": ["--foundry-compile-all", "--foundry-out-directory", "artifacts"] + } + }, + "logging": { + "level": "info", + "logDirectory": "", + "noColor": false + } +} diff --git a/packages/contracts-bedrock/package.json b/packages/contracts-bedrock/package.json index 7ae14a50c8b87..b2b51b0189d1d 100644 --- a/packages/contracts-bedrock/package.json +++ b/packages/contracts-bedrock/package.json @@ -46,6 +46,7 @@ "lint:fix": "yarn lint:contracts:fix && yarn lint:ts:fix", "lint": "yarn lint:fix && yarn lint:check", "typechain": "typechain --target ethers-v5 --out-dir dist/types --glob 'artifacts/!(build-info)/**/+([a-zA-Z0-9_]).json'", + "medusa": "FOUNDRY_PROFILE=medusa medusa fuzz", "echidna:aliasing": "echidna-test --contract EchidnaFuzzAddressAliasing --config ./echidna.yaml .", "echidna:burn:gas": "echidna-test --contract EchidnaFuzzBurnGas --config ./echidna.yaml .", "echidna:burn:eth": "echidna-test --contract EchidnaFuzzBurnEth --config ./echidna.yaml .",