Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions contracts/vault/ACCESS_CONTROL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
65 changes: 39 additions & 26 deletions contracts/vault/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<Address>::new(&env));
}

pub fn get_allowed_depositors(env: Env) -> Vec<Address> {
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) {
Expand Down Expand Up @@ -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: {} < {}",
Expand Down
4 changes: 2 additions & 2 deletions contracts/vault/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -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));
}
Expand Down
Loading