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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions contracts/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ members = [
"game_contract",
"emergency_circuit_breaker",
"ai_nft",
"referral_splitter",
"model_attestation",
]

[profile.release]
Expand Down
13 changes: 13 additions & 0 deletions contracts/model_attestation/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"] }
99 changes: 99 additions & 0 deletions contracts/model_attestation/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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,
}
}
}
13 changes: 13 additions & 0 deletions contracts/referral_splitter/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"] }
122 changes: 122 additions & 0 deletions contracts/referral_splitter/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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<Address> = 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<Address> {
env.storage()
.persistent()
.get(&DataKey::Referrer(referee))
}
}