Skip to content

fix: resolve issues #420, #409, #411, #408 — time skew, balance-delta… - #457

Merged
james2177 merged 2 commits into
stellar-vortex-protocol:mainfrom
whiteghost0001:fix/issues-408-409-411-420-ledger-skew-balance-delta-scoped-auth-chain-onboarding
Oct 2, 2026
Merged

james2177 merged 2 commits into
stellar-vortex-protocol:mainfrom
whiteghost0001:fix/issues-408-409-411-420-ledger-skew-balance-delta-scoped-auth-chain-onboarding

Conversation

@whiteghost0001

Copy link
Copy Markdown
Contributor

…, scoped auth, chain onboarding

Issue #420 — Ledger close-time skew: slash grace period and named boundary helpers Closes #420

Stellar ledger close times are validator-agreed and can drift from wall-clock time by up to ~7 s (two typical 5 s ledger intervals). A solver whose fill_intent transaction was broadcast before the deadline can land in a ledger whose close time is past the deadline and be slashed unfairly.

Changes:

  • Added SLASH_GRACE_SECS = 10 (2× worst-case ledger gap) constant. slash_solver now requires now >= deadline + SLASH_GRACE_SECS before executing a slash, giving solvers a timing buffer without widening the fill window itself.
  • Extracted all time-boundary comparisons into four named helper functions:
    fill_window_open(now, deadline) -> bool // now < deadline (exclusive)
    slash_eligible(now, deadline) -> bool // now >= deadline + SLASH_GRACE_SECS
    dispute_window_open(now, ddl) -> bool // now < dispute_deadline
    arbiter_timeout_reached(now, r) -> bool // now >= raised_at + ARBITER_WINDOW
  • fill_intent_inner now calls check_fill_guards (which uses fill_window_open)
    instead of a raw inline comparison, ensuring fill_intent and is_intent_fillable
    can never silently diverge (issue [High] Add an is_intent_fillable convenience view #259).
  • slash_solver calls slash_eligible instead of a raw now >= deadline check.
  • Added docs/420-ledger-time-skew-analysis.md: full analysis of Stellar
    close-time semantics, worst-case drift table, per-boundary grace rationale,
    boundary test matrix, and evaluation of ledger-sequence-based deadlines
    (deferred — ABI-breaking with negligible security gain at typical drift).

Issue #409 — Balance-delta accounting for fee-on-transfer tokens Closes #409

SEP-41 does not forbid transfer fees. Recording the requested amount rather than the actually-received amount creates phantom liabilities: the contract owes more than it holds, and the last withdrawer cannot be paid. This is a well-known DeFi insolvency vector.

Changes:

  • Added pull_exact(env, token, from, amount) -> i128 helper. It reads balance_before, executes the transfer, reads balance_after, and returns after - before — the actual amount received regardless of token behaviour.
  • register_solver_inner uses pull_exact for the bond deposit. If a fee-on-transfer bond token delivers less than requested, the stored bond is corrected to the actual received delta so the solver cannot accumulate phantom bond credit.
  • begin_fill uses pull_exact for the dst_token escrow deposit. The stored fill_amount is corrected to the real escrowed value so release_fill / resolve_dispute pay out exactly what arrived.
  • open_dispute uses pull_exact for the dispute bond and rejects (panics with Error::BondTokenFeeOnTransfer = 39) if received != requested, because the bond token is admin-configured USDC which must not levy a fee; a discrepancy indicates misconfiguration and we refuse to under-collateralise the dispute.
  • fill_intent direct solver→user transfers are explicitly exempt: the contract is never the receiver, so balance-delta measurement is neither possible nor needed. The exemption and its rationale are documented in fill_intent_inner and in SECURITY.md §Fee-on-transfer tokens.
  • SECURITY.md: added §Fee-on-transfer tokens documenting the pull_exact pattern, the per-path decision (accept delta vs. reject), the direct-fill exemption, and the rebasing-token non-support note for bond tokens.

Issue #411 — Scope solver authorization with require_auth_for_args Closes #411

With relayers and gasless flows, auth entries get passed around. An unscoped require_auth() on fill_intent lets a delegating invoker redirect a solver's signature to a different intent or a different fill amount — a concrete relay-reuse attack vector.

Changes:

  • fill_intent: upgraded from solver.require_auth() to solver.require_auth_for_args((solver, intent_id, fill_amount)). This is the highest-value call site: the solver authorises an outgoing token transfer. Scoping to (solver, intent_id, fill_amount) ensures a signed auth entry cannot be replayed for a different intent or amount. The solver address is included so the tuple is globally unique across contracts.
  • accept_intent: require_auth_for_args((intent_id,)) — prevents redirecting the solver's accept signature to a different intent.
  • accept_intent_with_bond: require_auth_for_args((intent_id, bond_token)) — same, plus pins the bond denomination.
  • begin_fill: require_auth_for_args((solver, intent_id, fill_amount)) — tokens move into escrow, same rationale as fill_intent.
  • batch_accept_intent: require_auth_for_args((intent_ids,)) — solver's sig covers exactly this ordered set of intent IDs.
  • batch_fill_intent: require_auth_for_args((fills,)) — covers both the intent IDs and fill amounts; a signature for one fill list cannot be replayed for a different list.
  • docs/auth-audit.md: updated the Upgraded table to include all seven scoped entrypoints with their args and rationale. Updated Integration impact section with the new payload shapes solver bots must sign.

Issue #408 — Atomic timelocked source-chain onboarding proposal Closes #408

Adding a new source chain previously required multiple separate admin operations across two contracts: add_allowed_src_chain, set_authorized_emitter on proof_registry, Axelar source config, and token-format rules. A partial configuration leaves the chain half-enabled — intents can be submitted but every fill fails with a proof error and the intents are slashed or expire, causing real user harm.

Changes (intent_settlement/src/lib.rs):

  • Added ChainConfig struct: { name, wormhole_id, axelar_name, emitter, axelar_source, token_format } — the complete per-chain configuration bundle.
  • Added DataKey::PendingChainOnboarding(String) and DataKey::ChainConfig(String) storage keys.
  • Added Error::NoPendingChainOnboarding = 36, ChainAlreadyConfigured = 37, ChainNotConfigured = 38.
  • propose_chain_onboarding(config: ChainConfig): admin-only, queues the config under a 48-hour timelock. A new proposal for the same chain overwrites any prior pending one (and resets the clock). Emits chain_onboarding_proposed.
  • execute_chain_onboarding(name): callable after timelock elapses. Atomically:
    1. Adds name to the src-chain allowlist.
    2. Calls proof_registry.configure_chain(wormhole_id, emitter) in the same transaction so both contracts are configured together.
    3. Writes a live ChainConfig entry for get_chain_config. Panics with NoPendingChainOnboarding, TimelockNotElapsed, or ChainAlreadyConfigured as appropriate. Emits chain_onboarding_executed.
  • execute_chain_offboarding(name): admin-only, immediate (no timelock). Removes the chain from the allowlist and calls proof_registry.remove_chain in one call. In-flight intents already submitted are unaffected — src_chain is validated at submit_intent time only, not at fill time. Existing proof records remain readable. Emits chain_offboarding_executed.
  • get_chain_config(name): view returning the live ChainConfig or None.

Changes (proof_registry/src/lib.rs):

  • Added ProofKey::Configurator storage key.
  • set_configurator(configurator): admin-only, grants the given address (typically intent_settlement) the narrow configurator role.
  • get_configurator(): view.
  • configure_chain(chain_id, emitter): configurator-only, registers the authorized Wormhole emitter for chain_id. Called by execute_chain_onboarding.
  • remove_chain(chain_id): configurator-only, removes the authorized emitter for chain_id. Called by execute_chain_offboarding. Existing stored proofs are unaffected.
  • Added Error::ConfiguratorNotSet = 11.

Documentation:

  • docs/132-supported-chains.md §6: atomic onboarding flow (propose + execute), offboarding behaviour (§6.2 — in-flight intents unaffected), and manual step-by-step reference (§6.3).

Code quality

  • Removed all duplicate const definitions (DISPUTE_WINDOW, ARBITER_WINDOW, MAX_EXTENSION_DURATION, DEFAULT_MIN_BOND/FILL_WINDOW/INTENT_EXPIRY/ PROTOCOL_FEE_BPS, SLASH_COOLDOWN, CANCEL_COOLDOWN, MAX_BATCH_SIZE) that had been left in the file from earlier merge conflicts.
  • Removed duplicate placeholder implementations of begin_fill, open_dispute, resolve_dispute, release_fill, batch_fill_intent, batch_cancel_intent, and get_pending_admin that had been superseded by the full implementations.
  • Removed placeholder validate_proof stub that was superseded by the real cross-contract validation implementation.

Summary

Related issue

Type of change

  • Bug fix
  • New feature
  • Refactor
  • Documentation
  • CI / tooling

Component

  • Contract (vortex-contract)
  • Backend (vortex-backend)
  • Frontend (vortex-frontend)

Checklist

  • My code follows the project's style and conventions
  • I ran lint / type-check / build locally and they pass
  • I added or updated tests where appropriate
  • I updated documentation where appropriate
  • My commits follow Conventional Commits

Screenshots / notes

…col#409, stellar-vortex-protocol#411, stellar-vortex-protocol#408 — time skew, balance-delta, scoped auth, chain onboarding

## Issue stellar-vortex-protocol#420 — Ledger close-time skew: slash grace period and named boundary helpers
Closes stellar-vortex-protocol#420

Stellar ledger close times are validator-agreed and can drift from wall-clock
time by up to ~7 s (two typical 5 s ledger intervals). A solver whose
fill_intent transaction was broadcast before the deadline can land in a ledger
whose close time is past the deadline and be slashed unfairly.

Changes:
- Added SLASH_GRACE_SECS = 10 (2× worst-case ledger gap) constant. slash_solver
  now requires now >= deadline + SLASH_GRACE_SECS before executing a slash,
  giving solvers a timing buffer without widening the fill window itself.
- Extracted all time-boundary comparisons into four named helper functions:
    fill_window_open(now, deadline) -> bool        // now < deadline (exclusive)
    slash_eligible(now, deadline)   -> bool        // now >= deadline + SLASH_GRACE_SECS
    dispute_window_open(now, ddl)   -> bool        // now < dispute_deadline
    arbiter_timeout_reached(now, r) -> bool        // now >= raised_at + ARBITER_WINDOW
- fill_intent_inner now calls check_fill_guards (which uses fill_window_open)
  instead of a raw inline comparison, ensuring fill_intent and is_intent_fillable
  can never silently diverge (issue stellar-vortex-protocol#259).
- slash_solver calls slash_eligible instead of a raw now >= deadline check.
- Added docs/420-ledger-time-skew-analysis.md: full analysis of Stellar
  close-time semantics, worst-case drift table, per-boundary grace rationale,
  boundary test matrix, and evaluation of ledger-sequence-based deadlines
  (deferred — ABI-breaking with negligible security gain at typical drift).

## Issue stellar-vortex-protocol#409 — Balance-delta accounting for fee-on-transfer tokens
Closes stellar-vortex-protocol#409

SEP-41 does not forbid transfer fees. Recording the requested amount rather
than the actually-received amount creates phantom liabilities: the contract
owes more than it holds, and the last withdrawer cannot be paid. This is a
well-known DeFi insolvency vector.

Changes:
- Added pull_exact(env, token, from, amount) -> i128 helper. It reads
  balance_before, executes the transfer, reads balance_after, and returns
  after - before — the actual amount received regardless of token behaviour.
- register_solver_inner uses pull_exact for the bond deposit. If a
  fee-on-transfer bond token delivers less than requested, the stored bond
  is corrected to the actual received delta so the solver cannot accumulate
  phantom bond credit.
- begin_fill uses pull_exact for the dst_token escrow deposit. The stored
  fill_amount is corrected to the real escrowed value so release_fill /
  resolve_dispute pay out exactly what arrived.
- open_dispute uses pull_exact for the dispute bond and rejects (panics with
  Error::BondTokenFeeOnTransfer = 39) if received != requested, because the
  bond token is admin-configured USDC which must not levy a fee; a discrepancy
  indicates misconfiguration and we refuse to under-collateralise the dispute.
- fill_intent direct solver→user transfers are explicitly exempt: the contract
  is never the receiver, so balance-delta measurement is neither possible nor
  needed. The exemption and its rationale are documented in fill_intent_inner
  and in SECURITY.md §Fee-on-transfer tokens.
- SECURITY.md: added §Fee-on-transfer tokens documenting the pull_exact
  pattern, the per-path decision (accept delta vs. reject), the direct-fill
  exemption, and the rebasing-token non-support note for bond tokens.

## Issue stellar-vortex-protocol#411 — Scope solver authorization with require_auth_for_args
Closes stellar-vortex-protocol#411

With relayers and gasless flows, auth entries get passed around. An unscoped
require_auth() on fill_intent lets a delegating invoker redirect a solver's
signature to a different intent or a different fill amount — a concrete
relay-reuse attack vector.

Changes:
- fill_intent: upgraded from solver.require_auth() to
  solver.require_auth_for_args((solver, intent_id, fill_amount)). This is the
  highest-value call site: the solver authorises an outgoing token transfer.
  Scoping to (solver, intent_id, fill_amount) ensures a signed auth entry
  cannot be replayed for a different intent or amount. The solver address is
  included so the tuple is globally unique across contracts.
- accept_intent: require_auth_for_args((intent_id,)) — prevents redirecting
  the solver's accept signature to a different intent.
- accept_intent_with_bond: require_auth_for_args((intent_id, bond_token)) —
  same, plus pins the bond denomination.
- begin_fill: require_auth_for_args((solver, intent_id, fill_amount)) — tokens
  move into escrow, same rationale as fill_intent.
- batch_accept_intent: require_auth_for_args((intent_ids,)) — solver's sig
  covers exactly this ordered set of intent IDs.
- batch_fill_intent: require_auth_for_args((fills,)) — covers both the intent
  IDs and fill amounts; a signature for one fill list cannot be replayed for
  a different list.
- docs/auth-audit.md: updated the Upgraded table to include all seven scoped
  entrypoints with their args and rationale. Updated Integration impact section
  with the new payload shapes solver bots must sign.

## Issue stellar-vortex-protocol#408 — Atomic timelocked source-chain onboarding proposal
Closes stellar-vortex-protocol#408

Adding a new source chain previously required multiple separate admin operations
across two contracts: add_allowed_src_chain, set_authorized_emitter on
proof_registry, Axelar source config, and token-format rules. A partial
configuration leaves the chain half-enabled — intents can be submitted but
every fill fails with a proof error and the intents are slashed or expire,
causing real user harm.

Changes (intent_settlement/src/lib.rs):
- Added ChainConfig struct: { name, wormhole_id, axelar_name, emitter,
  axelar_source, token_format } — the complete per-chain configuration bundle.
- Added DataKey::PendingChainOnboarding(String) and DataKey::ChainConfig(String)
  storage keys.
- Added Error::NoPendingChainOnboarding = 36, ChainAlreadyConfigured = 37,
  ChainNotConfigured = 38.
- propose_chain_onboarding(config: ChainConfig): admin-only, queues the config
  under a 48-hour timelock. A new proposal for the same chain overwrites any
  prior pending one (and resets the clock). Emits chain_onboarding_proposed.
- execute_chain_onboarding(name): callable after timelock elapses. Atomically:
    1. Adds name to the src-chain allowlist.
    2. Calls proof_registry.configure_chain(wormhole_id, emitter) in the same
       transaction so both contracts are configured together.
    3. Writes a live ChainConfig entry for get_chain_config.
  Panics with NoPendingChainOnboarding, TimelockNotElapsed, or
  ChainAlreadyConfigured as appropriate. Emits chain_onboarding_executed.
- execute_chain_offboarding(name): admin-only, immediate (no timelock). Removes
  the chain from the allowlist and calls proof_registry.remove_chain in one
  call. In-flight intents already submitted are unaffected — src_chain is
  validated at submit_intent time only, not at fill time. Existing proof records
  remain readable. Emits chain_offboarding_executed.
- get_chain_config(name): view returning the live ChainConfig or None.

Changes (proof_registry/src/lib.rs):
- Added ProofKey::Configurator storage key.
- set_configurator(configurator): admin-only, grants the given address
  (typically intent_settlement) the narrow configurator role.
- get_configurator(): view.
- configure_chain(chain_id, emitter): configurator-only, registers the
  authorized Wormhole emitter for chain_id. Called by execute_chain_onboarding.
- remove_chain(chain_id): configurator-only, removes the authorized emitter
  for chain_id. Called by execute_chain_offboarding. Existing stored proofs
  are unaffected.
- Added Error::ConfiguratorNotSet = 11.

Documentation:
- docs/132-supported-chains.md §6: atomic onboarding flow (propose +
  execute), offboarding behaviour (§6.2 — in-flight intents unaffected),
  and manual step-by-step reference (§6.3).

## Code quality
- Removed all duplicate const definitions (DISPUTE_WINDOW, ARBITER_WINDOW,
  MAX_EXTENSION_DURATION, DEFAULT_MIN_BOND/FILL_WINDOW/INTENT_EXPIRY/
  PROTOCOL_FEE_BPS, SLASH_COOLDOWN, CANCEL_COOLDOWN, MAX_BATCH_SIZE) that
  had been left in the file from earlier merge conflicts.
- Removed duplicate placeholder implementations of begin_fill, open_dispute,
  resolve_dispute, release_fill, batch_fill_intent, batch_cancel_intent, and
  get_pending_admin that had been superseded by the full implementations.
- Removed placeholder validate_proof stub that was superseded by the real
  cross-contract validation implementation.
@drips-wave

drips-wave Bot commented Sep 30, 2026

Copy link
Copy Markdown

@whiteghost0001 Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@james2177
james2177 merged commit eee82e6 into stellar-vortex-protocol:main Oct 2, 2026
3 of 24 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants