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

Generalized Proofs #346

Closed
wants to merge 56 commits into from
Closed
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
6b172db
init
Sidu28 Nov 28, 2023
ca2c983
added proof switch
Sidu28 Nov 28, 2023
b56976c
added proof switch turn on function
Sidu28 Nov 28, 2023
38dafc6
cleanup
Sidu28 Nov 29, 2023
0e3f0f1
cleanup
Sidu28 Nov 29, 2023
ceb5cb1
minor cleanup
Sidu28 Nov 29, 2023
d290f04
minor cleanup
Sidu28 Nov 29, 2023
59a328d
cleanup
Sidu28 Nov 29, 2023
a6ac41a
moved stuff to EPM
Sidu28 Nov 30, 2023
7613bea
added oracle root check
Sidu28 Nov 30, 2023
79d6950
fixed
Sidu28 Nov 30, 2023
258d95f
withdrawalProvenUntilTimestamp to mostRecentWithdrawalTimestamp
Sidu28 Nov 30, 2023
2096a24
removed maxFee
Sidu28 Nov 30, 2023
87f6de2
updated sumOfPartialWithdrawalsClaimedGwei
Sidu28 Nov 30, 2023
a907f7e
clean
Sidu28 Dec 5, 2023
7f45cac
added natspec
Sidu28 Dec 5, 2023
e25381c
Merge branch 'm2-mainnet' into partialpoc
Sidu28 Dec 5, 2023
4ce6725
code compiling
Sidu28 Dec 5, 2023
89db042
Merge branch 'partialpoc' of https://github.com/Layr-Labs/eigenlayer-…
Sidu28 Dec 5, 2023
64f6858
added functions to interface
Sidu28 Dec 5, 2023
673505f
added back tests
Dec 7, 2023
eb718a2
addressed comments
Dec 7, 2023
fc2c45e
fixed comparator
Sidu28 Dec 7, 2023
ab40644
bug fix, add non reentrant
Sidu28 Dec 7, 2023
fd6838a
addressed all changes
Sidu28 Dec 7, 2023
7fc43a5
addressed comments
Sidu28 Dec 7, 2023
2d5216e
fixed CEI
Sidu28 Dec 8, 2023
51721c0
added extra check of podAddress against podOwner
Sidu28 Dec 8, 2023
b281a62
testing
Sidu28 Dec 8, 2023
183dd32
switched wei to gwei for readability
Sidu28 Dec 9, 2023
1343c4d
enforce enough balance for fee
Sidu28 Dec 9, 2023
24c4d8a
added test for amounts
Sidu28 Dec 9, 2023
5407420
fixed breaking tests
Sidu28 Dec 12, 2023
0de6c2c
added verifier to EPM
Sidu28 Dec 14, 2023
2405226
everything compiling except EPM unit tests
Sidu28 Dec 14, 2023
cb9e679
everything compiling except EPM unit tests
Sidu28 Dec 14, 2023
75923d7
fixed abi.decode/encode, added tests
Sidu28 Dec 17, 2023
2e4b640
added check for verifier
Sidu28 Dec 17, 2023
e869b1e
removed abi.decode, added struct to input
Sidu28 Dec 21, 2023
336e245
removed abi.decode, added struct to input
Sidu28 Dec 21, 2023
914ada4
tests working
Sidu28 Dec 21, 2023
f91a038
fixed tests
Sidu28 Dec 21, 2023
8ea334d
addressed comments
Sidu28 Dec 21, 2023
dcf8a04
addressed comments
Sidu28 Dec 26, 2023
340a819
fixed missing if else statement
Sidu28 Dec 26, 2023
83c5347
Added length verification checks
Sidu28 Jan 3, 2024
4e1663c
tests compile
Sidu28 Jan 3, 2024
fd26c96
all tests working
Sidu28 Jan 3, 2024
eb5c18e
test building
Sidu28 Jan 6, 2024
e5054e1
fixed CEI and fee deduction logic
Sidu28 Jan 10, 2024
53a0c4c
fixed CEI and fee deduction logic
Sidu28 Jan 10, 2024
0720245
quick fix
Sidu28 Jan 10, 2024
39ea24d
moved pauser check from EP to EPM
Sidu28 Jan 10, 2024
a14f62b
extraneous check
Sidu28 Jan 10, 2024
f7d2b8e
added non revert test cases
Sidu28 Jan 10, 2024
c533844
Merge branch 'm2-mainnet' into partialpoc
Sidu28 Jan 10, 2024
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
6 changes: 5 additions & 1 deletion src/contracts/interfaces/IEigenPod.sol
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,6 @@ interface IEigenPod {
/// @notice Emitted when ETH that was previously received via the `receive` fallback is withdrawn
event NonBeaconChainETHWithdrawn(address indexed recipient, uint256 amountWithdrawn);


/// @notice The max amount of eth, in gwei, that can be restaked per validator
function MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR() external view returns (uint64);

Expand Down Expand Up @@ -215,4 +214,9 @@ interface IEigenPod {

/// @notice called by owner of a pod to remove any ERC20s deposited in the pod
function recoverTokens(IERC20[] memory tokenList, uint256[] memory amountsToWithdraw, address recipient) external;

function fulfillPartialWithdrawalProofRequest(
Sidu28 marked this conversation as resolved.
Show resolved Hide resolved
IEigenPodManager.WithdrawalCallbackInfo calldata withdrawalCallbackInfo,
address feeRecipient
) external;
}
28 changes: 28 additions & 0 deletions src/contracts/interfaces/IEigenPodManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ interface IEigenPodManager is IPausable {
/// @notice Emitted when `maxPods` value is updated from `previousValue` to `newValue`
event MaxPodsUpdated(uint256 previousValue, uint256 newValue);

/// @notice Emitted when a new proof fulfiller is added
event ProofServiceUpdated(address indexed proofService);

/// @notice Emitted when a withdrawal of beacon chain ETH is completed
event BeaconChainETHWithdrawalCompleted(
address indexed podOwner,
Expand All @@ -39,6 +42,24 @@ interface IEigenPodManager is IPausable {
bytes32 withdrawalRoot
);

//info for each withdrawal called back by proof service
struct WithdrawalCallbackInfo {
address podOwner;
Sidu28 marked this conversation as resolved.
Show resolved Hide resolved
uint64 startTimestamp;
uint64 endTimestamp;
uint256 provenPartialWithdrawalSumWei;
uint256 fee;
}


struct ProofService {
address caller;
// whether or not the proof fulfiller has been added
uint256 maxFee;
// the commission rate of the proof fulfiller
Sidu28 marked this conversation as resolved.
Show resolved Hide resolved
address feeRecipient;
}

/**
* @notice Creates an EigenPod for the sender.
* @dev Function will revert if the `msg.sender` already has an EigenPod.
Expand Down Expand Up @@ -143,4 +164,11 @@ interface IEigenPodManager is IPausable {
* @dev Reverts if `shares` is not a whole Gwei amount
*/
function withdrawSharesAsTokens(address podOwner, address destination, uint256 shares) external;


/// @notice Returns the status of the proof switch
function partialWithdrawalProofSwitch() external view returns (bool);
Sidu28 marked this conversation as resolved.
Show resolved Hide resolved

function updateProofService(address fulfiller, uint256 feeBips, address feeRecipient) external;

}
47 changes: 43 additions & 4 deletions src/contracts/pods/EigenPod.sol
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@

import "./EigenPodPausingConstants.sol";


import "forge-std/Test.sol";

/**
* @title The implementation contract used for restaking beacon chain ETH on EigenLayer
* @author Layr Labs, Inc.
Expand All @@ -35,7 +38,7 @@
* @dev Note that all beacon chain balances are stored as gwei within the beacon chain datastructures. We choose
* to account balances in terms of gwei in the EigenPod contract and convert to wei when making calls to other contracts
*/
contract EigenPod is IEigenPod, Initializable, ReentrancyGuardUpgradeable, EigenPodPausingConstants {
contract EigenPod is IEigenPod, Initializable, ReentrancyGuardUpgradeable, EigenPodPausingConstants, Test {
using BytesLib for bytes;
using SafeERC20 for IERC20;
using BeaconChainProofs for *;
Expand Down Expand Up @@ -94,6 +97,9 @@
/// @notice This variable tracks the total amount of partial withdrawals claimed via merkle proofs prior to a switch to ZK proofs for claiming partial withdrawals
uint64 public sumOfPartialWithdrawalsClaimedGwei;

/// @notice This variable tracks the timestamp at which the last partial withdrawal was proven via ZK proofs
uint64 public withdrawalProvenUntilTimestamp;
Sidu28 marked this conversation as resolved.
Show resolved Hide resolved

modifier onlyEigenPodManager() {
require(msg.sender == address(eigenPodManager), "EigenPod.onlyEigenPodManager: not eigenPodManager");
_;
Expand Down Expand Up @@ -124,6 +130,11 @@
_;
}

modifier partialWithdrawalProofSwitchOff() {
Sidu28 marked this conversation as resolved.
Show resolved Hide resolved
Sidu28 marked this conversation as resolved.
Show resolved Hide resolved
require(!eigenPodManager.partialWithdrawalProofSwitch(), "EigenPod.partialWithdrawalProofSwitchOff: partial withdrawal proof switch is on");
_;
}

/**
* @notice Based on 'Pausable' code, but uses the storage of the EigenPodManager instead of this contract. This construction
* is necessary for enabling pausing all EigenPods at the same time (due to EigenPods being Beacon Proxies).
Expand Down Expand Up @@ -311,7 +322,6 @@
(validatorFieldsProofs.length == validatorFields.length),
"EigenPod.verifyWithdrawalCredentials: validatorIndices and proofs must be same length"
);

/**
* Withdrawal credential proof should not be "stale" (older than VERIFY_BALANCE_UPDATE_WINDOW_SECONDS) as we are doing a balance check here
* The validator container persists as the state evolves and even after the validator exits. So we can use a more "fresh" credential proof within
Expand Down Expand Up @@ -428,6 +438,36 @@
_sendETH(recipient, amountWei);
}


Sidu28 marked this conversation as resolved.
Show resolved Hide resolved
/*******************************************************************************
EXTERNAL FUNCTIONS CALLABLE BY PERMISSIONED SERVICES
*******************************************************************************/

function fulfillPartialWithdrawalProofRequest(
IEigenPodManager.WithdrawalCallbackInfo calldata withdrawalCallbackInfo,
address feeRecipient
) external onlyEigenPodManager {
//TODO: figure if we wanna do the first proof from genesis time or not
if(withdrawalProvenUntilTimestamp == 0){
withdrawalProvenUntilTimestamp = GENESIS_TIME;
}
uint256 provenPartialWithdrawalSumWei = withdrawalCallbackInfo.provenPartialWithdrawalSumWei;

require(withdrawalCallbackInfo.startTimestamp < withdrawalCallbackInfo.endTimestamp, "EigenPod.fulfillPartialWithdrawalProofRequest: startTimestamp must precede endTimestamp");
wadealexc marked this conversation as resolved.
Show resolved Hide resolved
require(withdrawalCallbackInfo.startTimestamp == withdrawalProvenUntilTimestamp, "EigenPod.fulfillPartialWithdrawalProofRequest: startTimestamp must match withdrawalProvenUntilTimestamp");
provenPartialWithdrawalSumWei -= withdrawalCallbackInfo.fee;
//send proof service their fee
AddressUpgradeable.sendValue(payable(feeRecipient), withdrawalCallbackInfo.fee);

//subtract an partial withdrawals that may have been claimed via merkle proofs
Sidu28 marked this conversation as resolved.
Show resolved Hide resolved
if(provenPartialWithdrawalSumWei > sumOfPartialWithdrawalsClaimedGwei * GWEI_TO_WEI) {
Sidu28 marked this conversation as resolved.
Show resolved Hide resolved
provenPartialWithdrawalSumWei -= sumOfPartialWithdrawalsClaimedGwei * GWEI_TO_WEI;
Sidu28 marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Collaborator

Choose a reason for hiding this comment

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

What happens if a user proved a bunch of partial withdrawals and then did a zk proof on unproven withdrawals? They just don't see the funds?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

not sure I follow the question - if the try to "double prove" via merkle proofs then zk proofs, they would get nothing the second time aorund

Copy link
Collaborator

Choose a reason for hiding this comment

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

No i mean picture a person who does merkle proofs for some partial withdrawal. This necessarily increases their sumOfPartialWithdrawalsClaimedGwei

Then we enable the proof service and they prove zk withdrawals for different, totally unrelated partial withdrawals. You're subtracting from the amount they get, no?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

oh I see, we're always gonna have "startTimestamp == mostRecentWithdrawalTimestamp" when we submit the request to bonsai. So if there is any claimed partial withdrawals, the starting timestamp provided is such that it counts that

_sendETH_AsDelayedWithdrawal(podOwner, provenPartialWithdrawalSumWei);
}

withdrawalProvenUntilTimestamp = withdrawalCallbackInfo.endTimestamp;
}
Fixed Show fixed Hide fixed

/*******************************************************************************
INTERNAL FUNCTIONS
*******************************************************************************/
Expand Down Expand Up @@ -720,7 +760,7 @@
uint64 withdrawalTimestamp,
address recipient,
uint64 partialWithdrawalAmountGwei
) internal returns (VerifiedWithdrawal memory) {
) internal partialWithdrawalProofSwitchOff returns (VerifiedWithdrawal memory) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'd prefer we do an explicit require-type check, and put it where this method is called - rather than a modifier buried this deep in the stack trace

Copy link
Collaborator

Choose a reason for hiding this comment

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

(in _verifyAndProcessWithdrawal)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

is there a reason for this? Just readability or is it something else?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yes, readability and style. We don't typically have modifiers on internal functions, and especially when one is so deep in the function call stack like this, it should really just be an explicit require check instead, because modifiers are easier to overlook. People are used to seeing them on external functions, not in places like this.

The reason I'd prefer it to be in _verifyAndProcessWithdrawal is so that it's more clear when reviewers are looking at that function that the entire branch can be turned off when the proof service is enabled. It's easier to grok than having to remember that there's a check buried in the function itself.

emit PartialWithdrawalRedeemed(
validatorIndex,
withdrawalTimestamp,
Expand Down Expand Up @@ -786,7 +826,6 @@
return _validatorPubkeyHashToInfo[pubkeyHash].status;
}


/**
* @dev This empty reserved space is put in place to allow future versions to add new
* variables without shifting down storage in the inheritance chain.
Expand Down
37 changes: 37 additions & 0 deletions src/contracts/pods/EigenPodManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,16 @@ contract EigenPodManager is
_;
}

modifier onlyProofService() {
require(msg.sender == proofService.caller, "EigenPod.onlyProofService: not a permissioned fulfiller");
Sidu28 marked this conversation as resolved.
Show resolved Hide resolved
_;
}

modifier partialWithdrawalProofSwitchOn() {
require(partialWithdrawalProofSwitch, "EigenPod.partialWithdrawalProofSwitchOn: partial withdrawal proof switch is off");
_;
}
Sidu28 marked this conversation as resolved.
Show resolved Hide resolved

constructor(
IETHPOSDeposit _ethPOS,
IBeacon _eigenPodBeacon,
Expand Down Expand Up @@ -215,6 +225,33 @@ contract EigenPodManager is
ownerToPod[podOwner].withdrawRestakedBeaconChainETH(destination, shares);
}

function proofServiceCallback(
Sidu28 marked this conversation as resolved.
Show resolved Hide resolved
WithdrawalCallbackInfo[] calldata callbackInfo
) external onlyProofService partialWithdrawalProofSwitchOn {
Sidu28 marked this conversation as resolved.
Show resolved Hide resolved
for(uint256 i = 0; i < callbackInfo.length; i++) {
Sidu28 marked this conversation as resolved.
Show resolved Hide resolved
require(callbackInfo[i].fee <= proofService.maxFee, "EigenPod.fulfillPartialWithdrawalProofRequest: fee must be less than or equal to maxFee");
IEigenPod pod = ownerToPod[callbackInfo[i].podOwner];
Sidu28 marked this conversation as resolved.
Show resolved Hide resolved
pod.fulfillPartialWithdrawalProofRequest(callbackInfo[i], proofService.feeRecipient);
}
}
Sidu28 marked this conversation as resolved.
Show resolved Hide resolved

function flipPartialWithdrawalProofSwitch() external onlyOwner {
if(partialWithdrawalProofSwitch) {
Sidu28 marked this conversation as resolved.
Show resolved Hide resolved
partialWithdrawalProofSwitch = false;
} else {
partialWithdrawalProofSwitch = true;
}
}

function updateProofService(address caller, uint256 maxFee, address feeRecipient) external onlyOwner {
proofService = ProofService({
caller: caller,
maxFee: maxFee,
Sidu28 marked this conversation as resolved.
Show resolved Hide resolved
feeRecipient: feeRecipient
});
emit ProofServiceUpdated(proofService.caller);
Sidu28 marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* Sets the maximum number of pods that can be deployed
* @param newMaxPods The new maximum number of pods that can be deployed
Expand Down
8 changes: 7 additions & 1 deletion src/contracts/pods/EigenPodManagerStorage.sol
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,12 @@ abstract contract EigenPodManagerStorage is IEigenPodManager {
*/
mapping(address => int256) public podOwnerShares;

/// @notice This is the offchain proving service
ProofService public proofService;

/// @notice Switch to turn off partial withdrawal merkle proofs and turn on offchain proofs as a service
bool public partialWithdrawalProofSwitch;
Sidu28 marked this conversation as resolved.
Show resolved Hide resolved

constructor(
IETHPOSDeposit _ethPOS,
IBeacon _eigenPodBeacon,
Expand All @@ -83,5 +89,5 @@ abstract contract EigenPodManagerStorage is IEigenPodManager {
* variables without shifting down storage in the inheritance chain.
* See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
*/
uint256[45] private __gap;
uint256[43] private __gap;
}
4 changes: 4 additions & 0 deletions src/test/mocks/EigenPodManagerMock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -77,4 +77,8 @@ contract EigenPodManagerMock is IEigenPodManager, Test {
function numPods() external view returns (uint256) {}

function maxPods() external view returns (uint256) {}

function partialWithdrawalProofSwitch() external view returns (bool){}

function updateProofService(address fulfiller, uint256 feeBips, address feeRecipient) external{}
}
7 changes: 7 additions & 0 deletions src/test/mocks/EigenPodMock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -87,4 +87,11 @@ contract EigenPodMock is IEigenPod, Test {

/// @notice called by owner of a pod to remove any ERC20s deposited in the pod
function recoverTokens(IERC20[] memory tokenList, uint256[] memory amountsToWithdraw, address recipient) external {}

function updateProofService(address fulfiller, uint256 feeBips, address feeRecipient) external{}

function fulfillPartialWithdrawalProofRequest(
IEigenPodManager.WithdrawalCallbackInfo calldata withdrawalCallbackInfo,
address feeRecipient
) external {}
}
Loading