Skip to content
Merged
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
10 changes: 10 additions & 0 deletions contracts/pool/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1223,6 +1223,16 @@ fn test_fund_invoice_rejects_above_cap() {
te.pool.fund_invoice(&invoice_id);
}

#[test]
#[should_panic(expected = "Error(Contract, #12)")]
fn test_fund_invoice_rejects_when_utilization_cap_is_zero() {
let te = setup();
te.pool.set_max_utilization(&te.admin, &0);
te.pool.deposit(&te.lp, &10_000_000_000);
let invoice_id = create_and_list(&te, &te.usdc_id);
te.pool.fund_invoice(&invoice_id);
}

#[test]
fn test_fund_invoice_allowed_when_below_cap() {
let te = setup();
Expand Down
5 changes: 0 additions & 5 deletions contracts/registry/src/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,6 @@ pub fn buyer_registered(env: &Env, address: &Address) {
.publish((Symbol::new(env, "buyer_registered"), address.clone()), ());
}

pub fn metadata_updated(env: &Env, address: &Address) {
env.events()
.publish((Symbol::new(env, "metadata_updated"), address.clone()), ());
}

pub fn address_revoked(env: &Env, address: &Address) {
env.events()
.publish((Symbol::new(env, "address_revoked"), address.clone()), ());
Expand Down
104 changes: 42 additions & 62 deletions contracts/registry/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,11 +120,7 @@ impl RegistryContract {
panic_with_error!(&env, RegistryError::BatchSizeExceeded);
}

let admin: Address = env
.storage()
.instance()
.get(&DataKey::Admin)
.unwrap_or_else(|| panic_with_error!(&env, RegistryError::NotFound));
let admin = Self::require_admin(&env);
admin.require_auth();

// Pre-validate ALL entries' metadata before processing any of them.
Expand Down Expand Up @@ -181,7 +177,7 @@ impl RegistryContract {
///
/// # Panics
/// * `RegistryError::BatchSizeExceeded` if `entries.len() > 50`.
/// * `RegistryError::NotFound` if the contract admin is not set.
/// * `RegistryError::NotInitialized` if the contract admin is not set.
///
/// # Returns
/// * `Vec<Address>` - The list of addresses that were skipped (already
Expand All @@ -194,11 +190,7 @@ impl RegistryContract {
panic_with_error!(&env, RegistryError::BatchSizeExceeded);
}

let admin: Address = env
.storage()
.instance()
.get(&DataKey::Admin)
.unwrap_or_else(|| panic_with_error!(&env, RegistryError::NotFound));
let admin = Self::require_admin(&env);
admin.require_auth();

let mut skipped: Vec<Address> = Vec::new(&env);
Expand Down Expand Up @@ -329,6 +321,7 @@ impl RegistryContract {
.persistent()
.extend_ttl(&key, TTL_THRESHOLD, TTL_EXTEND_TO);
events::profile_updated(&env, &address);
Self::extend_instance_ttl(&env);
true
}

Expand All @@ -347,7 +340,7 @@ impl RegistryContract {
/// `MAX_METADATA_SIZE` entries, contains an empty key or value, or has a
/// key longer than `MAX_METADATA_KEY_LEN` or a value longer than
/// `MAX_METADATA_VALUE_LEN`.
/// * `RegistryError::NotFound` if the address is not registered.
/// * `RegistryError::NotRegistered` if the address is not registered.
///
/// # Example
/// ```ignore
Expand All @@ -361,13 +354,13 @@ impl RegistryContract {
.storage()
.persistent()
.get(&key)
.unwrap_or_else(|| panic_with_error!(&env, RegistryError::NotFound));
.unwrap_or_else(|| panic_with_error!(&env, RegistryError::NotRegistered));
profile.metadata = metadata;
env.storage().persistent().set(&key, &profile);
env.storage()
.persistent()
.extend_ttl(&key, TTL_THRESHOLD, TTL_EXTEND_TO);
events::metadata_updated(&env, &address);
events::profile_updated(&env, &address);
Self::extend_instance_ttl(&env);
true
}
Expand Down Expand Up @@ -485,11 +478,7 @@ impl RegistryContract {
/// let result = client.revoke(&issuer);
/// ```
pub fn revoke(env: Env, address: Address) -> bool {
let admin: Address = env
.storage()
.instance()
.get(&DataKey::Admin)
.unwrap_or_else(|| panic_with_error!(&env, RegistryError::NotFound));
let admin = Self::require_admin(&env);
admin.require_auth();
let key = DataKey::Profile(address.clone());
let mut profile: Profile = env
Expand Down Expand Up @@ -528,8 +517,8 @@ impl RegistryContract {
/// (read from `DataKey::Admin`) may reinstate a profile.
///
/// # Panics
/// * `RegistryError::NotFound` if the contract admin is not set (contract
/// was never initialized).
/// * `RegistryError::NotInitialized` if the contract admin is not set
/// (contract was never initialized).
/// * `RegistryError::NotFound` if no profile is stored for `address`.
///
/// # Returns
Expand All @@ -540,11 +529,7 @@ impl RegistryContract {
/// let ok = client.reinstate(&issuer);
/// ```
pub fn reinstate(env: Env, address: Address) -> bool {
let admin: Address = env
.storage()
.instance()
.get(&DataKey::Admin)
.unwrap_or_else(|| panic_with_error!(&env, RegistryError::NotFound));
let admin = Self::require_admin(&env);
admin.require_auth();
let key = DataKey::Profile(address.clone());
let mut profile: Profile = env
Expand All @@ -564,11 +549,7 @@ impl RegistryContract {
}

pub fn verify_profile(env: Env, address: Address, verify: bool) -> bool {
let admin: Address = env
.storage()
.instance()
.get(&DataKey::Admin)
.unwrap_or_else(|| panic_with_error!(&env, RegistryError::NotFound));
let admin = Self::require_admin(&env);
admin.require_auth();
let key = DataKey::Profile(address.clone());
let mut profile: Profile = env
Expand All @@ -591,28 +572,21 @@ impl RegistryContract {
true
}

/// Transfers admin ownership to a new address.
///
/// Requires authentication from both the current admin and the incoming
/// new admin, preventing accidental transfers to a wrong address. Callers
/// that intentionally need unilateral key rotation may use
/// [`transfer_admin`](Self::transfer_admin), but it does not provide this
/// dual-authorization safety guarantee.
/// # Arguments
/// * `env` - The Soroban environment.
/// * `new_admin` - The address that will become the new admin.
///
/// # Panics
/// * `RegistryError::NotInitialized` if the contract has not been initialized.
pub fn transfer_ownership(env: Env, new_admin: Address) {
// Transfers admin ownership to a new address.
//
// Requires authentication from BOTH the current admin and the incoming
// new admin, preventing accidental transfers to wrong addresses.
//
// # Arguments
// * `env` - The Soroban environment.
// * `new_admin` - The address that will become the new admin.
//
// # Panics
// * `NotFound` if the admin is not set.
//
// # Example
// ```ignore
// client.transfer_ownership(&new_admin);
// ```
let admin: Address = env
.storage()
.instance()
.get(&DataKey::Admin)
.unwrap_or_else(|| panic_with_error!(&env, RegistryError::NotFound));
let admin = Self::require_admin(&env);
admin.require_auth();
new_admin.require_auth();
env.storage().instance().set(&DataKey::Admin, &new_admin);
Expand All @@ -624,9 +598,12 @@ impl RegistryContract {
///
/// Unlike `transfer_ownership`, this function only requires auth from the
/// current admin — the new admin does not need to sign. This is useful
/// for key rotation scenarios where the current admin key may be
/// compromised or needs to be rotated without the new key holder's
/// involvement.
/// for key rotation scenarios where the new key holder's involvement is
/// unavailable. This is an intentional lower-security escape hatch: the
/// current admin can use it to bypass `transfer_ownership`'s dual-auth
/// guarantee, including transferring to an address that never authorized
/// the transfer. Integrators requiring that safety property must use
/// `transfer_ownership` and ensure both signatures are collected.
///
/// # Arguments
/// * `env` - The Soroban environment.
Expand All @@ -637,8 +614,8 @@ impl RegistryContract {
/// contract admin may call this function.
///
/// # Panics
/// * `RegistryError::NotFound` if the contract has not been initialized
/// (no admin is stored under `DataKey::Admin`).
/// * `RegistryError::NotInitialized` if the contract has not been
/// initialized.
///
/// # Returns
/// * `()` - No value is returned.
Expand All @@ -648,11 +625,7 @@ impl RegistryContract {
/// client.transfer_admin(&new_admin);
/// ```
pub fn transfer_admin(env: Env, new_admin: Address) {
let admin: Address = env
.storage()
.instance()
.get(&DataKey::Admin)
.unwrap_or_else(|| panic_with_error!(&env, RegistryError::NotFound));
let admin = Self::require_admin(&env);
admin.require_auth();
env.storage().instance().set(&DataKey::Admin, &new_admin);
events::admin_transferred(&env, &admin, &new_admin);
Expand Down Expand Up @@ -696,6 +669,13 @@ impl RegistryContract {
}

impl RegistryContract {
fn require_admin(env: &Env) -> Address {
env.storage()
.instance()
.get(&DataKey::Admin)
.unwrap_or_else(|| panic_with_error!(env, RegistryError::NotInitialized))
}

fn require_initialized(env: &Env) {
if !env.storage().instance().has(&DataKey::Admin) {
panic_with_error!(env, RegistryError::NotInitialized);
Expand Down
82 changes: 80 additions & 2 deletions contracts/registry/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,41 @@ fn test_register_buyer_before_initialize_panics() {
client.register_buyer(&Address::generate(&env), &map![&env]);
}

#[test]
#[should_panic(expected = "Error(Contract, #4)")]
fn test_batch_register_issuers_before_initialize_panics() {
let (env, client) = setup();
client.batch_register_issuers(&vec![&env]);
}

#[test]
#[should_panic(expected = "Error(Contract, #4)")]
fn test_revoke_before_initialize_panics() {
let (env, client) = setup();
client.revoke(&Address::generate(&env));
}

#[test]
#[should_panic(expected = "Error(Contract, #4)")]
fn test_reinstate_before_initialize_panics() {
let (env, client) = setup();
client.reinstate(&Address::generate(&env));
}

#[test]
#[should_panic(expected = "Error(Contract, #4)")]
fn test_verify_profile_before_initialize_panics() {
let (env, client) = setup();
client.verify_profile(&Address::generate(&env), &true);
}

#[test]
#[should_panic(expected = "Error(Contract, #4)")]
fn test_transfer_ownership_before_initialize_panics() {
let (env, client) = setup();
client.transfer_ownership(&Address::generate(&env));
}

#[test]
fn test_is_verified_returns_false_for_registered_but_unverified() {
let (env, client) = setup();
Expand Down Expand Up @@ -355,7 +390,7 @@ fn test_update_metadata_self_succeeds() {
}

#[test]
#[should_panic(expected = "Error(Contract, #3)")]
#[should_panic(expected = "Error(Contract, #7)")]
fn test_update_metadata_unregistered_panics() {
let (env, client) = setup();
let admin = Address::generate(&env);
Expand Down Expand Up @@ -1182,6 +1217,49 @@ fn test_transfer_admin_changes_admin() {
assert_eq!(client.get_admin(), new_admin);
}

#[test]
fn test_transfer_admin_bypasses_transfer_ownership_dual_auth() {
let env = Env::default();
let contract_id = env.register_contract(None, RegistryContract);
let client = RegistryContractClient::new(&env, &contract_id);
let admin = Address::generate(&env);
let new_admin = Address::generate(&env);

env.mock_auths(&[soroban_sdk::testutils::MockAuth {
address: &admin,
invoke: &soroban_sdk::testutils::MockAuthInvoke {
contract: &contract_id,
fn_name: "initialize",
args: (admin.clone(),).into_val(&env),
sub_invokes: &[],
},
}]);
client.initialize(&admin);

env.mock_auths(&[soroban_sdk::testutils::MockAuth {
address: &admin,
invoke: &soroban_sdk::testutils::MockAuthInvoke {
contract: &contract_id,
fn_name: "transfer_ownership",
args: (new_admin.clone(),).into_val(&env),
sub_invokes: &[],
},
}]);
assert!(client.try_transfer_ownership(&new_admin).is_err());

env.mock_auths(&[soroban_sdk::testutils::MockAuth {
address: &admin,
invoke: &soroban_sdk::testutils::MockAuthInvoke {
contract: &contract_id,
fn_name: "transfer_admin",
args: (new_admin.clone(),).into_val(&env),
sub_invokes: &[],
},
}]);
client.transfer_admin(&new_admin);
assert_eq!(client.get_admin(), new_admin);
}

#[test]
#[should_panic(expected = "Error(Auth, InvalidAction)")]
fn test_transfer_admin_by_non_admin_panics() {
Expand All @@ -1194,7 +1272,7 @@ fn test_transfer_admin_by_non_admin_panics() {
}

#[test]
#[should_panic(expected = "Error(Contract, #3)")]
#[should_panic(expected = "Error(Contract, #4)")]
fn test_transfer_admin_before_initialize_panics() {
let (env, client) = setup();
let new_admin = Address::generate(&env);
Expand Down