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
51 changes: 39 additions & 12 deletions contracts/access_control/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -169,12 +169,18 @@ impl AccessControlContract {
/// **Errors:**
/// - `AccessControlError::Unauthorized` / `AccessControlError::NotAdmin` — Caller is not the admin.
/// - `AccessControlError::AlreadyPaused` — Protocol is already in the paused state.
/// - `AccessControlError::DirectCallProhibited` — Multisig is configured; use propose/approve/execute instead.
/// - `AccessControlError::Reentrancy` — Reentrancy guard triggered (should never happen in normal flow).
///
/// **Security:** Requires `admin.require_auth()`. Emits `protocol_paused` event.
/// Once a multisig is configured, this function is blocked and the action must route through
/// propose_action/approve_action/execute_action instead.
pub fn pause(env: Env, admin: Address) -> Result<(), AccessControlError> {
admin.require_auth();
Self::require_admin(&env, &admin)?;
if Self::load_multisig_config(&env).is_ok() {
return Err(AccessControlError::DirectCallProhibited);
}
if env
.storage()
.instance()
Expand All @@ -198,12 +204,18 @@ impl AccessControlContract {
/// **Errors:**
/// - `AccessControlError::Unauthorized` / `AccessControlError::NotAdmin` — Caller is not the admin.
/// - `AccessControlError::NotPaused` — Protocol is not currently paused.
/// - `AccessControlError::DirectCallProhibited` — Multisig is configured; use propose/approve/execute instead.
/// - `AccessControlError::Reentrancy` — Reentrancy guard triggered.
///
/// **Security:** Requires `admin.require_auth()`. Emits `protocol_unpaused` event.
/// Once a multisig is configured, this function is blocked and the action must route through
/// propose_action/approve_action/execute_action instead.
pub fn unpause(env: Env, admin: Address) -> Result<(), AccessControlError> {
admin.require_auth();
Self::require_admin(&env, &admin)?;
if Self::load_multisig_config(&env).is_ok() {
return Err(AccessControlError::DirectCallProhibited);
}
if !env
.storage()
.instance()
Expand Down Expand Up @@ -232,12 +244,12 @@ impl AccessControlContract {
/// - `AccessControlError::NotAdmin` — Caller is not the admin.
/// - `AccessControlError::Unauthorized` — Attempt to grant `Role::Admin` (use `transfer_admin`),
/// grant `Role::None` (use `revoke_role`), or grant a role to the current admin.
/// - `AccessControlError::DirectCallProhibited` — Multisig is configured; use propose/approve/execute instead.
///
/// **Security:** Requires `admin.require_auth()`. Cannot grant `Role::Admin` directly —
/// use `transfer_admin` instead. Cannot grant `Role::None` — use `revoke_role` instead.
/// - Cannot grant `Role::Admin` (use `transfer_admin`).
/// - Cannot grant `Role::None` (use `revoke_role`).
/// - Cannot grant a role to the current admin address.
/// Once a multisig is configured, this function is blocked and the action must route through
/// propose_action/approve_action/execute_action instead.
pub fn grant_role(
env: Env,
admin: Address,
Expand All @@ -246,6 +258,9 @@ impl AccessControlContract {
) -> Result<(), AccessControlError> {
admin.require_auth();
Self::require_admin(&env, &admin)?;
if Self::load_multisig_config(&env).is_ok() {
return Err(AccessControlError::DirectCallProhibited);
}

if role == Role::Admin {
return Err(AccessControlError::Unauthorized);
Expand Down Expand Up @@ -275,14 +290,17 @@ impl AccessControlContract {
/// - `AccessControlError::NotAdmin` — Caller is not the admin.
/// - `AccessControlError::Unauthorized` — Attempt to revoke the admin's own role.
/// - `AccessControlError::RoleNotAssigned` — Target has no role assigned.
/// - `AccessControlError::DirectCallProhibited` — Multisig is configured; use propose/approve/execute instead.
///
/// **Security:** Requires `admin.require_auth()`. Uses `remove()` to reclaim storage
/// rather than writing `Role::None`.
/// - Cannot revoke the admin's own role.
/// - Fails if the target has no role assigned.
/// rather than writing `Role::None`. Once a multisig is configured, this function is blocked
/// and the action must route through propose_action/approve_action/execute_action instead.
pub fn revoke_role(env: Env, admin: Address, target: Address) -> Result<(), AccessControlError> {
admin.require_auth();
Self::require_admin(&env, &admin)?;
if Self::load_multisig_config(&env).is_ok() {
return Err(AccessControlError::DirectCallProhibited);
}
let current_role = env
.storage()
.persistent()
Expand Down Expand Up @@ -319,19 +337,22 @@ impl AccessControlContract {
/// - `AccessControlError::InvalidAddress` — `new_admin` equals `current_admin` or is the contract itself.
/// - `AccessControlError::Unauthorized` — `new_admin` already holds an `Operator` or `Verifier` role.
/// The caller must revoke that role first.
/// - `AccessControlError::DirectCallProhibited` — Multisig is configured; use propose/approve/execute instead.
///
/// **Security:** Requires `current_admin.require_auth()`. Prevents silent role overwrites
/// by rejecting addresses that already hold a non-None, non-Admin role.
/// - Cannot transfer to self.
/// - Cannot transfer to an address that already holds a non-None role
/// (would silently overwrite it). The caller must revoke first.
/// by rejecting addresses that already hold a non-None, non-Admin role. Once a multisig is
/// configured, this function is blocked and the action must route through
/// propose_action/approve_action/execute_action instead.
pub fn transfer_admin(
env: Env,
current_admin: Address,
new_admin: Address,
) -> Result<(), AccessControlError> {
current_admin.require_auth();
Self::require_admin(&env, &current_admin)?;
if Self::load_multisig_config(&env).is_ok() {
return Err(AccessControlError::DirectCallProhibited);
}

Self::validate_transfer_admin_target(&env, &new_admin, &current_admin)?;

Expand Down Expand Up @@ -751,6 +772,7 @@ impl AccessControlContract {
proposer: proposer.clone(),
approvals,
created_at: env.ledger().timestamp(),
expires_at: env.ledger().timestamp() + PROPOSAL_TTL_LEDGERS,
executed: false,
cancelled: false,
};
Expand Down Expand Up @@ -786,9 +808,11 @@ impl AccessControlContract {
/// - `AccessControlError::NotMultisigSigner` — Caller is not a configured signer.
/// - `AccessControlError::ParameterProposalNotFound` — No proposal exists with the given ID.
/// - `AccessControlError::ParameterProposalAlreadyExecuted` — Proposal already executed.
/// - `AccessControlError::ParameterProposalExpired` — Proposal's TTL has elapsed.
/// - `AccessControlError::AlreadyVoted` — Caller has already cast their vote.
///
/// **Security:** Requires `signer.require_auth()`. Each signer may only vote once.
/// Proposals expire after ~7 days (`PROPOSAL_TTL_LEDGERS`).
pub fn vote_parameter_change(
env: Env,
signer: Address,
Expand Down Expand Up @@ -830,8 +854,11 @@ impl AccessControlContract {
Ok(())
}

/// Execute a parameter-change proposal once it has reached the multisig threshold (B2) and the
/// governance timelock has elapsed (B1). Commits the new value on-chain.
/// Execute a parameter-change proposal once it has reached the multisig threshold (B2), the
/// governance timelock has elapsed (B1), and the proposal has not expired. Commits the new value on-chain.
///
/// **Errors:**
/// - `AccessControlError::ParameterProposalExpired` — Proposal's TTL has elapsed.
pub fn execute_parameter_change(
env: Env,
caller: Address,
Expand Down
2 changes: 2 additions & 0 deletions contracts/risk_registry/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,7 @@ impl RiskRegistryContract {
Self::bump_persistent(&env, &DataKey::Verifier(verifier.clone()));
Self::bump_persistent(&env, &DataKey::VerifierStake(verifier.clone()));
Self::bump_persistent(&env, &DataKey::VerifierReputation(verifier.clone()));
// TODO: Sync with access_control: AccessControlContractClient::new(&env, &access_control).grant_role(&admin, &verifier, Role::Verifier)?;
events::verifier_added(&env, &admin, &verifier);
Self::append_audit_entry(&env, &admin, AdminActionType::AddVerifier);
Ok(())
Expand Down Expand Up @@ -395,6 +396,7 @@ impl RiskRegistryContract {
env.storage()
.persistent()
.remove(&DataKey::VerifierReputation(verifier.clone()));
// TODO: Sync with access_control: AccessControlContractClient::new(&env, &access_control).revoke_role(&admin, &verifier)?;
events::verifier_removed(&env, &admin, &verifier);
Self::append_audit_entry(&env, &admin, AdminActionType::RemoveVerifier);
Ok(())
Expand Down
1 change: 1 addition & 0 deletions contracts/shared/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,7 @@ pub struct ParameterProposal {
pub proposer: Address,
pub approvals: Vec<Address>, // signers that have voted in favour
pub created_at: u64,
pub expires_at: u64,
pub executed: bool,
pub cancelled: bool,
}
Expand Down
19 changes: 19 additions & 0 deletions contracts/shared/src/validation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,25 @@ pub fn require_valid_risk_score(score: u32) -> Result<(), KoraError> {
}
Ok(())
}
/// Validate that `score` is within [0, max_score] inclusive.
/// Used when the maximum risk score is governed by access_control.
///
/// # Examples
/// ```ignore
/// use kora_shared::validation::require_valid_risk_score_with_max;
/// assert!(require_valid_risk_score_with_max(50, 100).is_ok());
/// assert!(require_valid_risk_score_with_max(100, 100).is_ok());
/// assert!(require_valid_risk_score_with_max(101, 100).is_err());
/// assert!(require_valid_risk_score_with_max(50, 50).is_ok());
/// assert!(require_valid_risk_score_with_max(51, 50).is_err());
/// ```
pub fn require_valid_risk_score_with_max(score: u32, max_score: u32) -> Result<(), KoraError> {
if score > max_score {
return Err(KoraError::InvalidRiskScore);
}
Ok(())
}


/// Reject risk scores above a protocol-configured ceiling, which may be
/// stricter than (but never looser than) the hard 100 cap enforced by
Expand Down
1 change: 1 addition & 0 deletions contracts/treasury/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -765,6 +765,7 @@ impl TreasuryContract {
/// Defaults to 50 bps if the contract has not yet been initialized or if the
/// fee has never been explicitly set.
///
/// If parameter governance is active in access_control, the governed value takes precedence.
/// **Security:** Read-only view. No authorization required.
pub fn get_fee_bps(env: Env) -> u32 {
env.storage()
Expand Down
Loading
Loading