Skip to content
Merged
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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
52 changes: 52 additions & 0 deletions contracts/ai_nft/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ const NFT_COUNTER: Symbol = symbol_short!("NFT_CNT");
const NFT_OWNERS: Symbol = symbol_short!("OWNERS");
const NFT_METADATA: Symbol = symbol_short!("METADATA");
const MINTER_REGISTRY: Symbol = symbol_short!("MINTER");
// Pausable extension (SC-11)
const PAUSED: Symbol = symbol_short!("PAUSED");

// Contract errors
#[contracterror]
Expand All @@ -33,6 +35,8 @@ pub enum ContractError {
AlreadyTransferred = 4,
InvalidOwner = 5,
MinterMismatch = 6,
/// Contract is paused for emergency halt (SC-11)
ContractPaused = 7,
}

#[contract]
Expand All @@ -55,13 +59,60 @@ impl AINFTContract {
env.storage().instance().get(&ADMIN).expect("Admin not set")
}

// ── Pausable extension (SC-11) ────────────────────────────────────────────

/// Pause the contract — blocks all state-mutating operations.
/// Only the contract admin may call this.
pub fn pause(env: Env, caller: Address) {
caller.require_auth();
let admin: Address = env.storage().instance().get(&ADMIN).expect("Admin not set");
if caller != admin {
panic!("Not admin");
}
if env.storage().instance().get(&PAUSED).unwrap_or(false) {
panic!("Already paused");
}
env.storage().instance().set(&PAUSED, &true);
env.events()
.publish((symbol_short!("paused"),), caller);
}

/// Unpause the contract — resumes normal operations.
/// Only the contract admin may call this.
pub fn unpause(env: Env, caller: Address) {
caller.require_auth();
let admin: Address = env.storage().instance().get(&ADMIN).expect("Admin not set");
if caller != admin {
panic!("Not admin");
}
if !env.storage().instance().get(&PAUSED).unwrap_or(false) {
panic!("Not paused");
}
env.storage().instance().set(&PAUSED, &false);
env.events()
.publish((symbol_short!("unpaused"),), caller);
}

/// Returns `true` if the contract is currently paused.
pub fn is_paused(env: Env) -> bool {
env.storage().instance().get(&PAUSED).unwrap_or(false)
}

/// Internal helper — panics with "Contract is paused" when the contract is paused.
fn check_not_paused(env: &Env) {
if env.storage().instance().get(&PAUSED).unwrap_or(false) {
panic!("Contract is paused");
}
}

/// Mint a new AI NFT with metadata hash
pub fn mint(
env: Env,
minter: Address,
metadata_hash: BytesN<32>,
personality_traits: String,
) -> u64 {
Self::check_not_paused(&env);
let admin = Self::admin(env.clone());
admin.require_auth();
minter.require_auth();
Expand Down Expand Up @@ -115,6 +166,7 @@ impl AINFTContract {

/// Transfer NFT from current owner to a new owner
pub fn transfer(env: Env, nft_id: u64, to: Address) -> Result<(), ContractError> {
Self::check_not_paused(&env);
let mut owners: Map<u64, Address> = env
.storage()
.instance()
Expand Down
84 changes: 84 additions & 0 deletions contracts/game_contract/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,9 @@ const ORACLE_CONTRACT: Symbol = symbol_short!("ORACLE"); // Address of oracle co
const TOURNAMENT_TIMELOCK: Symbol = symbol_short!("TL_DUR"); // u64 - lock duration in ledger sequences
const TOURNAMENT_ESCROWS: Symbol = symbol_short!("TL_ESC"); // Map<u64, TournamentEscrow>

// Pausable extension (SC-11)
const PAUSED: Symbol = symbol_short!("PAUSED"); // bool - whether contract is paused

// ────────────────────────────────────────────────────────────────────────────
// Multi-sig fee proposal type (#535)
// ────────────────────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -237,6 +240,8 @@ pub enum ContractError {
EmptyBatch = 37,
/// claim_puzzle_rewards_batch called with more proofs than MAX_BATCH_SIZE
BatchTooLarge = 38,
/// Contract is paused for emergency halt (SC-11)
ContractPaused = 39,
}

#[contract]
Expand Down Expand Up @@ -265,6 +270,60 @@ impl GameContract {
TokenClient::new(env, &Self::token_contract_address(env))
}

// ── Pausable extension (SC-11) ────────────────────────────────────────────

/// Pause the contract — blocks all state-mutating operations.
/// Only the contract admin may call this.
pub fn pause(env: Env, caller: Address) {
caller.require_auth();
let admin: Address = env
.storage()
.instance()
.get(&CONTRACT_ADMIN)
.expect("Not initialized");
if caller != admin {
panic!("Not admin");
}
if env.storage().instance().get(&PAUSED).unwrap_or(false) {
panic!("Already paused");
}
env.storage().instance().set(&PAUSED, &true);
env.events()
.publish((symbol_short!("paused"),), caller);
}

/// Unpause the contract — resumes normal operations.
/// Only the contract admin may call this.
pub fn unpause(env: Env, caller: Address) {
caller.require_auth();
let admin: Address = env
.storage()
.instance()
.get(&CONTRACT_ADMIN)
.expect("Not initialized");
if caller != admin {
panic!("Not admin");
}
if !env.storage().instance().get(&PAUSED).unwrap_or(false) {
panic!("Not paused");
}
env.storage().instance().set(&PAUSED, &false);
env.events()
.publish((symbol_short!("unpaused"),), caller);
}

/// Returns `true` if the contract is currently paused.
pub fn is_paused(env: Env) -> bool {
env.storage().instance().get(&PAUSED).unwrap_or(false)
}

/// Internal helper — panics with "Contract is paused" when the contract is paused.
fn check_not_paused(env: &Env) {
if env.storage().instance().get(&PAUSED).unwrap_or(false) {
panic!("Contract is paused");
}
}

/// Gas-optimized tournament payout — single pass, no redundant map reads.
/// Validates percentages and distributes atomically.
pub fn payout_tournament_optimized(
Expand All @@ -273,6 +332,7 @@ impl GameContract {
winners: Vec<Address>,
percentages: Vec<u32>,
) -> Result<(), ContractError> {
Self::check_not_paused(&env);
let mut games: Map<u64, Game> = env
.storage()
.instance()
Expand Down Expand Up @@ -346,6 +406,7 @@ impl GameContract {
player1: Address,
wager_amount: i128,
) -> Result<u64, ContractError> {
Self::check_not_paused(&env);
let max_stake: i128 = env.storage().instance().get(&MAX_STAKE).unwrap_or(1_000);
if wager_amount > max_stake {
return Err(ContractError::StakeLimitExceeded);
Expand Down Expand Up @@ -412,6 +473,7 @@ impl GameContract {
}

pub fn join_game(env: Env, game_id: u64, player2: Address) -> Result<(), ContractError> {
Self::check_not_paused(&env);
let mut games: Map<u64, Game> = env
.storage()
.instance()
Expand Down Expand Up @@ -484,6 +546,7 @@ impl GameContract {
player: Address,
move_data: Vec<u32>,
) -> Result<(), ContractError> {
Self::check_not_paused(&env);
let mut games: Map<u64, Game> = env
.storage()
.instance()
Expand Down Expand Up @@ -535,6 +598,7 @@ impl GameContract {
player: Address,
signature: BytesN<64>,
) -> Result<(), ContractError> {
Self::check_not_paused(&env);
let mut games: Map<u64, Game> = env
.storage()
.instance()
Expand Down Expand Up @@ -587,6 +651,7 @@ impl GameContract {
winner: Address,
signature: BytesN<64>,
) -> Result<(), ContractError> {
Self::check_not_paused(&env);
let mut games: Map<u64, Game> = env
.storage()
.instance()
Expand Down Expand Up @@ -640,6 +705,7 @@ impl GameContract {
}

pub fn cancel_game(env: Env, game_id: u64, player: Address) -> Result<(), ContractError> {
Self::check_not_paused(&env);
let mut games: Map<u64, Game> = env
.storage()
.instance()
Expand Down Expand Up @@ -681,6 +747,7 @@ impl GameContract {
}

pub fn forfeit(env: Env, game_id: u64, player: Address) -> Result<(), ContractError> {
Self::check_not_paused(&env);
let mut games: Map<u64, Game> = env
.storage()
.instance()
Expand Down Expand Up @@ -718,6 +785,7 @@ impl GameContract {
}

pub fn payout(env: Env, game_id: u64, winner: Address) -> Result<(), ContractError> {
Self::check_not_paused(&env);
let mut games: Map<u64, Game> = env
.storage()
.instance()
Expand Down Expand Up @@ -753,6 +821,7 @@ impl GameContract {
winners: Vec<Address>,
percentages: Vec<u32>,
) -> Result<(), ContractError> {
Self::check_not_paused(&env);
let mut games: Map<u64, Game> = env
.storage()
.instance()
Expand Down Expand Up @@ -1076,6 +1145,7 @@ impl GameContract {
nonce: u64,
signature: BytesN<64>,
) -> Result<(), ContractError> {
Self::check_not_paused(&env);
if reward_amount <= 0 || reward_amount > i64::MAX as i128 {
return Err(ContractError::InvalidAmount);
}
Expand Down Expand Up @@ -1189,6 +1259,7 @@ impl GameContract {
// • All proofs valid → every recipient balance incremented,
// treasury decremented by the sum, in one TX
pub fn claim_puzzle_rewards_batch(env: Env, proofs: Vec<Proof>) -> Result<(), ContractError> {
Self::check_not_paused(&env);
if proofs.is_empty() {
return Err(ContractError::EmptyBatch);
}
Expand Down Expand Up @@ -1360,6 +1431,7 @@ impl GameContract {
against: Address,
reason: Bytes,
) -> Result<u64, ContractError> {
Self::check_not_paused(&env);
let games: Map<u64, Game> = env
.storage()
.instance()
Expand Down Expand Up @@ -1437,6 +1509,7 @@ impl GameContract {
game_id: u64,
claimant: Address,
) -> Result<(), ContractError> {
Self::check_not_paused(&env);
let mut games: Map<u64, Game> = env
.storage()
.instance()
Expand Down Expand Up @@ -1523,6 +1596,7 @@ impl GameContract {
winner: Option<Address>,
resolution: Bytes,
) -> Result<(), ContractError> {
Self::check_not_paused(&env);
let stored_arbitrator: Address = env
.storage()
.instance()
Expand Down Expand Up @@ -1601,6 +1675,7 @@ impl GameContract {
arbitrator: Address,
reason: Bytes,
) -> Result<(), ContractError> {
Self::check_not_paused(&env);
// Verify arbitrator
let stored_arbitrator: Address = env
.storage()
Expand Down Expand Up @@ -1690,6 +1765,7 @@ impl GameContract {
nonce: BytesN<32>,
expiry: u64,
) -> Result<(), ContractError> {
Self::check_not_paused(&env);
let current_admin: Address = env
.storage()
.instance()
Expand Down Expand Up @@ -1739,6 +1815,7 @@ impl GameContract {
nonce: BytesN<32>,
signature: BytesN<64>,
) -> Result<(), ContractError> {
Self::check_not_paused(&env);
// 1. Load and validate the challenge
let mut challenges: Map<BytesN<32>, u64> = env
.storage()
Expand Down Expand Up @@ -1838,6 +1915,7 @@ impl GameContract {
signers: Vec<Address>,
threshold: u32,
) -> Result<(), ContractError> {
Self::check_not_paused(&env);
let current_admin: Address = env
.storage()
.instance()
Expand Down Expand Up @@ -1871,6 +1949,7 @@ impl GameContract {
new_fee_bips: u32,
new_treasury_address: Address,
) -> Result<(), ContractError> {
Self::check_not_paused(&env);
if new_fee_bips > 1000 {
return Err(ContractError::InvalidAmount);
}
Expand Down Expand Up @@ -1918,6 +1997,7 @@ impl GameContract {
///
/// When approvals reach the threshold the fee change is applied immediately.
pub fn approve_fee_proposal(env: Env, signer: Address) -> Result<bool, ContractError> {
Self::check_not_paused(&env);
let signers: Vec<Address> = env
.storage()
.instance()
Expand Down Expand Up @@ -1990,6 +2070,7 @@ impl GameContract {

/// Cancel the pending fee proposal (any signer may cancel).
pub fn cancel_fee_proposal(env: Env, signer: Address) -> Result<(), ContractError> {
Self::check_not_paused(&env);
let signers: Vec<Address> = env
.storage()
.instance()
Expand Down Expand Up @@ -2094,6 +2175,7 @@ impl GameContract {
admin: Address,
duration: u64,
) -> Result<(), ContractError> {
Self::check_not_paused(&env);
let current_admin: Address = env
.storage()
.instance()
Expand All @@ -2117,6 +2199,7 @@ impl GameContract {
/// Locks the total prize pool until `current_ledger + timelock_duration`.
/// Returns the escrow ID.
pub fn create_tournament_escrow(env: Env, game_id: u64) -> Result<u64, ContractError> {
Self::check_not_paused(&env);
let games: Map<u64, Game> = env
.storage()
.instance()
Expand Down Expand Up @@ -2181,6 +2264,7 @@ impl GameContract {
winners: Vec<Address>,
percentages: Vec<u32>,
) -> Result<(), ContractError> {
Self::check_not_paused(&env);
let current_admin: Address = env
.storage()
.instance()
Expand Down
Loading
Loading