Skip to content

Commit

Permalink
Update EIP-7002: return fee from getter and add usage example
Browse files Browse the repository at this point in the history
Merged by EIP-Bot.
  • Loading branch information
fjl authored Nov 27, 2024
1 parent b16c055 commit a00464b
Showing 1 changed file with 56 additions and 19 deletions.
75 changes: 56 additions & 19 deletions EIPS/eip-7002.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -143,15 +143,9 @@ def fake_exponential(factor: int, numerator: int, denominator: int) -> int:
return output // denominator
```

##### Excess Withdrawal Requests Getter
##### Fee 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

Expand Down Expand Up @@ -247,6 +241,7 @@ push20 0xfffffffffffffffffffffffffffffffffffffffe
eq
push1 0xcb
jumpi
push1 0x11
push0
sload
Expand All @@ -255,19 +250,22 @@ push32 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
eq
push2 0x01f4
jumpi
push1 0x01
dup3
mul
push1 0x01
swap1
push0
jumpdest
push0
dup3
gt
iszero
push1 0x68
jumpi
dup2
add
swap1
Expand All @@ -285,6 +283,7 @@ swap2
swap1
push1 0x4d
jump
jumpdest
swap1
swap4
Expand All @@ -299,22 +298,27 @@ push1 0x38
eq
push1 0x88
jumpi
calldatasize
push2 0x01f4
jumpi
callvalue
push2 0x01f4
jumpi
push0
mstore
push1 0x20
push0
return
jumpdest
callvalue
lt
push2 0x01f4
jumpi
push1 0x01
sload
push1 0x01
Expand Down Expand Up @@ -360,6 +364,7 @@ add
push1 0x03
sstore
stop
jumpdest
push1 0x03
sload
Expand All @@ -373,16 +378,20 @@ push1 0x10
gt
push1 0xdf
jumpi
pop
push1 0x10
jumpdest
push0
jumpdest
dup2
dup2
eq
push2 0x0183
jumpi
dup3
dup2
add
Expand Down Expand Up @@ -479,6 +488,7 @@ push1 0x01
add
push1 0xe1
jump
jumpdest
swap2
add
Expand All @@ -487,11 +497,13 @@ swap3
eq
push2 0x0195
jumpi
swap1
push1 0x02
sstore
push2 0x01a0
jump
jumpdest
swap1
pop
Expand All @@ -501,6 +513,7 @@ sstore
push0
push1 0x03
sstore
jumpdest
push0
sload
Expand All @@ -510,8 +523,10 @@ eq
iszero
push2 0x01cd
jumpi
pop
push0
jumpdest
push1 0x01
sload
Expand All @@ -522,16 +537,19 @@ add
gt
push2 0x01e2
jumpi
pop
pop
push0
push2 0x01e8
jump
jumpdest
add
push1 0x02
swap1
sub
jumpdest
push0
sstore
Expand All @@ -542,6 +560,7 @@ push1 0x4c
mul
push0
return
jumpdest
push0
push0
Expand Down Expand Up @@ -604,16 +623,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.
Expand Down Expand Up @@ -659,6 +668,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 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 send.

## Copyright

Copyright and related rights waived via [CC0](../LICENSE.md).

0 comments on commit a00464b

Please sign in to comment.