Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/safe module #107

Open
wants to merge 22 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
35d5ad8
feat: add eth vault gnosis safe module
samparsky Nov 6, 2023
4970baf
forge install: safe-contracts
samparsky Nov 6, 2023
a5eef60
add: safe module implementation
samparsky Nov 10, 2023
5b26779
add: simpleETHcontribution vault implementation and test
samparsky Nov 23, 2023
a96702c
chore: add validator test param
samparsky Nov 23, 2023
e8c73d9
chore: apply forge fmt to SimpleSimpleETHContributionVault and teset
samparsky Nov 23, 2023
8a219a6
chore: rename SimpleETHContributionVaultTest
samparsky Nov 23, 2023
f976d07
test: add SECVinvariant test
samparsky Nov 23, 2023
2c7df8a
test: fix compiler return warning
samparsky Nov 23, 2023
3dde397
test: add SECVinvariant invariant test functions
samparsky Nov 27, 2023
765d974
chore: update SECV docstring
samparsky Nov 27, 2023
c224748
test: add SECVinvariant depositValidator mock function
samparsky Nov 27, 2023
dea7cb1
test: fix invariant vault is solvent
samparsky Nov 27, 2023
a6bb0da
chore: remove safe-contracts submodule
samparsky Nov 27, 2023
784ab37
chore: add checks to depositValidator
samparsky Dec 20, 2023
7181fef
chore: add validation checks to SECV
samparsky Dec 20, 2023
03d831c
chore: arrange vars
samparsky Dec 20, 2023
65cf91d
chore: remove unused interface
samparsky Dec 20, 2023
7590760
chore: add deposit function
samparsky Dec 20, 2023
84e6025
chore: add fuzz deposit function
samparsky Dec 20, 2023
1cee2cd
chore: add invalid deposit data test
samparsky Dec 20, 2023
aab0b55
chore: fix testFuzz ragequit
samparsky Dec 20, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,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]
Expand Down
34 changes: 34 additions & 0 deletions src/interfaces/IETH2DepositContract.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
samparsky marked this conversation as resolved.
Show resolved Hide resolved

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);
}
148 changes: 148 additions & 0 deletions src/safe-modules/SimpleETHContributionVault.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
// 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 invalid deposit data
error Invalid__DepositData();

/// @notice Invalid Address
error Invalid__Address();

/// @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 user rage quit
/// @param to address that received amount
/// @param amount amount rage quitted
event RageQuit(address to, uint256 amount);

/// @notice Emitted on rescue funds
/// @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;

/// @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);
_;
}

constructor(address _safe, address eth2DepositContract) {
safe = _safe;
depositContract = IETH2DepositContract(eth2DepositContract);
}

receive() external payable {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we calculate at how many contributors might this break with 2100 gas, because the free gas in a receive is lower than the allocation of a new storage slot to write down their contribution?

We don't have to worry about fallback() when receive() is set, yes?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not we don't have to worry about fallback()

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The 2100 gas limit doesn't hold here. Any interaction via transfer/send will revert with OOG

_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
/// @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 onlySafe {
uint256 size = pubkeys.length;
samparsky marked this conversation as resolved.
Show resolved Hide resolved

if (
(withdrawal_credentials.length != size) ||
(signatures.length != size) ||
(deposit_data_roots.length != size)
) {
revert Invalid__DepositData();
}


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]
);

unchecked {
i++;
}
}
}

/// @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
function rageQuit(address to, uint256 amount) external {
if (to == address(0)) revert Invalid__Address();
if (activated == true) revert CannotRageQuit();

userBalances[msg.sender] -= amount;
to.safeTransferETH(amount);

emit RageQuit(to, amount);
}

/// @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 {
samparsky marked this conversation as resolved.
Show resolved Hide resolved
token.safeTransfer(safe, amount);
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);
}
}
Loading
Loading