From 846d5043a30472cb7f4035c76e30d9b1828dd1e1 Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Mon, 6 Jul 2026 22:57:19 +0200 Subject: [PATCH 1/2] builder_exits: implement fee-per-tx and queue disabling This implements the recent logic changes made to builder_deposits into the builder_exits contract. --- bytecode/builder_exits/ctor.hex | 2 +- bytecode/builder_exits/main.hex | 2 +- src/builder_exits/main.eas | 106 +++++++++++++++++---------- test/BuilderExit.t.sol | 123 ++++++++++++++++++++++++++------ 4 files changed, 172 insertions(+), 61 deletions(-) diff --git a/bytecode/builder_exits/ctor.hex b/bytecode/builder_exits/ctor.hex index 4867f29..6b8017f 100644 --- a/bytecode/builder_exits/ctor.hex +++ b/bytecode/builder_exits/ctor.hex @@ -1 +1 @@ -7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff5f5561018c80602d5f395ff33373fffffffffffffffffffffffffffffffffffffffe1460cb5760115f54807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1461018857600182026001905f5b5f82111560685781019083028483029004916001019190604d565b909390049250505036603014608857366101885734610188575f5260205ff35b341061018857600154600101600155600354806003026004013381556001015f35815560010160203590553360601b5f5260305f60143760445fa0600101600355005b6003546002548082038060101160df575060105b5f5b8181146101175782810160030260040181604402815460601b8152601401816001015481526020019060020154905260010160e1565b91018092146101295790600255610134565b90505f6002555f6003555b5f54807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff141561016157505f5b6001546002828201116101765750505f61017c565b01600290035b5f555f6001556044025ff35b5f5ffd \ No newline at end of file +7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff5f556101ca80602d5f395ff33373fffffffffffffffffffffffffffffffffffffffe1460e1575f54807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff146101c65760015460028111605157506057565b60029003015b601190600182026001905f5b5f821115607e57810190830284830290049160010191906063565b909390049250505036603014609e57366101c657346101c6575f5260205ff35b34106101c657600154600101600155600354806003026004013381556001015f35815560010160203590553360601b5f5260305f60143760445fa0600101600355005b6003546002548082038060101160f5575060105b5f5b81811461012d5782810160030260040181604402815460601b8152601401816001015481526020019060020154905260010160f7565b910180921461013f579060025561014a565b90505f6002555f6003555b36610198575f54600154817fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff146101865760028282011161018e575b50505f6101ba565b01600290036101ba565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff5b5f555f6001556044025ff35b5f5ffd \ No newline at end of file diff --git a/bytecode/builder_exits/main.hex b/bytecode/builder_exits/main.hex index 00ee279..774e05e 100644 --- a/bytecode/builder_exits/main.hex +++ b/bytecode/builder_exits/main.hex @@ -1 +1 @@ -3373fffffffffffffffffffffffffffffffffffffffe1460cb5760115f54807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1461018857600182026001905f5b5f82111560685781019083028483029004916001019190604d565b909390049250505036603014608857366101885734610188575f5260205ff35b341061018857600154600101600155600354806003026004013381556001015f35815560010160203590553360601b5f5260305f60143760445fa0600101600355005b6003546002548082038060101160df575060105b5f5b8181146101175782810160030260040181604402815460601b8152601401816001015481526020019060020154905260010160e1565b91018092146101295790600255610134565b90505f6002555f6003555b5f54807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff141561016157505f5b6001546002828201116101765750505f61017c565b01600290035b5f555f6001556044025ff35b5f5ffd \ No newline at end of file +3373fffffffffffffffffffffffffffffffffffffffe1460e1575f54807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff146101c65760015460028111605157506057565b60029003015b601190600182026001905f5b5f821115607e57810190830284830290049160010191906063565b909390049250505036603014609e57366101c657346101c6575f5260205ff35b34106101c657600154600101600155600354806003026004013381556001015f35815560010160203590553360601b5f5260305f60143760445fa0600101600355005b6003546002548082038060101160f5575060105b5f5b81811461012d5782810160030260040181604402815460601b8152601401816001015481526020019060020154905260010160f7565b910180921461013f579060025561014a565b90505f6002555f6003555b36610198575f54600154817fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff146101865760028282011161018e575b50505f6101ba565b01600290036101ba565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff5b5f555f6001556044025ff35b5f5ffd \ No newline at end of file diff --git a/src/builder_exits/main.eas b/src/builder_exits/main.eas index 068d99e..e44b335 100644 --- a/src/builder_exits/main.eas +++ b/src/builder_exits/main.eas @@ -19,20 +19,23 @@ ;;; CONSTANTS ------------------------------------------------------------------ ;;; ---------------------------------------------------------------------------- -#define SYSTEM_ADDR = .address(0xfffffffffffffffffffffffffffffffffffffffe) +#define SYSTEM_ADDR = address(0xfffffffffffffffffffffffffffffffffffffffe) -#define SLOT_EXCESS = 0 -#define SLOT_COUNT = 1 +#define SLOT_EXCESS = 0 ; slot: stores the count of excess requests +#define SLOT_COUNT = 1 ; slot: stores count of requests added in current block +#define QUEUE_HEAD = 2 ; slot: stores head index of queue +#define QUEUE_TAIL = 3 ; slot: stores tail index of queue -#define QUEUE_HEAD = 2 -#define QUEUE_TAIL = 3 -#define QUEUE_OFFSET = 4 +#define QUEUE_OFFSET = 4 ; count of storage slots. beyond this, queue data is stored +;;; This value is stored into SLOT_EXCESS to disable input into the queue. +#define INHIBITOR = (1 << 256) - 1 + +;;; Fee parameters. #define MIN_FEE = 1 #define TARGET_PER_BLOCK = 2 #define MAX_PER_BLOCK = 16 #define FEE_UPDATE_FRACTION = 17 -#define EXCESS_INHIBITOR = 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff #define INPUT_SIZE = 48 ; the size of (pubkey) #define RECORD_SIZE = 68 ; the size of (address ++ pubkey) @@ -53,17 +56,40 @@ ;;; USER SUBROUTINE ---------------------------------------------------------- ;;; -------------------------------------------------------------------------- - ;; Compute the fee using fake expo and the current excess exit requests. + ;; Compute the current excess deposit requests. + push SLOT_EXCESS ; [excess_slot] + sload ; [excess] + + ;; Stop deposit if inhibitor is set. + dup1 ; [excess, excess] + push INHIBITOR ; [inhibit, excess, excess] + eq ; [excess==inhibit, excess] + jumpi @revert ; [excess] + + ;; Now ensure the excess value also includes any requests added in the + ;; current block. To do this, we check if this block already had more than + ;; TARGET_PER_BLOCK requests and add the difference onto the long-term excess + ;; value. + push SLOT_COUNT ; [count_slot, excess] + sload ; [count, excess] + push TARGET_PER_BLOCK ; [target, count, excess] + dup2 ; [count, target, count, excess] + gt ; [count>target, count, excess] + jumpi @bump_excess ; [count, excess] + + pop ; [excess] + jump @compute_user_fee + +bump_excess: ; [count, excess] + push TARGET_PER_BLOCK ; [target, count, excess] + swap1 ; [count, target, excess] + sub ; [count-target, excess] + add ; [excess=excess+count-target] + +compute_user_fee: + ;; Compute the fee using the fake exponential. push FEE_UPDATE_FRACTION - push SLOT_EXCESS ; [excess_slot, update_fraction] - sload ; [excess, update_fraction] - - ;; Check if the pre-fork inhibitor is still active, revert if so. - dup1 ; [excess, excess, update_fraction] - push EXCESS_INHIBITOR ; [inhibitor, excess, excess, update_fraction] - eq ; [inhibitor == excess, excess, update_fraction] - jumpi @revert ; [excess, update_fraction] - + swap1 ; [excess, update_fraction] push MIN_FEE ; [min_fee, excess, update_fraction] #include "../common/fake_expo.eas" @@ -338,37 +364,35 @@ reset_queue: sstore ; [count] update_excess: - ;; Update the new excess exit requests. + ;; If the system call receives any input, disable the queue by storing INHIBITOR + ;; into the excess slot. + calldatasize ; [calldatasize, count] + jumpi @set_inhibitor ; [count] + + ;; Load current excess and added-in-block counter values. push SLOT_EXCESS ; [excess_slot, count] sload ; [excess, count] - - ;; Check if excess needs to be reset to 0 for first iteration after - ;; activation. - dup1 ; [excess, excess, count] - push EXCESS_INHIBITOR ; [inhibitor, excess, excess, count] - eq ; [inhibitor == excess, excess, count] - iszero ; [inhibitor != excess, excess, count] - jumpi @skip_reset ; [excess, count] - - ;; Drop the excess from stack and use 0. - pop ; [count] - push 0 ; [reset_excess, count] - -skip_reset: push SLOT_COUNT ; [count_slot, excess, count] - sload ; [count, excess, count] + sload ; [added_count, excess, count] + + ;; If inhibitor is set, restore excess to zero, i.e. re-enable the queue. + dup2 ; [excess, added_count, excess, count] + push INHIBITOR ; [inhibitor, excess, added_count, excess, count] + eq ; [excess==inhibitor, added_count, excess, count] + jumpi @zero_excess ; [added_count, excess, count] ;; If the sum of the previous excess requests and requests added in the ;; current block is greater than the target, subtract the target from the sum ;; and set it as the new excess requests value. - push TARGET_PER_BLOCK ; [target, count, excess, count] - dup3 ; [excess, target, count, excess, count] - dup3 ; [count, excess, target, count, excess, count] - add ; [count+excess, target, count, excess, count] - gt ; [count+excess > target, count, excess, count] - jumpi @compute_excess ; [count, excess, count] + push TARGET_PER_BLOCK ; [target, added_count, excess, count] + dup3 ; [excess, target, added_count, excess, count] + dup3 ; [added_count, excess, target, added_count, excess, count] + add ; [added_count+excess, target, added_count, excess, count] + gt ; [added_count+excess > target, added_count, excess, count] + jumpi @compute_excess ; [added_count, excess, count] ;; Zero out excess. +zero_excess: pop ; [excess, count] pop ; [count] push 0 ; [0, count] @@ -379,6 +403,10 @@ compute_excess: push TARGET_PER_BLOCK ; [target, count+excess, count] swap1 ; [count+excess, target, count] sub ; [new_excess, count] + jump @store_excess + +set_inhibitor: ; [count] + push INHIBITOR ; [new_excess, count] store_excess: push SLOT_EXCESS ; [excess_slot, new_excess, count] diff --git a/test/BuilderExit.t.sol b/test/BuilderExit.t.sol index 1055ddd..4a1582a 100644 --- a/test/BuilderExit.t.sol +++ b/test/BuilderExit.t.sol @@ -91,7 +91,8 @@ contract BuilderExitTest is Test { // Add more exit requests than the max per block (16) so that the queue is // not immediately emptied. for (uint256 i = 0; i < max_per_block+1; i++) { - addRequest(address(uint160(i)), makeExit(i), 2); + uint256 fee = getCurrentFee(); + addRequest(address(uint160(i)), makeExit(i), fee); } assertStorage(count_slot, max_per_block+1, "unexpected request count"); @@ -102,7 +103,8 @@ contract BuilderExitTest is Test { // Add another batch of max exit requests per block (16) so the next read // leaves a single exit request in the queue. for (uint256 i = 17; i < 33; i++) { - addRequest(address(uint160(i)), makeExit(i), 2); + uint256 fee = getCurrentFee(); + addRequest(address(uint160(i)), makeExit(i), fee); } assertStorage(count_slot, max_per_block, "unexpected request count"); @@ -120,7 +122,8 @@ contract BuilderExitTest is Test { // Add five (5) more requests to check that new requests can be added after // the queue is reset. for (uint256 i = 33; i < 38; i++) { - addRequest(address(uint160(i)), makeExit(i), 4); + uint256 fee = getCurrentFee(); + addRequest(address(uint160(i)), makeExit(i), fee); } assertStorage(count_slot, 5, "unexpected request count"); @@ -137,7 +140,13 @@ contract BuilderExitTest is Test { // Add a bunch of requests. for (; idx < count; idx++) { - addRequest(address(uint160(idx)), makeExit(idx), 1); + uint256 fee = getCurrentFee(); + if (idx < target_per_block) { + assertEq(fee, 1, "unexpected fee for request below excess"); + } else { + assertEq(fee, computeFee(idx - target_per_block), "unexpected fee"); + } + addRequest(address(uint160(idx)), makeExit(idx), fee); } assertStorage(count_slot, count, "unexpected request count"); checkExits(0, max_per_block); @@ -145,10 +154,10 @@ contract BuilderExitTest is Test { uint256 read = max_per_block; uint256 excess = count - target_per_block; - // Attempt to add an exit request with fee too low and an exit request with - // fee exactly correct. This should cause the excess requests counter to - // decrease by 1 each iteration. - for (uint256 i = 0; i < count; i++) { + // Attempt to add a deposit request one wei short of the stake plus fee and a + // deposit request with exactly stake plus fee. This should cause the excess + // requests counter to decrease until it returns to 0. + while (excess != 0) { assertExcess(excess); uint256 fee = computeFee(excess); @@ -158,14 +167,41 @@ contract BuilderExitTest is Test { uint256 expected = min(idx-read+1, max_per_block); checkExits(read, expected); - if (excess != 0) { - excess--; + if (excess + 1 > target_per_block) { + excess = excess + 1 - target_per_block; + } else { + excess = 0; } read += expected; idx++; } } + // testFeePerTx checks how fees are computed within a single block. + function testFeePerTx() public { + // first requests have a fee of 1 + uint256 idx = 0; + for (; idx <= target_per_block+12; idx++) { + addRequest(address(uint160(idx)), makeExit(idx), 1); + } + assertStorage(count_slot, idx, "unexpected request count in storage"); + + // now fee rises. Here we just run it until the fee exceeds 100 gwei. + uint256 prevFee = 1; + while (true) { + uint256 fee = getCurrentFee(); + if (fee >= 100 gwei) { + break; + } + assertGe(fee, prevFee, "fee did not rise"); + addRequest(address(uint160(idx)), makeExit(idx), fee); + idx++; + } + + assertEq(idx, 433, "unexpected request count"); + assertStorage(count_slot, idx, "unexpected request count in storage"); + } + // testFeeGetterRejectsValue verifies the empty-calldata fee getter reverts // when value is attached, preventing accidentally lost funds. function testFeeGetterRejectsValue() public { @@ -174,21 +210,61 @@ contract BuilderExitTest is Test { assertEq(ret, false, "fee getter must reject callvalue"); } - // testInhibitorReset verifies that after the first system call the excess - // value is reset to 0. - function testInhibitorReset() public { - vm.store(addr, bytes32(0), bytes32(inhibitor)); + // testSystemCallWithInput verifies that a system call with input drains the queue, and + // sets the inhibitor to prevent further additions. + function testSystemCallWithInput() public { + addRequest(address(this), makeExit(1), 1); + + // Disable the queue with a system call that carries input data. vm.prank(sysaddr); - (bool ret, bytes memory data) = addr.call(""); - assertStorage(excess_slot, 0, "expected excess requests to be reset"); + (bool ret, bytes memory data) = addr.call(hex"01"); + assertEq(ret, true); + assertEq(data.length, 68, "system call should drain the queue"); + assertStorage(excess_slot, inhibitor, "expected inhibitor in excess storage slot"); + + // Check that requesting the current fee fails. + (ret,) = addr.staticcall(""); + assertEq(ret, false, "expected fee getter to fail"); - vm.store(addr, bytes32(0), bytes32(inhibitor)); - addFailedRequest(address(uint160(0)), makeExit(0), 1); + // Check that adding a request fails. + addFailedRequest(address(this), makeExit(2), 1); - vm.store(addr, bytes32(0), bytes32(inhibitor-1)); + // Now re-enable the queue through a system call with no input. vm.prank(sysaddr); (ret, data) = addr.call(""); - assertStorage(excess_slot, inhibitor-target_per_block-1, "didn't expect excess to be reset"); + assertEq(ret, true); + assertEq(data.length, 0, "system call should return empty data since there are no requests"); + assertStorage(excess_slot, 0, "expected zero excess requests after re-enabling queue"); + + // Check that adding a requests succeeds again. + addRequest(address(this), makeExit(3), 1); + } + + // testQueueDisableFeeReset verifies that re-enabling the queue resets the fee to 1. + function testQueueDisableFeeReset() public { + uint256 requestCount = max_per_block*4; + for (uint64 i = 0; i < requestCount; i++) { + uint256 fee = getCurrentFee(); + addRequest(address(this), makeExit(uint256(i)), fee); + } + assertStorage(count_slot, requestCount, "unexpected request count"); + + // Disable the queue with a system call that carries input data. + vm.prank(sysaddr); + (bool ret, bytes memory data) = addr.call(hex"01"); + assertEq(ret, true); + assertEq(data.length, max_per_block*68, "system call should drain the queue"); + assertStorage(excess_slot, inhibitor, "expected inhibitor in excess storage slot"); + + // Now re-enable the queue through a system call with no input. + vm.prank(sysaddr); + (ret, data) = addr.call(""); + assertEq(ret, true); + assertEq(data.length, max_per_block*68, "system call should drain the queue"); + assertStorage(excess_slot, 0, "expected zero excess requests after re-enabling queue"); + + // Check that adding a requests succeeds again with fee 1. + addRequest(address(this), makeExit(999), 1); } // -------------------------------------------------------------------------- @@ -249,4 +325,11 @@ contract BuilderExitTest is Test { } return pk; } + + // getCurrentFee returns the current fee computed by the system contract. + function getCurrentFee() internal view returns(uint256) { + (bool ok, bytes memory data) = addr.staticcall(""); + assert(ok); + return uint256(bytes32(data)); + } } From 968876c028266a3eb6aab964267c0b194966e145 Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Mon, 6 Jul 2026 23:13:09 +0200 Subject: [PATCH 2/2] builder_exits: clean up comment --- src/builder_exits/main.eas | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/builder_exits/main.eas b/src/builder_exits/main.eas index e44b335..08c1a87 100644 --- a/src/builder_exits/main.eas +++ b/src/builder_exits/main.eas @@ -56,11 +56,11 @@ ;;; USER SUBROUTINE ---------------------------------------------------------- ;;; -------------------------------------------------------------------------- - ;; Compute the current excess deposit requests. + ;; Compute the current excess requests. push SLOT_EXCESS ; [excess_slot] sload ; [excess] - ;; Stop deposit if inhibitor is set. + ;; Stop if inhibitor is set. dup1 ; [excess, excess] push INHIBITOR ; [inhibit, excess, excess] eq ; [excess==inhibit, excess]