@@ -20,28 +20,7 @@ mod test;
2020mod proptest;
2121
2222use soroban_sdk:: token:: TokenInterface ;
23- use soroban_sdk:: {
24- contract, contracterror, contractimpl, contracttype, Address , BytesN , Env , String ,
25- } ;
26-
27- /// Errors returned by the token contract.
28- #[ contracterror]
29- #[ derive( Copy , Clone , Debug , Eq , PartialEq , PartialOrd , Ord ) ]
30- #[ repr( u32 ) ]
31- pub enum TokenError {
32- /// The contract was initialized more than once.
33- AlreadyInitialized = 1 ,
34- /// The contract has not been initialized yet.
35- NotInitialized = 2 ,
36- /// The source account does not have enough tokens.
37- InsufficientBalance = 3 ,
38- /// The approved allowance is too small for the requested action.
39- InsufficientAllowance = 4 ,
40- /// The provided amount is invalid for this operation.
41- InvalidAmount = 5 ,
42- /// The contract is currently paused.
43- ContractPaused = 6 ,
44- }
23+ use soroban_sdk:: { contract, contractimpl, contracttype, vec, Address , Env , String , Vec } ;
4524use soroban_sdk:: { contract, contractimpl, contracttype, Address , Env , String , Vec } ;
4625use soroban_sdk:: { contract, contractimpl, contracttype, Address , BytesN , Env , String } ;
4726use bc_forge_admin:: { self as admin, Role } ;
@@ -94,6 +73,14 @@ pub enum TokenAction {
9473 Unpause ,
9574}
9675
76+ /// Represents a mint recipient with address and amount.
77+ #[ derive( Clone ) ]
78+ #[ contracttype]
79+ pub struct Recipient {
80+ pub address : Address ,
81+ pub amount : i128 ,
82+ }
83+
9784// ─────────────────────────────────────────────────────────────────────────────
9885// Contract Definition
9986// ─────────────────────────────────────────────────────────────────────────────
@@ -409,6 +396,52 @@ impl BcForgeToken {
409396 events:: emit_clawback ( & env, & claw_admin, & from, & to, amount) ;
410397 }
411398
399+ /// Mints tokens to multiple recipients in a single transaction. Admin-only.
400+ ///
401+ /// # Arguments
402+ /// * `recipients` - Vector of (address, amount) pairs.
403+ ///
404+ /// # Panics
405+ /// Panics if caller is not admin, contract is paused, any amount is non-positive,
406+ /// or if the recipients list is empty.
407+ ///
408+ /// # Note
409+ /// All mints are atomic - if any recipient has an invalid amount, the entire batch reverts.
410+ pub fn batch_mint ( env : Env , recipients : Vec < Recipient > ) {
411+ bc_forge_lifecycle:: require_not_paused ( & env) ;
412+
413+ let admin = Self :: read_admin ( & env) ;
414+ admin. require_auth ( ) ;
415+
416+ if recipients. is_empty ( ) {
417+ panic ! ( "recipients list cannot be empty" ) ;
418+ }
419+
420+ // First pass: validate all amounts are positive
421+ for i in 0 ..recipients. len ( ) {
422+ let recipient = recipients. get ( i) . expect ( "recipient should exist" ) ;
423+ if recipient. amount <= 0 {
424+ panic ! ( "mint amount must be positive for all recipients" ) ;
425+ }
426+ }
427+
428+ // Second pass: perform all mints and calculate total
429+ let mut total_minted: i128 = 0 ;
430+ for i in 0 ..recipients. len ( ) {
431+ let recipient = recipients. get ( i) . expect ( "recipient should exist" ) ;
432+ let balance = Self :: read_balance ( & env, & recipient. address ) + recipient. amount ;
433+ Self :: write_balance ( & env, & recipient. address , balance) ;
434+ total_minted += recipient. amount ;
435+
436+ // Emit individual mint event per recipient
437+ events:: emit_mint ( & env, & admin, & recipient. address , recipient. amount , balance, Self :: read_supply ( & env) + total_minted) ;
438+ }
439+
440+ // Update total supply atomically once at the end
441+ let new_supply = Self :: read_supply ( & env) + total_minted;
442+ Self :: write_supply ( & env, new_supply) ;
443+ }
444+
412445 /// Transfers the admin role to a new address. Current admin-only.
413446 ///
414447 /// ⚠️ DEPRECATED: Use propose_owner() + accept_ownership() for safer two-step transfer.
@@ -525,6 +558,61 @@ impl BcForgeToken {
525558 Self :: read_pending_admin ( & env)
526559 }
527560
561+ /// Proposes a new admin for two-step ownership transfer. Current admin-only.
562+ ///
563+ /// # Arguments
564+ /// * `new_admin` - The address to propose as the new admin.
565+ ///
566+ /// # Panics
567+ /// Panics if caller is not the current admin.
568+ pub fn propose_owner ( env : Env , new_admin : Address ) {
569+ let admin = Self :: read_admin ( & env) ;
570+ admin. require_auth ( ) ;
571+
572+ env. storage ( ) . instance ( ) . set ( & DataKey :: PendingAdmin , & new_admin) ;
573+ events:: emit_ownership_proposed ( & env, & admin, & new_admin) ;
574+ }
575+
576+ /// Accepts pending ownership transfer. Only the pending admin can call this.
577+ ///
578+ /// # Panics
579+ /// Panics if there is no pending admin or if caller is not the pending admin.
580+ pub fn accept_ownership ( env : Env ) {
581+ let pending_admin = Self :: read_pending_admin ( & env)
582+ . expect ( "no pending ownership transfer" ) ;
583+
584+ pending_admin. require_auth ( ) ;
585+
586+ let old_admin = Self :: read_admin ( & env) ;
587+ env. storage ( ) . instance ( ) . set ( & DataKey :: Admin , & pending_admin) ;
588+ env. storage ( ) . instance ( ) . remove ( & DataKey :: PendingAdmin ) ;
589+
590+ events:: emit_ownership_accepted ( & env, & old_admin, & pending_admin) ;
591+ }
592+
593+ /// Cancels a pending ownership transfer. Current admin-only.
594+ ///
595+ /// # Panics
596+ /// Panics if caller is not the current admin or if there is no pending transfer.
597+ pub fn cancel_transfer ( env : Env ) {
598+ let admin = Self :: read_admin ( & env) ;
599+ admin. require_auth ( ) ;
600+
601+ let pending_admin = Self :: read_pending_admin ( & env)
602+ . expect ( "no pending ownership transfer" ) ;
603+
604+ env. storage ( ) . instance ( ) . remove ( & DataKey :: PendingAdmin ) ;
605+ events:: emit_ownership_cancelled ( & env, & admin, & pending_admin) ;
606+ }
607+
608+ /// Returns the pending admin address if there is a pending transfer.
609+ ///
610+ /// # Returns
611+ /// Some(Address) if there is a pending admin, None otherwise.
612+ pub fn pending_owner ( env : Env ) -> Option < Address > {
613+ Self :: read_pending_admin ( & env)
614+ }
615+
528616 /// Returns the total token supply.
529617 pub fn supply ( env : Env ) -> i128 {
530618 Self :: panic_on_err ( & env, Self :: ensure_initialized ( & env) ) ;
@@ -677,8 +765,6 @@ impl TokenInterface for BcForgeToken {
677765
678766 Self :: move_balance ( & env, & from, & to, amount) ;
679767 Self :: write_allowance ( & env, & from, & spender, allowance - amount, 0 ) ; // Keep original expiration
680- let _ = Self :: panic_on_err ( & env, Self :: move_balance ( & env, & from, & to, amount) ) ;
681- Self :: write_allowance ( & env, & from, & spender, allowance - amount) ;
682768 events:: emit_transfer_from ( & env, & spender, & from, & to, amount, allowance - amount) ;
683769 }
684770
0 commit comments