Skip to content
Open
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
7 changes: 7 additions & 0 deletions .github/workflows/contracts.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,16 @@ jobs:
- uses: dtolnay/rust-toolchain@stable
with:
targets: wasm32v1-none
components: clippy
- uses: Swatinem/rust-cache@v2
- name: Install cargo-audit
run: cargo install cargo-audit --locked
- name: Check workspace
run: cargo check --all
- name: Run Clippy with warnings denied
run: cargo clippy --all-targets --all-features -- -D warnings
- name: Run dependency audit
run: cargo audit
- name: Check contract WASM targets
run: |
cargo check -p renaissance-counter --target wasm32v1-none --release
Expand Down
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Renaissance Contract

This workspace contains the Soroban smart contracts for the Renaissance betting and NFT flow.

## Security review and verification

The repository now includes a structured security review package:

- Audit checklist: [docs/security-review-checklist.md](docs/security-review-checklist.md)
- Formal verification specs: [docs/formal-verification-specs.md](docs/formal-verification-specs.md)

## CI enforcement

The contract workflow enforces:

- `cargo clippy --all-targets --all-features -- -D warnings`
- `cargo audit`

## Unsafe code policy

The audited contract crates use `#![forbid(unsafe_code)]` and any future `unsafe` block must be documented with a justification before it is merged.
5 changes: 4 additions & 1 deletion betting/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,7 @@ crate-type = ["cdylib", "rlib"] # cdylib is required to emit optimized WASM bina

[dependencies]
soroban-sdk = { workspace = true }
renaissance-core = { workspace = true }
renaissance-core = { workspace = true }

[package.metadata]
security-review = "requires formal payout verification"
24 changes: 22 additions & 2 deletions betting/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#![no_std]
#![forbid(unsafe_code)]

//! `#![no_std]` `renaissance-betting` smart contract.
//!
Expand Down Expand Up @@ -624,7 +625,7 @@ mod test {
(env, admin, oracle, token)
}

fn initialize(env: &Env, admin: &Address) -> RenaissanceBettingContractClient<'_> {
fn initialize<'a>(env: &'a Env, admin: &'a Address) -> RenaissanceBettingContractClient<'a> {
let contract_id = env.register_contract(None, RenaissanceBettingContract);
let client = RenaissanceBettingContractClient::new(env, &contract_id);
client.initialize(admin);
Expand Down Expand Up @@ -659,7 +660,7 @@ mod test {

let new_hash = BytesN::from_array(&env, &[9; 32]);
client.upgrade(&new_hash).unwrap();
let res = client.try_upgrade(&new_hash);
let res = client.try_upgrade(&admin, &new_hash);
assert!(res.is_err());

let match_data = client.get_match(&20u64).unwrap();
Expand Down Expand Up @@ -1014,6 +1015,25 @@ mod test {
assert!(res.is_err());
}

#[test]
fn test_claim_payout_matches_documented_formula() {
let (env, admin, oracle, token) = setup();
let client = initialize(&env, &admin);
client.register_match(&21u64, &oracle, &token, &deadline_in(&env, 3_600));

let winner = Address::generate(&env);
let loser = Address::generate(&env);
mint(&env, &token, &winner, 1_000);
mint(&env, &token, &loser, 1_000);

client.place_bet(&winner, &21u64, &Outcome::HomeWin, &100i128);
client.place_bet(&loser, &21u64, &Outcome::Draw, &300i128);
client.settle_bet(&oracle, &21u64, &Outcome::HomeWin);

let payout = client.claim_payout(&winner, &21).unwrap();
assert_eq!(payout, 400);
}

// ── refund_bet ────────────────────────────────────────────────────────────

#[test]
Expand Down
32 changes: 32 additions & 0 deletions docs/formal-verification-specs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Formal verification specs

## Betting contract: payout correctness

For a settled match, let:

- $w$ be the winning outcome pool
- $t$ be the total pool across all outcomes
- $b$ be the bettor's stake for the winning outcome

The payout for a winning bet is:

$$
\text{payout} = b + \left\lfloor \frac{b \cdot (t - w)}{w} \right\rfloor
$$

The implementation must ensure:

1. The payout is computed only for bets on the winning outcome.
2. The winning pool is strictly positive before division.
3. The payout is computed with checked arithmetic to avoid overflow.
4. The bet is marked as claimed and the transfer amount equals the computed payout.

## Player NFT contract: ownership invariant

The ownership invariant is:

- Each token has exactly one current owner at any time.
- The owner identity returned by the contract must match the most recent successful transfer or mint event.
- Ownership transitions are monotonic with respect to the authorized transfer flow.

The current implementation is intentionally a contract boundary with no transfer logic yet; the invariant is documented here so future NFT ownership logic can be verified against it.
20 changes: 20 additions & 0 deletions docs/security-review-checklist.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Security review checklist

This checklist is intended for pre-mainnet review of the Renaissance contracts.

## Checklist

- [x] Reentrancy: review the transfer and claim flows for reentrancy assumptions and ensure state is updated before external token transfers.
- [x] Overflow / underflow: use checked arithmetic for pool totals, payouts, and any arithmetic derived from user balances.
- [x] Access control: admin-only actions require authenticated principals and are gated by explicit authorization checks.
- [x] Front-running: value-changing actions are ordered around immutable state updates and settlement remains a dedicated oracle-controlled entry point.
- [x] Oracle manipulation: only the configured oracle can settle a match and the oracle address is replaceable only before settlement.

## Static analysis

- CI runs `cargo clippy --all-targets --all-features -- -D warnings`.
- CI runs `cargo audit` to surface dependency vulnerabilities before mainnet deployment.

## Unsafe code policy

The audited contract crates use `#![forbid(unsafe_code)]`. Any future `unsafe` block must be accompanied by a short justification describing why it is required and how it is bounded.
24 changes: 12 additions & 12 deletions oracle/src/test.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#![cfg(test)]

use super::*;
use soroban_sdk::testutils::Ledger;
use soroban_sdk::testutils::{Address as _, Ledger};
use soroban_sdk::{testutils::AddressEnvTestUtils, Address, Env, Vec};

#[test]
Expand Down Expand Up @@ -71,7 +71,7 @@ fn test_submit_result_success() {
let finished_at = now - 600; // 10 minutes ago

oracle1.require_auth();
client.submit_result(&match_id, &2, &1, &started_at, &finished_at);
client.submit_result(&oracle1, &match_id, &2, &1, &started_at, &finished_at);

// Result shouldn't be finalized yet (needs second confirmation)
assert!(!client.is_finalized(&match_id));
Expand Down Expand Up @@ -101,7 +101,7 @@ fn test_submit_result_unauthorized() {

// Unauthorized address tries to submit
let match_id = 123;
client.submit_result(&match_id, &2, &1, &(now - 3600), &(now - 600));
client.submit_result(&unauthorized, &match_id, &2, &1, &(now - 3600), &(now - 600));
}

#[test]
Expand Down Expand Up @@ -161,11 +161,11 @@ fn test_double_submit() {

// First submission from oracle1
oracle1.require_auth();
client.submit_result(&match_id, &2, &1, &started_at, &finished_at);
client.submit_result(&oracle1, &match_id, &2, &1, &started_at, &finished_at);

// Second submission from oracle2 for the same match - should fail
oracle2.require_auth();
client.submit_result(&match_id, &3, &1, &started_at, &finished_at);
client.submit_result(&oracle2, &match_id, &3, &1, &started_at, &finished_at);
}

#[test]
Expand Down Expand Up @@ -194,11 +194,11 @@ fn test_confirm_and_finalize() {

// Submit from oracle1
oracle1.require_auth();
client.submit_result(&match_id, &2, &1, &started_at, &finished_at);
client.submit_result(&oracle1, &match_id, &2, &1, &started_at, &finished_at);

// Confirm from oracle2 - this should finalize
oracle2.require_auth();
client.confirm_result(&match_id);
client.confirm_result(&oracle2, &match_id);

// Check if finalized
assert!(client.is_finalized(&match_id));
Expand Down Expand Up @@ -238,11 +238,11 @@ fn test_cannot_confirm_own_submission() {

// Submit from oracle1
oracle1.require_auth();
client.submit_result(&match_id, &2, &1, &started_at, &finished_at);
client.submit_result(&oracle1, &match_id, &2, &1, &started_at, &finished_at);

// Try to confirm own submission - should fail
oracle1.require_auth();
client.confirm_result(&match_id);
client.confirm_result(&oracle1, &match_id);
}

#[test]
Expand Down Expand Up @@ -274,13 +274,13 @@ fn test_double_confirm() {

// Submit from oracle1
oracle1.require_auth();
client.submit_result(&match_id, &2, &1, &started_at, &finished_at);
client.submit_result(&oracle1, &match_id, &2, &1, &started_at, &finished_at);

// Confirm from oracle2
oracle2.require_auth();
client.confirm_result(&match_id);
client.confirm_result(&oracle2, &match_id);

// Try to confirm again from oracle2 - should fail
oracle2.require_auth();
client.confirm_result(&match_id);
client.confirm_result(&oracle2, &match_id);
}
3 changes: 3 additions & 0 deletions player-nft/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,6 @@ crate-type = ["cdylib", "rlib"]
[dependencies]
soroban-sdk = { workspace = true }
renaissance-core = { workspace = true }

[package.metadata]
security-review = "requires formal ownership invariant review"
14 changes: 14 additions & 0 deletions player-nft/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#![no_std]
#![forbid(unsafe_code)]

use soroban_sdk::{contract, contractimpl, Env, Symbol};

Expand All @@ -12,3 +13,16 @@ impl PlayerNftContract {
Symbol::new(&env, "player_nft")
}
}

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

#[test]
fn contract_name_is_stable() {
let env = Env::default();
let contract_id = env.register_contract(None, PlayerNftContract);
let client = PlayerNftContractClient::new(&env, &contract_id);
assert_eq!(client.contract_name(), Symbol::new(&env, "player_nft"));
}
}
4 changes: 2 additions & 2 deletions rewards/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -353,7 +353,7 @@ impl FanRewardsContract {
#[cfg(test)]
mod test {
use super::*;
use soroban_sdk::{symbol_short, testutils::Address as _, Symbol};
use soroban_sdk::{symbol_short, testutils::{Address as _, Events}, Symbol};

fn setup() -> (Env, Address, Address) {
let env = Env::default();
Expand Down Expand Up @@ -391,7 +391,7 @@ mod test {
assert!(res.is_err());
}

fn client(env: &Env, contract_id: &Address) -> FanRewardsContractClient {
fn client<'a>(env: &'a Env, contract_id: &'a Address) -> FanRewardsContractClient<'a> {
FanRewardsContractClient::new(env, contract_id)
}

Expand Down
1 change: 1 addition & 0 deletions vault/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#![no_std]
#![forbid(unsafe_code)]

//! `renaissance-vault` token vault contract for betting stakes.
//!
Expand Down
Loading
Loading