diff --git a/packages/confidential/src/lib.rs b/packages/confidential/src/lib.rs index 42b8b367a..1d5f0032e 100644 --- a/packages/confidential/src/lib.rs +++ b/packages/confidential/src/lib.rs @@ -441,7 +441,8 @@ pub trait ConfidentialToken { /// account so its spending key is available for the `dvk` escrow ECDH. /// * `live_until_ledger` - The ledger number at which the delegation /// expires. Spending is authorized while `ledger.sequence() <= - /// live_until_ledger`. The escrowed value persists until + /// live_until_ledger`. Must be between the current ledger and + /// `max_live_until_ledger`, inclusive. The escrowed value persists until /// [`ConfidentialToken::revoke_spender`]. /// * `data` - XDR-encoded [`SetSpenderData`]. /// @@ -596,6 +597,10 @@ pub enum ConfidentialTokenError { /// boundary to keep stored state and emitted events byte-unique per /// logical value. NonCanonicalEncoding = 3514, + /// Indicates `live_until_ledger` is outside the accepted range: below the + /// current ledger, so the delegation would be expired on creation, or above + /// `max_live_until_ledger`. + InvalidLiveUntilLedger = 3515, } // ################## CONSTANTS ################## diff --git a/packages/confidential/src/storage.rs b/packages/confidential/src/storage.rs index 2f894263a..86fc1148f 100644 --- a/packages/confidential/src/storage.rs +++ b/packages/confidential/src/storage.rs @@ -841,11 +841,15 @@ pub fn confidential_transfer_from( /// account so its spending key is available for the `dvk` escrow ECDH. /// * `live_until_ledger` - The ledger number at which the delegation expires. /// Spending is authorized while `ledger.sequence() <= live_until_ledger`. +/// Must be between the current ledger and `max_live_until_ledger`, inclusive. /// * `payload` - The decoded [`SetSpenderPayload`]. /// * `proof` - The raw UltraHonk proof bytes. /// /// # Errors /// +/// * [`ConfidentialTokenError::InvalidLiveUntilLedger`] - When +/// `live_until_ledger` is lower than the current ledger sequence or greater +/// than `max_live_until_ledger`. /// * [`ConfidentialTokenError::AccountNotRegistered`] - When `account` or /// `spender` is not registered. /// * [`ConfidentialTokenError::DelegationAlreadyExists`] - When a delegation @@ -874,6 +878,14 @@ pub fn set_spender( payload: &SetSpenderPayload, proof: &Bytes, ) { + // Same bounds as the fungible `set_allowance`, checked before the account + // lookups, the auditor call and proof verification. + if live_until_ledger < e.ledger().sequence() + || live_until_ledger > e.ledger().max_live_until_ledger() + { + 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..cc060d212 100644 --- a/packages/confidential/src/test.rs +++ b/packages/confidential/src/test.rs @@ -713,6 +713,65 @@ 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] +#[should_panic(expected = "Error(Contract, #3515)")] +fn set_spender_beyond_max_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)); + + let max = h.e.ledger().max_live_until_ledger(); + h.token.set_spender(&alice, &spender, &(max + 1), &set_spender_data(&h.e)); +} + +#[test] +fn set_spender_with_max_live_until_ledger_succeeds() { + 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)); + + let max = h.e.ledger().max_live_until_ledger(); + h.token.set_spender(&alice, &spender, &max, &set_spender_data(&h.e)); + + assert_eq!(h.token.get_spender_delegation(&alice, &spender).live_until_ledger, max); +} + +#[test] +fn set_spender_with_current_ledger_as_live_until_ledger_succeeds() { + 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();