From 35d5ad8180b7136570598815e8b078fdc90220da Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Mon, 6 Nov 2023 11:07:29 +0200 Subject: [PATCH 01/22] feat: add eth vault gnosis safe module --- src/interfaces/IETH2DepositContract.sol | 34 +++++++++++++++ src/safe-modules/ETHVaultSafeModule.sol | 56 +++++++++++++++++++++++++ 2 files changed, 90 insertions(+) create mode 100644 src/interfaces/IETH2DepositContract.sol create mode 100644 src/safe-modules/ETHVaultSafeModule.sol diff --git a/src/interfaces/IETH2DepositContract.sol b/src/interfaces/IETH2DepositContract.sol new file mode 100644 index 0000000..3dad0db --- /dev/null +++ b/src/interfaces/IETH2DepositContract.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +interface IETH2DepositContract { + /// @notice A processed deposit event. + event DepositEvent( + bytes pubkey, + bytes withdrawal_credentials, + bytes amount, + bytes signature, + bytes index + ); + + /// @notice Submit a Phase 0 DepositData object. + /// @param pubkey A BLS12-381 public key. + /// @param withdrawal_credentials Commitment to a public key for withdrawals. + /// @param signature A BLS12-381 signature. + /// @param deposit_data_root The SHA-256 hash of the SSZ-encoded DepositData object. + /// Used as a protection against malformed input. + function deposit( + bytes calldata pubkey, + bytes calldata withdrawal_credentials, + bytes calldata signature, + bytes32 deposit_data_root + ) external payable; + + /// @notice Query the current deposit root hash. + /// @return The deposit root hash. + function get_deposit_root() external view returns (bytes32); + + /// @notice Query the current deposit count. + /// @return The deposit count encoded as a little endian 64-bit number. + function get_deposit_count() external view returns (bytes memory); +} \ No newline at end of file diff --git a/src/safe-modules/ETHVaultSafeModule.sol b/src/safe-modules/ETHVaultSafeModule.sol new file mode 100644 index 0000000..a61d162 --- /dev/null +++ b/src/safe-modules/ETHVaultSafeModule.sol @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.19; +import {ERC20} from "solady/tokens/ERC20.sol"; +import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol"; +import {IETH2DepositContract} from "../interfaces/IETH2DepositContract.sol"; + +contract ETHVaultSafeModule is ERC20 { + using SafeTransferLib for address; + + IETH2DepositContract immutable public depositContract; + + constructor(address eth2DepositContract) { + depositContract = IETH2DepositContract(eth2DepositContract); + } + + function name() public view override returns (string memory) { + // return + } + + function symbol() public view override returns (string memory) { + + } + + function setup() external { + + } + + function depositValidator( + bytes calldata pubkey, + bytes calldata withdrawal_credentials, + bytes calldata signature, + bytes32 deposit_data_root, + uint256 amount + ) external payable { + depositContract.deposit{value: amount}( + pubkey, + withdrawal_credentials, + signature, + deposit_data_root + ); + } + + function _deposit(address to, uint256 amount) internal { + _mint(to, amount); + } + + function redeem(address to, uint256 amount) external { + _burn(msg.sender, amount); + to.safeTransferETH(amount); + } + + receive() external payable { + _deposit(msg.sender, msg.value); + } + +} \ No newline at end of file From 4970baf1457dba2e509da217113c77844a676dee Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Mon, 6 Nov 2023 11:28:36 +0200 Subject: [PATCH 02/22] forge install: safe-contracts v1.4.1 --- .gitmodules | 3 +++ lib/safe-contracts | 1 + 2 files changed, 4 insertions(+) create mode 160000 lib/safe-contracts diff --git a/.gitmodules b/.gitmodules index 4a0b298..1dcb42a 100644 --- a/.gitmodules +++ b/.gitmodules @@ -15,3 +15,6 @@ [submodule "lib/solmate"] path = lib/solmate url = https://github.com/transmissions11/solmate +[submodule "lib/safe-contracts"] + path = lib/safe-contracts + url = https://github.com/safe-global/safe-contracts diff --git a/lib/safe-contracts b/lib/safe-contracts new file mode 160000 index 0000000..bf943f8 --- /dev/null +++ b/lib/safe-contracts @@ -0,0 +1 @@ +Subproject commit bf943f80fec5ac647159d26161446ac5d716a294 From a5eef6060289736d52a03c5891e851a6a432b79f Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Fri, 10 Nov 2023 17:03:12 +0300 Subject: [PATCH 03/22] add: safe module implementation --- foundry.toml | 1 + src/safe-modules/ETHVaultSafeModule.sol | 9 ++++----- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/foundry.toml b/foundry.toml index f92eb61..ef28c39 100644 --- a/foundry.toml +++ b/foundry.toml @@ -7,6 +7,7 @@ remappings = [ 'solmate/=lib/solmate/src/', 'splits-tests/=lib/splits-utils/test/', 'solady/=lib/solady/src/', + 'safe-contracts/=lib/safe-contracts/contracts', ] solc_version = '0.8.19' gas_reports = ["*"] diff --git a/src/safe-modules/ETHVaultSafeModule.sol b/src/safe-modules/ETHVaultSafeModule.sol index a61d162..7c04fb0 100644 --- a/src/safe-modules/ETHVaultSafeModule.sol +++ b/src/safe-modules/ETHVaultSafeModule.sol @@ -3,8 +3,10 @@ pragma solidity 0.8.19; import {ERC20} from "solady/tokens/ERC20.sol"; import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol"; import {IETH2DepositContract} from "../interfaces/IETH2DepositContract.sol"; +import { HandlerContext } from "safe-contracts/handler/HandlerContext.sol"; -contract ETHVaultSafeModule is ERC20 { + +contract ETHVaultSafeModule is ERC20, HandlerContext { using SafeTransferLib for address; IETH2DepositContract immutable public depositContract; @@ -21,10 +23,6 @@ contract ETHVaultSafeModule is ERC20 { } - function setup() external { - - } - function depositValidator( bytes calldata pubkey, bytes calldata withdrawal_credentials, @@ -32,6 +30,7 @@ contract ETHVaultSafeModule is ERC20 { bytes32 deposit_data_root, uint256 amount ) external payable { + bytes memory data = abi.encodeCall(depositContract.deposit, ) depositContract.deposit{value: amount}( pubkey, withdrawal_credentials, From 5b267798d4924057cc363d544f6050f74aeeabed Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Thu, 23 Nov 2023 11:39:28 +0200 Subject: [PATCH 04/22] add: simpleETHcontribution vault implementation and test --- script/data/lido-data-sample-json | 44 ++++ src/interfaces/IGnosisSafe.sol | 22 ++ src/safe-modules/ETHVaultSafeModule.sol | 55 ----- .../SimpleETHContributionVault.sol | 130 +++++++++++ .../safe/SimpleETHContributionVault.t.sol | 213 ++++++++++++++++++ 5 files changed, 409 insertions(+), 55 deletions(-) create mode 100644 script/data/lido-data-sample-json create mode 100644 src/interfaces/IGnosisSafe.sol delete mode 100644 src/safe-modules/ETHVaultSafeModule.sol create mode 100644 src/safe-modules/SimpleETHContributionVault.sol create mode 100644 src/test/safe/SimpleETHContributionVault.t.sol diff --git a/script/data/lido-data-sample-json b/script/data/lido-data-sample-json new file mode 100644 index 0000000..a7e36bc --- /dev/null +++ b/script/data/lido-data-sample-json @@ -0,0 +1,44 @@ +[ + { + "accounts": [ + "0x0000000000000000000000000000000000000001", + "0x0000000000000000000000000000000000000002", + "0x0000000000000000000000000000000000000003" + ], + "controller": "0x0000000000000000000000000000000000000004", + "distributorFee": 1, + "percentAllocations": [ + 840000, + 60000, + 100000 + ] + }, + { + "accounts": [ + "0x0000000000000000000000000000000000000001", + "0x0000000000000000000000000000000000000002", + "0x0000000000000000000000000000000000000003" + ], + "controller": "0x0000000000000000000000000000000000000004", + "distributorFee": 1, + "percentAllocations": [ + 840000, + 60000, + 100000 + ] + }, + { + "accounts": [ + "0x0000000000000000000000000000000000000001", + "0x0000000000000000000000000000000000000002", + "0x0000000000000000000000000000000000000003" + ], + "controller": "0x0000000000000000000000000000000000000004", + "distributorFee": 1, + "percentAllocations": [ + 840000, + 60000, + 100000 + ] + } +] \ No newline at end of file diff --git a/src/interfaces/IGnosisSafe.sol b/src/interfaces/IGnosisSafe.sol new file mode 100644 index 0000000..0c35294 --- /dev/null +++ b/src/interfaces/IGnosisSafe.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.18; + +interface IGnosisSafe { + + enum Operation { + Call, + DelegateCall + } + + /// @dev Allows a Module to execute a Safe transaction without any further confirmations. + /// @param to Destination address of module transaction. + /// @param value Ether value of module transaction. + /// @param data Data payload of module transaction. + /// @param operation Operation type of module transaction. + function execTransactionFromModule( + address to, + uint256 value, + bytes calldata data, + Operation operation + ) external returns (bool success); +} \ No newline at end of file diff --git a/src/safe-modules/ETHVaultSafeModule.sol b/src/safe-modules/ETHVaultSafeModule.sol deleted file mode 100644 index 7c04fb0..0000000 --- a/src/safe-modules/ETHVaultSafeModule.sol +++ /dev/null @@ -1,55 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity 0.8.19; -import {ERC20} from "solady/tokens/ERC20.sol"; -import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol"; -import {IETH2DepositContract} from "../interfaces/IETH2DepositContract.sol"; -import { HandlerContext } from "safe-contracts/handler/HandlerContext.sol"; - - -contract ETHVaultSafeModule is ERC20, HandlerContext { - using SafeTransferLib for address; - - IETH2DepositContract immutable public depositContract; - - constructor(address eth2DepositContract) { - depositContract = IETH2DepositContract(eth2DepositContract); - } - - function name() public view override returns (string memory) { - // return - } - - function symbol() public view override returns (string memory) { - - } - - function depositValidator( - bytes calldata pubkey, - bytes calldata withdrawal_credentials, - bytes calldata signature, - bytes32 deposit_data_root, - uint256 amount - ) external payable { - bytes memory data = abi.encodeCall(depositContract.deposit, ) - depositContract.deposit{value: amount}( - pubkey, - withdrawal_credentials, - signature, - deposit_data_root - ); - } - - function _deposit(address to, uint256 amount) internal { - _mint(to, amount); - } - - function redeem(address to, uint256 amount) external { - _burn(msg.sender, amount); - to.safeTransferETH(amount); - } - - receive() external payable { - _deposit(msg.sender, msg.value); - } - -} \ No newline at end of file diff --git a/src/safe-modules/SimpleETHContributionVault.sol b/src/safe-modules/SimpleETHContributionVault.sol new file mode 100644 index 0000000..702c7d6 --- /dev/null +++ b/src/safe-modules/SimpleETHContributionVault.sol @@ -0,0 +1,130 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.19; +import {ERC20} from "solady/tokens/ERC20.sol"; +import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol"; +import {IETH2DepositContract} from "../interfaces/IETH2DepositContract.sol"; + + +contract SimpleETHContributionVault { + using SafeTransferLib for address; + + /// @notice unathorised user + /// @param user address of unauthorized user + error Unauthorized(address user); + + /// @notice cannot rage quit + error CannotRageQuit(); + + /// @notice incomplete contribution + error IncompleteContribution(uint256 actual, uint256 expected); + + /// @notice Amount of ETH validator stake + uint256 internal constant ETH_STAKE = 32 ether; + + event Deposit(address to, uint256 amount); + + event DepositValidator( + bytes[] pubkeys, + bytes[] withdrawal_credentials, + bytes[] signatures, + bytes32[] deposit_data_roots + ); + event RageQuit(address to, uint256 amount); + event RescueFunds(uint256 amount); + + /// @notice + mapping (address => uint256) public userBalances; + + IETH2DepositContract public immutable depositContract; + + address public immutable safe; + + bool public activated; + + modifier onlySafe() { + if(msg.sender != safe) revert Unauthorized(msg.sender); + _; + } + + constructor( + address _safe, + address eth2DepositContract + ) { + safe = _safe; + depositContract = IETH2DepositContract(eth2DepositContract); + } + + receive() external payable { + _deposit(msg.sender, msg.value); + } + + /// Deposit ETH into ETH2 deposit contract + /// @param pubkeys Array of validator pub keys + /// @param withdrawal_credentials Array of validator withdrawal credentials + /// @param signatures Array of validator signatures + /// @param deposit_data_roots Array of validator deposit data roots + function depositValidator( + bytes[] calldata pubkeys, + bytes[] calldata withdrawal_credentials, + bytes[] calldata signatures, + bytes32[] calldata deposit_data_roots + ) external payable onlySafe { + uint256 size = pubkeys.length; + + if (address(this).balance < size * ETH_STAKE ) { + revert IncompleteContribution(address(this).balance, ETH_STAKE * size); + } + + for(uint256 i = 0; i < size;) { + depositContract.deposit{value: ETH_STAKE}( + pubkeys[i], + withdrawal_credentials[i], + signatures[i], + deposit_data_roots[i] + ); + + unchecked { + i++; + } + } + + activated = true; + + emit DepositValidator( + pubkeys, + withdrawal_credentials, + signatures, + deposit_data_roots + ); + } + + /// Exit contribution vault prior to deposit starts + /// It allows a contributor to exit the vault before the validators are + /// activated + /// @param to Address to send funds to + /// @param amount balance to withdraw + function rageQuit(address to, uint256 amount) external { + if (activated == true) revert CannotRageQuit(); + + userBalances[msg.sender] -= amount; + to.safeTransferETH(amount); + + emit RageQuit(to, amount); + } + + /// Rescue non-ETH tokens stuck to the safe + /// @param token Token address + /// @param amount amount of balance to transfer to Safe + function rescueFunds(address token, uint256 amount) external { + token.safeTransfer(safe, amount); + emit RescueFunds(amount); + } + + + function _deposit(address to, uint256 amount) internal { + userBalances[to] += amount; + emit Deposit(to, amount); + } + + +} \ No newline at end of file diff --git a/src/test/safe/SimpleETHContributionVault.t.sol b/src/test/safe/SimpleETHContributionVault.t.sol new file mode 100644 index 0000000..2077022 --- /dev/null +++ b/src/test/safe/SimpleETHContributionVault.t.sol @@ -0,0 +1,213 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.19; + +import "forge-std/Test.sol"; +import {MockERC20} from "src/test/utils/mocks/MockERC20.sol"; +import {SimpleETHContributionVault} from "src/safe-modules/SimpleETHContributionVault.sol"; + + +contract ETHVaultSafeModuleTest is Test { + error CannotRageQuit(); + error Unauthorized(address user); + + event Deposit(address to, uint256 amount); + event DepositValidator( + bytes[] pubkeys, + bytes[] withdrawal_credentials, + bytes[] signatures, + bytes32[] deposit_data_roots + ); + event RageQuit(address to, uint256 amount); + event RescueFunds(uint256 amount); + + address constant ETH_DEPOSIT_CONTRACT = address(0x0); + uint256 internal constant ETH_STAKE = 32 ether; + + SimpleETHContributionVault contributionVault; + + address safe; + address user1; + address user2; + address user3; + MockERC20 mERC20; + + function setUp() public { + safe = makeAddr("safe"); + contributionVault = new SimpleETHContributionVault( + safe, + ETH_DEPOSIT_CONTRACT + ); + + mERC20 = new MockERC20("Test Token", "TOK", 18); + mERC20.mint(type(uint256).max); + } + + function test_deposit() external { + vm.deal(user1, ETH_STAKE); + + vm.expectEmit(false, false, false, true); + emit Deposit(user1, ETH_STAKE); + + vm.prank(user1); + payable(contributionVault).transfer(ETH_STAKE); + + assertEq( + contributionVault.userBalances(user1), + ETH_STAKE, + "failed to credit user balance" + ); + } + + function testFuzz_deposit( + address user, + uint256 amount + ) external { + vm.deal(user, amount); + + vm.expectEmit(false, false, false, true); + + vm.prank(user); + payable(contributionVault).transfer(amount); + + assertEq( + contributionVault.userBalances(user), + amount + ); + } + + function test_rageQuit() external { + vm.deal(user1, ETH_STAKE); + + vm.prank(user1); + payable(contributionVault).transfer(ETH_STAKE); + + vm.expectEmit(false, false, false, true); + emit RageQuit(user1, ETH_STAKE); + + contributionVault.rageQuit(user1, ETH_STAKE); + } + + function testFuzz_rageQuit( + address user, + uint256 amount + ) external { + vm.deal(user, amount); + + vm.prank(user); + payable(contributionVault).transfer(amount); + + vm.expectEmit(false, false, false, true); + emit RageQuit(user, amount); + + contributionVault.rageQuit(user, amount); + } + + function test_cannotRageQuitAfterDeposit() external { + vm.deal(user1, ETH_STAKE); + + vm.prank(user1); + payable(contributionVault).transfer(ETH_STAKE); + + bytes[] memory pubkeys = new bytes[](1); + pubkeys[0] = bytes(""); + bytes[] memory withdrawal_credentials = new bytes[](1); + withdrawal_credentials[0] = bytes(""); + bytes[] memory signatures = new bytes[](1); + signatures[0] = bytes(""); + bytes32[] memory deposit_data_roots = new bytes32[](1); + deposit_data_roots[0] = bytes32(0); + + contributionVault.depositValidator( + pubkeys, + withdrawal_credentials, + signatures, + deposit_data_roots + ); + + vm.expectRevert(CannotRageQuit.selector); + contributionVault.rageQuit( + user1, + ETH_STAKE + ); + } + + function test_depositValidator() external { + vm.deal(user1, ETH_STAKE); + vm.prank(user1); + payable(contributionVault).transfer(ETH_STAKE); + + ( + bytes[] memory pubkeys, + bytes[] memory withdrawal_credentials, + bytes[] memory signatures, + bytes32[] memory deposit_data_roots + ) = getETHValidatorData(); + + vm.prank(safe); + contributionVault.depositValidator( + pubkeys, + withdrawal_credentials, + signatures, + deposit_data_roots + ); + } + + function test_OnlySafeDepositValidator() external { + ( + bytes[] memory pubkeys, + bytes[] memory withdrawal_credentials, + bytes[] memory signatures, + bytes32[] memory deposit_data_roots + ) = getETHValidatorData(); + + vm.expectRevert( + abi.encodeWithSelector(Unauthorized.selector, address(this)) + ); + contributionVault.depositValidator( + pubkeys, + withdrawal_credentials, + signatures, + deposit_data_roots + ); + } + + function test_rescueFunds() external { + uint256 amount = 1 ether; + mERC20.transfer(address(contributionVault), amount); + + vm.expectEmit(false, false, false, true); + emit RescueFunds(amount); + + contributionVault.rescueFunds(address(mERC20), amount); + } + + function testFuzz_rescueFundETH( + uint256 amount + ) external { + mERC20.transfer(address(contributionVault), amount); + vm.expectEmit(false, false, false, true); + emit RescueFunds(amount); + + contributionVault.rescueFunds(address(mERC20), amount); + } +} + +function getETHValidatorData() pure returns ( + bytes[] memory pubkeys, + bytes[] memory withdrawal_credentials, + bytes[] memory signatures, + bytes32[] memory deposit_data_roots +) { + + pubkeys = new bytes[](1); + pubkeys[0] = bytes(""); + + withdrawal_credentials = new bytes[](1); + withdrawal_credentials[0] = bytes(""); + + signatures = new bytes[](1); + signatures[0] = bytes(""); + + deposit_data_roots = new bytes32[](1); + deposit_data_roots[0] = bytes32(0); +} \ No newline at end of file From a96702cfa54208d81588148d69ad66c98331d03c Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Thu, 23 Nov 2023 12:08:04 +0200 Subject: [PATCH 05/22] chore: add validator test param --- .../safe/SimpleETHContributionVault.t.sol | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/test/safe/SimpleETHContributionVault.t.sol b/src/test/safe/SimpleETHContributionVault.t.sol index 2077022..7ce6ac8 100644 --- a/src/test/safe/SimpleETHContributionVault.t.sol +++ b/src/test/safe/SimpleETHContributionVault.t.sol @@ -20,7 +20,7 @@ contract ETHVaultSafeModuleTest is Test { event RageQuit(address to, uint256 amount); event RescueFunds(uint256 amount); - address constant ETH_DEPOSIT_CONTRACT = address(0x0); + address constant ETH_DEPOSIT_CONTRACT = 0x00000000219ab540356cBB839Cbe05303d7705Fa; uint256 internal constant ETH_STAKE = 32 ether; SimpleETHContributionVault contributionVault; @@ -32,6 +32,9 @@ contract ETHVaultSafeModuleTest is Test { MockERC20 mERC20; function setUp() public { + uint256 mainnetBlock = 17_421_005; + vm.createSelectFork(getChain("mainnet").rpcUrl, mainnetBlock); + safe = makeAddr("safe"); contributionVault = new SimpleETHContributionVault( safe, @@ -108,14 +111,12 @@ contract ETHVaultSafeModuleTest is Test { vm.prank(user1); payable(contributionVault).transfer(ETH_STAKE); - bytes[] memory pubkeys = new bytes[](1); - pubkeys[0] = bytes(""); - bytes[] memory withdrawal_credentials = new bytes[](1); - withdrawal_credentials[0] = bytes(""); - bytes[] memory signatures = new bytes[](1); - signatures[0] = bytes(""); - bytes32[] memory deposit_data_roots = new bytes32[](1); - deposit_data_roots[0] = bytes32(0); + ( + bytes[] memory pubkeys, + bytes[] memory withdrawal_credentials, + bytes[] memory signatures, + bytes32[] memory deposit_data_roots + ) = getETHValidatorData(); contributionVault.depositValidator( pubkeys, @@ -198,16 +199,15 @@ function getETHValidatorData() pure returns ( bytes[] memory signatures, bytes32[] memory deposit_data_roots ) { - pubkeys = new bytes[](1); - pubkeys[0] = bytes(""); + pubkeys[0] = bytes("0x83fa9495bb0944a74fc6a66e699039b66134b22a52a710f8d0f7cde318a2db3da40081a5867667389d206e21b5e37e52"); withdrawal_credentials = new bytes[](1); - withdrawal_credentials[0] = bytes(""); + withdrawal_credentials[0] = bytes("0x010000000000000000000000e839a3e9efb32c6a56ab7128e51056585275506c"); signatures = new bytes[](1); - signatures[0] = bytes(""); + signatures[0] = bytes("0x95f00435e80e59a8fed41581e2050a3fe56272d6be845686ef014a57909c6621d7847fa550b77cb8e541b955f3c2ea031983d9e4336f215e75c8ba75d94e05f1e23460de6611a980ef629d3e32ca09cffaf2a63372496079b1ee22310d336ded"); deposit_data_roots = new bytes32[](1); - deposit_data_roots[0] = bytes32(0); + deposit_data_roots[0] = bytes32(0x2d33b096d02f7a53dbbdaf840755dfc6b9269be39cff6b0d2701d15b4c1b639c); } \ No newline at end of file From e8c73d952d389d6515af1a01f10bc7966808c585 Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Thu, 23 Nov 2023 12:09:33 +0200 Subject: [PATCH 06/22] chore: apply forge fmt to SimpleSimpleETHContributionVault and teset --- .../SimpleETHContributionVault.sol | 189 +++++----- .../safe/SimpleETHContributionVault.t.sol | 327 ++++++++---------- 2 files changed, 233 insertions(+), 283 deletions(-) diff --git a/src/safe-modules/SimpleETHContributionVault.sol b/src/safe-modules/SimpleETHContributionVault.sol index 702c7d6..a51109c 100644 --- a/src/safe-modules/SimpleETHContributionVault.sol +++ b/src/safe-modules/SimpleETHContributionVault.sol @@ -1,130 +1,113 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity 0.8.19; + import {ERC20} from "solady/tokens/ERC20.sol"; import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol"; import {IETH2DepositContract} from "../interfaces/IETH2DepositContract.sol"; - contract SimpleETHContributionVault { - using SafeTransferLib for address; + using SafeTransferLib for address; - /// @notice unathorised user - /// @param user address of unauthorized user - error Unauthorized(address user); + /// @notice unathorised user + /// @param user address of unauthorized user + error Unauthorized(address user); - /// @notice cannot rage quit - error CannotRageQuit(); + /// @notice cannot rage quit + error CannotRageQuit(); - /// @notice incomplete contribution - error IncompleteContribution(uint256 actual, uint256 expected); + /// @notice incomplete contribution + error IncompleteContribution(uint256 actual, uint256 expected); - /// @notice Amount of ETH validator stake - uint256 internal constant ETH_STAKE = 32 ether; + /// @notice Amount of ETH validator stake + uint256 internal constant ETH_STAKE = 32 ether; - event Deposit(address to, uint256 amount); + event Deposit(address to, uint256 amount); - event DepositValidator( - bytes[] pubkeys, - bytes[] withdrawal_credentials, - bytes[] signatures, - bytes32[] deposit_data_roots - ); - event RageQuit(address to, uint256 amount); - event RescueFunds(uint256 amount); + event DepositValidator( + bytes[] pubkeys, bytes[] withdrawal_credentials, bytes[] signatures, bytes32[] deposit_data_roots + ); + event RageQuit(address to, uint256 amount); + event RescueFunds(uint256 amount); - /// @notice - mapping (address => uint256) public userBalances; + /// @notice + mapping(address => uint256) public userBalances; - IETH2DepositContract public immutable depositContract; + IETH2DepositContract public immutable depositContract; - address public immutable safe; + address public immutable safe; - bool public activated; + bool public activated; - modifier onlySafe() { - if(msg.sender != safe) revert Unauthorized(msg.sender); - _; - } + modifier onlySafe() { + if (msg.sender != safe) revert Unauthorized(msg.sender); + _; + } - constructor( - address _safe, - address eth2DepositContract - ) { - safe = _safe; - depositContract = IETH2DepositContract(eth2DepositContract); - } + constructor(address _safe, address eth2DepositContract) { + safe = _safe; + depositContract = IETH2DepositContract(eth2DepositContract); + } - receive() external payable { - _deposit(msg.sender, msg.value); - } + receive() external payable { + _deposit(msg.sender, msg.value); + } - /// Deposit ETH into ETH2 deposit contract - /// @param pubkeys Array of validator pub keys - /// @param withdrawal_credentials Array of validator withdrawal credentials - /// @param signatures Array of validator signatures - /// @param deposit_data_roots Array of validator deposit data roots - function depositValidator( - bytes[] calldata pubkeys, - bytes[] calldata withdrawal_credentials, - bytes[] calldata signatures, - bytes32[] calldata deposit_data_roots - ) external payable onlySafe { - uint256 size = pubkeys.length; - - if (address(this).balance < size * ETH_STAKE ) { - revert IncompleteContribution(address(this).balance, ETH_STAKE * size); - } - - for(uint256 i = 0; i < size;) { - depositContract.deposit{value: ETH_STAKE}( - pubkeys[i], - withdrawal_credentials[i], - signatures[i], - deposit_data_roots[i] - ); - - unchecked { - i++; - } - } - - activated = true; - - emit DepositValidator( - pubkeys, - withdrawal_credentials, - signatures, - deposit_data_roots - ); - } + /// Deposit ETH into ETH2 deposit contract + /// @param pubkeys Array of validator pub keys + /// @param withdrawal_credentials Array of validator withdrawal credentials + /// @param signatures Array of validator signatures + /// @param deposit_data_roots Array of validator deposit data roots + function depositValidator( + bytes[] calldata pubkeys, + bytes[] calldata withdrawal_credentials, + bytes[] calldata signatures, + bytes32[] calldata deposit_data_roots + ) external payable onlySafe { + uint256 size = pubkeys.length; - /// Exit contribution vault prior to deposit starts - /// It allows a contributor to exit the vault before the validators are - /// activated - /// @param to Address to send funds to - /// @param amount balance to withdraw - function rageQuit(address to, uint256 amount) external { - if (activated == true) revert CannotRageQuit(); - - userBalances[msg.sender] -= amount; - to.safeTransferETH(amount); - - emit RageQuit(to, amount); + if (address(this).balance < size * ETH_STAKE) { + revert IncompleteContribution(address(this).balance, ETH_STAKE * size); } - /// Rescue non-ETH tokens stuck to the safe - /// @param token Token address - /// @param amount amount of balance to transfer to Safe - function rescueFunds(address token, uint256 amount) external { - token.safeTransfer(safe, amount); - emit RescueFunds(amount); - } + for (uint256 i = 0; i < size;) { + depositContract.deposit{value: ETH_STAKE}( + pubkeys[i], withdrawal_credentials[i], signatures[i], deposit_data_roots[i] + ); - - function _deposit(address to, uint256 amount) internal { - userBalances[to] += amount; - emit Deposit(to, amount); + unchecked { + i++; + } } - -} \ No newline at end of file + activated = true; + + emit DepositValidator(pubkeys, withdrawal_credentials, signatures, deposit_data_roots); + } + + /// Exit contribution vault prior to deposit starts + /// It allows a contributor to exit the vault before the validators are + /// activated + /// @param to Address to send funds to + /// @param amount balance to withdraw + function rageQuit(address to, uint256 amount) external { + if (activated == true) revert CannotRageQuit(); + + userBalances[msg.sender] -= amount; + to.safeTransferETH(amount); + + emit RageQuit(to, amount); + } + + /// Rescue non-ETH tokens stuck to the safe + /// @param token Token address + /// @param amount amount of balance to transfer to Safe + function rescueFunds(address token, uint256 amount) external { + token.safeTransfer(safe, amount); + emit RescueFunds(amount); + } + + function _deposit(address to, uint256 amount) internal { + userBalances[to] += amount; + emit Deposit(to, amount); + } +} diff --git a/src/test/safe/SimpleETHContributionVault.t.sol b/src/test/safe/SimpleETHContributionVault.t.sol index 7ce6ac8..a541f16 100644 --- a/src/test/safe/SimpleETHContributionVault.t.sol +++ b/src/test/safe/SimpleETHContributionVault.t.sol @@ -5,209 +5,176 @@ import "forge-std/Test.sol"; import {MockERC20} from "src/test/utils/mocks/MockERC20.sol"; import {SimpleETHContributionVault} from "src/safe-modules/SimpleETHContributionVault.sol"; - contract ETHVaultSafeModuleTest is Test { - error CannotRageQuit(); - error Unauthorized(address user); - - event Deposit(address to, uint256 amount); - event DepositValidator( - bytes[] pubkeys, - bytes[] withdrawal_credentials, - bytes[] signatures, - bytes32[] deposit_data_roots - ); - event RageQuit(address to, uint256 amount); - event RescueFunds(uint256 amount); - - address constant ETH_DEPOSIT_CONTRACT = 0x00000000219ab540356cBB839Cbe05303d7705Fa; - uint256 internal constant ETH_STAKE = 32 ether; - - SimpleETHContributionVault contributionVault; - - address safe; - address user1; - address user2; - address user3; - MockERC20 mERC20; - - function setUp() public { - uint256 mainnetBlock = 17_421_005; - vm.createSelectFork(getChain("mainnet").rpcUrl, mainnetBlock); - - safe = makeAddr("safe"); - contributionVault = new SimpleETHContributionVault( + error CannotRageQuit(); + error Unauthorized(address user); + + event Deposit(address to, uint256 amount); + event DepositValidator( + bytes[] pubkeys, bytes[] withdrawal_credentials, bytes[] signatures, bytes32[] deposit_data_roots + ); + event RageQuit(address to, uint256 amount); + event RescueFunds(uint256 amount); + + address constant ETH_DEPOSIT_CONTRACT = 0x00000000219ab540356cBB839Cbe05303d7705Fa; + uint256 internal constant ETH_STAKE = 32 ether; + + SimpleETHContributionVault contributionVault; + + address safe; + address user1; + address user2; + address user3; + MockERC20 mERC20; + + function setUp() public { + uint256 mainnetBlock = 17_421_005; + vm.createSelectFork(getChain("mainnet").rpcUrl, mainnetBlock); + + safe = makeAddr("safe"); + contributionVault = new SimpleETHContributionVault( safe, ETH_DEPOSIT_CONTRACT ); - - mERC20 = new MockERC20("Test Token", "TOK", 18); - mERC20.mint(type(uint256).max); - } - - function test_deposit() external { - vm.deal(user1, ETH_STAKE); - - vm.expectEmit(false, false, false, true); - emit Deposit(user1, ETH_STAKE); - - vm.prank(user1); - payable(contributionVault).transfer(ETH_STAKE); - - assertEq( - contributionVault.userBalances(user1), - ETH_STAKE, - "failed to credit user balance" - ); - } - function testFuzz_deposit( - address user, - uint256 amount - ) external { - vm.deal(user, amount); + mERC20 = new MockERC20("Test Token", "TOK", 18); + mERC20.mint(type(uint256).max); + } - vm.expectEmit(false, false, false, true); + function test_deposit() external { + vm.deal(user1, ETH_STAKE); - vm.prank(user); - payable(contributionVault).transfer(amount); + vm.expectEmit(false, false, false, true); + emit Deposit(user1, ETH_STAKE); - assertEq( - contributionVault.userBalances(user), - amount - ); - } - - function test_rageQuit() external { - vm.deal(user1, ETH_STAKE); + vm.prank(user1); + payable(contributionVault).transfer(ETH_STAKE); - vm.prank(user1); - payable(contributionVault).transfer(ETH_STAKE); + assertEq(contributionVault.userBalances(user1), ETH_STAKE, "failed to credit user balance"); + } - vm.expectEmit(false, false, false, true); - emit RageQuit(user1, ETH_STAKE); + function testFuzz_deposit(address user, uint256 amount) external { + vm.deal(user, amount); - contributionVault.rageQuit(user1, ETH_STAKE); - } + vm.expectEmit(false, false, false, true); - function testFuzz_rageQuit( - address user, - uint256 amount - ) external { - vm.deal(user, amount); + vm.prank(user); + payable(contributionVault).transfer(amount); - vm.prank(user); - payable(contributionVault).transfer(amount); + assertEq(contributionVault.userBalances(user), amount); + } - vm.expectEmit(false, false, false, true); - emit RageQuit(user, amount); + function test_rageQuit() external { + vm.deal(user1, ETH_STAKE); - contributionVault.rageQuit(user, amount); - } + vm.prank(user1); + payable(contributionVault).transfer(ETH_STAKE); - function test_cannotRageQuitAfterDeposit() external { - vm.deal(user1, ETH_STAKE); + vm.expectEmit(false, false, false, true); + emit RageQuit(user1, ETH_STAKE); - vm.prank(user1); - payable(contributionVault).transfer(ETH_STAKE); + contributionVault.rageQuit(user1, ETH_STAKE); + } - ( - bytes[] memory pubkeys, - bytes[] memory withdrawal_credentials, - bytes[] memory signatures, - bytes32[] memory deposit_data_roots - ) = getETHValidatorData(); + function testFuzz_rageQuit(address user, uint256 amount) external { + vm.deal(user, amount); - contributionVault.depositValidator( - pubkeys, - withdrawal_credentials, - signatures, - deposit_data_roots - ); + vm.prank(user); + payable(contributionVault).transfer(amount); - vm.expectRevert(CannotRageQuit.selector); - contributionVault.rageQuit( - user1, - ETH_STAKE - ); - } - - function test_depositValidator() external { - vm.deal(user1, ETH_STAKE); - vm.prank(user1); - payable(contributionVault).transfer(ETH_STAKE); - - ( - bytes[] memory pubkeys, - bytes[] memory withdrawal_credentials, - bytes[] memory signatures, - bytes32[] memory deposit_data_roots - ) = getETHValidatorData(); - - vm.prank(safe); - contributionVault.depositValidator( - pubkeys, - withdrawal_credentials, - signatures, - deposit_data_roots - ); - } - - function test_OnlySafeDepositValidator() external { - ( - bytes[] memory pubkeys, - bytes[] memory withdrawal_credentials, - bytes[] memory signatures, - bytes32[] memory deposit_data_roots - ) = getETHValidatorData(); - - vm.expectRevert( - abi.encodeWithSelector(Unauthorized.selector, address(this)) - ); - contributionVault.depositValidator( - pubkeys, - withdrawal_credentials, - signatures, - deposit_data_roots - ); - } - - function test_rescueFunds() external { - uint256 amount = 1 ether; - mERC20.transfer(address(contributionVault), amount); - - vm.expectEmit(false, false, false, true); - emit RescueFunds(amount); - - contributionVault.rescueFunds(address(mERC20), amount); - } - - function testFuzz_rescueFundETH( - uint256 amount - ) external { - mERC20.transfer(address(contributionVault), amount); - vm.expectEmit(false, false, false, true); - emit RescueFunds(amount); - - contributionVault.rescueFunds(address(mERC20), amount); - } + vm.expectEmit(false, false, false, true); + emit RageQuit(user, amount); + + contributionVault.rageQuit(user, amount); + } + + function test_cannotRageQuitAfterDeposit() external { + vm.deal(user1, ETH_STAKE); + + vm.prank(user1); + payable(contributionVault).transfer(ETH_STAKE); + + ( + bytes[] memory pubkeys, + bytes[] memory withdrawal_credentials, + bytes[] memory signatures, + bytes32[] memory deposit_data_roots + ) = getETHValidatorData(); + + contributionVault.depositValidator(pubkeys, withdrawal_credentials, signatures, deposit_data_roots); + + vm.expectRevert(CannotRageQuit.selector); + contributionVault.rageQuit(user1, ETH_STAKE); + } + + function test_depositValidator() external { + vm.deal(user1, ETH_STAKE); + vm.prank(user1); + payable(contributionVault).transfer(ETH_STAKE); + + ( + bytes[] memory pubkeys, + bytes[] memory withdrawal_credentials, + bytes[] memory signatures, + bytes32[] memory deposit_data_roots + ) = getETHValidatorData(); + + vm.prank(safe); + contributionVault.depositValidator(pubkeys, withdrawal_credentials, signatures, deposit_data_roots); + } + + function test_OnlySafeDepositValidator() external { + ( + bytes[] memory pubkeys, + bytes[] memory withdrawal_credentials, + bytes[] memory signatures, + bytes32[] memory deposit_data_roots + ) = getETHValidatorData(); + + vm.expectRevert(abi.encodeWithSelector(Unauthorized.selector, address(this))); + contributionVault.depositValidator(pubkeys, withdrawal_credentials, signatures, deposit_data_roots); + } + + function test_rescueFunds() external { + uint256 amount = 1 ether; + mERC20.transfer(address(contributionVault), amount); + + vm.expectEmit(false, false, false, true); + emit RescueFunds(amount); + + contributionVault.rescueFunds(address(mERC20), amount); + } + + function testFuzz_rescueFundETH(uint256 amount) external { + mERC20.transfer(address(contributionVault), amount); + vm.expectEmit(false, false, false, true); + emit RescueFunds(amount); + + contributionVault.rescueFunds(address(mERC20), amount); + } } -function getETHValidatorData() pure returns ( +function getETHValidatorData() + pure + returns ( bytes[] memory pubkeys, bytes[] memory withdrawal_credentials, bytes[] memory signatures, bytes32[] memory deposit_data_roots -) { - pubkeys = new bytes[](1); - pubkeys[0] = bytes("0x83fa9495bb0944a74fc6a66e699039b66134b22a52a710f8d0f7cde318a2db3da40081a5867667389d206e21b5e37e52"); - - withdrawal_credentials = new bytes[](1); - withdrawal_credentials[0] = bytes("0x010000000000000000000000e839a3e9efb32c6a56ab7128e51056585275506c"); - - signatures = new bytes[](1); - signatures[0] = bytes("0x95f00435e80e59a8fed41581e2050a3fe56272d6be845686ef014a57909c6621d7847fa550b77cb8e541b955f3c2ea031983d9e4336f215e75c8ba75d94e05f1e23460de6611a980ef629d3e32ca09cffaf2a63372496079b1ee22310d336ded"); - - deposit_data_roots = new bytes32[](1); - deposit_data_roots[0] = bytes32(0x2d33b096d02f7a53dbbdaf840755dfc6b9269be39cff6b0d2701d15b4c1b639c); -} \ No newline at end of file + ) +{ + pubkeys = new bytes[](1); + pubkeys[0] = + bytes("0x83fa9495bb0944a74fc6a66e699039b66134b22a52a710f8d0f7cde318a2db3da40081a5867667389d206e21b5e37e52"); + + withdrawal_credentials = new bytes[](1); + withdrawal_credentials[0] = bytes("0x010000000000000000000000e839a3e9efb32c6a56ab7128e51056585275506c"); + + signatures = new bytes[](1); + signatures[0] = bytes( + "0x95f00435e80e59a8fed41581e2050a3fe56272d6be845686ef014a57909c6621d7847fa550b77cb8e541b955f3c2ea031983d9e4336f215e75c8ba75d94e05f1e23460de6611a980ef629d3e32ca09cffaf2a63372496079b1ee22310d336ded" + ); + + deposit_data_roots = new bytes32[](1); + deposit_data_roots[0] = bytes32(0x2d33b096d02f7a53dbbdaf840755dfc6b9269be39cff6b0d2701d15b4c1b639c); +} From 8a219a64c4b4d12f6a12e20095ebec51c85479b7 Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Thu, 23 Nov 2023 12:09:52 +0200 Subject: [PATCH 07/22] chore: rename SimpleETHContributionVaultTest --- src/test/safe/SimpleETHContributionVault.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/safe/SimpleETHContributionVault.t.sol b/src/test/safe/SimpleETHContributionVault.t.sol index a541f16..ee8615c 100644 --- a/src/test/safe/SimpleETHContributionVault.t.sol +++ b/src/test/safe/SimpleETHContributionVault.t.sol @@ -5,7 +5,7 @@ import "forge-std/Test.sol"; import {MockERC20} from "src/test/utils/mocks/MockERC20.sol"; import {SimpleETHContributionVault} from "src/safe-modules/SimpleETHContributionVault.sol"; -contract ETHVaultSafeModuleTest is Test { +contract SimpleETHContributionVaultTest is Test { error CannotRageQuit(); error Unauthorized(address user); From f976d07a613aeb480a9c9e12efab137fa9be9e49 Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Thu, 23 Nov 2023 16:59:56 +0200 Subject: [PATCH 08/22] test: add SECVinvariant test --- foundry.toml | 2 +- .../SimpleETHContributionVault.sol | 35 ++++++++++++++--- .../safe/SimpleETHContributionVault.t.sol | 38 ++++++++++++++----- src/test/safe/invariant/SECVInvariant.t.sol | 14 +++++++ 4 files changed, 73 insertions(+), 16 deletions(-) create mode 100644 src/test/safe/invariant/SECVInvariant.t.sol diff --git a/foundry.toml b/foundry.toml index ef28c39..816cdf8 100644 --- a/foundry.toml +++ b/foundry.toml @@ -30,4 +30,4 @@ tab_width = 2 wrap_comments = true [fuzz] -runs = 100 +runs = 1 diff --git a/src/safe-modules/SimpleETHContributionVault.sol b/src/safe-modules/SimpleETHContributionVault.sol index a51109c..c8975f1 100644 --- a/src/safe-modules/SimpleETHContributionVault.sol +++ b/src/safe-modules/SimpleETHContributionVault.sol @@ -21,23 +21,45 @@ contract SimpleETHContributionVault { /// @notice Amount of ETH validator stake uint256 internal constant ETH_STAKE = 32 ether; + /// @notice Emitted on deposit ETH + /// @param to address the credited ETH + /// @param amount Amount of ETH deposit event Deposit(address to, uint256 amount); + /// @notice Emitted on validator deposit + /// @param pubkeys array of validator pubkeys + /// @param withdrawal_credentials array of validator 0x1 withdrawal credentials + /// @param signatures array of validator signatures + /// @param deposit_data_roots array of deposit data roots event DepositValidator( - bytes[] pubkeys, bytes[] withdrawal_credentials, bytes[] signatures, bytes32[] deposit_data_roots + bytes[] pubkeys, + bytes[] withdrawal_credentials, + bytes[] signatures, + bytes32[] deposit_data_roots ); + + /// @notice Emitted on user rage quit + /// @param to address that received amount + /// @param amount amount rage quitted event RageQuit(address to, uint256 amount); - event RescueFunds(uint256 amount); - /// @notice - mapping(address => uint256) public userBalances; + /// @notice Emitted on rescue funds + /// @param amount amount of funds rescued + event RescueFunds(uint256 amount); + /// @notice ETH2 deposit contract IETH2DepositContract public immutable depositContract; - + + /// @notice Address of gnosis safe address public immutable safe; + /// @notice adress => amount deposited + mapping(address => uint256) public userBalances; + + /// @notice tracks if validator have been activated bool public activated; + modifier onlySafe() { if (msg.sender != safe) revert Unauthorized(msg.sender); _; @@ -106,6 +128,9 @@ contract SimpleETHContributionVault { emit RescueFunds(amount); } + /// @notice a user deposit + /// @param to address to receive the deposit + /// @param amount amount of deposit function _deposit(address to, uint256 amount) internal { userBalances[to] += amount; emit Deposit(to, amount); diff --git a/src/test/safe/SimpleETHContributionVault.t.sol b/src/test/safe/SimpleETHContributionVault.t.sol index ee8615c..661e6d6 100644 --- a/src/test/safe/SimpleETHContributionVault.t.sol +++ b/src/test/safe/SimpleETHContributionVault.t.sol @@ -5,7 +5,9 @@ import "forge-std/Test.sol"; import {MockERC20} from "src/test/utils/mocks/MockERC20.sol"; import {SimpleETHContributionVault} from "src/safe-modules/SimpleETHContributionVault.sol"; + contract SimpleETHContributionVaultTest is Test { + error CannotRageQuit(); error Unauthorized(address user); @@ -48,43 +50,53 @@ contract SimpleETHContributionVaultTest is Test { emit Deposit(user1, ETH_STAKE); vm.prank(user1); - payable(contributionVault).transfer(ETH_STAKE); + payable(contributionVault).call{value: ETH_STAKE}(""); assertEq(contributionVault.userBalances(user1), ETH_STAKE, "failed to credit user balance"); } function testFuzz_deposit(address user, uint256 amount) external { + vm.assume(amount > 0); + vm.assume(user != address(0)); + vm.deal(user, amount); vm.expectEmit(false, false, false, true); + emit Deposit(user, amount); vm.prank(user); - payable(contributionVault).transfer(amount); + payable(contributionVault).call{value: amount}(""); assertEq(contributionVault.userBalances(user), amount); } function test_rageQuit() external { vm.deal(user1, ETH_STAKE); - + vm.prank(user1); - payable(contributionVault).transfer(ETH_STAKE); + (bool _result, bytes memory _data) = payable(contributionVault).call{value: ETH_STAKE}(""); + vm.expectEmit(false, false, false, true); emit RageQuit(user1, ETH_STAKE); + vm.prank(user1); contributionVault.rageQuit(user1, ETH_STAKE); } function testFuzz_rageQuit(address user, uint256 amount) external { + vm.assume(amount > 0); + vm.assume(user != address(0)); + vm.deal(user, amount); vm.prank(user); - payable(contributionVault).transfer(amount); + payable(contributionVault).call{value: amount}(""); vm.expectEmit(false, false, false, true); emit RageQuit(user, amount); + vm.prank(user); contributionVault.rageQuit(user, amount); } @@ -92,7 +104,7 @@ contract SimpleETHContributionVaultTest is Test { vm.deal(user1, ETH_STAKE); vm.prank(user1); - payable(contributionVault).transfer(ETH_STAKE); + payable(contributionVault).call{value: ETH_STAKE}(""); ( bytes[] memory pubkeys, @@ -101,16 +113,20 @@ contract SimpleETHContributionVaultTest is Test { bytes32[] memory deposit_data_roots ) = getETHValidatorData(); + vm.prank(safe); contributionVault.depositValidator(pubkeys, withdrawal_credentials, signatures, deposit_data_roots); vm.expectRevert(CannotRageQuit.selector); + + vm.prank(user1); contributionVault.rageQuit(user1, ETH_STAKE); } function test_depositValidator() external { vm.deal(user1, ETH_STAKE); + vm.prank(user1); - payable(contributionVault).transfer(ETH_STAKE); + payable(contributionVault).call{value: ETH_STAKE}(""); ( bytes[] memory pubkeys, @@ -146,6 +162,8 @@ contract SimpleETHContributionVaultTest is Test { } function testFuzz_rescueFundETH(uint256 amount) external { + vm.assume(amount > 0); + mERC20.transfer(address(contributionVault), amount); vm.expectEmit(false, false, false, true); emit RescueFunds(amount); @@ -165,14 +183,14 @@ function getETHValidatorData() { pubkeys = new bytes[](1); pubkeys[0] = - bytes("0x83fa9495bb0944a74fc6a66e699039b66134b22a52a710f8d0f7cde318a2db3da40081a5867667389d206e21b5e37e52"); + bytes(abi.encodePacked(hex"83fa9495bb0944a74fc6a66e699039b66134b22a52a710f8d0f7cde318a2db3da40081a5867667389d206e21b5e37e52")); withdrawal_credentials = new bytes[](1); - withdrawal_credentials[0] = bytes("0x010000000000000000000000e839a3e9efb32c6a56ab7128e51056585275506c"); + withdrawal_credentials[0] = bytes(abi.encodePacked(hex"010000000000000000000000e839a3e9efb32c6a56ab7128e51056585275506c")); signatures = new bytes[](1); signatures[0] = bytes( - "0x95f00435e80e59a8fed41581e2050a3fe56272d6be845686ef014a57909c6621d7847fa550b77cb8e541b955f3c2ea031983d9e4336f215e75c8ba75d94e05f1e23460de6611a980ef629d3e32ca09cffaf2a63372496079b1ee22310d336ded" + abi.encodePacked(hex"95f00435e80e59a8fed41581e2050a3fe56272d6be845686ef014a57909c6621d7847fa550b77cb8e541b955f3c2ea031983d9e4336f215e75c8ba75d94e05f1e23460de6611a980ef629d3e32ca09cffaf2a63372496079b1ee22310d336ded") ); deposit_data_roots = new bytes32[](1); diff --git a/src/test/safe/invariant/SECVInvariant.t.sol b/src/test/safe/invariant/SECVInvariant.t.sol new file mode 100644 index 0000000..964b57b --- /dev/null +++ b/src/test/safe/invariant/SECVInvariant.t.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.19; + +contract SECVInvariant { + + function setUp() public { + + } + + function testInvariant_deposit() { + + } + +} \ No newline at end of file From 2c7df8ace856d4b53b4093825c0bbf8d97719604 Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Thu, 23 Nov 2023 18:11:50 +0200 Subject: [PATCH 09/22] test: fix compiler return warning --- src/test/safe/SimpleETHContributionVault.t.sol | 18 ++++++++++++------ src/test/safe/invariant/SECVInvariant.t.sol | 6 +++--- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/test/safe/SimpleETHContributionVault.t.sol b/src/test/safe/SimpleETHContributionVault.t.sol index 661e6d6..2f00e58 100644 --- a/src/test/safe/SimpleETHContributionVault.t.sol +++ b/src/test/safe/SimpleETHContributionVault.t.sol @@ -50,7 +50,8 @@ contract SimpleETHContributionVaultTest is Test { emit Deposit(user1, ETH_STAKE); vm.prank(user1); - payable(contributionVault).call{value: ETH_STAKE}(""); + (bool _success,) = payable(contributionVault).call{value: ETH_STAKE}(""); + assertTrue(_success, "call failed"); assertEq(contributionVault.userBalances(user1), ETH_STAKE, "failed to credit user balance"); } @@ -65,7 +66,8 @@ contract SimpleETHContributionVaultTest is Test { emit Deposit(user, amount); vm.prank(user); - payable(contributionVault).call{value: amount}(""); + (bool _success,) = payable(contributionVault).call{value: amount}(""); + assertTrue(_success, "call failed"); assertEq(contributionVault.userBalances(user), amount); } @@ -74,7 +76,8 @@ contract SimpleETHContributionVaultTest is Test { vm.deal(user1, ETH_STAKE); vm.prank(user1); - (bool _result, bytes memory _data) = payable(contributionVault).call{value: ETH_STAKE}(""); + (bool _success,) = payable(contributionVault).call{value: ETH_STAKE}(""); + assertTrue(_success, "call failed"); vm.expectEmit(false, false, false, true); @@ -91,7 +94,8 @@ contract SimpleETHContributionVaultTest is Test { vm.deal(user, amount); vm.prank(user); - payable(contributionVault).call{value: amount}(""); + (bool _success,) = payable(contributionVault).call{value: amount}(""); + assertTrue(_success, "call failed"); vm.expectEmit(false, false, false, true); emit RageQuit(user, amount); @@ -104,7 +108,8 @@ contract SimpleETHContributionVaultTest is Test { vm.deal(user1, ETH_STAKE); vm.prank(user1); - payable(contributionVault).call{value: ETH_STAKE}(""); + (bool _success,) = payable(contributionVault).call{value: ETH_STAKE}(""); + assertTrue(_success, "call failed"); ( bytes[] memory pubkeys, @@ -126,7 +131,8 @@ contract SimpleETHContributionVaultTest is Test { vm.deal(user1, ETH_STAKE); vm.prank(user1); - payable(contributionVault).call{value: ETH_STAKE}(""); + (bool _success,) = payable(contributionVault).call{value: ETH_STAKE}(""); + assertTrue(_success, "call failed"); ( bytes[] memory pubkeys, diff --git a/src/test/safe/invariant/SECVInvariant.t.sol b/src/test/safe/invariant/SECVInvariant.t.sol index 964b57b..eb6990b 100644 --- a/src/test/safe/invariant/SECVInvariant.t.sol +++ b/src/test/safe/invariant/SECVInvariant.t.sol @@ -4,11 +4,11 @@ pragma solidity ^0.8.19; contract SECVInvariant { function setUp() public { - + } - function testInvariant_deposit() { + // function testInvariant_deposit() { - } + // } } \ No newline at end of file From 3dde39759e0f9e83d7c1c55c97fca83739810e45 Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Mon, 27 Nov 2023 13:53:22 +0200 Subject: [PATCH 10/22] test: add SECVinvariant invariant test functions --- foundry.toml | 3 +- src/test/safe/invariant/SECVInvariant.t.sol | 87 +++++++++++++++++++-- 2 files changed, 84 insertions(+), 6 deletions(-) diff --git a/foundry.toml b/foundry.toml index 816cdf8..b81e1a4 100644 --- a/foundry.toml +++ b/foundry.toml @@ -16,6 +16,7 @@ fs_permissions = [{ access = "read-write", path = "./"}] [rpc_endpoints] goerli = "${GOERLI_RPC_URL}" mainnet = "${MAINNET_RPC_URL}" + # See more config options https://github.com/gakonst/foundry/tree/master/config [fmt] @@ -30,4 +31,4 @@ tab_width = 2 wrap_comments = true [fuzz] -runs = 1 +runs = 100 diff --git a/src/test/safe/invariant/SECVInvariant.t.sol b/src/test/safe/invariant/SECVInvariant.t.sol index eb6990b..849e8ca 100644 --- a/src/test/safe/invariant/SECVInvariant.t.sol +++ b/src/test/safe/invariant/SECVInvariant.t.sol @@ -1,14 +1,91 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.8.19; +import {Test} from "forge-std/Test.sol"; +import {StdInvariant} from "forge-std/StdInvariant.sol"; +import {SimpleETHContributionVault} from "src/safe-modules/SimpleETHContributionVault.sol"; +import {CommonBase} from "forge-std/Base.sol"; +import {StdCheats} from "forge-std/StdCheats.sol"; +import {StdUtils} from "forge-std/StdUtils.sol"; -contract SECVInvariant { +contract MockDepositContract { + event Deposit ( + bytes[] pubkeys, + bytes[] withdrawal_credentials, + bytes[] signatures, + bytes32[] deposit_data_roots + ); + function depositValidator( + bytes[] calldata, + bytes[] calldata, + bytes[] calldata, + bytes32[] calldata + ) external payable {} +} + +contract SECVHandler is CommonBase, StdCheats, StdUtils { + SimpleETHContributionVault public contributionVault; + + uint256 public constant ETH_SUPPLY = 100_000 ether; + + uint256 public ghost_depositSum; + uint256 public ghost_rageQuitSum; + + receive() external payable {} + + constructor(SimpleETHContributionVault vault) { + contributionVault = vault; + deal(address(this), ETH_SUPPLY); + } + + function deposit(uint256 amount) external payable { + amount = bound(amount, 0, address(this).balance); + (bool _success,) = payable(contributionVault).call{value: amount}(""); + assert(_success); + + ghost_depositSum += amount; + + } + + function rageQuit(uint256 amount) external payable { + amount = bound(amount, 0 , contributionVault.userBalances(address(this))); + contributionVault.rageQuit(address(this), amount); + + ghost_rageQuitSum += amount; + } +} + +contract SECVInvariant is Test { + + MockDepositContract public mockDepositContract; + SimpleETHContributionVault public contributionVault; + SECVHandler public handler; + + address public safe; function setUp() public { + safe = makeAddr("safe"); + + mockDepositContract = new MockDepositContract(); + contributionVault = new SimpleETHContributionVault( + safe, + address(mockDepositContract) + ); + handler = new SECVHandler(contributionVault); + targetContract(address(handler)); } - // function testInvariant_deposit() { - - // } + function invariant_balanceEqual() public { + assertEq( + handler.ETH_SUPPLY(), + address(handler).balance + contributionVault.userBalances(address(handler)) + ); + } -} \ No newline at end of file + function invariant_vaultIsSolvent() public { + assertEq( + address(contributionVault).balance, + handler.ghost_depositSum() - handler.ghost_rageQuitSum() + ); + } +} From 765d9741d4819b752cd7075d0788122ea638c3a2 Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Mon, 27 Nov 2023 14:09:38 +0200 Subject: [PATCH 11/22] chore: update SECV docstring --- src/safe-modules/SimpleETHContributionVault.sol | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/safe-modules/SimpleETHContributionVault.sol b/src/safe-modules/SimpleETHContributionVault.sol index c8975f1..169be9a 100644 --- a/src/safe-modules/SimpleETHContributionVault.sol +++ b/src/safe-modules/SimpleETHContributionVault.sol @@ -74,7 +74,7 @@ contract SimpleETHContributionVault { _deposit(msg.sender, msg.value); } - /// Deposit ETH into ETH2 deposit contract + /// @notice Deposit ETH into ETH2 deposit contract /// @param pubkeys Array of validator pub keys /// @param withdrawal_credentials Array of validator withdrawal credentials /// @param signatures Array of validator signatures @@ -106,8 +106,8 @@ contract SimpleETHContributionVault { emit DepositValidator(pubkeys, withdrawal_credentials, signatures, deposit_data_roots); } - /// Exit contribution vault prior to deposit starts - /// It allows a contributor to exit the vault before the validators are + /// @notice Exit contribution vault prior to deposit starts + /// It allows a contributor to exit the vault before any validator is /// activated /// @param to Address to send funds to /// @param amount balance to withdraw @@ -120,7 +120,7 @@ contract SimpleETHContributionVault { emit RageQuit(to, amount); } - /// Rescue non-ETH tokens stuck to the safe + /// @notice Rescue non-ETH tokens stuck to the safe /// @param token Token address /// @param amount amount of balance to transfer to Safe function rescueFunds(address token, uint256 amount) external { From c224748a66f81ad314933c6b754c6e296ea8d8fc Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Mon, 27 Nov 2023 15:29:49 +0200 Subject: [PATCH 12/22] test: add SECVinvariant depositValidator mock function --- script/data/lido-data-sample-json | 44 ----- .../SimpleETHContributionVault.sol | 8 +- .../safe/SimpleETHContributionVault.t.sol | 19 ++- src/test/safe/invariant/SECVInvariant.t.sol | 150 +++++++++++------- 4 files changed, 105 insertions(+), 116 deletions(-) delete mode 100644 script/data/lido-data-sample-json diff --git a/script/data/lido-data-sample-json b/script/data/lido-data-sample-json deleted file mode 100644 index a7e36bc..0000000 --- a/script/data/lido-data-sample-json +++ /dev/null @@ -1,44 +0,0 @@ -[ - { - "accounts": [ - "0x0000000000000000000000000000000000000001", - "0x0000000000000000000000000000000000000002", - "0x0000000000000000000000000000000000000003" - ], - "controller": "0x0000000000000000000000000000000000000004", - "distributorFee": 1, - "percentAllocations": [ - 840000, - 60000, - 100000 - ] - }, - { - "accounts": [ - "0x0000000000000000000000000000000000000001", - "0x0000000000000000000000000000000000000002", - "0x0000000000000000000000000000000000000003" - ], - "controller": "0x0000000000000000000000000000000000000004", - "distributorFee": 1, - "percentAllocations": [ - 840000, - 60000, - 100000 - ] - }, - { - "accounts": [ - "0x0000000000000000000000000000000000000001", - "0x0000000000000000000000000000000000000002", - "0x0000000000000000000000000000000000000003" - ], - "controller": "0x0000000000000000000000000000000000000004", - "distributorFee": 1, - "percentAllocations": [ - 840000, - 60000, - 100000 - ] - } -] \ No newline at end of file diff --git a/src/safe-modules/SimpleETHContributionVault.sol b/src/safe-modules/SimpleETHContributionVault.sol index 169be9a..10ccf1b 100644 --- a/src/safe-modules/SimpleETHContributionVault.sol +++ b/src/safe-modules/SimpleETHContributionVault.sol @@ -32,10 +32,7 @@ contract SimpleETHContributionVault { /// @param signatures array of validator signatures /// @param deposit_data_roots array of deposit data roots event DepositValidator( - bytes[] pubkeys, - bytes[] withdrawal_credentials, - bytes[] signatures, - bytes32[] deposit_data_roots + bytes[] pubkeys, bytes[] withdrawal_credentials, bytes[] signatures, bytes32[] deposit_data_roots ); /// @notice Emitted on user rage quit @@ -49,7 +46,7 @@ contract SimpleETHContributionVault { /// @notice ETH2 deposit contract IETH2DepositContract public immutable depositContract; - + /// @notice Address of gnosis safe address public immutable safe; @@ -59,7 +56,6 @@ contract SimpleETHContributionVault { /// @notice tracks if validator have been activated bool public activated; - modifier onlySafe() { if (msg.sender != safe) revert Unauthorized(msg.sender); _; diff --git a/src/test/safe/SimpleETHContributionVault.t.sol b/src/test/safe/SimpleETHContributionVault.t.sol index 2f00e58..21ee6ae 100644 --- a/src/test/safe/SimpleETHContributionVault.t.sol +++ b/src/test/safe/SimpleETHContributionVault.t.sol @@ -5,9 +5,7 @@ import "forge-std/Test.sol"; import {MockERC20} from "src/test/utils/mocks/MockERC20.sol"; import {SimpleETHContributionVault} from "src/safe-modules/SimpleETHContributionVault.sol"; - contract SimpleETHContributionVaultTest is Test { - error CannotRageQuit(); error Unauthorized(address user); @@ -74,12 +72,11 @@ contract SimpleETHContributionVaultTest is Test { function test_rageQuit() external { vm.deal(user1, ETH_STAKE); - + vm.prank(user1); (bool _success,) = payable(contributionVault).call{value: ETH_STAKE}(""); assertTrue(_success, "call failed"); - vm.expectEmit(false, false, false, true); emit RageQuit(user1, ETH_STAKE); @@ -188,15 +185,21 @@ function getETHValidatorData() ) { pubkeys = new bytes[](1); - pubkeys[0] = - bytes(abi.encodePacked(hex"83fa9495bb0944a74fc6a66e699039b66134b22a52a710f8d0f7cde318a2db3da40081a5867667389d206e21b5e37e52")); + pubkeys[0] = bytes( + abi.encodePacked( + hex"83fa9495bb0944a74fc6a66e699039b66134b22a52a710f8d0f7cde318a2db3da40081a5867667389d206e21b5e37e52" + ) + ); withdrawal_credentials = new bytes[](1); - withdrawal_credentials[0] = bytes(abi.encodePacked(hex"010000000000000000000000e839a3e9efb32c6a56ab7128e51056585275506c")); + withdrawal_credentials[0] = + bytes(abi.encodePacked(hex"010000000000000000000000e839a3e9efb32c6a56ab7128e51056585275506c")); signatures = new bytes[](1); signatures[0] = bytes( - abi.encodePacked(hex"95f00435e80e59a8fed41581e2050a3fe56272d6be845686ef014a57909c6621d7847fa550b77cb8e541b955f3c2ea031983d9e4336f215e75c8ba75d94e05f1e23460de6611a980ef629d3e32ca09cffaf2a63372496079b1ee22310d336ded") + abi.encodePacked( + hex"95f00435e80e59a8fed41581e2050a3fe56272d6be845686ef014a57909c6621d7847fa550b77cb8e541b955f3c2ea031983d9e4336f215e75c8ba75d94e05f1e23460de6611a980ef629d3e32ca09cffaf2a63372496079b1ee22310d336ded" + ) ); deposit_data_roots = new bytes32[](1); diff --git a/src/test/safe/invariant/SECVInvariant.t.sol b/src/test/safe/invariant/SECVInvariant.t.sol index 849e8ca..cd4c735 100644 --- a/src/test/safe/invariant/SECVInvariant.t.sol +++ b/src/test/safe/invariant/SECVInvariant.t.sol @@ -1,91 +1,125 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.8.19; + import {Test} from "forge-std/Test.sol"; import {StdInvariant} from "forge-std/StdInvariant.sol"; import {SimpleETHContributionVault} from "src/safe-modules/SimpleETHContributionVault.sol"; import {CommonBase} from "forge-std/Base.sol"; import {StdCheats} from "forge-std/StdCheats.sol"; import {StdUtils} from "forge-std/StdUtils.sol"; +import {getETHValidatorData} from "../SimpleETHContributionVault.t.sol"; contract MockDepositContract { - event Deposit ( - bytes[] pubkeys, - bytes[] withdrawal_credentials, - bytes[] signatures, - bytes32[] deposit_data_roots - ); - function depositValidator( - bytes[] calldata, - bytes[] calldata, - bytes[] calldata, - bytes32[] calldata - ) external payable {} + uint256 public ghost_depositSum; + + event Deposit(bytes[] pubkeys, bytes[] withdrawal_credentials, bytes[] signatures, bytes32[] deposit_data_roots); + + function deposit(bytes[] calldata, bytes[] calldata, bytes[] calldata, bytes32[] calldata) external payable { + ghost_depositSum += msg.value; + } } -contract SECVHandler is CommonBase, StdCheats, StdUtils { - SimpleETHContributionVault public contributionVault; +contract SECVMock is SimpleETHContributionVault { + constructor(address _safe, address eth2DepositContract) SimpleETHContributionVault(_safe, eth2DepositContract) {} + + /// @notice Mock depositValidator function + function depositValidatorMock( + bytes[] calldata pubkeys, + bytes[] calldata withdrawal_credentials, + bytes[] calldata signatures, + bytes32[] calldata deposit_data_roots + ) external payable { + for (uint256 i = 0; i < 1;) { + depositContract.deposit{value: msg.value}( + pubkeys[i], withdrawal_credentials[i], signatures[i], deposit_data_roots[i] + ); + unchecked { + i++; + } + } + } +} - uint256 public constant ETH_SUPPLY = 100_000 ether; +contract SECVBoundedHandler is CommonBase, StdCheats, StdUtils { + SECVMock public contributionVault; - uint256 public ghost_depositSum; - uint256 public ghost_rageQuitSum; + uint256 public constant ETH_SUPPLY = 1_000_000 ether; - receive() external payable {} + uint256 public ghost_depositSum; + uint256 public ghost_rageQuitSum; - constructor(SimpleETHContributionVault vault) { - contributionVault = vault; - deal(address(this), ETH_SUPPLY); - } + receive() external payable {} - function deposit(uint256 amount) external payable { - amount = bound(amount, 0, address(this).balance); - (bool _success,) = payable(contributionVault).call{value: amount}(""); - assert(_success); + constructor(SECVMock vault) { + contributionVault = vault; + deal(address(this), ETH_SUPPLY); + } - ghost_depositSum += amount; + function deposit(uint256 amount) external payable { + amount = bound(amount, 0, address(this).balance); + (bool _success,) = payable(contributionVault).call{value: amount}(""); + assert(_success); - } + ghost_depositSum += amount; + } - function rageQuit(uint256 amount) external payable { - amount = bound(amount, 0 , contributionVault.userBalances(address(this))); - contributionVault.rageQuit(address(this), amount); + function rageQuit(uint256 amount) external payable { + amount = bound(amount, 0, contributionVault.userBalances(address(this))); + contributionVault.rageQuit(address(this), amount); - ghost_rageQuitSum += amount; - } + ghost_rageQuitSum += amount; + } + + function depositValidator(uint256 amount) external payable { + amount = bound(amount, 0, address(contributionVault).balance); + ( + bytes[] memory pubkeys, + bytes[] memory withdrawal_credentials, + bytes[] memory signatures, + bytes32[] memory deposit_data_roots + ) = getETHValidatorData(); + + contributionVault.depositValidatorMock{value: amount}( + pubkeys, withdrawal_credentials, signatures, deposit_data_roots + ); + } } contract SECVInvariant is Test { + MockDepositContract public mockDepositContract; + SECVMock public contributionVault; + SECVBoundedHandler public handler; - MockDepositContract public mockDepositContract; - SimpleETHContributionVault public contributionVault; - SECVHandler public handler; - - address public safe; + address public safe; - function setUp() public { - safe = makeAddr("safe"); + function setUp() public { + safe = makeAddr("safe"); - mockDepositContract = new MockDepositContract(); - contributionVault = new SimpleETHContributionVault( + mockDepositContract = new MockDepositContract(); + contributionVault = new SECVMock( safe, address(mockDepositContract) ); - handler = new SECVHandler(contributionVault); + handler = new SECVBoundedHandler(contributionVault); - targetContract(address(handler)); - } - - function invariant_balanceEqual() public { - assertEq( - handler.ETH_SUPPLY(), - address(handler).balance + contributionVault.userBalances(address(handler)) - ); - } + targetContract(address(handler)); + } - function invariant_vaultIsSolvent() public { - assertEq( - address(contributionVault).balance, - handler.ghost_depositSum() - handler.ghost_rageQuitSum() - ); - } + /// @notice This invariant checks that the sum of balances in the handler, + /// vault and mock deposit contract is equal + function invariant_balanceEqual() public { + assertEq( + handler.ETH_SUPPLY(), + address(handler).balance + contributionVault.userBalances(address(handler)) + address(mockDepositContract).balance + ); + } + + /// @notice This invariant checks that the vault is + /// always solvent when a user ragequits. + function invariant_vaultIsSolvent() public { + assertEq( + address(contributionVault).balance, + handler.ghost_depositSum() + mockDepositContract.ghost_depositSum() - handler.ghost_rageQuitSum() + ); + } } From dea7cb15bbe7e8c8ff445491fda8811d70aa2338 Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Mon, 27 Nov 2023 16:07:46 +0200 Subject: [PATCH 13/22] test: fix invariant vault is solvent --- .../SimpleETHContributionVault.sol | 2 +- src/test/safe/invariant/SECVInvariant.t.sol | 23 +++++++++---------- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/src/safe-modules/SimpleETHContributionVault.sol b/src/safe-modules/SimpleETHContributionVault.sol index 10ccf1b..c8a0ced 100644 --- a/src/safe-modules/SimpleETHContributionVault.sol +++ b/src/safe-modules/SimpleETHContributionVault.sol @@ -80,7 +80,7 @@ contract SimpleETHContributionVault { bytes[] calldata withdrawal_credentials, bytes[] calldata signatures, bytes32[] calldata deposit_data_roots - ) external payable onlySafe { + ) external onlySafe { uint256 size = pubkeys.length; if (address(this).balance < size * ETH_STAKE) { diff --git a/src/test/safe/invariant/SECVInvariant.t.sol b/src/test/safe/invariant/SECVInvariant.t.sol index cd4c735..7545bc8 100644 --- a/src/test/safe/invariant/SECVInvariant.t.sol +++ b/src/test/safe/invariant/SECVInvariant.t.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.19; import {Test} from "forge-std/Test.sol"; +import "forge-std/console.sol"; import {StdInvariant} from "forge-std/StdInvariant.sol"; import {SimpleETHContributionVault} from "src/safe-modules/SimpleETHContributionVault.sol"; import {CommonBase} from "forge-std/Base.sol"; @@ -12,9 +13,9 @@ import {getETHValidatorData} from "../SimpleETHContributionVault.t.sol"; contract MockDepositContract { uint256 public ghost_depositSum; - event Deposit(bytes[] pubkeys, bytes[] withdrawal_credentials, bytes[] signatures, bytes32[] deposit_data_roots); + receive() external payable {} - function deposit(bytes[] calldata, bytes[] calldata, bytes[] calldata, bytes32[] calldata) external payable { + function deposit(bytes calldata, bytes calldata, bytes calldata, bytes32) external payable { ghost_depositSum += msg.value; } } @@ -28,9 +29,9 @@ contract SECVMock is SimpleETHContributionVault { bytes[] calldata withdrawal_credentials, bytes[] calldata signatures, bytes32[] calldata deposit_data_roots - ) external payable { + ) external { for (uint256 i = 0; i < 1;) { - depositContract.deposit{value: msg.value}( + depositContract.deposit{value: address(this).balance}( pubkeys[i], withdrawal_credentials[i], signatures[i], deposit_data_roots[i] ); unchecked { @@ -70,8 +71,7 @@ contract SECVBoundedHandler is CommonBase, StdCheats, StdUtils { ghost_rageQuitSum += amount; } - function depositValidator(uint256 amount) external payable { - amount = bound(amount, 0, address(contributionVault).balance); + function depositValidator() external payable { ( bytes[] memory pubkeys, bytes[] memory withdrawal_credentials, @@ -79,9 +79,8 @@ contract SECVBoundedHandler is CommonBase, StdCheats, StdUtils { bytes32[] memory deposit_data_roots ) = getETHValidatorData(); - contributionVault.depositValidatorMock{value: amount}( - pubkeys, withdrawal_credentials, signatures, deposit_data_roots - ); + // use entire vault balance + contributionVault.depositValidatorMock(pubkeys, withdrawal_credentials, signatures, deposit_data_roots); } } @@ -110,16 +109,16 @@ contract SECVInvariant is Test { function invariant_balanceEqual() public { assertEq( handler.ETH_SUPPLY(), - address(handler).balance + contributionVault.userBalances(address(handler)) + address(mockDepositContract).balance + address(handler).balance + address(contributionVault).balance + address(mockDepositContract).balance ); } /// @notice This invariant checks that the vault is - /// always solvent when a user ragequits. + /// always solvent function invariant_vaultIsSolvent() public { assertEq( address(contributionVault).balance, - handler.ghost_depositSum() + mockDepositContract.ghost_depositSum() - handler.ghost_rageQuitSum() + handler.ghost_depositSum() - mockDepositContract.ghost_depositSum() - handler.ghost_rageQuitSum() ); } } From a6bb0dab750dfbef0699a9a4060063e05f966da7 Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Mon, 27 Nov 2023 16:13:18 +0200 Subject: [PATCH 14/22] chore: remove safe-contracts submodule --- .gitmodules | 3 --- foundry.toml | 1 - lib/safe-contracts | 1 - 3 files changed, 5 deletions(-) delete mode 160000 lib/safe-contracts diff --git a/.gitmodules b/.gitmodules index 1dcb42a..4a0b298 100644 --- a/.gitmodules +++ b/.gitmodules @@ -15,6 +15,3 @@ [submodule "lib/solmate"] path = lib/solmate url = https://github.com/transmissions11/solmate -[submodule "lib/safe-contracts"] - path = lib/safe-contracts - url = https://github.com/safe-global/safe-contracts diff --git a/foundry.toml b/foundry.toml index b81e1a4..4cc8df3 100644 --- a/foundry.toml +++ b/foundry.toml @@ -7,7 +7,6 @@ remappings = [ 'solmate/=lib/solmate/src/', 'splits-tests/=lib/splits-utils/test/', 'solady/=lib/solady/src/', - 'safe-contracts/=lib/safe-contracts/contracts', ] solc_version = '0.8.19' gas_reports = ["*"] diff --git a/lib/safe-contracts b/lib/safe-contracts deleted file mode 160000 index bf943f8..0000000 --- a/lib/safe-contracts +++ /dev/null @@ -1 +0,0 @@ -Subproject commit bf943f80fec5ac647159d26161446ac5d716a294 From 784ab37e8aa88bc9b0785856d3d3be8442a4b688 Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Wed, 20 Dec 2023 11:48:16 +0200 Subject: [PATCH 15/22] chore: add checks to depositValidator --- src/safe-modules/SimpleETHContributionVault.sol | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/safe-modules/SimpleETHContributionVault.sol b/src/safe-modules/SimpleETHContributionVault.sol index c8a0ced..154f02f 100644 --- a/src/safe-modules/SimpleETHContributionVault.sol +++ b/src/safe-modules/SimpleETHContributionVault.sol @@ -18,6 +18,9 @@ contract SimpleETHContributionVault { /// @notice incomplete contribution error IncompleteContribution(uint256 actual, uint256 expected); + /// @notice invalid deposit data + error InvalidDepositData(); + /// @notice Amount of ETH validator stake uint256 internal constant ETH_STAKE = 32 ether; @@ -83,10 +86,21 @@ contract SimpleETHContributionVault { ) external onlySafe { uint256 size = pubkeys.length; + if ( + (withdrawal_credentials.length != size) || + (signatures.length != size) || + (deposit_data_roots.length != size) + ) { + revert InvalidDepositData(); + } + + if (address(this).balance < size * ETH_STAKE) { revert IncompleteContribution(address(this).balance, ETH_STAKE * size); } + activated = true; + for (uint256 i = 0; i < size;) { depositContract.deposit{value: ETH_STAKE}( pubkeys[i], withdrawal_credentials[i], signatures[i], deposit_data_roots[i] @@ -97,8 +111,6 @@ contract SimpleETHContributionVault { } } - activated = true; - emit DepositValidator(pubkeys, withdrawal_credentials, signatures, deposit_data_roots); } From 7181fefd64c89d4c9713f2967973f478e2939e3a Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Wed, 20 Dec 2023 14:30:04 +0200 Subject: [PATCH 16/22] chore: add validation checks to SECV --- src/safe-modules/SimpleETHContributionVault.sol | 4 ++++ src/test/safe/SimpleETHContributionVault.t.sol | 10 ++++++++++ 2 files changed, 14 insertions(+) diff --git a/src/safe-modules/SimpleETHContributionVault.sol b/src/safe-modules/SimpleETHContributionVault.sol index 154f02f..766adb1 100644 --- a/src/safe-modules/SimpleETHContributionVault.sol +++ b/src/safe-modules/SimpleETHContributionVault.sol @@ -21,6 +21,9 @@ contract SimpleETHContributionVault { /// @notice invalid deposit data error InvalidDepositData(); + /// @notice Invalid Address + error Invalid_Address(); + /// @notice Amount of ETH validator stake uint256 internal constant ETH_STAKE = 32 ether; @@ -120,6 +123,7 @@ contract SimpleETHContributionVault { /// @param to Address to send funds to /// @param amount balance to withdraw function rageQuit(address to, uint256 amount) external { + if (to == address(0)) revert Invalid_Address(); if (activated == true) revert CannotRageQuit(); userBalances[msg.sender] -= amount; diff --git a/src/test/safe/SimpleETHContributionVault.t.sol b/src/test/safe/SimpleETHContributionVault.t.sol index 21ee6ae..1e29d86 100644 --- a/src/test/safe/SimpleETHContributionVault.t.sol +++ b/src/test/safe/SimpleETHContributionVault.t.sol @@ -7,6 +7,7 @@ import {SimpleETHContributionVault} from "src/safe-modules/SimpleETHContribution contract SimpleETHContributionVaultTest is Test { error CannotRageQuit(); + error Invalid_Address(); error Unauthorized(address user); event Deposit(address to, uint256 amount); @@ -124,6 +125,15 @@ contract SimpleETHContributionVaultTest is Test { contributionVault.rageQuit(user1, ETH_STAKE); } + function test_cannotRageQuitIfZeroAddress() external { + vm.expectRevert(Invalid_Address.selector); + contributionVault.rageQuit( + address(0), + 1 + ); + } + + function test_depositValidator() external { vm.deal(user1, ETH_STAKE); From 03d831c471c733b28dbe4551bfb8e72a913aabc2 Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Wed, 20 Dec 2023 14:30:44 +0200 Subject: [PATCH 17/22] chore: arrange vars --- src/safe-modules/SimpleETHContributionVault.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/safe-modules/SimpleETHContributionVault.sol b/src/safe-modules/SimpleETHContributionVault.sol index 766adb1..8626f6e 100644 --- a/src/safe-modules/SimpleETHContributionVault.sol +++ b/src/safe-modules/SimpleETHContributionVault.sol @@ -24,9 +24,6 @@ contract SimpleETHContributionVault { /// @notice Invalid Address error Invalid_Address(); - /// @notice Amount of ETH validator stake - uint256 internal constant ETH_STAKE = 32 ether; - /// @notice Emitted on deposit ETH /// @param to address the credited ETH /// @param amount Amount of ETH deposit @@ -50,6 +47,9 @@ contract SimpleETHContributionVault { /// @param amount amount of funds rescued event RescueFunds(uint256 amount); + /// @notice Amount of ETH validator stake + uint256 internal constant ETH_STAKE = 32 ether; + /// @notice ETH2 deposit contract IETH2DepositContract public immutable depositContract; From 65cf91d65197646555356287d96d81afeff64386 Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Wed, 20 Dec 2023 14:42:06 +0200 Subject: [PATCH 18/22] chore: remove unused interface --- src/interfaces/IGnosisSafe.sol | 22 ---------------------- 1 file changed, 22 deletions(-) delete mode 100644 src/interfaces/IGnosisSafe.sol diff --git a/src/interfaces/IGnosisSafe.sol b/src/interfaces/IGnosisSafe.sol deleted file mode 100644 index 0c35294..0000000 --- a/src/interfaces/IGnosisSafe.sol +++ /dev/null @@ -1,22 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.8.18; - -interface IGnosisSafe { - - enum Operation { - Call, - DelegateCall - } - - /// @dev Allows a Module to execute a Safe transaction without any further confirmations. - /// @param to Destination address of module transaction. - /// @param value Ether value of module transaction. - /// @param data Data payload of module transaction. - /// @param operation Operation type of module transaction. - function execTransactionFromModule( - address to, - uint256 value, - bytes calldata data, - Operation operation - ) external returns (bool success); -} \ No newline at end of file From 759076078f351e5303f780217fb527fccfc94d51 Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Wed, 20 Dec 2023 14:53:47 +0200 Subject: [PATCH 19/22] chore: add deposit function --- .../SimpleETHContributionVault.sol | 20 +++++++++---------- .../safe/SimpleETHContributionVault.t.sol | 19 +++++++++++++++++- 2 files changed, 27 insertions(+), 12 deletions(-) diff --git a/src/safe-modules/SimpleETHContributionVault.sol b/src/safe-modules/SimpleETHContributionVault.sol index 8626f6e..16fa4c7 100644 --- a/src/safe-modules/SimpleETHContributionVault.sol +++ b/src/safe-modules/SimpleETHContributionVault.sol @@ -29,15 +29,6 @@ contract SimpleETHContributionVault { /// @param amount Amount of ETH deposit event Deposit(address to, uint256 amount); - /// @notice Emitted on validator deposit - /// @param pubkeys array of validator pubkeys - /// @param withdrawal_credentials array of validator 0x1 withdrawal credentials - /// @param signatures array of validator signatures - /// @param deposit_data_roots array of deposit data roots - event DepositValidator( - bytes[] pubkeys, bytes[] withdrawal_credentials, bytes[] signatures, bytes32[] deposit_data_roots - ); - /// @notice Emitted on user rage quit /// @param to address that received amount /// @param amount amount rage quitted @@ -76,6 +67,15 @@ contract SimpleETHContributionVault { _deposit(msg.sender, msg.value); } + /// @notice deposit ether into the contract + /// @param to address to credit + function deposit( + address to + ) external payable { + if (to == address(0)) revert Invalid_Address(); + _deposit(to, msg.value); + } + /// @notice Deposit ETH into ETH2 deposit contract /// @param pubkeys Array of validator pub keys /// @param withdrawal_credentials Array of validator withdrawal credentials @@ -113,8 +113,6 @@ contract SimpleETHContributionVault { i++; } } - - emit DepositValidator(pubkeys, withdrawal_credentials, signatures, deposit_data_roots); } /// @notice Exit contribution vault prior to deposit starts diff --git a/src/test/safe/SimpleETHContributionVault.t.sol b/src/test/safe/SimpleETHContributionVault.t.sol index 1e29d86..b0f5da8 100644 --- a/src/test/safe/SimpleETHContributionVault.t.sol +++ b/src/test/safe/SimpleETHContributionVault.t.sol @@ -42,7 +42,7 @@ contract SimpleETHContributionVaultTest is Test { mERC20.mint(type(uint256).max); } - function test_deposit() external { + function test_receive() external { vm.deal(user1, ETH_STAKE); vm.expectEmit(false, false, false, true); @@ -55,6 +55,23 @@ contract SimpleETHContributionVaultTest is Test { assertEq(contributionVault.userBalances(user1), ETH_STAKE, "failed to credit user balance"); } + function test_deposit() external { + vm.deal(user1, ETH_STAKE); + + vm.expectEmit(false, false, false, true); + emit Deposit(user1, ETH_STAKE); + + vm.prank(user1); + contributionVault.deposit{value: ETH_STAKE}(user1); + + assertEq(contributionVault.userBalances(user1), ETH_STAKE, "failed to credit user balance"); + } + + function test_cannotDepositInvalidZero() external { + vm.expectRevert(Invalid_Address.selector); + contributionVault.deposit{value: ETH_STAKE}(address(0)); + } + function testFuzz_deposit(address user, uint256 amount) external { vm.assume(amount > 0); vm.assume(user != address(0)); From 84e6025f9ae95e848d76a6181991c9e3e8a7e419 Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Wed, 20 Dec 2023 15:55:59 +0200 Subject: [PATCH 20/22] chore: add fuzz deposit function --- .../safe/SimpleETHContributionVault.t.sol | 38 +++++++++++++++---- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/src/test/safe/SimpleETHContributionVault.t.sol b/src/test/safe/SimpleETHContributionVault.t.sol index b0f5da8..2ad32bb 100644 --- a/src/test/safe/SimpleETHContributionVault.t.sol +++ b/src/test/safe/SimpleETHContributionVault.t.sol @@ -29,10 +29,15 @@ contract SimpleETHContributionVaultTest is Test { MockERC20 mERC20; function setUp() public { + safe = makeAddr("safe"); + user1 = makeAddr("user1"); + user2 = makeAddr("user2"); + user3 = makeAddr("user3"); + uint256 mainnetBlock = 17_421_005; vm.createSelectFork(getChain("mainnet").rpcUrl, mainnetBlock); - safe = makeAddr("safe"); + contributionVault = new SimpleETHContributionVault( safe, ETH_DEPOSIT_CONTRACT @@ -55,6 +60,23 @@ contract SimpleETHContributionVaultTest is Test { assertEq(contributionVault.userBalances(user1), ETH_STAKE, "failed to credit user balance"); } + function testFuzz_receive(address user, uint256 amount) external { + vm.assume(amount > 0); + vm.assume(user != address(0)); + amount = bound(amount, 0, address(this).balance); + + vm.deal(user, amount); + + vm.expectEmit(false, false, false, true); + emit Deposit(user, amount); + + vm.prank(user); + (bool _success,) = payable(contributionVault).call{value: amount}(""); + assertTrue(_success, "call failed"); + + assertEq(contributionVault.userBalances(user), amount); + } + function test_deposit() external { vm.deal(user1, ETH_STAKE); @@ -67,14 +89,10 @@ contract SimpleETHContributionVaultTest is Test { assertEq(contributionVault.userBalances(user1), ETH_STAKE, "failed to credit user balance"); } - function test_cannotDepositInvalidZero() external { - vm.expectRevert(Invalid_Address.selector); - contributionVault.deposit{value: ETH_STAKE}(address(0)); - } - function testFuzz_deposit(address user, uint256 amount) external { vm.assume(amount > 0); vm.assume(user != address(0)); + amount = bound(amount, 0, address(this).balance); vm.deal(user, amount); @@ -82,12 +100,16 @@ contract SimpleETHContributionVaultTest is Test { emit Deposit(user, amount); vm.prank(user); - (bool _success,) = payable(contributionVault).call{value: amount}(""); - assertTrue(_success, "call failed"); + contributionVault.deposit{value: amount}(user); assertEq(contributionVault.userBalances(user), amount); } + function test_cannotDepositInvalidZero() external { + vm.expectRevert(Invalid_Address.selector); + contributionVault.deposit{value: ETH_STAKE}(address(0)); + } + function test_rageQuit() external { vm.deal(user1, ETH_STAKE); From 1cee2cd8cdbec609553fc7eca4f56c2f9a0c2a4a Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Wed, 20 Dec 2023 16:36:25 +0200 Subject: [PATCH 21/22] chore: add invalid deposit data test --- .../SimpleETHContributionVault.sol | 10 +++++----- src/test/safe/SimpleETHContributionVault.t.sol | 18 +++++++++++++++--- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/src/safe-modules/SimpleETHContributionVault.sol b/src/safe-modules/SimpleETHContributionVault.sol index 16fa4c7..b76d928 100644 --- a/src/safe-modules/SimpleETHContributionVault.sol +++ b/src/safe-modules/SimpleETHContributionVault.sol @@ -19,10 +19,10 @@ contract SimpleETHContributionVault { error IncompleteContribution(uint256 actual, uint256 expected); /// @notice invalid deposit data - error InvalidDepositData(); + error Invalid__DepositData(); /// @notice Invalid Address - error Invalid_Address(); + error Invalid__Address(); /// @notice Emitted on deposit ETH /// @param to address the credited ETH @@ -72,7 +72,7 @@ contract SimpleETHContributionVault { function deposit( address to ) external payable { - if (to == address(0)) revert Invalid_Address(); + if (to == address(0)) revert Invalid__Address(); _deposit(to, msg.value); } @@ -94,7 +94,7 @@ contract SimpleETHContributionVault { (signatures.length != size) || (deposit_data_roots.length != size) ) { - revert InvalidDepositData(); + revert Invalid__DepositData(); } @@ -121,7 +121,7 @@ contract SimpleETHContributionVault { /// @param to Address to send funds to /// @param amount balance to withdraw function rageQuit(address to, uint256 amount) external { - if (to == address(0)) revert Invalid_Address(); + if (to == address(0)) revert Invalid__Address(); if (activated == true) revert CannotRageQuit(); userBalances[msg.sender] -= amount; diff --git a/src/test/safe/SimpleETHContributionVault.t.sol b/src/test/safe/SimpleETHContributionVault.t.sol index 2ad32bb..ca2f725 100644 --- a/src/test/safe/SimpleETHContributionVault.t.sol +++ b/src/test/safe/SimpleETHContributionVault.t.sol @@ -7,7 +7,8 @@ import {SimpleETHContributionVault} from "src/safe-modules/SimpleETHContribution contract SimpleETHContributionVaultTest is Test { error CannotRageQuit(); - error Invalid_Address(); + error Invalid__Address(); + error Invalid__DepositData(); error Unauthorized(address user); event Deposit(address to, uint256 amount); @@ -106,7 +107,7 @@ contract SimpleETHContributionVaultTest is Test { } function test_cannotDepositInvalidZero() external { - vm.expectRevert(Invalid_Address.selector); + vm.expectRevert(Invalid__Address.selector); contributionVault.deposit{value: ETH_STAKE}(address(0)); } @@ -164,8 +165,19 @@ contract SimpleETHContributionVaultTest is Test { contributionVault.rageQuit(user1, ETH_STAKE); } + function test_invalidDepositData() external { + ( + bytes[] memory pubkeys, + bytes[] memory withdrawal_credentials,, + ) = getETHValidatorData(); + + vm.prank(safe); + vm.expectRevert(Invalid__DepositData.selector); + contributionVault.depositValidator(pubkeys, withdrawal_credentials, new bytes[](0), new bytes32[](0)); + } + function test_cannotRageQuitIfZeroAddress() external { - vm.expectRevert(Invalid_Address.selector); + vm.expectRevert(Invalid__Address.selector); contributionVault.rageQuit( address(0), 1 From aab0b557e41ef3a9bb34cc73b0877284771f63b1 Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Wed, 20 Dec 2023 17:19:18 +0200 Subject: [PATCH 22/22] chore: fix testFuzz ragequit --- src/test/safe/SimpleETHContributionVault.t.sol | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/test/safe/SimpleETHContributionVault.t.sol b/src/test/safe/SimpleETHContributionVault.t.sol index ca2f725..315a786 100644 --- a/src/test/safe/SimpleETHContributionVault.t.sol +++ b/src/test/safe/SimpleETHContributionVault.t.sol @@ -128,6 +128,7 @@ contract SimpleETHContributionVaultTest is Test { function testFuzz_rageQuit(address user, uint256 amount) external { vm.assume(amount > 0); vm.assume(user != address(0)); + amount = bound(amount, 0, address(this).balance); vm.deal(user, amount); @@ -225,7 +226,7 @@ contract SimpleETHContributionVaultTest is Test { contributionVault.rescueFunds(address(mERC20), amount); } - function testFuzz_rescueFundETH(uint256 amount) external { + function testFuzz_rescueFunds(uint256 amount) external { vm.assume(amount > 0); mERC20.transfer(address(contributionVault), amount);