diff --git a/src/lib.rs b/src/lib.rs index 5f9e2a5..2bad7ad 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,7 +7,9 @@ pub mod escrow; pub mod event; pub mod events; pub mod fees; +pub mod mint; pub mod multisig; +pub mod schema; pub mod token; pub mod utils; diff --git a/src/mint.rs b/src/mint.rs new file mode 100644 index 0000000..fdddd9a --- /dev/null +++ b/src/mint.rs @@ -0,0 +1,31 @@ +use crate::schema::TokenClient; +use soroban_sdk::symbol_short; +use soroban_sdk::{contract, contractclient, contractimpl, Address, Env}; + +#[contract] +pub struct MintContract; + +#[contractimpl] +impl MintContract { + pub fn init(env: Env, backend: Address) { + backend.require_auth(); + // Store the admin (backend) + env.storage() + .persistent() + .set(&symbol_short!("admin"), &backend); + } + + // Only admin can mint + pub fn mint_token(env: Env, recipient: Address, amount: i128, token: Address) { + let admin: Address = env + .storage() + .persistent() + .get(&symbol_short!("admin")) + .expect("admin not set"); + admin.require_auth(); + + // Emit an event (for transparency) + env.events() + .publish((symbol_short!("mint"), recipient.clone()), amount); + } +} diff --git a/src/schema.rs b/src/schema.rs index 1ca52f7..939cad4 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -1,87 +1,42 @@ +use soroban_sdk::symbol_short; +use soroban_sdk::{contractclient, contracttype, Address, Env, Map, Symbol, Vec}; -use soroban_sdk::{contracttype, Address, Symbol, Vec, Map, Env}; +#[contractclient(name = "TokenClient")] +pub trait TokenTrait { + fn mint(env: Env, to: Address, amount: i128); +} + +#[contracttype] +pub enum Event { + FeeCollected(Address, i128), + OfferCreated(u64, Address, i128), + OfferAccepted(u64, Address), + OfferCancelled(u64), +} /// Represents a swap offer in the contract's storage #[contracttype] #[derive(Clone)] pub struct SwapOffer { - /// The account that created this swap offer pub creator: Address, - /// The token being offered pub offer_token: Address, - /// The amount of tokens being offered pub offer_amount: i128, - /// The token requested in exchange pub request_token: Address, - /// The amount of tokens requested pub request_amount: i128, - /// Timestamp when this offer expires pub expires_at: u64, } -/// Represents the configuration of the swap contract #[contracttype] #[derive(Clone)] pub struct SwapConfig { - /// Admin address that can configure fees pub admin: Address, - /// Fee percentage taken on each swap (basis points, e.g. 25 = 0.25%) pub fee_bps: u32, - /// Address where fees are collected pub fee_collector: Address, } -/// Events emitted by the swap contract -#[contracttype] -#[derive(Clone)] -pub enum SwapEvent { - /// Emitted when a new swap offer is created - OfferCreated { - /// Unique ID of the offer - offer_id: u64, - /// Address of the creator - creator: Address, - /// Token being offered - offer_token: Address, - /// Amount being offered - offer_amount: i128, - /// Token being requested - request_token: Address, - /// Amount being requested - request_amount: i128, - /// Timestamp when this expires - expires_at: u64, - }, - /// Emitted when a swap offer is accepted - OfferAccepted { - /// Unique ID of the offer - offer_id: u64, - /// Address of the acceptor - acceptor: Address, - }, - /// Emitted when a swap offer is cancelled - OfferCancelled { - /// Unique ID of the offer - offer_id: u64, - }, - /// Emitted when fees are collected - FeeCollected { - /// Token collected as fee - token: Address, - /// Amount collected as fee - amount: i128, - }, -} - -/// Interface definition for swap contract functions pub trait SwapTrait { - /// Initializes the swap contract with default settings fn initialize(env: Env, admin: Address) -> SwapConfig; - - /// Updates the fee configuration fn update_fee(env: Env, fee_bps: u32, fee_collector: Address) -> SwapConfig; - - /// Creates a new swap offer fn create_offer( env: Env, offer_token: Address, @@ -90,16 +45,9 @@ pub trait SwapTrait { request_amount: i128, expires_at: u64, ) -> u64; - - /// Accepts an existing swap offer + fn accept_offer(env: Env, offer_id: u64) -> bool; - - /// Cancels an existing swap offer fn cancel_offer(env: Env, offer_id: u64) -> bool; - - /// Gets details of an existing swap offer fn get_offer(env: Env, offer_id: u64) -> SwapOffer; - - /// Gets the current contract configuration fn get_config(env: Env) -> SwapConfig; -} \ No newline at end of file +} diff --git a/test_snapshots/test_non_admin_cannot_mint.1.json b/test_snapshots/test_non_admin_cannot_mint.1.json new file mode 100644 index 0000000..64715dd --- /dev/null +++ b/test_snapshots/test_non_admin_cannot_mint.1.json @@ -0,0 +1,109 @@ +{ + "generators": { + "address": 6, + "nonce": 1 + }, + "auth": [ + [], + [], + [] + ], + "ledger": { + "protocol_version": 22, + "sequence_number": 0, + "timestamp": 0, + "network_id": "0000000000000000000000000000000000000000000000000000000000000000", + "base_reserve": 0, + "min_persistent_entry_ttl": 4096, + "min_temp_entry_ttl": 16, + "max_entry_ttl": 6312000, + "ledger_entries": [ + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": "ledger_key_contract_instance", + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4", + "key": "ledger_key_contract_instance", + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_code": { + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_code": { + "ext": "v0", + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "code": "" + } + }, + "ext": "v0" + }, + 4095 + ] + ] + ] + }, + "events": [] +} \ No newline at end of file diff --git a/test_snapshots/test_successful_minting_by_admin.1.json b/test_snapshots/test_successful_minting_by_admin.1.json new file mode 100644 index 0000000..b0b3805 --- /dev/null +++ b/test_snapshots/test_successful_minting_by_admin.1.json @@ -0,0 +1,246 @@ +{ + "generators": { + "address": 4, + "nonce": 0 + }, + "auth": [ + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "function_name": "init", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "function_name": "mint_token", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + }, + { + "i128": { + "hi": 0, + "lo": 1000 + } + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + ] + } + }, + "sub_invocations": [] + } + ] + ] + ], + "ledger": { + "protocol_version": 22, + "sequence_number": 0, + "timestamp": 0, + "network_id": "0000000000000000000000000000000000000000000000000000000000000000", + "base_reserve": 0, + "min_persistent_entry_ttl": 4096, + "min_temp_entry_ttl": 16, + "max_entry_ttl": 6312000, + "ledger_entries": [ + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 801925984706572462 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 801925984706572462 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 6311999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 5541220902715666415 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "ledger_key_nonce": { + "nonce": 5541220902715666415 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 6311999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": { + "symbol": "admin" + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": { + "symbol": "admin" + }, + "durability": "persistent", + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": "ledger_key_contract_instance", + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_code": { + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_code": { + "ext": "v0", + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "code": "" + } + }, + "ext": "v0" + }, + 4095 + ] + ] + ] + }, + "events": [ + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000004", + "type_": "contract", + "body": { + "v0": { + "topics": [ + { + "symbol": "mint" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ], + "data": { + "i128": { + "hi": 0, + "lo": 1000 + } + } + } + } + }, + "failed_call": false + } + ] +} \ No newline at end of file diff --git a/tests/mint_tests.rs b/tests/mint_tests.rs new file mode 100644 index 0000000..35e2cf8 --- /dev/null +++ b/tests/mint_tests.rs @@ -0,0 +1,55 @@ +use soroban_sdk::testutils::MockAuth; +use soroban_sdk::testutils::MockAuthInvoke; +use soroban_sdk::{symbol_short, testutils::Address as _, Address, Env, IntoVal}; +use stellar_multisig_contract::{mint::MintContract, mint::MintContractClient}; + +#[test] +fn test_successful_minting_by_admin() { + let env = Env::default(); + let admin = Address::generate(&env); + let user = Address::generate(&env); + let token = Address::generate(&env); + + // Dummy token contract (mocked) + struct DummyToken; + impl DummyToken { + pub fn mint(env: &Env, to: &Address, amount: &i128) { + env.events() + .publish((symbol_short!("minted"), to.clone()), amount.clone()); + } + } + + let mint_contract_id = env.register_contract(None, MintContract); + env.mock_all_auths(); + + let client = MintContractClient::new(&env, &mint_contract_id); + client.init(&admin); + client.mint_token(&user, &1000, &token); +} + +#[test] +#[should_panic] +fn test_non_admin_cannot_mint() { + let env = Env::default(); + let admin = Address::generate(&env); + let backend = Address::generate(&env); + let attacker = Address::generate(&env); + let token = Address::generate(&env); + let user = Address::generate(&env); + + let contract_id = env.register_contract(None, MintContract); + let client = MintContractClient::new(&env, &contract_id); + + env.mock_auths(&[MockAuth { + address: &admin, + invoke: &MockAuthInvoke { + contract: &contract_id, + fn_name: "mint_token", + args: (&user, &1000i128, &token).into_val(&env), + sub_invokes: &[], + }, + }]); + + client.init(&backend); + client.mint_token(&attacker, &500, &token); +}