forked from ethereum-optimism/optimism
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
1404bca
commit 028c189
Showing
19 changed files
with
1,561 additions
and
11 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
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: Setup Node.js 16.x | ||
uses: actions/setup-node@master | ||
with: | ||
node-version: 16.x | ||
cache: yarn | ||
|
||
- 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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
52 changes: 52 additions & 0 deletions
52
packages/contracts-bedrock/contracts/test/invariants/PROPERTIES.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
# Scope | ||
|
||
- OptimismPortal's withdrawEthBalance function: contracts/L1/OptimismPortal.sol:504 | ||
- L1StandardBridge's withdrawErc20Balance function: contracts/L1/L1StandardBridge.sol:272 | ||
- BalanceClaimer contract: contracts/L1/winddown/BalanceClaimer.sol | ||
|
||
# Properties | ||
|
||
| Id | Properties | Type | Checked | | ||
| --- | --------------------------------------------------- | ------------ | --- | | ||
| 1 | a valid claim should be redeemable once | State transition | [x] | | ||
| 2 | a valid claim should not be redeemable more than once | State transition | [x] | | ||
| 3 | a user should be set as claimed when they process a claim | State transition | [x] | | ||
| 4 | an invalid claim should not be redeemable | State transition | [x] | | ||
| 5 | for each token, token.balanceOf(L1StandardBridge) == initialBalance - sum of claims | High-level | [x] | | ||
| 6 | OptimismPortal.balance == initialBalance - sum of claims | High-level | [x] | | ||
|
||
|
||
## testing methodology | ||
The fact that the state root is not writeable in the lifetime of the contract is cool from a design standpoint, but that also means the root has to be generated, and the valid claims chosen, before it makes sense to call any other handler. | ||
|
||
As a first approach, we've meta-programmed a solidity source file with a hard-coded set of valid claims, which can be refreshed by calling `./generate-random-tree.sh`. | ||
This is not ideal, as all fuzzing runs are going to run on the same merkle tree instead of letting the fuzzer explore new ones. Some alternatives are described below: | ||
|
||
### 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) | ||
- [ ] 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 | ||
|
||
This has the downside of being dissimilar to the actual production usage in a very crucial way, but also the invariant we would be breaking (the state root not changing) can be easily enforced by the compiler (ie: make the field immutable), and the upside of exploring a lot of possible trees in a simpler way | ||
|
||
### use a modifier to ensure the first call of the sequence initializes a state root | ||
this would involve | ||
- [ ] not creating the balanceClaimer in the constructor | ||
- [ ] 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 | ||
- [ ] use a full uint256 for the range of the amounts in merkle tree | ||
- [ ] use bigger number in script | ||
- [ ] handle fails caused by insufficient balances | ||
- [ ] create a larger share of claims with incomplete list of tokens or zero eth | ||
- [ ] handlers for withdraw{Erc20,Eth}Balance methods | ||
- [ ] guided | ||
- [ ] unguided | ||
|
8 changes: 8 additions & 0 deletions
8
packages/contracts-bedrock/contracts/test/invariants/balance-claimer/FuzzTest.t.sol
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 {} |
54 changes: 54 additions & 0 deletions
54
packages/contracts-bedrock/contracts/test/invariants/balance-claimer/generate-random-tree.sh
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
#!/bin/sh | ||
|
||
claims_file=./setup/ClaimList.t.sol | ||
claims_amount=100 | ||
users_amount=100 | ||
|
||
random_int() { | ||
# 2^ 64 - 2 , max range for shuf, == 18e18, not ideal. | ||
echo "$(shuf -n 1 -i 0-18446744073709551614)" | ||
} | ||
|
||
cat - > "$claims_file" <<EOF | ||
// SPDX-License-Identifier: UNLICENSED | ||
pragma solidity 0.8.15; | ||
contract ClaimsList { | ||
struct ClaimEntry { | ||
address recipient; | ||
uint256 ethAmount; | ||
uint256 daiAmount; | ||
uint256 gtcAmount; | ||
uint256 usdtAmount; | ||
uint256 usdcAmount; | ||
} | ||
ClaimEntry[] internal randomClaims; | ||
constructor() { | ||
// ensure at least one all-zero claim | ||
randomClaims.push(ClaimEntry(0x0000000000000000000000000000000000010000, 0, 0, 0, 0, 0)); | ||
// one zero-erc20 claim | ||
randomClaims.push(ClaimEntry(0x0000000000000000000000000000000000020000, 1e18, 0, 0, 0, 0)); | ||
// one zero-eth claim | ||
randomClaims.push(ClaimEntry(0x0000000000000000000000000000000000030000, 0, 100e18, 0, 0, 0)); | ||
// remaining randomly-generated claims | ||
EOF | ||
for i in $(seq "$claims_amount") ; do | ||
# have some overlap with medusa's actors | ||
recipient="address($(shuf -n 1 -i 1-${users_amount}) << 16)"; | ||
ethAmount=$(random_int) | ||
daiAmount=$(random_int) | ||
gtcAmount=$(random_int) | ||
usdtAmount=$(random_int) | ||
usdcAmount=$(random_int) | ||
echo " randomClaims.push(ClaimEntry($recipient, $ethAmount, $daiAmount, $gtcAmount, $usdtAmount, $usdcAmount));" >> "$claims_file" | ||
|
||
done | ||
|
||
|
||
cat - >> "$claims_file" <<EOF | ||
} | ||
} | ||
EOF | ||
forge fmt "$claims_file" |
26 changes: 26 additions & 0 deletions
26
...ts-bedrock/contracts/test/invariants/balance-claimer/handlers/guided/BalanceClaimer.t.sol
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
// SPDX-License-Identifier: UNLICENSED | ||
pragma solidity 0.8.15; | ||
|
||
import {IErc20BalanceWithdrawer} from "contracts/L1/interfaces/winddown/IErc20BalanceWithdrawer.sol"; | ||
import {BalanceClaimerSetup} from "../../setup/BalanceClaimer.t.sol"; | ||
|
||
contract BalanceClaimerGuidedHandlers is BalanceClaimerSetup { | ||
function handler_claim(uint256 claimIndex) external { | ||
claimIndex = bound(claimIndex, 0, ghost_validClaims.length - 1); | ||
Claim memory claim = ghost_validClaims[claimIndex]; | ||
bytes32 hashedClaim = _hashClaim(claim); | ||
bytes32[] memory proof = getProof(tree, getIndex(tree, hashedClaim)); | ||
vm.prank(msg.sender); | ||
// prop-id 1 | ||
try balanceClaimer.claim(proof, claim.user, claim.ethAmount, _claimToErc20ClaimArray(claim)) { | ||
ghost_claimed[claim.user] = true; | ||
ghost_claimedEther += claim.ethAmount; | ||
for (uint256 i = 0; i < claim.tokens.length; i++) { | ||
ghost_claimedTokens[claim.tokens[i]] += claim.tokenAmounts[i]; | ||
} | ||
} catch { | ||
// prop-id 2 | ||
assert(ghost_claimed[claim.user]); | ||
} | ||
} | ||
} |
51 changes: 51 additions & 0 deletions
51
...-bedrock/contracts/test/invariants/balance-claimer/handlers/unguided/BalanceClaimer.t.sol
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
// SPDX-License-Identifier: UNLICENSED | ||
pragma solidity 0.8.15; | ||
|
||
import {BalanceClaimerSetup} from "../../setup/BalanceClaimer.t.sol"; | ||
import {IErc20BalanceWithdrawer} from "contracts/L1/interfaces/winddown/IErc20BalanceWithdrawer.sol"; | ||
import {IBalanceClaimer} from "contracts/L1/interfaces/winddown/IBalanceClaimer.sol"; | ||
|
||
contract BalanceClaimerUnguidedHandlers is BalanceClaimerSetup { | ||
function handler_claim( | ||
bytes32[] calldata _proof, | ||
address _user, | ||
uint256 _ethBalance, | ||
IErc20BalanceWithdrawer.Erc20BalanceClaim[] calldata _erc20Claim, | ||
address _caller | ||
) external { | ||
bytes32 hash = _hashClaim(_user, _ethBalance, _erc20Claim); | ||
vm.prank(_caller); | ||
try balanceClaimer.claim(_proof, _user, _ethBalance, _erc20Claim) { | ||
//prop-id 1 | ||
assert(ghost_claimInTree[hash]); | ||
//prop-id 2; | ||
assert(!ghost_claimed[_user]); | ||
ghost_claimed[_user]=true; | ||
} catch { | ||
assert( | ||
!ghost_claimInTree[hash] // prop-id 4 | ||
|| ghost_claimed[_user] //prop-id 3 | ||
); | ||
} | ||
} | ||
|
||
function handler_canClaim( | ||
bytes32[] calldata _proof, | ||
address _user, | ||
uint256 _ethBalance, | ||
IErc20BalanceWithdrawer.Erc20BalanceClaim[] calldata _erc20Claim | ||
) external { | ||
bytes32 hash = _hashClaim(_user, _ethBalance, _erc20Claim); | ||
if (balanceClaimer.canClaim(_proof, _user, _ethBalance, _erc20Claim)) { | ||
//prop-id 1 | ||
assert(ghost_claimInTree[hash]); | ||
// prop-id 2 | ||
assert(!ghost_claimed[_user]); | ||
} else { | ||
assert( | ||
!ghost_claimInTree[hash] // prop-id 4 | ||
|| ghost_claimed[_user] //prop-id 3 | ||
); | ||
} | ||
} | ||
} |
23 changes: 23 additions & 0 deletions
23
...ntracts-bedrock/contracts/test/invariants/balance-claimer/properties/BalanceClaimer.t.sol
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
// SPDX-License-Identifier: UNLICENSED | ||
pragma solidity 0.8.15; | ||
|
||
import {BalanceClaimerSetup} from "../setup/BalanceClaimer.t.sol"; | ||
|
||
contract BalanceClaimerProperties is BalanceClaimerSetup { | ||
/// @custom:property-id 5 | ||
/// @custom:property for each token, token.balanceOf(L1StandardBridge) == initialBalance - sum of claims | ||
function property_tokenBalancesSum() external view { | ||
for (uint256 i = 0; i < supportedTokens.length; i++) { | ||
assert( | ||
supportedTokens[i].balanceOf(address(l1StandardBridge)) | ||
== INITIAL_BALANCE - ghost_claimedTokens[address(supportedTokens[i])] | ||
); | ||
} | ||
} | ||
|
||
/// @custom:property-id 6 | ||
/// @custom:property OptimismPortal.balance == initialBalance - sum of claims | ||
function property_ethBalancesSum() external view { | ||
assert(address(optimismPortal).balance == INITIAL_BALANCE - ghost_claimedEther); | ||
} | ||
} |
66 changes: 66 additions & 0 deletions
66
...es/contracts-bedrock/contracts/test/invariants/balance-claimer/setup/BalanceClaimer.t.sol
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
// SPDX-License-Identifier: UNLICENSED | ||
pragma solidity 0.8.15; | ||
|
||
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 {FuzzERC20} from "./Tokens.t.sol"; | ||
import {Claims} from "./Claims.t.sol"; | ||
|
||
import {CommonBase} from "forge-std/Base.sol"; | ||
import {StdUtils} from "forge-std/StdUtils.sol"; | ||
|
||
contract BalanceClaimerSetup is CommonBase, StdUtils, Claims { | ||
L1StandardBridge internal l1StandardBridge; | ||
OptimismPortal internal optimismPortal; | ||
BalanceClaimer internal balanceClaimer; | ||
|
||
constructor() { | ||
Proxy balanceClaimerProxy = new Proxy(address(this)); | ||
|
||
L1StandardBridge l1StandardBridgeImpl = new L1StandardBridge(payable(0), payable(address(balanceClaimerProxy))); | ||
OptimismPortal optimismPortalImpl = new OptimismPortal({ | ||
_l2Oracle: L2OutputOracle(address(0)), | ||
_guardian: address(0), | ||
_paused: false, | ||
_config: SystemConfig(address(0)), | ||
_balanceClaimer: address(balanceClaimerProxy) | ||
}); | ||
// Get the proxies for L1StandardBridge and OptimismPortal | ||
L1ChugSplashProxy l1StandardBridgeProxy = new L1ChugSplashProxy(address(this)); | ||
Proxy optimismPortalProxy = new Proxy(address(this)); | ||
|
||
BalanceClaimer balanceClaimerImpl = | ||
new BalanceClaimer(address(optimismPortalProxy), address(l1StandardBridgeProxy), tree[0]); | ||
|
||
// Set BalanceClaimer implementation | ||
balanceClaimerProxy.upgradeTo(address(balanceClaimerImpl)); | ||
optimismPortalProxy.upgradeTo(address(optimismPortalImpl)); | ||
l1StandardBridgeProxy.setCode(address(l1StandardBridgeImpl).code); | ||
|
||
optimismPortal = OptimismPortal(payable(optimismPortalProxy)); | ||
l1StandardBridge = L1StandardBridge(payable(l1StandardBridgeProxy)); | ||
balanceClaimer = BalanceClaimer(address(balanceClaimerProxy)); | ||
|
||
// cant do this in Tokens because l1StandardBridge address is not set at that time | ||
vm.deal(address(optimismPortal), INITIAL_BALANCE); | ||
for (uint256 i = 0; i < TOKENS; i++) { | ||
FuzzERC20(address(supportedTokens[i])).mint(address(l1StandardBridge), INITIAL_BALANCE); | ||
} | ||
} | ||
|
||
/// @custom:prop-id 0 | ||
/// @custom:prop sanity checks for setup | ||
function property_setup() external { | ||
assert(address(optimismPortal.BALANCE_CLAIMER()) == address(balanceClaimer)); | ||
assert(address(balanceClaimer.ETH_BALANCE_WITHDRAWER()) == address(optimismPortal)); | ||
assert(address(balanceClaimer.ERC20_BALANCE_WITHDRAWER()) == address(l1StandardBridge)); | ||
assert(address(l1StandardBridge.BALANCE_CLAIMER()) == address(balanceClaimer)); | ||
assert(balanceClaimer.ROOT() == tree[0]); | ||
} | ||
} |
Oops, something went wrong.