From 06d377566ffedf61b37a60fd3cfb3dac10ba689a Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Thu, 7 Nov 2024 14:22:07 +0100 Subject: [PATCH] EIP-7002: return fee from getter and add usage example The get operation is meant to be used by contracts to compute the exact amount of ether required to add a withdrawal request. It does not return the fee directly though, but it returns the count of 'excess requests' instead. The caller has to compute the fee themselves by applying the fee formula. I think this is not great. The fee logic, while reasonably straightforward, is an implementation detail of the contract. Duplicating it into caller contracts could lead to a mismatch in the computed values, and it's not necessary. I propose we change the system contract to return the fee directly. This contract change has also been submitted in this PR: https://github.com/lightclient/sys-asm/pull/33 The Rationale section of the EIP also had some outdated text about returning fee overage to the caller. The contract does not return overage, so I am removing that section here, and adding recommendations & example code for calling the contract. --- EIPS/eip-7002.md | 139 +++++++++++++++++++++++++---------------------- 1 file changed, 75 insertions(+), 64 deletions(-) diff --git a/EIPS/eip-7002.md b/EIPS/eip-7002.md index afd8756c51d46..8056fd1545864 100644 --- a/EIPS/eip-7002.md +++ b/EIPS/eip-7002.md @@ -82,7 +82,7 @@ The contract has three different code paths, which can be summarized at a high l 1. Add withdrawal request - requires a `56` byte input, the validator's public key concatenated with a big-endian `uint64` amount value. -2. Excess withdrawal requests getter - if the input length is zero, return the current excess withdrawal requests count. +2. Fee getter - if the input length is zero, return the current fee required to add a withdrawal request. 3. System process - if called by system address, pop off the withdrawal requests for the current block from the queue. ##### Add Withdrawal Request @@ -145,13 +145,7 @@ def fake_exponential(factor: int, numerator: int, denominator: int) -> int: ##### Excess Withdrawal Requests Getter -When the input to the contract is length zero, interpret this as a get request for the current excess withdrawal requests count. - -```python -def get_excess_withdrawal_requests(): - count = sload(WITHDRAWAL_REQUEST_PREDEPLOY_ADDRESS, EXCESS_WITHDRAWAL_REQUESTS_STORAGE_SLOT) - return count -``` +When the input to the contract is length zero, interpret this as a get request for the current fee, i.e. the contract returns the result of `get_fee()`. ##### System Call @@ -245,35 +239,13 @@ eq push1 0xc7 jumpi -calldatasize -iszero -iszero -push1 0x28 -jumpi - -push0 -sload -push0 -mstore -push1 0x20 -push0 -return - -jumpdest -calldatasize -push1 0x38 -eq -iszero -push2 0x01f0 -jumpi - push1 0x11 push0 sload dup1 push32 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff eq -push2 0x01f0 +push2 0x01f3 jumpi push1 0x01 @@ -288,7 +260,7 @@ push0 dup3 gt iszero -push1 0x80 +push1 0x68 jumpi dup2 @@ -306,7 +278,7 @@ push1 0x01 add swap2 swap1 -push1 0x65 +push1 0x4d jump jumpdest @@ -314,9 +286,29 @@ swap1 swap4 swap1 div +calldatasize +push1 0x38 +eq +push1 0x84 +jumpi + +calldatasize +push2 0x01f3 +jumpi + +callvalue +push2 0x01f3 +jumpi +push0 +mstore +push1 0x20 +push0 +return + +jumpdest callvalue lt -push2 0x01f0 +push2 0x01f3 jumpi push1 0x01 @@ -389,14 +381,11 @@ jumpdest dup2 dup2 eq -push2 0x017f +push2 0x0182 jumpi -dup1 -push1 0x4c -mul -dup4 dup3 +dup2 add push1 0x03 mul @@ -414,26 +403,32 @@ push1 0x01 add sload swap2 +dup4 +push1 0x4c +mul +swap1 push1 0x60 shl -dup4 +dup2 mstore -dup3 push1 0x14 add +swap1 +dup2 mstore -dup1 +push1 0x20 +add +dup2 push32 0xffffffffffffffffffffffffffffffff00000000000000000000000000000000 and -dup3 -push1 0x34 -add +dup2 mstore +push1 0x10 +add +swap1 push1 0x40 shr swap1 -push1 0x44 -add dup2 push1 0x38 shr @@ -495,13 +490,12 @@ add dup1 swap3 eq -push2 0x0191 +push2 0x0194 jumpi - swap1 push1 0x02 sstore -push2 0x019c +push2 0x019f jump jumpdest @@ -521,9 +515,8 @@ dup1 push32 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff eq iszero -push2 0x01c9 +push2 0x01cc jumpi - pop push0 @@ -535,13 +528,13 @@ dup3 dup3 add gt -push2 0x01de +push2 0x01e1 jumpi pop pop push0 -push2 0x01e4 +push2 0x01e7 jump jumpdest @@ -623,16 +616,6 @@ Although there is a maximum number of withdrawal requests that can passed to the The alternative design considered was to have calls to the contract fail after `MAX_WITHDRAWAL_REQUESTS_PER_BLOCK` successful calls were made within the context of a single block. This would eliminate the need for the message queue, but would come at the cost of a bad UX of contract call failures in times of high exiting. The complexity to mitigate this bad UX is relatively low and is currently favored. -### Utilizing `CALL` to return excess payment - -Calls to the contract require a fee payment defined by the current state of the contract. Smart contracts can easily perform a read/calculation to pay the precise fee, whereas EOAs will likely need to compute and send some amount over the current fee at time of signing the transaction. This will result in EOAs having fee payment overages in the normal case. These should be returned to the caller. - -There are two potential designs to return excess fee payments to the caller (1) use an EVM `CALL` with some gas stipend or (2) have special functionality to allow the contract to "credit" the caller's account with the excess fee. - -Option (1) has been selected in the current specification because it utilizes less exceptional functionality and is likely simpler to implement and ensure correctness. The current version sends a gas stipen of 2300. This is following the (outdated) solidity pattern primarily to simplify contract gas accounting (allowing it to be a fixed instead of dynamic cost). The `CALL` could forward the maximum allowed gas but would then require the cost of the contract to be dynamic. - -Option (2) utilizes custom logic (exceptional to base EVM logic) to credit the excess back to the callers balance. This would potentially simplify concerns around contract gas costs/metering, but at the cost of non-standard EVM complexity. We are open to this path, but want to solicit more input before writing it into the specification. - ### Rate limiting using a fee Transactions are naturally rate-limited in the execution layer via the gas limit, but an adversary willing to pay market-rate gas fees (and potentially utilize builder markets to pay for front-of-block transaction inclusion) can fill up the exit operation limits for relatively cheap, thus griefing honest validators that want to make a withdrawal request. @@ -678,6 +661,34 @@ There might be existing custody relationships and/or products that rely upon the In the event that existing validators/custodians rely on this, then the validators can be exited and restaked utilizing 0x01 withdrawal credentials pointing to a smart contract that simulates this behaviour. +### Fee Overpayment + +Calls to the system contract require a fee payment defined by the current contract state. Overpaid fees are not returned to the caller. It is not generally possible to compute the exact required fee amount ahead of time. When adding a withdrawal request from a contract, the contract can perform a read operation to check for the current fee and then pay exactly the required amount. Here is an example in Solidity: + +``` +function addWithdrawal(bytes memory pubkey, uint64 amount) private { + assert(pubkey.length == 48); + + // Read current fee from the contract. + (bool readOK, bytes memory feeData) = WithdrawalsContract.staticcall(''); + if (!readOK) { + revert('reading fee failed'); + } + uint256 fee = uint256(bytes32(feeData)); + + // Add the request. + bytes memory callData = abi.encodePacked(pubkey, amount); + (bool writeOK,) = WithdrawalsContract.call{value: fee}(callData); + if (!writeOK) { + revert('adding request failed'); + } +} +``` + +Note: the system contract uses the EVM `CALLER` operation (Solidity: `msg.sender`) as the target address for withdrawals, i.e. the address that calls the system contract must match the 0x01 withdrawal credential recorded in the beacon state. + +Using an EOA to request withdrawals directly will always result in overpayment of fees. There is no way for an EOA to use a wrapper contract to request a withdrawal. And even if a way existed, the gas cost of returning the overage would likely be higher than the overage itself. If requesting withdrawals to an EOA through the system contract is desired, we recommend that users perform transaction simulations to estimate a reasonable fee amount to sent. + ## Copyright Copyright and related rights waived via [CC0](../LICENSE.md).