Skip to content
Open
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
16 changes: 16 additions & 0 deletions .rustfmt.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# bc-forge workspace formatting configuration
# Applied by: cargo fmt --all
# Stable-channel options only.
# See: https://rust-lang.github.io/rustfmt/

# ── Line length ──────────────────────────────────────────────────────────────
max_width = 100

# ── Imports ───────────────────────────────────────────────────────────────────
reorder_imports = true
reorder_modules = true

# ── Misc ──────────────────────────────────────────────────────────────────────
edition = "2021"
newline_style = "Unix"
use_field_init_shorthand = true
102 changes: 71 additions & 31 deletions contracts/admin/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

#![no_std]

use soroban_sdk::{contracttype, Address, Env, Vec, vec, String};
use soroban_sdk::{contracttype, vec, Address, Env, String, Vec};

/// Storage keys used by the admin module.
#[derive(Clone)]
Expand All @@ -15,6 +15,14 @@ pub enum AdminKey {
Admin,
/// Role assignments: (Role, Address) -> bool
Role(Role, Address),
/// The pool of administrator addresses for multi-sig.
AdminPool,
/// Minimum signatures required for multi-sig actions.
Threshold,
/// Active proposals: proposal_id -> Proposal.
Proposal(u64),
/// Counter for generating unique proposal IDs.
ProposalIdCounter,
}

/// Enumeration of available roles.
Expand All @@ -23,16 +31,7 @@ pub enum AdminKey {
pub enum Role {
/// Global administrator with full control.
Admin = 0,
/// Account authorized to mint tokens.
Minter = 1,
/// The pool of administrator addresses for multi-sig.
AdminPool,
/// Minimum signatures required for multi-sig actions.
Threshold,
/// Active proposals: proposal_id -> Proposal.
Proposal(u64),
/// Counter for generating unique proposal IDs.
ProposalIdCounter,
}

/// A proposal for a multi-signature action.
Expand Down Expand Up @@ -77,22 +76,33 @@ pub fn grant_role(env: &Env, role: Role, address: &Address) {
if has_admin(env) {
require_admin(env);
}
env.storage().persistent().set(&AdminKey::Role(role, address.clone()), &true);
env.storage()
.persistent()
.set(&AdminKey::Role(role, address.clone()), &true);
}

/// Revokes a role from an address. Only callable by an Admin.
pub fn revoke_role(env: &Env, role: Role, address: &Address) {
require_admin(env);
env.storage().persistent().remove(&AdminKey::Role(role, address.clone()));
env.storage()
.persistent()
.remove(&AdminKey::Role(role, address.clone()));
}

/// Returns `true` if the address has the specified role.
pub fn has_role(env: &Env, role: Role, address: &Address) -> bool {
// Admins implicitly have all roles.
if env.storage().persistent().has(&AdminKey::Role(Role::Admin, address.clone())) {
if env
.storage()
.persistent()
.has(&AdminKey::Role(Role::Admin, address.clone()))
{
return true;
}
env.storage().persistent().has(&AdminKey::Role(role, address.clone()))
env.storage()
.persistent()
.has(&AdminKey::Role(role, address.clone()))
}
// ─── Multi-Sig Primitives ───────────────────────────────────────────────────

/// Configures the multi-signature admin pool.
Expand All @@ -101,7 +111,9 @@ pub fn set_admin_pool(env: &Env, pool: Vec<Address>, threshold: u32) {
panic!("invalid threshold for admin pool");
}
env.storage().instance().set(&AdminKey::AdminPool, &pool);
env.storage().instance().set(&AdminKey::Threshold, &threshold);
env.storage()
.instance()
.set(&AdminKey::Threshold, &threshold);
}

/// Retrieves the admin pool. Defaults to the singular admin if no pool is set.
Expand All @@ -120,7 +132,10 @@ pub fn get_admin_pool(env: &Env) -> Vec<Address> {

/// Retrieves the quorum threshold for the admin pool.
pub fn get_threshold(env: &Env) -> u32 {
env.storage().instance().get(&AdminKey::Threshold).unwrap_or(1)
env.storage()
.instance()
.get(&AdminKey::Threshold)
.unwrap_or(1)
}

// ─── Guards ──────────────────────────────────────────────────────────────────
Expand All @@ -137,6 +152,7 @@ pub fn require_role(env: &Env, role: Role, address: &Address) {
panic!("unauthorized: missing role");
}
address.require_auth();
}
// ─── Proposals ──────────────────────────────────────────────────────────────

/// Creates a new proposal for an administrative action.
Expand All @@ -147,8 +163,14 @@ pub fn create_proposal(env: &Env, creator: Address, description: String) -> u64
panic!("only admins can create proposals");
}

let id = env.storage().instance().get(&AdminKey::ProposalIdCounter).unwrap_or(0);
env.storage().instance().set(&AdminKey::ProposalIdCounter, &(id + 1));
let id = env
.storage()
.instance()
.get(&AdminKey::ProposalIdCounter)
.unwrap_or(0);
env.storage()
.instance()
.set(&AdminKey::ProposalIdCounter, &(id + 1));

let proposal = Proposal {
creator: creator.clone(),
Expand All @@ -157,7 +179,9 @@ pub fn create_proposal(env: &Env, creator: Address, description: String) -> u64
executed: false,
};

env.storage().instance().set(&AdminKey::Proposal(id), &proposal);
env.storage()
.instance()
.set(&AdminKey::Proposal(id), &proposal);
id
}

Expand All @@ -169,7 +193,10 @@ pub fn approve_proposal(env: &Env, admin: Address, proposal_id: u64) {
panic!("only admins can approve proposals");
}

let mut proposal: Proposal = env.storage().instance().get(&AdminKey::Proposal(proposal_id))
let mut proposal: Proposal = env
.storage()
.instance()
.get(&AdminKey::Proposal(proposal_id))
.expect("proposal not found");

if proposal.executed {
Expand All @@ -180,30 +207,40 @@ pub fn approve_proposal(env: &Env, admin: Address, proposal_id: u64) {
}

proposal.approvals.push_back(admin);
env.storage().instance().set(&AdminKey::Proposal(proposal_id), &proposal);
env.storage()
.instance()
.set(&AdminKey::Proposal(proposal_id), &proposal);
}

/// Checks if a proposal has met its quorum threshold.
pub fn is_proposal_ready(env: &Env, proposal_id: u64) -> bool {
let proposal: Proposal = env.storage().instance().get(&AdminKey::Proposal(proposal_id))
let proposal: Proposal = env
.storage()
.instance()
.get(&AdminKey::Proposal(proposal_id))
.expect("proposal not found");
proposal.approvals.len() >= get_threshold(env)
}

/// Marks a proposal as executed. Useful for preventing re-execution.
pub fn mark_executed(env: &Env, proposal_id: u64) {
let mut proposal: Proposal = env.storage().instance().get(&AdminKey::Proposal(proposal_id))
let mut proposal: Proposal = env
.storage()
.instance()
.get(&AdminKey::Proposal(proposal_id))
.expect("proposal not found");

if proposal.executed {
panic!("already executed");
}
if !is_proposal_ready(env, proposal_id) {
panic!("threshold not met");
}

proposal.executed = true;
env.storage().instance().set(&AdminKey::Proposal(proposal_id), &proposal);
env.storage()
.instance()
.set(&AdminKey::Proposal(proposal_id), &proposal);
}

#[cfg(test)]
Expand Down Expand Up @@ -252,15 +289,18 @@ mod tests {
let admin1 = Address::generate(&env);
let admin2 = Address::generate(&env);
let admin3 = Address::generate(&env);

let contract_id = env.register(AdminContract, ());
let client = AdminContractClient::new(&env, &contract_id);

client.set_pool(&vec![&env, admin1.clone(), admin2.clone(), admin3.clone()], 2);


client.set_pool(
&vec![&env, admin1.clone(), admin2.clone(), admin3.clone()],
2,
);

let id = client.propose(&admin1, &String::from_str(&env, "test"));
assert!(!client.ready(&id));

client.approve(&admin2, &id);
assert!(client.ready(&id));
}
Expand Down
12 changes: 5 additions & 7 deletions contracts/token/src/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
//! Structured event emission for all token contract operations.
//! Events are emitted to the ledger for indexing by off-chain services.

use soroban_sdk::{symbol_short, Address, BytesN, Env, String};
use soroban_sdk::{symbol_short, Address, BytesN, Env, String, Symbol};

/// Emitted when the token contract is initialized.
pub fn emit_initialized(env: &Env, admin: &Address, decimals: u32, name: &String, symbol: &String) {
Expand Down Expand Up @@ -90,15 +90,15 @@ pub fn emit_ownership_proposed(env: &Env, old_admin: &Address, pending_admin: &A
/// Emitted when pending admin accepts ownership.
pub fn emit_ownership_accepted(env: &Env, old_admin: &Address, new_admin: &Address) {
env.events().publish(
(symbol_short!("own_accept"),),
(Symbol::new(env, "own_accept"),),
(old_admin.clone(), new_admin.clone()),
);
}

/// Emitted when ownership transfer is cancelled.
pub fn emit_ownership_cancelled(env: &Env, admin: &Address, cancelled_admin: &Address) {
env.events().publish(
(symbol_short!("own_cancel"),),
(Symbol::new(env, "own_cancel"),),
(admin.clone(), cancelled_admin.clone()),
);
}
Expand Down Expand Up @@ -133,10 +133,8 @@ pub fn emit_locked(env: &Env, user: &Address, amount: i128, unlock_time: u64) {

/// Emitted when locked tokens are withdrawn.
pub fn emit_withdraw_locked(env: &Env, user: &Address, amount: i128) {
env.events().publish(
(symbol_short!("unlock"),),
(user.clone(), amount),
);
env.events()
.publish((symbol_short!("unlock"),), (user.clone(), amount));
}

/// Emitted when the contract is upgraded.
Expand Down
Loading
Loading