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
1 change: 0 additions & 1 deletion contracts/admin/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,6 @@ pub fn create_proposal(env: &Env, creator: Address, description: String) -> u64

let proposal = Proposal {
creator: creator.clone(),
action_type,
description,
approvals: vec![env, creator],
executed: false,
Expand Down
22 changes: 22 additions & 0 deletions contracts/freeze/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
[package]
name = "bc-forge-freeze"
version = "0.1.0"
edition = "2021"
publish = false
description = "Freeze blacklist primitives for bc-forge token contracts"
repository = "https://github.com/BCPathway/bc-forge"
license = "MIT"
keywords = ["soroban", "freeze", "token", "control"]
categories = ["cryptography::cryptocurrencies"]

[lib]
crate-type = ["rlib"]

[dependencies]
soroban-sdk = "22.0.0"

[dev-dependencies]
soroban-sdk = { version = "22.0.0", features = ["testutils"] }

[features]
testutils = ["soroban-sdk/testutils"]
157 changes: 157 additions & 0 deletions contracts/freeze/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
#![no_std]

use soroban_sdk::{contracttype, symbol_short, Address, Env};

#[derive(Clone)]
#[contracttype]
pub enum FreezeKey {
Address(Address),
All,
}

pub fn freeze_address(env: &Env, admin: Address, address: Address) {
admin.require_auth();
env.storage()
.persistent()
.set(&FreezeKey::Address(address.clone()), &true);
emit_address_frozen(env, &admin, &address);
}

pub fn unfreeze_address(env: &Env, admin: Address, address: Address) {
admin.require_auth();
env.storage()
.persistent()
.remove(&FreezeKey::Address(address.clone()));
emit_address_unfrozen(env, &admin, &address);
}

pub fn is_address_frozen(env: &Env, address: &Address) -> bool {
env.storage()
.persistent()
.get(&FreezeKey::Address(address.clone()))
.unwrap_or(false)
}

pub fn freeze_all(env: &Env, admin: Address) {
admin.require_auth();
env.storage().persistent().set(&FreezeKey::All, &true);
emit_global_frozen(env, &admin);
}

pub fn unfreeze_all(env: &Env, admin: Address) {
admin.require_auth();
env.storage().persistent().remove(&FreezeKey::All);
emit_global_unfrozen(env, &admin);
}

pub fn is_all_frozen(env: &Env) -> bool {
env.storage()
.persistent()
.get(&FreezeKey::All)
.unwrap_or(false)
}

pub fn is_frozen(env: &Env, address: &Address) -> bool {
if is_all_frozen(env) {
true
} else {
is_address_frozen(env, address)
}
}

fn emit_address_frozen(env: &Env, admin: &Address, address: &Address) {
env.events().publish(
(symbol_short!("frze"),),
(admin.clone(), address.clone()),
);
}

fn emit_address_unfrozen(env: &Env, admin: &Address, address: &Address) {
env.events().publish(
(symbol_short!("unfr"),),
(admin.clone(), address.clone()),
);
}

fn emit_global_frozen(env: &Env, admin: &Address) {
env.events().publish((symbol_short!("glfz"),), (admin.clone(),));
}

fn emit_global_unfrozen(env: &Env, admin: &Address) {
env.events().publish((symbol_short!("gufz"),), (admin.clone(),));
}

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

#[contract]
struct FreezeContract;

#[contractimpl]
impl FreezeContract {
pub fn freeze_address(env: Env, admin: Address, address: Address) {
super::freeze_address(&env, admin, address);
}

pub fn unfreeze_address(env: Env, admin: Address, address: Address) {
super::unfreeze_address(&env, admin, address);
}

pub fn is_address_frozen(env: Env, address: Address) -> bool {
super::is_address_frozen(&env, &address)
}

pub fn freeze_all(env: Env, admin: Address) {
super::freeze_all(&env, admin);
}

pub fn unfreeze_all(env: Env, admin: Address) {
super::unfreeze_all(&env, admin);
}

pub fn is_all_frozen(env: Env) -> bool {
super::is_all_frozen(&env)
}

pub fn is_frozen(env: Env, address: Address) -> bool {
super::is_frozen(&env, &address)
}
}

#[test]
fn test_freeze_and_unfreeze_address() {
let env = Env::default();
env.mock_all_auths();
let contract_id = env.register(FreezeContract, ());
let client = FreezeContractClient::new(&env, &contract_id);
let admin = Address::generate(&env);
let user = Address::generate(&env);

client.freeze_address(&admin, &user);
assert!(client.is_address_frozen(&user));

client.unfreeze_address(&admin, &user);
assert!(!client.is_address_frozen(&user));
}

#[test]
fn test_freeze_all_blocks_addresses() {
let env = Env::default();
env.mock_all_auths();
let contract_id = env.register(FreezeContract, ());
let client = FreezeContractClient::new(&env, &contract_id);
let admin = Address::generate(&env);
let user = Address::generate(&env);

client.freeze_all(&admin);
assert!(client.is_all_frozen());
assert!(client.is_frozen(&user));

client.unfreeze_all(&admin);
assert!(!client.is_all_frozen());
assert!(!client.is_frozen(&user));
}
}
1 change: 1 addition & 0 deletions contracts/token/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ crate-type = ["cdylib", "rlib"]
soroban-sdk = "22.0.0"
bc-forge-admin = { path = "../admin" }
bc-forge-lifecycle = { path = "../lifecycle" }
bc-forge-freeze = { path = "../freeze" }

[dev-dependencies]
soroban-sdk = { version = "22.0.0", features = ["testutils"] }
Expand Down
98 changes: 65 additions & 33 deletions contracts/token/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ mod events;
mod test;

use bc_forge_admin::{self as admin, Role};
use bc_forge_freeze as freeze;
use soroban_sdk::token::TokenInterface;
use soroban_sdk::{
contract, contracterror, contractimpl, contracttype, Address, BytesN, Env, String, Vec,
Expand All @@ -23,11 +24,9 @@ pub enum DataKey {
/// The contract admin address (singular).
Admin,
PendingAdmin,
/// Spending allowance: (owner, spender) → amount and expiration.
/// Stored allowance info for (owner, spender).
Allowance(Address, Address),
/// Token balance for an address.
Allowance(Address, Address),
AllowanceExp(Address, Address),
Balance(Address),
Name,
Symbol,
Expand Down Expand Up @@ -79,6 +78,8 @@ pub enum TokenError {
InsufficientBalance = 4,
InsufficientAllowance = 5,
ContractPaused = 6,
ContractFrozen = 7,
AddressFrozen = 8,
}

#[contract]
Expand Down Expand Up @@ -138,30 +139,12 @@ impl BcForgeToken {
.persistent()
.get(&DataKey::Allowance(from.clone(), spender.clone()))
.unwrap_or(AllowanceInfo { amount: 0, exp_ledger: 0 });

// Check if allowance has expired
if allowance_info.exp_ledger > 0 {
let current_ledger = env.ledger().sequence();
if current_ledger > allowance_info.exp_ledger as u64 {
return 0; // Allowance expired
}
}

allowance_info.amount
if let Some(exp_ledger) = env
.storage()
.persistent()
.get::<_, u32>(&DataKey::AllowanceExp(from.clone(), spender.clone()))
{
if exp_ledger > 0 && env.ledger().sequence() > exp_ledger {
return 0;
}

if allowance_info.exp_ledger > 0 && env.ledger().sequence() > allowance_info.exp_ledger {
return 0;
}

env.storage()
.persistent()
.get(&DataKey::Allowance(from.clone(), spender.clone()))
.unwrap_or(0)
allowance_info.amount
}

fn write_allowance(env: &Env, from: &Address, spender: &Address, amount: i128, exp: u32) {
Expand All @@ -177,10 +160,16 @@ impl BcForgeToken {
.persistent()
.get(&DataKey::Allowance(from.clone(), spender.clone()))
.unwrap_or(AllowanceInfo { amount: 0, exp_ledger: 0 })
.set(&DataKey::Allowance(from.clone(), spender.clone()), &amount);
env.storage()
.persistent()
.set(&DataKey::AllowanceExp(from.clone(), spender.clone()), &exp);
}

fn ensure_not_frozen(env: &Env, address: &Address) -> Result<(), TokenError> {
if freeze::is_all_frozen(env) {
return Err(TokenError::ContractFrozen);
}
if freeze::is_address_frozen(env, address) {
return Err(TokenError::AddressFrozen);
}
Ok(())
}

fn move_balance(
Expand Down Expand Up @@ -266,6 +255,7 @@ impl BcForgeToken {
Self::ensure_not_paused(&env)?;
let current_admin = Self::read_admin(&env)?;
current_admin.require_auth();
Self::ensure_not_frozen(&env, &to)?;
Self::internal_mint(&env, &current_admin, &to, amount)
}

Expand All @@ -280,6 +270,7 @@ impl BcForgeToken {
if recipient.amount <= 0 {
return Err(TokenError::InvalidAmount);
}
Self::ensure_not_frozen(&env, &recipient.address)?;
}

for i in 0..recipients.len() {
Expand All @@ -290,17 +281,56 @@ impl BcForgeToken {
Ok(())
}

pub fn freeze_address(env: Env, address: Address) -> Result<(), TokenError> {
Self::ensure_initialized(&env)?;
let current_admin = Self::read_admin(&env)?;
current_admin.require_auth();
freeze::freeze_address(&env, current_admin.clone(), address);
Ok(())
}

pub fn unfreeze_address(env: Env, address: Address) -> Result<(), TokenError> {
Self::ensure_initialized(&env)?;
let current_admin = Self::read_admin(&env)?;
current_admin.require_auth();
freeze::unfreeze_address(&env, current_admin.clone(), address);
Ok(())
}

pub fn freeze_all(env: Env) -> Result<(), TokenError> {
Self::ensure_initialized(&env)?;
let current_admin = Self::read_admin(&env)?;
current_admin.require_auth();
freeze::freeze_all(&env, current_admin.clone());
Ok(())
}

pub fn unfreeze_all(env: Env) -> Result<(), TokenError> {
Self::ensure_initialized(&env)?;
let current_admin = Self::read_admin(&env)?;
current_admin.require_auth();
freeze::unfreeze_all(&env, current_admin.clone());
Ok(())
}

pub fn is_frozen(env: Env, address: Address) -> bool {
Self::panic_on_err(&env, Self::ensure_initialized(&env));
freeze::is_frozen(&env, &address)
}

pub fn batch_transfer(env: Env, from: Address, recipients: Vec<(Address, i128)>) {
Self::panic_on_err(&env, Self::ensure_initialized(&env));
Self::panic_on_err(&env, Self::ensure_not_paused(&env));
from.require_auth();
Self::panic_on_err(&env, Self::ensure_not_frozen(&env, &from));

let mut total: i128 = 0;
for i in 0..recipients.len() {
let (_, amount) = recipients.get(i).expect("recipient should exist");
let (to, amount) = recipients.get(i).expect("recipient should exist");
if amount <= 0 {
soroban_sdk::panic_with_error!(&env, TokenError::InvalidAmount);
}
Self::panic_on_err(&env, Self::ensure_not_frozen(&env, &to));
total = match total.checked_add(amount) {
Some(total) => total,
None => soroban_sdk::panic_with_error!(&env, TokenError::InvalidAmount),
Expand Down Expand Up @@ -592,6 +622,8 @@ impl TokenInterface for BcForgeToken {
Self::panic_on_err(&env, Self::ensure_initialized(&env));
Self::panic_on_err(&env, Self::ensure_not_paused(&env));
from.require_auth();
Self::panic_on_err(&env, Self::ensure_not_frozen(&env, &from));
Self::panic_on_err(&env, Self::ensure_not_frozen(&env, &to));

if amount <= 0 {
soroban_sdk::panic_with_error!(&env, TokenError::InvalidAmount);
Expand All @@ -605,6 +637,9 @@ impl TokenInterface for BcForgeToken {
Self::panic_on_err(&env, Self::ensure_initialized(&env));
Self::panic_on_err(&env, Self::ensure_not_paused(&env));
spender.require_auth();
Self::panic_on_err(&env, Self::ensure_not_frozen(&env, &spender));
Self::panic_on_err(&env, Self::ensure_not_frozen(&env, &from));
Self::panic_on_err(&env, Self::ensure_not_frozen(&env, &to));

if amount <= 0 {
soroban_sdk::panic_with_error!(&env, TokenError::InvalidAmount);
Expand All @@ -615,12 +650,9 @@ impl TokenInterface for BcForgeToken {
soroban_sdk::panic_with_error!(&env, TokenError::InsufficientAllowance);
}

Self::move_balance(&env, &from, &to, amount);
// Preserve the original expiration
let allowance_info = Self::read_allowance_info(&env, &from, &spender);
Self::write_allowance(&env, &from, &spender, allowance - amount, allowance_info.exp_ledger);
let _ = Self::panic_on_err(&env, Self::move_balance(&env, &from, &to, amount));
Self::write_allowance(&env, &from, &spender, allowance - amount, 0);
events::emit_transfer_from(&env, &spender, &from, &to, amount, allowance - amount);
}

Expand Down
Loading