diff --git a/contracts/Cargo.toml b/contracts/Cargo.toml index 4d60379..981aff7 100644 --- a/contracts/Cargo.toml +++ b/contracts/Cargo.toml @@ -4,6 +4,8 @@ members = [ "game_contract", "emergency_circuit_breaker", "ai_nft", + "referral_splitter", + "model_attestation", ] [profile.release] diff --git a/contracts/model_attestation/Cargo.toml b/contracts/model_attestation/Cargo.toml new file mode 100644 index 0000000..dcfb361 --- /dev/null +++ b/contracts/model_attestation/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "model_attestation" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +soroban-sdk = "21.0.0" + +[dev-dependencies] +soroban-sdk = { version = "21.0.0", features = ["testutils"] } diff --git a/contracts/model_attestation/src/lib.rs b/contracts/model_attestation/src/lib.rs new file mode 100644 index 0000000..6cf5d64 --- /dev/null +++ b/contracts/model_attestation/src/lib.rs @@ -0,0 +1,99 @@ +#![no_std] +//! AI-41: On-Chain Model Checkpoint Hash Attestation +//! +//! Stores SHA-256 / Blake3 hashes of AI model weights in a Soroban registry +//! so tournament organizers can prove bots used identical, untampered models +//! throughout a tournament. + +use soroban_sdk::{ + contract, contractimpl, contracttype, contracterror, panic_with_error, + Address, Bytes, Env, String, +}; + +#[contracttype] +pub enum DataKey { + Admin, + /// model_id → ModelRecord + Model(String), +} + +/// A stored model checkpoint record. +#[contracttype] +#[derive(Clone, Debug)] +pub struct ModelRecord { + /// Hex-encoded SHA-256 or Blake3 hash of the model weights + pub hash: String, + /// Tournament or version identifier + pub tournament_id: String, + /// Ledger timestamp at submission + pub submitted_at: u64, + /// Address that submitted this record + pub submitted_by: Address, +} + +#[contracterror] +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum Error { + AlreadyInitialized = 1, + NotAdmin = 2, + /// Model ID already attested — records are immutable + AlreadyAttested = 3, + ModelNotFound = 4, +} + +#[contract] +pub struct ModelAttestation; + +#[contractimpl] +impl ModelAttestation { + /// Initialise the registry with an admin address. + pub fn initialize(env: Env, admin: Address) { + if env.storage().instance().has(&DataKey::Admin) { + panic_with_error!(&env, Error::AlreadyInitialized); + } + admin.require_auth(); + env.storage().instance().set(&DataKey::Admin, &admin); + } + + /// Submit a model weight hash for a given model_id. Immutable once stored. + pub fn attest( + env: Env, + submitter: Address, + model_id: String, + hash: String, + tournament_id: String, + ) { + submitter.require_auth(); + let key = DataKey::Model(model_id.clone()); + if env.storage().persistent().has(&key) { + panic_with_error!(&env, Error::AlreadyAttested); + } + let record = ModelRecord { + hash: hash.clone(), + tournament_id: tournament_id.clone(), + submitted_at: env.ledger().timestamp(), + submitted_by: submitter, + }; + env.storage().persistent().set(&key, &record); + env.events().publish( + (soroban_sdk::symbol_short!("attested"),), + (model_id, hash, tournament_id), + ); + } + + /// Look up a stored model record by model_id. + pub fn get_record(env: Env, model_id: String) -> ModelRecord { + env.storage() + .persistent() + .get(&DataKey::Model(model_id)) + .unwrap_or_else(|| panic_with_error!(&env, Error::ModelNotFound)) + } + + /// Verify a hash matches the stored attestation. Returns true if it matches. + pub fn verify(env: Env, model_id: String, hash: String) -> bool { + match env.storage().persistent().get::<_, ModelRecord>(&DataKey::Model(model_id)) { + Some(record) => record.hash == hash, + None => false, + } + } +} diff --git a/contracts/referral_splitter/Cargo.toml b/contracts/referral_splitter/Cargo.toml new file mode 100644 index 0000000..53ab1dc --- /dev/null +++ b/contracts/referral_splitter/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "referral_splitter" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +soroban-sdk = "21.0.0" + +[dev-dependencies] +soroban-sdk = { version = "21.0.0", features = ["testutils"] } diff --git a/contracts/referral_splitter/src/lib.rs b/contracts/referral_splitter/src/lib.rs new file mode 100644 index 0000000..71e6882 --- /dev/null +++ b/contracts/referral_splitter/src/lib.rs @@ -0,0 +1,122 @@ +#![no_std] +//! SC-54: Decentralized Referral & Affiliate Commission Splitter +//! +//! Records permanent on-chain referee→referrer bindings and automatically +//! splits a configurable fee percentage to the referrer on every wager. +//! Self-referral loops are rejected at registration time. + +use soroban_sdk::{ + contract, contractimpl, contracttype, contracterror, panic_with_error, + Address, Env, Vec, +}; + +/// Fee denominator: commission_bps / 10_000 = commission fraction. +const FEE_DENOMINATOR: i128 = 10_000; + +#[contracttype] +pub enum DataKey { + /// admin address + Admin, + /// referrer for a given referee address + Referrer(Address), + /// cumulative earnings for a referrer + Earnings(Address), + /// configurable commission in basis points (e.g. 1000 = 10%) + CommissionBps, +} + +#[contracterror] +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum Error { + AlreadyInitialized = 1, + NotAdmin = 2, + /// Referee already has a referrer registered + AlreadyReferred = 3, + /// Self-referral is forbidden + SelfReferral = 4, + InvalidAmount = 5, + InvalidCommission = 6, +} + +#[contract] +pub struct ReferralSplitter; + +#[contractimpl] +impl ReferralSplitter { + /// Initialise the contract; sets admin and commission in basis points. + pub fn initialize(env: Env, admin: Address, commission_bps: u32) { + if env.storage().instance().has(&DataKey::Admin) { + panic_with_error!(&env, Error::AlreadyInitialized); + } + if commission_bps > 10_000 { + panic_with_error!(&env, Error::InvalidCommission); + } + admin.require_auth(); + env.storage().instance().set(&DataKey::Admin, &admin); + env.storage().instance().set(&DataKey::CommissionBps, &commission_bps); + } + + /// Register a referee→referrer binding. Permanent once set; self-referral rejected. + pub fn register_referral(env: Env, referee: Address, referrer: Address) { + referee.require_auth(); + if referee == referrer { + panic_with_error!(&env, Error::SelfReferral); + } + let key = DataKey::Referrer(referee.clone()); + if env.storage().persistent().has(&key) { + panic_with_error!(&env, Error::AlreadyReferred); + } + env.storage().persistent().set(&key, &referrer); + } + + /// Settle a wager of `amount` stroops. Splits commission to the referrer + /// (if one exists) and returns the referrer's cut. Emits a referral_earnings event. + pub fn settle_wager(env: Env, referee: Address, amount: i128) -> i128 { + if amount <= 0 { + panic_with_error!(&env, Error::InvalidAmount); + } + let referrer: Option
= env + .storage() + .persistent() + .get(&DataKey::Referrer(referee)); + if let Some(ref r) = referrer { + let bps: u32 = env + .storage() + .instance() + .get(&DataKey::CommissionBps) + .unwrap_or(1_000); + let cut = amount * (bps as i128) / FEE_DENOMINATOR; + if cut > 0 { + let prev: i128 = env + .storage() + .persistent() + .get(&DataKey::Earnings(r.clone())) + .unwrap_or(0); + env.storage() + .persistent() + .set(&DataKey::Earnings(r.clone()), &(prev + cut)); + env.events().publish( + (soroban_sdk::symbol_short!("ref_earn"),), + (r.clone(), cut), + ); + return cut; + } + } + 0 + } + + /// Returns cumulative earnings for a referrer. + pub fn get_earnings(env: Env, referrer: Address) -> i128 { + env.storage() + .persistent() + .get(&DataKey::Earnings(referrer)) + .unwrap_or(0) + } + + /// Returns the referrer registered for a referee, if any. + pub fn get_referrer(env: Env, referee: Address) -> Option
{ + env.storage() + .persistent() + .get(&DataKey::Referrer(referee)) + } +}