diff --git a/contracts/vault/ACCESS_CONTROL.md b/contracts/vault/ACCESS_CONTROL.md index db340d7..7f20dd2 100644 --- a/contracts/vault/ACCESS_CONTROL.md +++ b/contracts/vault/ACCESS_CONTROL.md @@ -75,3 +75,29 @@ The `transfer_ownership` function allows the current owner to hand over full con ### Admin Transition The `set_admin` function allows the current admin (typically the owner initially) to delegate operational control (like settlement and distribution) to a dedicated service account. + +--- + +## Migration: Owner-Only Metering → Backend-Signed Metering + +### Background + +In the initial deployment model, only the vault owner could invoke `deduct` and `batch_deduct`. This required the owner's key to be present in the metering path, which is impractical for automated, high-frequency backend services. + +### Current Model + +The `set_authorized_caller` function allows the owner to designate a single backend address (e.g., a matching engine or metering service) that may call deduct flows alongside the owner. Both the owner and the authorized caller are permitted; all other addresses are rejected. + +### Migration Steps + +1. Deploy or upgrade the vault contract containing `set_authorized_caller`. +2. The owner calls `set_authorized_caller(owner, backend_address)` to register the backend signing key. +3. The backend service signs and submits `deduct` / `batch_deduct` transactions using its own key. +4. The owner's key is no longer required in the hot metering path. + +### Operational Notes + +- Only the owner may call `set_authorized_caller`; the backend cannot self-register. +- Rotating the backend key requires calling `set_authorized_caller` again with the new address. The previous address is replaced atomically. +- Every change emits a `set_auth_caller` event (topics: `("set_auth_caller", owner)`, data: `new_caller`) for audit purposes. +- Passing the vault contract's own address or the currently stored address as `new_caller` is rejected. diff --git a/contracts/vault/src/lib.rs b/contracts/vault/src/lib.rs index f27769b..cd61c23 100644 --- a/contracts/vault/src/lib.rs +++ b/contracts/vault/src/lib.rs @@ -209,33 +209,45 @@ impl CalloraVault { } } - pub fn clear_allowed_depositors(env: Env, caller: Address) { - caller.require_auth(); - Self::require_owner(env.clone(), caller); - env.storage() - .instance() - .remove(&StorageKey::AllowedDepositors); - env.storage() - .instance() - .set(&StorageKey::DepositorList, &Vec::
::new(&env)); - } - - pub fn get_allowed_depositors(env: Env) -> Vec
{ - env.storage() - .instance() - .get(&StorageKey::DepositorList) - .unwrap_or(Vec::new(&env)) - } - - pub fn set_authorized_caller(env: Env, caller: Address) { + /// Sets the address permitted to invoke deduct flows alongside the owner. + /// + /// Only the vault owner may call this function. The new authorized caller must + /// differ from the address already stored; passing the same address is rejected + /// as a meaningless no-op. Passing the vault contract's own address is also + /// rejected. + /// + /// # Arguments + /// * `env` – The environment running the contract. + /// * `owner` – Must be the current vault owner; must authorize this call. + /// * `new_caller` – Address to grant deduction rights. + /// + /// # Panics + /// * `"unauthorized: owner only"` – if `owner` is not the vault owner. + /// * `"new_caller must differ from current authorized caller"` – if `new_caller` + /// is already the stored authorized caller (meaningless update). + /// * `"new_caller must not be the vault contract itself"` – if `new_caller` is + /// the vault's own contract address. + /// + /// # Events + /// Emits topic `("set_auth_caller", owner)` with data `new_caller` on success. + pub fn set_authorized_caller(env: Env, owner: Address, new_caller: Address) { + owner.require_auth(); + Self::require_owner(env.clone(), owner.clone()); + assert!( + new_caller != env.current_contract_address(), + "new_caller must not be the vault contract itself" + ); let mut meta = Self::get_meta(env.clone()); - meta.owner.require_auth(); - meta.authorized_caller = Some(caller.clone()); + if let Some(ref current) = meta.authorized_caller { + assert!( + new_caller != *current, + "new_caller must differ from current authorized caller" + ); + } + meta.authorized_caller = Some(new_caller.clone()); env.storage().instance().set(&StorageKey::Meta, &meta); - env.events().publish( - (Symbol::new(&env, "set_auth_caller"), meta.owner.clone()), - caller, - ); + env.events() + .publish((Symbol::new(&env, "set_auth_caller"), owner), new_caller); } pub fn pause(env: Env, caller: Address) { @@ -278,7 +290,8 @@ impl CalloraVault { Self::is_authorized_depositor(env.clone(), caller.clone()), "unauthorized: only owner or allowed depositor can deposit" ); - let meta = Self::get_meta(env.clone()); + + let mut meta = Self::get_meta(env.clone()); assert!( amount >= meta.min_deposit, "deposit below minimum: {} < {}", diff --git a/contracts/vault/src/test.rs b/contracts/vault/src/test.rs index e05f35a..b91b563 100644 --- a/contracts/vault/src/test.rs +++ b/contracts/vault/src/test.rs @@ -545,7 +545,7 @@ fn set_authorized_caller_sets_and_emits_event() { env.mock_all_auths(); client.init(&owner, &usdc, &Some(200), &None, &None, &None, &None); - client.set_authorized_caller(&new_caller); + client.set_authorized_caller(&owner, &new_caller); let events = env.events().all(); let ev = events.last().expect("expected set_auth_caller event"); @@ -1963,7 +1963,7 @@ fn test_set_authorized_caller() { env.mock_all_auths(); client.init(&owner, &usdc, &None, &None, &None, &None, &None); - client.set_authorized_caller(&auth_caller); + client.set_authorized_caller(&owner, &auth_caller); let meta = client.get_meta(); assert_eq!(meta.authorized_caller, Some(auth_caller)); }