Skip to content

fix: bound set_admin acceptance window (B1)#40

Open
AquiGorka wants to merge 1 commit into
mainfrom
fix/b1-acceptance-window
Open

fix: bound set_admin acceptance window (B1)#40
AquiGorka wants to merge 1 commit into
mainfrom
fix/b1-acceptance-window

Conversation

@AquiGorka

Copy link
Copy Markdown
Contributor

What

Both channel-auth and privacy-channel had set_admin(new_admin) call ownable::transfer_ownership(e, &new_admin, e.ledger().max_live_until_ledger()), hardcoding the acceptance window of a pending ownership transfer to the network max (~180 days). A pending transfer therefore never auto-expired, so a PendingOwner key compromised while a transfer lingered stayed exploitable for months, and the library's 0-cancel path was unreachable.

Approved fix (RV audit finding B1):

  • set_admin(e, new_admin, live_until_ledger: u32)required parameter, forwarded to transfer_ownership. No default, so max can never silently return.
  • In-contract 7-day ceiling: a non-zero live_until_ledger beyond current_ledger + 7 * DAY_IN_LEDGERS (120_960 ledgers) panics the new MoonlightError::AcceptanceWindowTooLong (code 1014). Panics rather than silently clamping.
  • live_until_ledger == 0 is exempt from the ceiling and preserves the library's cancel path (cancel names the current pending address).
  • The two-step accept_admin and cancel semantics are unchanged — the ceiling is a tighter check layered on top of the library's existing < current / > max validation.
  • The standard operating window for a real handover is 3 days (current_ledger + 3 * DAY_IN_LEDGERS = 51_840 ledgers), documented on set_admin; sensible range 24h–7d. Both contracts get the identical change.

Tests

  • New (both contracts): boundary — current + 120_960 accepted, +1 rejects with AcceptanceWindowTooLong; 0 cancels a pending transfer (formerly-pending admin can no longer accept).
  • Existing two-step accept/reject and admin-transfer tests updated to the new signature and still pass.
  • cargo test --workspace: 66 passed / 0 failed. Both WASMs build (stellar contract build). fmt/clippy clean on changed code.

Notes

  • Breaking ABI change to set_admin. Downstream cascade (handled separately after merge, NOT in this PR): regenerate SDK/TS bindings, bump SDK + consumers, update ops/deploy scripts that call set_admin, redeploy + re-init the WASMs.
  • Held for internal review — do not merge yet.

Refs audit finding B1.

/// 120_960 ledgers past the current ledger) panics [`MoonlightError::AcceptanceWindowTooLong`].
/// `live_until_ledger == 0` is exempt from the ceiling and cancels a pending transfer (the
/// library requires the cancel call to name the current pending address).
pub fn set_admin(e: &Env, new_admin: Address, live_until_ledger: u32) {

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.

Receiving the arg from the external invocation is a good approach here to ensure there is consistency between simulation and submission.

One common mistake would be to dynamically calculate a value based on the current ledger, which could cause the transaction to be simulated on block n and submitted on block n+1, which could invalidate it due to mismatching values.

Comment thread contracts/channel-auth/src/contract.rs Outdated
const INSTANCE_BUMP_AMOUNT: u32 = 7 * DAY_IN_LEDGERS;
const INSTANCE_LIFETIME_THRESHOLD: u32 = INSTANCE_BUMP_AMOUNT - DAY_IN_LEDGERS;

// B1: cap the acceptance window of a pending ownership transfer in-contract. Without this the

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.

I assume the B1 stands for the audit finding, right?
Is there an added benefit of having such reference in the comment?

My gut feeling is that this will become historical context that won't be explicit for future readers when simply looking into the source. The comment context seems detailed enough to explain the section.

…(B1)

set_admin now takes a required live_until_ledger and forwards it to
ownable::transfer_ownership instead of hardcoding max_live_until_ledger(),
which pinned every pending ownership transfer to the network-max (~180 day)
window. A non-zero window beyond an in-contract 7-day ceiling
(7 * DAY_IN_LEDGERS = 120_960 ledgers past the current ledger) panics the new
MoonlightError::AcceptanceWindowTooLong; live_until_ledger == 0 is exempt and
preserves the library's cancel path. The two-step accept_admin is unchanged.
The standard operating window for a real handover is 3 days (51_840 ledgers),
documented on set_admin; there is deliberately no default so max can never
silently return.

Addresses RV audit finding B1.
@AquiGorka AquiGorka force-pushed the fix/b1-acceptance-window branch from af05839 to e769ee7 Compare July 10, 2026 18:20
@AquiGorka AquiGorka requested review from fazzatti and removed request for pselle July 13, 2026 18:00
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.

2 participants