Skip to content
117 changes: 117 additions & 0 deletions creator-keys/tests/balance_after_mixed_trades.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
//! Tests for key balance tracking through mixed buy and sell sequences.
//!
//! Uses the `compute_expected_balance_after_trades` helper to verify that
//! balance tracking remains correct regardless of trade order.

mod contract_test_env;

use contract_test_env::{
compute_expected_balance_after_trades, register_creator_keys, register_test_creator,
set_key_price_for_tests, test_env_with_auths, TradeOperation,
};
use soroban_sdk::testutils::Address as _;
use soroban_sdk::Address;

#[test]
fn test_balance_after_sequence_of_buys_and_sells() {
let env = test_env_with_auths();
let (client, _) = register_creator_keys(&env);
let _ = set_key_price_for_tests(&env, &client, 100i128);
let creator = register_test_creator(&env, &client, "alice");
let buyer = Address::generate(&env);

// Execute a mixed sequence: buy, buy, sell, buy, sell, sell
let trades = vec![
TradeOperation::Buy,
TradeOperation::Buy,
TradeOperation::Sell,
TradeOperation::Buy,
TradeOperation::Sell,
TradeOperation::Sell,
];

// Initial balance is 0, so final expected balance is 0
let expected = compute_expected_balance_after_trades(0, &trades);
assert_eq!(expected, 0);

// Execute trades
client.buy_key(&creator, &buyer, &100i128);
client.buy_key(&creator, &buyer, &100i128);
client.sell_key(&creator, &buyer);
client.buy_key(&creator, &buyer, &100i128);
client.sell_key(&creator, &buyer);
client.sell_key(&creator, &buyer);

// Verify actual balance matches expected
let actual = client.get_key_balance(&creator, &buyer);
assert_eq!(actual, expected);
}

#[test]
fn test_balance_after_buys_then_sells() {
let env = test_env_with_auths();
let (client, _) = register_creator_keys(&env);
set_key_price_for_tests(&env, &client, 100i128);
let creator = register_test_creator(&env, &client, "alice");
let buyer = Address::generate(&env);

// Execute 5 buys followed by 2 sells: (5 - 2) = 3 remaining
let trades = vec![
TradeOperation::Buy,
TradeOperation::Buy,
TradeOperation::Buy,
TradeOperation::Buy,
TradeOperation::Buy,
TradeOperation::Sell,
TradeOperation::Sell,
];

let expected = compute_expected_balance_after_trades(0, &trades);
assert_eq!(expected, 3);

for _ in 0..5 {
client.buy_key(&creator, &buyer, &100i128);
}
for _ in 0..2 {
client.sell_key(&creator, &buyer);
}

let actual = client.get_key_balance(&creator, &buyer);
assert_eq!(actual, expected);
}

#[test]
fn test_balance_with_non_zero_initial() {
let env = test_env_with_auths();
let (client, _) = register_creator_keys(&env);
set_key_price_for_tests(&env, &client, 100i128);
let creator = register_test_creator(&env, &client, "alice");
let buyer = Address::generate(&env);

// Buy 4 keys first (initial balance = 4)
for _ in 0..4 {
client.buy_key(&creator, &buyer, &100i128);
}

// Then apply additional trades: buy, sell, sell, buy, buy
let additional_trades = vec![
TradeOperation::Buy,
TradeOperation::Sell,
TradeOperation::Sell,
TradeOperation::Buy,
TradeOperation::Buy,
];

// Starting from 4: 4+1-1-1+1+1 = 5
let expected = compute_expected_balance_after_trades(4, &additional_trades);
assert_eq!(expected, 5);

client.buy_key(&creator, &buyer, &100i128);
client.sell_key(&creator, &buyer);
client.sell_key(&creator, &buyer);
client.buy_key(&creator, &buyer, &100i128);
client.buy_key(&creator, &buyer, &100i128);

let actual = client.get_key_balance(&creator, &buyer);
assert_eq!(actual, expected);
}
32 changes: 32 additions & 0 deletions creator-keys/tests/contract_test_env/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -187,3 +187,35 @@ pub fn compute_expected_sell_price(_supply: u32, base_price: i128) -> i128 {
pub fn compute_expected_protocol_fee(price: i128, protocol_bps: u32) -> i128 {
(price * protocol_bps as i128) / 10_000
}

/// Represents a trade operation (buy or sell) in a sequence.
#[derive(Debug, Clone, Copy)]
pub enum TradeOperation {
/// A buy operation (increases balance by 1).
Buy,
/// A sell operation (decreases balance by 1).
Sell,
}

/// Computes the expected key balance after a sequence of buy and sell operations.
///
/// Takes an initial balance and applies a sequence of trades, returning the
/// final expected balance. Each buy increases the balance by 1, each sell
/// decreases it by 1 (stopping at 0 if a sell would go negative).
///
/// This helper makes test fixtures clearer by replacing magic numbers with
/// explicit trade sequences and reduces maintenance burden when test logic changes.
/// Mirrors the actual contract balance tracking logic.
pub fn compute_expected_balance_after_trades(
initial_balance: u32,
trades: &[TradeOperation],
) -> u32 {
let mut balance = initial_balance as i32;
for trade in trades {
match trade {
TradeOperation::Buy => balance += 1,
TradeOperation::Sell => balance = (balance - 1).max(0),
}
}
balance as u32
}
12 changes: 12 additions & 0 deletions creator-keys/tests/protocol_config_initialized.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,18 @@
use creator_keys::{CreatorKeysContract, CreatorKeysContractClient};
use soroban_sdk::{testutils::Address as _, Env};

#[test]
fn test_is_protocol_config_initialized_returns_false_before_initialization() {
let env = Env::default();
env.mock_all_auths();

let contract_id = env.register(CreatorKeysContract, ());
let client = CreatorKeysContractClient::new(&env, &contract_id);

// Before any initialization, the flag should be false
assert!(!client.is_protocol_config_initialized());
}

#[test]
fn test_is_protocol_config_initialized_returns_true_after_fee_config_is_set() {
let env = Env::default();
Expand Down
101 changes: 101 additions & 0 deletions creator-keys/tests/sell_event_seller_address.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
//! Regression test for sell event seller address field integrity.
//!
//! Verifies that the seller address in the emitted sell event matches
//! the address that initiated the sell call, preventing potential issues
//! where event reporting and actual execution could diverge.

use creator_keys::{events, CreatorKeysContract, CreatorKeysContractClient};
use soroban_sdk::{
testutils::{Address as _, Events},
Address, Env, IntoVal, String, Symbol, Val,
};

const KEY_PRICE: i128 = 100;

#[test]
fn test_sell_event_seller_address_matches_caller() {
let env = Env::default();
env.mock_all_auths();

// Setup
let contract_id = env.register(CreatorKeysContract, ());
let client = CreatorKeysContractClient::new(&env, &contract_id);
let admin = Address::generate(&env);
let creator = Address::generate(&env);
let seller = Address::generate(&env);

// Configure contract
client.set_key_price(&admin, &KEY_PRICE);
client.register_creator(&creator, &String::from_str(&env, "alice"));

// Buyer purchases keys
client.buy_key(&creator, &seller, &KEY_PRICE);

// Clear event log and then perform the sell
env.events().all(); // Clear existing events
client.sell_key(&creator, &seller);

// Extract and verify the sell event
let event_log = env.events().all();
let (_, topics, _) = event_log
.last()
.expect("sell event should be present in event log");

// Verify event name is sell
let event_name: Symbol = topics
.get(events::TOPIC_EVENT_NAME_INDEX)
.expect("event name topic should be present")
.into_val(&env);
assert_eq!(event_name, events::SELL_EVENT_NAME);

// Verify seller address field matches caller
let event_seller: Address = topics
.get(events::TOPIC_BUYER_INDEX)
.expect("seller address field should be present in event")
.into_val(&env);
assert_eq!(
event_seller, seller,
"seller address in event must match the caller"
);

// Verify creator address field is present and correct
let event_creator: Address = topics
.get(events::TOPIC_CREATOR_INDEX)
.expect("creator address field should be present in event")
.into_val(&env);
assert_eq!(event_creator, creator);
}

#[test]
fn test_sell_event_seller_address_field_is_non_zero() {
let env = Env::default();
env.mock_all_auths();

// Setup
let contract_id = env.register(CreatorKeysContract, ());
let client = CreatorKeysContractClient::new(&env, &contract_id);
let admin = Address::generate(&env);
let creator = Address::generate(&env);
let seller = Address::generate(&env);

// Configure and execute
client.set_key_price(&admin, &KEY_PRICE);
client.register_creator(&creator, &String::from_str(&env, "alice"));
client.buy_key(&creator, &seller, &KEY_PRICE);
client.sell_key(&creator, &seller);

// Verify the seller address field is present and non-zero
let event_log = env.events().all();
let (_, topics, _) = event_log.last().expect("sell event should be present");

let seller_field: Option<Val> = topics.get(events::TOPIC_BUYER_INDEX);
assert!(
seller_field.is_some(),
"seller address field must be present in sell event"
);

let event_seller: Address = seller_field.unwrap().into_val(&env);
// An Address cannot be zero in Soroban (it's always a valid address),
// but we verify it's the expected seller to confirm field integrity
assert_eq!(event_seller, seller);
}
115 changes: 115 additions & 0 deletions docs/VERSIONING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# Contract Versioning and WASM Hash Tracking

## Overview

The AccessLayer CreatorKeys contract is deployed as a Soroban smart contract on the Stellar blockchain. Each deployment produces a unique WASM binary with a distinct hash, which serves as the primary identifier for the contract version across different networks and deployment environments.

## Versioning Approach

### Semantic Versioning

The project follows semantic versioning for releases:
- Major version increments for breaking changes (e.g., incompatible state migrations, removed methods)
- Minor version increments for backward-compatible feature additions
- Patch version increments for backward-compatible bug fixes

Version information can be found in `Cargo.toml` in the workspace root.

### WASM Hash as Deployment Identifier

When the contract is compiled and deployed to the Stellar blockchain, the resulting WASM binary has a unique SHA-256 hash. This hash is not the same as a semantic version number, and a single semantic version may produce different hashes depending on the build environment (Rust compiler version, dependency versions, linker settings, etc.).

The WASM hash is deterministic for a given source tree and build configuration, making it the reliable way to verify that the exact code is running on-chain.

## Retrieving Contract WASM Hash

### From a Deployed Contract

Once a contract is deployed on Stellar, the WASM hash is publicly queryable:

```bash
soroban contract info \
--id <CONTRACT_ID> \
--network <NETWORK> \
--rpc-url <RPC_ENDPOINT>
```

This returns metadata including the WASM hash of the deployed contract.

### From Compiled Source

To compute the WASM hash before deployment or to verify against a deployed instance:

1. Build the contract with the same environment:
```bash
cd creator-keys
cargo build --target wasm32-unknown-unknown --release
```

2. Compute the SHA-256 hash of the compiled binary:
```bash
sha256sum target/wasm32-unknown-unknown/release/creator_keys.wasm
```

3. Compare the computed hash against the on-chain hash to verify they match.

## Canonical Hash Recording

### Release Artifacts

For each tagged release, the canonical WASM hash is recorded in:
- **Release notes** on GitHub under the version tag
- **Deployment logs** in the operations documentation (if available)
- **Contract registry** maintained by AccessLayer maintainers

### Verification Workflow

To verify you are running the expected contract version:

1. Note the WASM hash from the GitHub release notes for the version you intend to deploy
2. Query the contract hash on the target Stellar network (see "From a Deployed Contract" above)
3. Compare the two hashes; they must match exactly
4. If building from source, compile using the exact commit/tag and verify the build hash matches

## Build Determinism

The contract build is designed to be deterministic. However, differences may arise from:
- Rust toolchain version (specified in `rust-toolchain.toml`)
- Cargo dependency resolution (use `Cargo.lock` for reproducible builds)
- Build flags and optimization levels

To ensure reproducible builds when verifying a release:
1. Check out the exact release tag from git
2. Use the Rust version specified in `rust-toolchain.toml`
3. Ensure `Cargo.lock` is present and unmodified
4. Build with `--release` flag

## Best Practices

### For Operators
- Always verify the WASM hash before deploying a new contract version
- Record the hash alongside deployment timestamps and network information
- Use the canonical hash from GitHub releases as the source of truth

### For Contributors
- Do not manually edit `Cargo.lock` unless necessary for dependency updates
- When submitting a release, include the WASM hash in release notes
- Document any build configuration changes that might affect reproducibility

### For Users
- Verify the WASM hash when deploying through a third party
- Be skeptical of deployments with WASM hashes that don't match release records
- Use the `soroban contract info` command to inspect any running contract

## Emergency: Hash Mismatch

If the on-chain WASM hash does not match the expected canonical hash:
1. **Stop**: Do not assume it is safe; investigate before proceeding
2. **Verify source**: Recompile from the release tag and check your build environment
3. **Query deployment history**: Check logs or transaction records to understand when the mismatch occurred
4. **Escalate**: Contact AccessLayer maintainers via the security contact in `SECURITY.md`

A mismatch could indicate:
- Accidental deployment of an unreviewed version
- A build environment with different compiler/dependency versions
- A potential security incident (though this is rare if following proper deployment procedures)
Loading