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
9 changes: 8 additions & 1 deletion Cargo.lock

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

131 changes: 131 additions & 0 deletions contracts/compliance/tests/operator_privilege_boundary_test.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
use compliance::{ComplianceContract, ComplianceContractClient, ContractError};
use soroban_sdk::{testutils::Address as _, Address, Env};

fn setup() -> (Env, Address, Address, ComplianceContractClient<'static>) {
let env = Env::default();
env.mock_all_auths();
let admin = Address::generate(&env);
let subject = Address::generate(&env);
let id = env.register_contract(None, ComplianceContract);
let client = ComplianceContractClient::new(&env, &id);
client.initialize(&admin);
(env, admin, subject, client)
}

#[test]
fn operator_can_call_address_status() {
let (env, admin, subject, client) = setup();
let operator = Address::generate(&env);

// Allow a test subject
client.allow_address(&admin, &subject);

// Set operator
client.set_operator(&admin, &operator);

// Operator should be able to call address_status
let result = client.try_address_status(&operator, &subject);
assert!(result.is_ok());
}

#[test]
fn operator_rejected_from_allow_address() {
let (env, admin, subject, client) = setup();
let operator = Address::generate(&env);

// Set operator
client.set_operator(&admin, &operator);

// Operator should NOT be able to call allow_address
let result = client.try_allow_address(&operator, &subject);
assert_eq!(result, Err(Ok(ContractError::Unauthorized)));
}

#[test]
fn operator_rejected_from_block_address() {
let (env, admin, subject, client) = setup();
let operator = Address::generate(&env);

// Allow subject first
client.allow_address(&admin, &subject);

// Set operator
client.set_operator(&admin, &operator);

// Operator should NOT be able to call block_address
let result = client.try_block_address(&operator, &subject, &None);
assert_eq!(result, Err(Ok(ContractError::Unauthorized)));
}

#[test]
fn operator_rejected_from_clear_address() {
let (env, admin, subject, client) = setup();
let operator = Address::generate(&env);

// Allow subject first
client.allow_address(&admin, &subject);

// Set operator
client.set_operator(&admin, &operator);

// Operator should NOT be able to call clear_address
let result = client.try_clear_address(&operator, &subject);
assert_eq!(result, Err(Ok(ContractError::Unauthorized)));
}

#[test]
fn admin_can_call_all_operations() {
let (env, admin, subject, client) = setup();
let operator = Address::generate(&env);

// Set operator
client.set_operator(&admin, &operator);

// Admin should still be able to call all operations
client.allow_address(&admin, &subject);
assert!(client.is_allowed(&subject));

let result = client.try_address_status(&admin, &subject);
assert!(result.is_ok());

client.block_address(&admin, &subject, &None);
assert!(!client.is_allowed(&subject));

client.clear_address(&admin, &subject);
assert!(client.is_allowed(&subject));
}

#[test]
fn operator_privilege_correctly_distinguished_in_multiple_operations() {
let (env, admin, subject1, client) = setup();
let subject2 = Address::generate(&env);
let operator = Address::generate(&env);

// Setup initial state
client.allow_address(&admin, &subject1);
client.allow_address(&admin, &subject2);

// Set operator
client.set_operator(&admin, &operator);

// Operator can read address_status
let result1 = client.try_address_status(&operator, &subject1);
assert!(result1.is_ok());

let result2 = client.try_address_status(&operator, &subject2);
assert!(result2.is_ok());

// Operator cannot perform admin operations (block, allow, clear)
assert!(client
.try_block_address(&operator, &subject1, &None)
.is_err());
assert!(client.try_allow_address(&operator, &subject2).is_err());
assert!(client.try_clear_address(&operator, &subject1).is_err());

// Admin can still perform all operations
client.block_address(&admin, &subject1, &None);
assert!(!client.is_allowed(&subject1));

client.clear_address(&admin, &subject1);
assert!(client.is_allowed(&subject1));
}
1 change: 1 addition & 0 deletions contracts/invoice/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,5 @@ invoice-errors = { package = "comebackhere-invoice-errors", path = "../../crates
[dev-dependencies]
hex = "0.4"
proptest = "1"
serde_json = "1"
soroban-sdk = { workspace = true, features = ["testutils"] }
189 changes: 189 additions & 0 deletions contracts/invoice/tests/amount_validation_differential_test.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
// Differential fuzzing test: compares Rust invoice amount validation against
// an independent Python reference implementation with the same test vectors.
// This catches bugs that might be shared between implementation and test if
// both were written in the same language with the same mental model.

use invoice::{InvoiceContract, InvoiceContractClient, MaybeAddress, MaybeBytes};
use serde_json::Value;
use soroban_sdk::{testutils::Address as _, Address, Env};
use std::io::Write;
use std::process::Command;

const USDC_FACTOR: i128 = 10_000_000;

fn client() -> (Env, InvoiceContractClient<'static>) {
let env = Env::default();
env.mock_all_auths();
let admin = Address::generate(&env);
let id = env.register_contract(None, InvoiceContract);
let c = InvoiceContractClient::new(&env, &id);
c.initialize(&admin);
(env, c)
}

/// Calls the Python reference implementation with the given amounts.
/// Returns (valid, error_name) where valid=true means no error, valid=false means error with error_name.
fn python_validate(amount_usdc: i128, gross_usdc: i128) -> (bool, Option<String>) {
// Use string representation to avoid JSON number overflow issues with large i128 values
let input = format!(
r#"{{"amount_usdc": {}, "gross_usdc": {}}}"#,
amount_usdc, gross_usdc
);

let mut child = Command::new("python3")
.arg("scripts/reference_amount_validation.py")
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.current_dir("/workspaces/COMEBACKHERE-contracts")
.spawn()
.expect("failed to spawn Python process");

{
let stdin = child.stdin.as_mut().expect("failed to open stdin");
stdin
.write_all(input.as_bytes())
.expect("failed to write to stdin");
}

let output = child.wait_with_output().expect("failed to wait on Python");
let stdout = String::from_utf8_lossy(&output.stdout);

let result: Value = serde_json::from_str(&stdout)
.expect(&format!("failed to parse Python output: {}", stdout));

let valid = result["valid"].as_bool().expect("missing 'valid' field");
let error = if let Some(e) = result["error"].as_str() {
Some(e.to_string())
} else if result["error"].is_null() {
None
} else {
Some(result["error"].to_string())
};

(valid, error)
}

/// Attempts to create an invoice with the given amounts via the Rust implementation.
/// Returns true if successful, false if rejected.
fn rust_validate(amount_usdc: i128, gross_usdc: i128) -> bool {
let (env, client) = client();
let merchant = Address::generate(&env);

client
.try_create_invoice(
&merchant,
&amount_usdc,
&gross_usdc,
&3600,
&MaybeBytes::None,
&MaybeBytes::None,
&0,
&MaybeAddress::None,
)
.is_ok()
}

#[test]
fn differential_fuzz_canonical_test_cases() {
// Test cases that must be handled the same way in both implementations.
let test_cases: &[(i128, i128)] = &[
// Invalid: negative amounts
(-1, USDC_FACTOR),
(-USDC_FACTOR, USDC_FACTOR),
(i128::MIN, i128::MAX),
// Invalid: zero amounts
(0, 0),
(0, USDC_FACTOR),
// Invalid: gross < amount
(USDC_FACTOR, USDC_FACTOR - 1),
(2 * USDC_FACTOR, USDC_FACTOR),
// Invalid: below USDC_FACTOR precision
(1, 1),
(100, USDC_FACTOR),
(USDC_FACTOR - 1, USDC_FACTOR - 1),
(USDC_FACTOR - 1, USDC_FACTOR),
(USDC_FACTOR, USDC_FACTOR - 1),
// Valid: minimum valid amounts
(USDC_FACTOR, USDC_FACTOR),
(USDC_FACTOR, 2 * USDC_FACTOR),
// Valid: round numbers
(10 * USDC_FACTOR, 10 * USDC_FACTOR),
(100 * USDC_FACTOR, 100 * USDC_FACTOR),
(1000 * USDC_FACTOR, 1000 * USDC_FACTOR),
// Valid: large amounts
(i128::MAX / 2, i128::MAX / 2),
(i128::MAX, i128::MAX),
];

for &(amount, gross) in test_cases {
let rust_ok = rust_validate(amount, gross);
let (python_ok, _python_error) = python_validate(amount, gross);

assert_eq!(
rust_ok, python_ok,
"Differential mismatch for amount={} gross={}: Rust said {}, Python said {}",
amount, gross, rust_ok, python_ok
);
}
}

#[test]
fn differential_fuzz_boundary_cases() {
// Systematic boundary testing around USDC_FACTOR
let boundaries: &[i128] = &[
USDC_FACTOR - 2,
USDC_FACTOR - 1,
USDC_FACTOR,
USDC_FACTOR + 1,
USDC_FACTOR + 2,
2 * USDC_FACTOR - 1,
2 * USDC_FACTOR,
2 * USDC_FACTOR + 1,
];

for &a in boundaries {
for &g in boundaries {
if a > 0 && g > 0 && g >= a {
// Skip cases that would be too slow to test with Python subprocess each time
// (but this is a boundary test, not exhaustive)
let rust_ok = rust_validate(a, g);
let (python_ok, python_error) = python_validate(a, g);

assert_eq!(
rust_ok, python_ok,
"Differential mismatch at boundary: amount={} gross={}: Rust={}, Python={} ({:?})",
a, g, rust_ok, python_ok, python_error
);
}
}
}
}

#[test]
fn differential_fuzz_off_by_one() {
// Test values one off from precision boundaries
let values: &[i128] = &[
USDC_FACTOR - 1,
USDC_FACTOR,
USDC_FACTOR + 1,
10 * USDC_FACTOR - 1,
10 * USDC_FACTOR,
10 * USDC_FACTOR + 1,
];

for &amount in values {
for &gross in values {
if amount > 0 && gross >= amount {
let rust_ok = rust_validate(amount, gross);
let (python_ok, python_error) = python_validate(amount, gross);

assert_eq!(
rust_ok, python_ok,
"Off-by-one mismatch: amount={} gross={}: Rust={}, Python={} ({:?})",
amount, gross, rust_ok, python_ok, python_error
);
}
}
}
}
Loading