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
105 changes: 88 additions & 17 deletions contracts/admin/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,15 @@ pub enum AdminKey {
ProposalIdCounter,
}

/// Enumeration of available roles.
#[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]
Expand Down Expand Up @@ -73,6 +82,7 @@ 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()
Expand All @@ -84,23 +94,6 @@ pub fn has_role(env: &Env, role: Role, address: &Address) -> bool {
.persistent()
.has(&AdminKey::Role(role, address.clone()))
}

// ─── 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();
}

// ─── Multi-Sig Primitives ───────────────────────────────────────────────────

pub fn set_admin_pool(env: &Env, pool: Vec<Address>, threshold: u32) {
Expand Down Expand Up @@ -133,6 +126,21 @@ pub fn get_threshold(env: &Env) -> u32 {
.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.
Expand Down Expand Up @@ -220,3 +228,66 @@ pub fn mark_executed(env: &Env, proposal_id: u64) {
.instance()
.set(&AdminKey::Proposal(proposal_id), &proposal);
}

#[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));
}
}
6 changes: 3 additions & 3 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_acc"),),
(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_can"),),
(Symbol::new(env, "own_cancel"),),
(admin.clone(), cancelled_admin.clone()),
);
}
Expand Down
Loading