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: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion contracts/predictify-hybrid/src/audit_trail.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,9 @@ pub enum AuditAction {
FeesWithdrawn,
FeeConfigUpdated,

// Oracle & Config Actions
// Token & Oracle Actions
OracleConfigUpdated,
TokenVerified,
BetLimitsUpdated,

// Resolution & Disputes
Expand Down
84 changes: 84 additions & 0 deletions contracts/predictify-hybrid/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7114,6 +7114,90 @@ impl PredictifyHybrid {
/// # Errors
///
/// This entrypoint surfaces contract errors via panic in internal calls.
/// Verify SAC token decimals match declared value (admin only).
///
/// This function performs a critical security check on SAC tokens to prevent
/// denomination mistakes that have caused real on-chain losses. It verifies that
/// the token's on-chain decimals() value matches what was declared during registration.
///
/// This can be called:
/// - Automatically during token registration (via add_global_verified/add_event_verified)
/// - Manually by admin as part of periodic audits or security reviews
///
/// # Parameters
///
/// * `env` - The Soroban environment for blockchain operations
/// * `admin` - The administrator address (must be authorized)
/// * `token_contract` - Address of the token contract to verify
/// * `declared_decimals` - The decimals value that was declared during registration
///
/// # Returns
///
/// Returns `Result<(), Error>` where:
/// - `Ok(())` - Decimals match (token is safe)
/// - `Err(Error::TokenDecimalsMismatch)` - Mismatch detected (token rejected)
/// - `Err(Error::Unauthorized)` - Caller is not admin
///
/// # Cross-Contract Call
///
/// This function performs a cross-contract call to the token contract's
/// `decimals()` function using the Soroban token interface.
///
/// # Security Notes
///
/// - Verifies via on-chain decimals() call (cannot be spoofed)
/// - Mismatch indicates potential token misconfiguration
/// - Rejected tokens cannot be used for betting/payouts
/// - All registration paths should use verified variants
///
/// # Example
///
/// ```rust,ignore
/// let token_contract = Address::from_string("GBUQW...");
/// PredictifyHybrid::re_verify_token(&env, &admin, &token_contract, 7)?;
/// // Returns Ok if decimals match, TokenDecimalsMismatch error otherwise
/// ```
///
/// # Errors
///
/// Returns [`Error`] when:
/// - `Error::Unauthorized` - Caller is not the contract admin
/// - `Error::TokenDecimalsMismatch` - On-chain decimals don't match declared value
/// - Other errors from cross-contract call or storage operations
///
/// # Events
///
/// Emits audit trail record of verification attempt (success or failure).
pub fn re_verify_token(
env: Env,
admin: Address,
token_contract: Address,
declared_decimals: u32,
) -> Result<(), Error> {
// Verify admin authorization
Self::require_primary_admin(&env, &admin)?;

// Create temporary asset for verification
let asset = crate::tokens::Asset {
contract: token_contract.clone(),
symbol: Symbol::new(&env, "TEMP"),
decimals: declared_decimals,
};

// Perform decimals verification via cross-contract call
crate::tokens::verify_token_decimals(&env, &asset)?;

// Record verification in audit trail
crate::audit_trail::AuditTrailManager::append_record(
&env,
crate::audit_trail::AuditAction::TokenVerified,
admin.clone(),
Map::new(&env),
);

Ok(())
}

///
/// # Events
///
Expand Down
119 changes: 119 additions & 0 deletions contracts/predictify-hybrid/src/tokens.rs
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,66 @@ impl TokenRegistry {
global_assets.iter().any(|a| a == *asset)
}

/// Adds an asset to the global allowed registry with decimals verification.
///
/// This function performs a critical security check by verifying that the
/// declared decimals match the on-chain SAC decimals() value. This prevents
/// denomination mistakes that have caused real losses on other Stellar protocols.
///
/// # Errors
/// * `Error::TokenDecimalsMismatch` if declared decimals don't match on-chain value.
///
/// # Security Notes
/// - Performs cross-contract call to token's decimals() function
/// - Rejects registration if mismatch detected
/// - Should only be called by admin
pub fn add_global_verified(env: &Env, asset: &Asset) -> Result<(), Error> {
// Verify decimals before registration
verify_token_decimals(env, asset)?;

let global_key = Symbol::new(env, "allowed_assets_global");
let mut global_assets: Vec<Asset> = env
.storage()
.persistent()
.get(&global_key)
.unwrap_or(Vec::new(env));
if !global_assets.iter().any(|a| a == *asset) {
global_assets.push_back(asset.clone());
env.storage().persistent().set(&global_key, &global_assets);
}
Ok(())
}

/// Adds an asset to a specific market's allowed registry with decimals verification.
///
/// # Parameters
/// * `env` - Soroban environment.
/// * `market_id` - Market identifier.
/// * `asset` - The asset to register.
///
/// # Errors
/// * `Error::TokenDecimalsMismatch` if declared decimals don't match on-chain value.
pub fn add_event_verified(env: &Env, market_id: &Symbol, asset: &Asset) -> Result<(), Error> {
// Verify decimals before registration
verify_token_decimals(env, asset)?;

let event_key = Symbol::new(env, "allowed_assets_evt");
let per_event_empty: soroban_sdk::Map<Symbol, Vec<Asset>> = soroban_sdk::Map::new(env);
let mut per_event: soroban_sdk::Map<Symbol, Vec<Asset>> = env
.storage()
.persistent()
.get(&event_key)
.unwrap_or(per_event_empty);
let empty_assets: Vec<Asset> = Vec::new(env);
let mut assets: Vec<Asset> = per_event.get(market_id.clone()).unwrap_or(empty_assets);
if !assets.iter().any(|a| a == *asset) {
assets.push_back(asset.clone());
per_event.set(market_id.clone(), assets);
env.storage().persistent().set(&event_key, &per_event);
}
Ok(())
}

/// Adds an asset to the global allowed registry.
pub fn add_global(env: &Env, asset: &Asset) {
let global_key = Symbol::new(env, "allowed_assets_global");
Expand Down Expand Up @@ -508,6 +568,65 @@ pub fn validate_token_operation(
Ok(())
}

// ===== SAC DECIMALS VERIFICATION =====

/// Verifies that a token's declared decimals match the on-chain value.
///
/// This is a critical security check that prevents denomination mistakes.
/// Real-world on-chain losses have occurred on other Stellar protocols when
/// tokens with mismatched decimals were trusted without verification.
///
/// # Parameters
/// * `env` - Soroban environment.
/// * `asset` - The asset to verify. Uses the declared decimals value.
///
/// # Returns
/// * `Ok(())` if the declared decimals match the SAC's decimals() output.
/// * `Err(Error::TokenDecimalsMismatch)` if they don't match.
///
/// # Cross-Contract Call
/// This function performs a cross-contract call to the token contract's
/// `decimals()` function using the Soroban token interface.
///
/// # Example
/// ```rust,ignore
/// let asset = Asset::new(token_contract, "USDC".into(), 7);
/// verify_token_decimals(&env, &asset)?; // Verifies on-chain
/// ```
pub fn verify_token_decimals(env: &Env, asset: &Asset) -> Result<(), Error> {
// Create a token client for cross-contract call
let client = token::Client::new(env, &asset.contract);

// Call the on-chain decimals() function
let on_chain_decimals: u32 = client.decimals();

// Compare with declared decimals
if on_chain_decimals != asset.decimals {
return Err(Error::TokenDecimalsMismatch);
}

Ok(())
}

/// Batch verification of multiple assets' decimals.
///
/// Useful for verifying all globally allowed assets or market-specific assets
/// during initialization or periodic audits.
///
/// # Parameters
/// * `env` - Soroban environment.
/// * `assets` - Vector of assets to verify.
///
/// # Returns
/// * `Ok(())` if all assets pass verification.
/// * `Err(Error::TokenDecimalsMismatch)` if any asset fails (first failure only).
pub fn verify_token_decimals_batch(env: &Env, assets: &Vec<Asset>) -> Result<(), Error> {
for asset in assets.iter() {
verify_token_decimals(env, &asset)?;
}
Ok(())
}

#[cfg(test)]
mod test {
use super::*;
Expand Down
Loading