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
111 changes: 100 additions & 11 deletions contracts/admin/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,16 @@ pub enum AdminKey {
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
#[contracttype]
pub enum Role {
/// Global administrator with full control.
Admin = 0,
Minter = 1,
}

/// Enumeration of available roles.
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
#[contracttype]
pub enum Role {
/// Global administrator with full control.
Admin,
Minter,
}
Expand Down Expand Up @@ -93,6 +103,14 @@ pub fn revoke_role(env: &Env, role: Role, address: &Address) {
}

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()))
{
return true;
}
env.storage()
.persistent()
.has(&AdminKey::Role(Role::Admin, address.clone()))
Expand All @@ -101,17 +119,7 @@ pub fn has_role(env: &Env, role: Role, address: &Address) -> bool {
.persistent()
.has(&AdminKey::Role(role, address.clone()))
}

pub fn require_admin(env: &Env) {
get_admin(env).require_auth();
}

pub fn require_role(env: &Env, role: Role, address: &Address) {
if !has_role(env, role, address) {
panic!("unauthorized: missing role");
}
address.require_auth();
}
// ─── Multi-Sig Primitives ───────────────────────────────────────────────────

pub fn set_admin_pool(env: &Env, pool: Vec<Address>, threshold: u32) {
if threshold == 0 || threshold > pool.len() {
Expand All @@ -138,6 +146,24 @@ pub fn get_threshold(env: &Env) -> u32 {
env.storage().instance().get(&AdminKey::Threshold).unwrap_or(1)
}

// ─── Guards ──────────────────────────────────────────────────────────────────

/// Requires that the stored admin has authorized the current invocation.
pub fn require_admin(env: &Env) {
let admin = get_admin(env);
admin.require_auth();
}

/// Requires that the specified address has the given role and has authorized the invocation.
pub fn require_role(env: &Env, role: Role, address: &Address) {
if !has_role(env, role, address) {
panic!("unauthorized: missing role");
}
address.require_auth();
}
// ─── Proposals ──────────────────────────────────────────────────────────────

/// Creates a new proposal for an administrative action.
pub fn create_proposal(env: &Env, creator: Address, description: String) -> u64 {
creator.require_auth();
let pool = get_admin_pool(env);
Expand Down Expand Up @@ -269,3 +295,66 @@ mod tests {
assert!(client.has_role(&Role::Minter, &role_holder));
}
}

#[cfg(test)]
mod tests {
use super::*;
use soroban_sdk::testutils::Address as _;
use soroban_sdk::{contract, contractimpl};

#[contract]
struct AdminContract;

#[contractimpl]
impl AdminContract {
pub fn set(env: Env, admin: Address) {
set_admin(&env, &admin);
}
pub fn set_pool(env: Env, admins: Vec<Address>, threshold: u32) {
set_admin_pool(&env, admins, threshold);
}
pub fn propose(env: Env, creator: Address, desc: String) -> u64 {
create_proposal(&env, creator, desc)
}
pub fn approve(env: Env, admin: Address, id: u64) {
approve_proposal(&env, admin, id);
}
pub fn ready(env: Env, id: u64) -> bool {
is_proposal_ready(&env, id)
}
}

#[test]
fn test_set_and_get_admin() {
let env = Env::default();
env.mock_all_auths();
let admin = Address::generate(&env);
let contract_id = env.register(AdminContract, ());
let client = AdminContractClient::new(&env, &contract_id);

client.set(&admin);
}

#[test]
fn test_multi_sig() {
let env = Env::default();
env.mock_all_auths();
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,
);

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

client.approve(&admin2, &id);
assert!(client.ready(&id));
}
}
27 changes: 26 additions & 1 deletion contracts/token/src/events.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! Structured event emission for the token contract.

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

pub fn emit_initialized(env: &Env, admin: &Address, decimals: u32, name: &String, symbol: &String) {
env.events().publish(
Expand Down Expand Up @@ -69,6 +69,31 @@ pub fn emit_ownership_transferred(env: &Env, old_admin: &Address, new_admin: &Ad
);
}

/// Emitted when a new admin is proposed (two-step transfer).
pub fn emit_ownership_proposed(env: &Env, old_admin: &Address, pending_admin: &Address) {
env.events().publish(
(symbol_short!("own_prop"),),
(old_admin.clone(), pending_admin.clone()),
);
}

/// Emitted when pending admin accepts ownership.
pub fn emit_ownership_accepted(env: &Env, old_admin: &Address, new_admin: &Address) {
env.events().publish(
(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::new(env, "own_cancel"),),
(admin.clone(), cancelled_admin.clone()),
);
}

/// Emitted when the contract is paused.
pub fn emit_paused(env: &Env, admin: &Address) {
env.events()
.publish((symbol_short!("paused"),), (admin.clone(),));
Expand Down
Loading