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
1 change: 1 addition & 0 deletions contracts/compliance/src/allowlist.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use soroban_sdk::{contracterror, contracttype, Address};
pub enum DataKey {
Admin,
PendingAdmin,
Operator,
Allowed(Address),
Blocked(Address),
AllowedUntil(Address),
Expand Down
83 changes: 83 additions & 0 deletions contracts/compliance/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,71 @@ impl ComplianceContract {
Ok(())
}

/// Assign an operator address. Only admin may call this.
/// The operator can invoke read-only compliance queries (`get_allow_expiry`,
/// `address_status`) without holding admin privileges.
pub fn set_operator(env: Env, admin: Address, operator: Address) -> Result<(), ContractError> {
Self::require_admin(&env, &admin)?;
env.storage().instance().set(&DataKey::Operator, &operator);
env.events()
.publish((Symbol::new(&env, "operator_set"),), operator);
Ok(())
}

/// Returns the raw expiry timestamp (seconds since epoch) for `address`, or
/// `None` if the address has no time-limited allow entry.
/// Requires admin or operator authentication.
pub fn get_allow_expiry(
env: Env,
caller: Address,
address: Address,
) -> Result<Option<u64>, ContractError> {
Self::require_admin_or_operator(&env, &caller)?;
Ok(env
.storage()
.persistent()
.get::<_, u64>(&DataKey::AllowedUntil(address)))
}

/// Returns the compliance state for `address` (Allowed, Blocked, or Expired).
/// Requires admin or operator authentication.
pub fn address_status(
env: Env,
caller: Address,
address: Address,
) -> Result<AddressState, ContractError> {
Self::require_admin_or_operator(&env, &caller)?;
let blocked: bool = env
.storage()
.persistent()
.get(&DataKey::Blocked(address.clone()))
.unwrap_or(false);
if blocked {
return Ok(AddressState::Blocked);
}
let allowed: bool = env
.storage()
.persistent()
.get(&DataKey::Allowed(address.clone()))
.unwrap_or(false);
if !allowed {
return Ok(AddressState::Blocked);
}
if let Some(expires_at) = env
.storage()
.persistent()
.get::<_, u64>(&DataKey::AllowedUntil(address))
{
if env.ledger().timestamp() < expires_at {
Ok(AddressState::Allowed)
} else {
Ok(AddressState::Expired)
}
} else {
Ok(AddressState::Allowed)
}
}

fn require_admin(env: &Env, admin: &Address) -> Result<(), ContractError> {
admin.require_auth();
let stored: Address = env.storage().instance().get(&DataKey::Admin).unwrap();
Expand All @@ -262,6 +327,24 @@ impl ComplianceContract {
Ok(())
}

fn require_admin_or_operator(env: &Env, caller: &Address) -> Result<(), ContractError> {
caller.require_auth();
let stored_admin: Address = env.storage().instance().get(&DataKey::Admin).unwrap();
if stored_admin == *caller {
return Ok(());
}
if let Some(operator) = env
.storage()
.instance()
.get::<_, Address>(&DataKey::Operator)
{
if operator == *caller {
return Ok(());
}
}
Err(ContractError::Unauthorized)
}

fn require_not_paused(env: &Env) -> Result<(), ContractError> {
let paused: bool = env
.storage()
Expand Down
Loading