Skip to content

feat(L2): add system address refunding to FeeDisburser#358

Merged
0xth4nh merged 12 commits into
mainfrom
mux/contracts-claude-mrb0yjit
Jul 14, 2026
Merged

feat(L2): add system address refunding to FeeDisburser#358
0xth4nh merged 12 commits into
mainfrom
mux/contracts-claude-mrb0yjit

Conversation

@0xth4nh

@0xth4nh 0xth4nh commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

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:

  • `disburseFees()` iterates over configured system addresses after vault withdrawals and before bridging. Each address is refilled up to its target balance via `SafeCall.send` (non-reverting — a failing recipient does not abort the rest).
  • `setSystemAddresses(address payable[], uint256[])` added — ProxyAdmin-gated setter for the system address/target-balance config. Validates: ≤20 entries, equal-length arrays, no `address(0)`, no zero target. Passing empty arrays clears the config and disables refunding.
  • `nonReentrantDisbursement` modifier added — custom reentrancy guard, not OZ's `ReentrancyGuardUpgradeable`. OZ cannot be used here because `FeeDisburser` is an upgradeable proxy with existing deployed storage; inheriting `ReentrancyGuardUpgradeable` would prepend its `_status` slot to the layout, shifting every existing variable and corrupting proxy storage on upgrade. Instead, `_disburseFeesEntered` is appended after the existing storage slots. It uses 1/2 sentinels (not 0/1/0) so the slot stays nonzero after first use, keeping subsequent SSTOREs warm; `0` (uninitialized proxy slot) is treated as not-entered.
  • `FeeDisburser` now inherits `ProxyAdminOwnedBase` (provides `_assertOnlyProxyAdmin` — reads the EIP-1967 admin slot directly, compatible with the raw L2-alias admin used in production without requiring a `ProxyAdmin` contract).
  • New events: `ProcessedFunds(systemAddress, success, balanceNeeded, balanceSent)`, `SystemAddressesUpdated(systemAddressCount)`.
  • New errors: `ArrayLengthMismatch`, `TooManySystemAddresses`, `ZeroTargetBalance`, `ReentrantCall`.
  • If refunds consume the entire balance, `FeesDisbursed(time, 0, 0)` is emitted and `lastDisbursementTime` is still updated (disbursement did occur, just nothing left to bridge).
  • Version bumped `1.0.0` → `1.1.0`.

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 -vvv

Tests cover:

  1. Full refund — system address reaches target, remainder bridges to L1.
  2. Partial refund (insufficient funds) — target exceeds available balance; system address receives all available funds, nothing bridged.
  3. Skip when already at target — `ProcessedFunds(addr, false, 0, 0)` emitted, full balance bridges.
  4. Reverting recipient — recipient reverts on receive; `SafeCall.send` catches it, `success=false` emitted, full balance still bridges.
  5. Reentrancy blocked — system address calls back into `disburseFees()` on receive; inner call reverts with `ReentrantCall()`, caught by `SafeCall`, outer call completes normally.
  6. Multiple system addresses — correct ordering and cumulative deduction from fee balance.
  7. Clear config — passing empty arrays succeeds and disables refunding.
  8. `setSystemAddresses` access control — non-ProxyAdmin caller reverts.

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>
@linear

linear Bot commented Jul 8, 2026

Copy link
Copy Markdown

CHAIN-4644

@cb-heimdall

cb-heimdall commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

✅ Heimdall Review Status

Requirement Status More Info
Reviews 1/1
Denominator calculation
Show calculation
1 if user is bot 0
1 if user is external 0
2 if repo is sensitive 0
From .codeflow.yml 1
Additional review requirements
Show calculation
Max 0
0
From CODEOWNERS 0
Global minimum 0
Max 1
1
1 if commit is unverified 0
Sum 1

0xth4nh and others added 5 commits July 9, 2026 15:35
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 jackchuma left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Comment thread src/L2/FeeDisburser.sol
Comment thread src/L2/FeeDisburser.sol Outdated
Comment thread src/L2/FeeDisburser.sol Outdated
Comment thread test/L2/FeeDisburser.t.sol Outdated
Comment thread test/L2/FeeDisburser.t.sol Outdated
Comment thread test/L2/FeeDisburser.t.sol Outdated
Comment thread test/L2/FeeDisburser.t.sol Outdated
- 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>
@cb-heimdall cb-heimdall dismissed jackchuma’s stale review July 10, 2026 03:45

Approved review 4667004766 from jackchuma is now dismissed due to new commit. Re-request for approval.

0xth4nh and others added 3 commits July 10, 2026 14:54
Co-authored-by: Codex <codex-noreply@coinbase.com>
Co-authored-by: Codex <codex-noreply@coinbase.com>
Co-authored-by: Codex <codex-noreply@coinbase.com>
@0xth4nh 0xth4nh requested a review from jackchuma July 10, 2026 20:10
Comment thread src/L2/FeeDisburser.sol
Comment thread src/L2/FeeDisburser.sol
Comment thread test/L2/FeeDisburser.t.sol Outdated
Comment thread test/L2/FeeDisburser.t.sol
Co-authored-by: Codex <codex-noreply@coinbase.com>
@0xth4nh 0xth4nh requested a review from jackchuma July 13, 2026 15:36
Comment thread src/L2/FeeDisburser.sol Outdated
}

lastDisbursementTime = block.timestamp;
uint256 disbursementTime = block.timestamp;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: block.timestamp is fixed for the transaction, so disbursementTime is only an alias used to assign storage and emit the event. Assign lastDisbursementTime = block.timestamp and emit block.timestamp directly to remove the temporary variable

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed.

Comment thread src/L2/FeeDisburser.sol Outdated

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: feeBalance is only used for the immediately following zero-balance check. Nothing can change this contract’s balance between the assignment and condition, so if (address(this).balance == 0) removes a local without changing behavior

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed.

Co-authored-by: Codex <codex-noreply@coinbase.com>
@0xth4nh 0xth4nh merged commit a2c10a5 into main Jul 14, 2026
6 checks passed
@0xth4nh 0xth4nh deleted the mux/contracts-claude-mrb0yjit branch July 14, 2026 16:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants