Skip to content

Commit

Permalink
test: all handlers & properties against a single-element merkle tree
Browse files Browse the repository at this point in the history
  • Loading branch information
0xteddybear committed Oct 26, 2024
1 parent 95e57d1 commit 1b694c5
Show file tree
Hide file tree
Showing 7 changed files with 135 additions and 60 deletions.
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.15;

import "forge-std/console.sol";

import {BalanceClaimerGuidedHandlers} from "./handlers/guided/BalanceClaimer.t.sol";
import {BalanceClaimerUnguidedHandlers} from "./handlers/unguided/BalanceClaimer.t.sol";
import {BalanceClaimerProperties} from "./properties/BalanceClaimer.t.sol";
import {IErc20BalanceWithdrawer} from "contracts/L1/interfaces/winddown/IErc20BalanceWithdrawer.sol";

contract FuzzTest is BalanceClaimerGuidedHandlers, BalanceClaimerUnguidedHandlers, BalanceClaimerProperties {}
Original file line number Diff line number Diff line change
@@ -1,9 +1,24 @@
// 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 {BalanceClaimerSetup} from "../../setup/BalanceClaimer.t.sol";

contract BalanceClaimerGuidedHandlers is BalanceClaimerSetup {
function handler_fooGuided() external {
}
function handler_claim(uint256 claimIndex) external {
claimIndex = bound(claimIndex, 0, ghost_validClaims.length - 1);
Claim memory claim = ghost_validClaims[claimIndex];
bytes32[] memory proof = new bytes32[](0);
bytes32 hashedClaim = _hashClaim(claim);
vm.prank(msg.sender);
try balanceClaimer.claim(proof, claim.user, claim.ethAmount, _claimToErc20ClaimArray(claim)) {
ghost_claimed[hashedClaim] = true;
ghost_claimedEther += claim.ethAmount;
for (uint256 i = 0; i < claim.tokens.length; i++) {
ghost_claimedTokens[claim.tokens[i]] += claim.tokenAmounts[i];
}
} catch {
assert(ghost_claimed[hashedClaim]);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.15;

import "forge-std/console.sol";

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";
Expand All @@ -23,13 +21,13 @@ contract BalanceClaimerUnguidedHandlers is BalanceClaimerSetup {
IErc20BalanceWithdrawer.Erc20BalanceClaim[] calldata _erc20Claim,
address _caller
) external {
bytes32 hash = _hashClaim(_user, _ethBalance, _erc20Claim);
vm.prank(_caller);
try balanceClaimer.claim(_proof, _user, _ethBalance, _erc20Claim) {
// TODO: check claim isnt in the valid set
assert(false); // random claim got accepted
assert(ghost_claimInTree[hash]);
assert(!ghost_claimed[hash]);
} catch {
// TODO: assert claim is not in tree
// TODO: assert user already claimed
assert(!ghost_claimInTree[hash] || ghost_claimed[hash]);
}
}

Expand All @@ -39,30 +37,12 @@ contract BalanceClaimerUnguidedHandlers is BalanceClaimerSetup {
uint256 _ethBalance,
IErc20BalanceWithdrawer.Erc20BalanceClaim[] calldata _erc20Claim
) external {
bytes32 hash = _hashClaim(_user, _ethBalance, _erc20Claim);
if (balanceClaimer.canClaim(_proof, _user, _ethBalance, _erc20Claim)) {
// TODO: check claim isnt in the valid set
assert(false); // random claim got accepted
assert(ghost_claimInTree[hash]);
assert(!ghost_claimed[hash]);
} else {
// TODO: assert claim is not in tree
// TODO: assert user already claimed
assert(!ghost_claimInTree[hash] || ghost_claimed[hash]);
}
}

function handler_withdrawEthBalance(address _user, uint256 _ethClaim, address _caller) external {
try optimismPortal.withdrawEthBalance(_user, _ethClaim) {
assert(_caller == address(balanceClaimer));
// TODO: update ghost variables
} catch {}
}

function handler_withdrawErc20Balance(
address _user,
IErc20BalanceWithdrawer.Erc20BalanceClaim[] calldata _tokenClaims,
address _caller
) external {
try l1StandardBridge.withdrawErc20Balance(_user, _tokenClaims) {
assert(_caller == address(balanceClaimer));
// TODO: update ghost variables
} catch {}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,16 @@ contract BalanceClaimerProperties is BalanceClaimerSetup {
/// @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++) {
// TODO: subtract claimed balances
assert(supportedTokens[i].balanceOf(address(l1StandardBridge)) == INITIAL_BALANCE);
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 {
// TODO: subtract claimed balances
assert(address(optimismPortal).balance == INITIAL_BALANCE);
assert(address(optimismPortal).balance == INITIAL_BALANCE- ghost_claimedEther);
}
}
Original file line number Diff line number Diff line change
@@ -1,33 +1,24 @@
// 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 {IERC20} from "forge-std/interfaces/IERC20.sol";
import {MockERC20} from "forge-std/mocks/MockERC20.sol";

import {CommonBase} from "forge-std/Base.sol";

import {FuzzERC20} from "./Tokens.t.sol";
import {Claims} from "./Claims.t.sol";

contract FuzzERC20 is MockERC20 {
function mint(address _to, uint256 _amount) public {
_mint(_to, _amount);
}
}
import {CommonBase} from "forge-std/Base.sol";
import {StdUtils} from "forge-std/StdUtils.sol";

contract BalanceClaimerSetup is CommonBase{
uint256 internal constant INITIAL_BALANCE = 100000e18;
contract BalanceClaimerSetup is CommonBase, StdUtils, Claims {
L1StandardBridge internal l1StandardBridge;
OptimismPortal internal optimismPortal;
BalanceClaimer internal balanceClaimer;
IERC20[] internal supportedTokens;

constructor() {
Proxy balanceClaimerProxy = new Proxy(address(this));
Expand All @@ -53,7 +44,7 @@ contract BalanceClaimerSetup is CommonBase{
balanceClaimerImpl.initialize.selector,
address(optimismPortalProxy),
address(l1StandardBridgeProxy),
bytes32(0)
tree[0]
)
);
optimismPortalProxy.upgradeTo(address(optimismPortalImpl));
Expand All @@ -63,13 +54,11 @@ contract BalanceClaimerSetup is CommonBase{
l1StandardBridge = L1StandardBridge(payable(l1StandardBridgeProxy));
balanceClaimer = BalanceClaimer(address(balanceClaimerProxy));

for (uint256 i = 0; i < 4; i++) {
FuzzERC20 token = new FuzzERC20();
token.initialize("name", "symbol", i == 0 ? 6 : 18);
token.mint(address(l1StandardBridge), INITIAL_BALANCE);
supportedTokens.push(token);
}
// 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
Expand All @@ -79,6 +68,6 @@ contract BalanceClaimerSetup is CommonBase{
assert(address(balanceClaimer.ethBalanceWithdrawer()) == address(optimismPortal));
assert(address(balanceClaimer.erc20BalanceWithdrawer()) == address(l1StandardBridge));
assert(address(l1StandardBridge.BALANCE_CLAIMER()) == address(balanceClaimer));
assert(balanceClaimer.root() == bytes32(0));
assert(balanceClaimer.root() == tree[0]);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.15;

import {IErc20BalanceWithdrawer} from "contracts/L1/interfaces/winddown/IErc20BalanceWithdrawer.sol";
import {Tokens} from "./Tokens.t.sol";

contract Claims is Tokens {
struct Claim {
address user;
uint256 ethAmount;
address[] tokens;
uint256[] tokenAmounts;
}

bytes32[] internal tree;
Claim[] internal ghost_validClaims;
mapping(bytes32 => bool) internal ghost_claimed;
mapping(bytes32 => bool) internal ghost_claimInTree;

constructor() {
address user = 0x0000000000000000000000000000000000010000;
address[] memory tokens = new address[](1);
tokens[0] = address(supportedTokens[0]);
uint256[] memory amounts = new uint256[](1);
amounts[0] = 1 ether;
Claim memory claim = Claim({user: user, ethAmount: 1 ether, tokens: tokens, tokenAmounts: amounts});
ghost_validClaims.push(claim);
ghost_claimInTree[_hashClaim(claim)] = true;
tree.push(_hashClaim(claim));
}

function _hashClaim(Claim memory claim) internal pure returns (bytes32) {
IErc20BalanceWithdrawer.Erc20BalanceClaim[] memory erc20Claims =
new IErc20BalanceWithdrawer.Erc20BalanceClaim[](claim.tokens.length);
for (uint256 i = 0; i < claim.tokens.length; i++) {
erc20Claims[i].token = claim.tokens[i];
erc20Claims[i].balance = claim.tokenAmounts[i];
}
return keccak256(bytes.concat(keccak256(abi.encode(claim.user, claim.ethAmount, erc20Claims))));
}

function _hashClaim(address user, uint256 ethAmount, IErc20BalanceWithdrawer.Erc20BalanceClaim[] memory erc20Claims)
internal
pure
returns (bytes32)
{
return keccak256(bytes.concat(keccak256(abi.encode(user, ethAmount, erc20Claims))));
}

function _claimToErc20ClaimArray(Claim memory claim)
internal
pure
returns (IErc20BalanceWithdrawer.Erc20BalanceClaim[] memory)
{
IErc20BalanceWithdrawer.Erc20BalanceClaim[] memory erc20Claims =
new IErc20BalanceWithdrawer.Erc20BalanceClaim[](claim.tokens.length);
for (uint256 i = 0; i < claim.tokens.length; i++) {
erc20Claims[i].token = claim.tokens[i];
erc20Claims[i].balance = claim.tokenAmounts[i];
}
return erc20Claims;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.15;

import {MockERC20} from "forge-std/mocks/MockERC20.sol";
import {IERC20} from "forge-std/interfaces/IERC20.sol";

contract FuzzERC20 is MockERC20 {
function mint(address _to, uint256 _amount) public {
_mint(_to, _amount);
}
}

contract Tokens {
uint8 internal constant TOKENS = 4;
uint256 internal constant INITIAL_BALANCE = 100000e18;
IERC20[] internal supportedTokens;

mapping(address => uint256) internal ghost_claimedTokens;
uint256 internal ghost_claimedEther;

constructor() {
for (uint256 i = 0; i < TOKENS; i++) {
// TODO: use bytecode from production tokens
FuzzERC20 token = new FuzzERC20();
// TODO: use 6 decimals for usdt
token.initialize("name", "symbol", 18);
supportedTokens.push(token);
}
}
}

0 comments on commit 1b694c5

Please sign in to comment.