feat(L2): add system address refunding to FeeDisburser#358
Conversation
Before bridging collected fees to L1, FeeDisburser now tops up configured L2 system addresses to their target balances, mirroring the BalanceTracker pattern on L1. Remaining balance after refunds is bridged to L1_WALLET as before. Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
🟡 Heimdall Review Status
|
Adds tests covering the new setSystemAddresses function, disburseFees with system address refunds, and reentrancy guard. Also fixes setSystemAddresses access control to use _assertOnlyProxyAdmin() instead of _assertOnlyProxyAdminOwner() so the L2 alias (raw EIP-1967 admin slot) can call it directly via deposit tx without requiring a ProxyAdmin contract. Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
Inline _processSystemAddressRefunds into disburseFees and _validateSystemAddressConfig into setSystemAddresses — both were private, single-caller wrappers. Add _makeSingleConfig test helper to collapse repeated 4-line array setup. Drop no-op vm.deal(addr, 0) calls. FeeDisburser now matches BalanceTracker's pattern on L1. Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
- Fix ProcessedFunds NatSpec: clarify balanceSent is amount attempted, not received - Remove EmptySystemAddresses error; allow empty arrays to clear config - Change reentrancy guard from 0/1/0 to 1/2/1 to keep slot warm after first call - Fix setSystemAddresses NatSpec: "ProxyAdmin owner" → "ProxyAdmin" - Remove virtual from _refillBalanceIfNeeded (matches BalanceTracker pattern) - Add RevertingReceiver helper and tests for clearing config, multiple system addresses ordering, and reverting recipients Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
These files are not part of this PR; the fmt changes are noise. Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
jackchuma
left a comment
There was a problem hiding this comment.
The patch leaves required generated snapshots stale, which will fail repository checks, and the new setter is not reachable through the standard proxy admin owner flow. These should be fixed before the change is considered correct.
- Replace setSystemAddresses with initialize(reinitializer(2)) so config is set atomically via upgradeAndCall, resolving P2 access control issue - Regenerate ABI, storage layout, and semver-lock snapshots (P1) - Simplify ReentrantReceiver to be stateless (use msg.sender in receive) - Replace RevertingReceiver helper with vm.mockCallRevert - Remove redundant vm.deal(vault, 0) lines in refund tests (fresh state is 0) - Delete test_disburseFees_success_allFundsConsumedByRefund (covered by the more general partial refund test) Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
Approved review 4667004766 from jackchuma is now dismissed due to new commit. Re-request for approval.
Co-authored-by: Codex <codex-noreply@coinbase.com>
Co-authored-by: Codex <codex-noreply@coinbase.com>
Co-authored-by: Codex <codex-noreply@coinbase.com>
| function initialize( | ||
| address payable[] memory systemAddresses_, | ||
| uint256[] memory targetBalances_ | ||
| ) | ||
| external | ||
| reinitializer(2) |
There was a problem hiding this comment.
Restrict one-time fee recipient initialization. If this proxy is upgraded or deployed without executing this initializer in the same transaction, any account can call the public reinitializer first, permanently consume version 2, and set systemAddresses/targetBalances to addresses they control; subsequent disburseFees calls will top those addresses up before bridging fees. This should be limited to the ProxyAdmin/owner or otherwise made impossible to front-run
| } | ||
| } | ||
|
|
||
| uint256 bridgeBalance = address(this).balance; |
There was a problem hiding this comment.
nit: the zero-bridge case has its own early return, but it emits the same event shape as the normal path with bridgeBalance == 0. You can avoid the duplicated FeesDisbursed path by only guarding the bridge call:
uint256 bridgeBalance = address(this).balance;
if (bridgeBalance != 0) {
IL2StandardBridge(payable(Predeploys.L2_STANDARD_BRIDGE)).bridgeETHTo{ value: bridgeBalance }(
L1_WALLET, WITHDRAWAL_MIN_GAS, bytes("")
);
}
emit FeesDisbursed(disbursementTime, 0, bridgeBalance);| for (uint256 i; i < n; i++) { | ||
| addrs[i] = payable(address(uint160(0x6000 + i))); | ||
| balances[i] = 1 ether; | ||
| } |
There was a problem hiding this comment.
nit: this test expects TooManySystemAddresses, which happens before the initializer reads any array values. Filling every address and balance makes the test look like it depends on valid contents, but it only depends on length. These lines can be deleted
| feeDisburser.initialize(addrs, balances); | ||
| } | ||
|
|
||
| function test_initialize_revert_arrayLengthMismatch() public { |
There was a problem hiding this comment.
Populating the arrays in this test is unnecessary since only the length mismatch is what is tested
Closes CHAIN-4644
What changed?
`FeeDisburser` now tops up a configurable set of L2 system addresses before bridging the remaining fees to L1. Previously, the full collected balance was always bridged.
Specifically:
Mirrors the existing `BalanceTracker` pattern on L1 (`src/L1/BalanceTracker.sol`).
Why?
L2 system addresses currently must be funded out-of-band, independently of the fee collection cycle. Routing a first-priority top-up through `FeeDisburser` (same contract that already holds all collected fees) eliminates that operational dependency and keeps the L1 and L2 funding patterns consistent.
How to test?
forge test --match-path test/L2/FeeDisburser.t.sol -vvvTests cover: