From 904c939c14b5d8b3d3595113a8156cbf473bdec3 Mon Sep 17 00:00:00 2001 From: NingLin Liu Date: Wed, 28 Oct 2026 12:40:42 +0800 Subject: [PATCH] fix(confidential): reject set_spender with a past live_until_ledger set_spender stored a live_until_ledger that was already in the past. The proof was verified, the allowance was escrowed out of the owner's spendable balance, and the delegation was expired on arrival, so every confidential_transfer_from reverted with DelegationExpired until the owner revoked it. Revert with the new ConfidentialTokenError::InvalidLiveUntilLedger (3515) when live_until_ledger < ledger.sequence(), before the auditor lookup and proof verification. The bound is inclusive like the liveness check and the fungible set_allowance, so live_until_ledger == ledger.sequence() is still a valid single-ledger delegation. Fixes #947 --- packages/confidential/src/lib.rs | 4 ++++ packages/confidential/src/storage.rs | 9 +++++++++ packages/confidential/src/test.rs | 30 ++++++++++++++++++++++++++++ 3 files changed, 43 insertions(+) diff --git a/packages/confidential/src/lib.rs b/packages/confidential/src/lib.rs index 42b8b367a..9c8abd36a 100644 --- a/packages/confidential/src/lib.rs +++ b/packages/confidential/src/lib.rs @@ -596,6 +596,10 @@ pub enum ConfidentialTokenError { /// boundary to keep stored state and emitted events byte-unique per /// logical value. NonCanonicalEncoding = 3514, + /// Indicates `live_until_ledger` is already in the past + /// (`live_until_ledger < ledger.sequence()`), so the delegation would be + /// expired on arrival. + InvalidLiveUntilLedger = 3515, } // ################## CONSTANTS ################## diff --git a/packages/confidential/src/storage.rs b/packages/confidential/src/storage.rs index 2f894263a..3bb12012c 100644 --- a/packages/confidential/src/storage.rs +++ b/packages/confidential/src/storage.rs @@ -846,6 +846,8 @@ pub fn confidential_transfer_from( /// /// # Errors /// +/// * [`ConfidentialTokenError::InvalidLiveUntilLedger`] - When +/// `live_until_ledger` is less than the current ledger number. /// * [`ConfidentialTokenError::AccountNotRegistered`] - When `account` or /// `spender` is not registered. /// * [`ConfidentialTokenError::DelegationAlreadyExists`] - When a delegation @@ -874,6 +876,13 @@ pub fn set_spender( payload: &SetSpenderPayload, proof: &Bytes, ) { + // Reject a delegation that would be expired on arrival before paying for + // the auditor lookup and proof verification. The bound is inclusive to + // match the liveness check (`ledger.sequence() <= live_until_ledger`). + if live_until_ledger < e.ledger().sequence() { + panic_with_error!(e, ConfidentialTokenError::InvalidLiveUntilLedger); + } + let owner = get_account(e, account); let spender_account = get_account(e, spender); let auditor = ConfidentialAuditorClient::new(e, &get_auditor(e)); diff --git a/packages/confidential/src/test.rs b/packages/confidential/src/test.rs index e5dae3de2..edd68e888 100644 --- a/packages/confidential/src/test.rs +++ b/packages/confidential/src/test.rs @@ -713,6 +713,36 @@ fn set_spender_twice_panics() { h.token.set_spender(&alice, &spender, &1_000u32, &set_spender_data(&h.e)); } +#[test] +#[should_panic(expected = "Error(Contract, #3515)")] +fn set_spender_with_past_live_until_ledger_panics() { + let h = setup(); + let alice = Address::generate(&h.e); + let spender = Address::generate(&h.e); + + h.token.register(&alice, &1u32, ®ister_data(&h.e)); + h.token.register(&spender, &1u32, ®ister_data(&h.e)); + + h.e.ledger().set_sequence_number(100); + h.token.set_spender(&alice, &spender, &99u32, &set_spender_data(&h.e)); +} + +#[test] +fn set_spender_with_current_ledger_is_live() { + let h = setup(); + let alice = Address::generate(&h.e); + let spender = Address::generate(&h.e); + + h.token.register(&alice, &1u32, ®ister_data(&h.e)); + h.token.register(&spender, &1u32, ®ister_data(&h.e)); + + h.e.ledger().set_sequence_number(100); + h.token.set_spender(&alice, &spender, &100u32, &set_spender_data(&h.e)); + + assert_eq!(h.token.get_spender_delegation(&alice, &spender).live_until_ledger, 100); + assert!(h.token.is_spender(&alice, &spender)); +} + #[test] fn revoke_spender_deletes_delegation() { let h = setup();