Skip to content
Closed
Show file tree
Hide file tree
Changes from 4 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
11 changes: 11 additions & 0 deletions gateway-contract/contracts/core_contract/src/errors.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
use soroban_sdk::contracterror;

#[contracterror]
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum ContractError {
AlreadyInitialized = 1,
NotInitialized = 2,
RootMismatch = 3,
InvalidProof = 4,
DuplicateCommitment = 5,
}
20 changes: 5 additions & 15 deletions gateway-contract/contracts/core_contract/src/events.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,6 @@
#![allow(dead_code)]
use soroban_sdk::{contractevent, BytesN};

use soroban_sdk::{symbol_short, Symbol};

pub const INIT_EVENT: Symbol = symbol_short!("INIT");
pub const TRANSFER_EVENT: Symbol = symbol_short!("TRANSFER");
pub const REGISTER_EVENT: Symbol = symbol_short!("REGISTER");
pub const ROOT_UPDATED: Symbol = symbol_short!("ROOT_UPD");
pub const MASTER_SET: Symbol = symbol_short!("MSTR_SET");
pub const ADDR_ADDED: Symbol = symbol_short!("ADDR_ADD");
pub const CHAIN_ADD: Symbol = symbol_short!("CHAIN_ADD");
pub const CHAIN_REM: Symbol = symbol_short!("CHAIN_REM");
pub const VAULT_CREATE: Symbol = symbol_short!("VAULT_CRT");
pub const DEPOSIT: Symbol = symbol_short!("DEPOSIT");
pub const WITHDRAW: Symbol = symbol_short!("WITHDRAW");
pub const SCHED_PAY: Symbol = symbol_short!("SCHED_PAY");
#[contractevent]
pub struct UsernameRegistered {
pub commitment: BytesN<32>,
}
98 changes: 58 additions & 40 deletions gateway-contract/contracts/core_contract/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,58 +1,76 @@
#![no_std]

pub mod events;
pub mod types;
mod errors;
mod events;
mod storage;
mod types;

use soroban_sdk::{
contract, contracterror, contractimpl, contracttype, panic_with_error, Address, BytesN, Env,
};
use types::ResolveData;
#[cfg(test)]
mod test;

#[contract]
pub struct Contract;
use soroban_sdk::{contract, contractclient, contractimpl, panic_with_error, Address, BytesN, Env};

#[contracttype]
#[derive(Clone)]
pub enum DataKey {
Resolver(BytesN<32>),
}
pub use crate::errors::ContractError;
pub use crate::events::UsernameRegistered;
pub use crate::types::{Proof, PublicSignals};

#[contract]
pub struct CoreContract;

#[contracterror]
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum ResolverError {
NotFound = 1,
#[contractclient(name = "VerifierContractClient")]
pub trait VerifierContract {
fn verify_proof(env: Env, proof: Proof, public_signals: PublicSignals) -> bool;
}

#[contractimpl]
impl Contract {
pub fn register_resolver(env: Env, commitment: BytesN<32>, wallet: Address, memo: Option<u64>) {
let key = DataKey::Resolver(commitment);
let data = ResolveData { wallet, memo };
impl CoreContract {
pub fn init(env: Env, verifier: Address, root: BytesN<32>) {
if storage::is_initialized(&env) {
panic_with_error!(&env, ContractError::AlreadyInitialized);
}

env.storage().persistent().set(&key, &data);
storage::set_verifier(&env, &verifier);
storage::set_root(&env, &root);
Comment on lines +27 to +33
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Gate init behind authorization.

The first caller permanently chooses verifier and root. That lets a front-runner initialize the contract with malicious values and lock the contract into bad state forever.

🔐 Possible fix
-    pub fn init(env: Env, verifier: Address, root: BytesN<32>) {
+    pub fn init(env: Env, admin: Address, verifier: Address, root: BytesN<32>) {
+        admin.require_auth();
         if storage::is_initialized(&env) {
             panic_with_error!(&env, ContractError::AlreadyInitialized);
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
pub fn init(env: Env, verifier: Address, root: BytesN<32>) {
if storage::is_initialized(&env) {
panic_with_error!(&env, ContractError::AlreadyInitialized);
}
env.storage().persistent().set(&key, &data);
storage::set_verifier(&env, &verifier);
storage::set_root(&env, &root);
pub fn init(env: Env, admin: Address, verifier: Address, root: BytesN<32>) {
admin.require_auth();
if storage::is_initialized(&env) {
panic_with_error!(&env, ContractError::AlreadyInitialized);
}
storage::set_verifier(&env, &verifier);
storage::set_root(&env, &root);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@gateway-contract/contracts/core_contract/src/lib.rs` around lines 27 - 33,
The init function must be gated by an authorization check so a front-runner
cannot set verifier/root; before calling storage::is_initialized or
storage::set_* add a check that the invoker is an authorized admin (for example
compare env.invoker() to the expected deployer/admin address or a stored admin
value) and panic_with_error! with an Unauthorized/ContractError if it does not
match; keep the rest of init (storage::is_initialized, storage::set_verifier,
storage::set_root) intact and perform the authorization check at the very start
of the init function (referencing init, storage::is_initialized,
storage::set_verifier, storage::set_root).

}

pub fn set_memo(env: Env, commitment: BytesN<32>, memo_id: u64) {
let key = DataKey::Resolver(commitment);
let mut data = env
.storage()
.persistent()
.get::<DataKey, ResolveData>(&key)
.unwrap_or_else(|| panic_with_error!(&env, ResolverError::NotFound));
pub fn submit_proof(env: Env, proof: Proof, public_signals: PublicSignals) {
let current_root = storage::get_root(&env)
.unwrap_or_else(|| panic_with_error!(&env, ContractError::NotInitialized));

data.memo = Some(memo_id);
env.storage().persistent().set(&key, &data);
}
if current_root != public_signals.old_root.clone() {
panic_with_error!(&env, ContractError::RootMismatch);
}

if storage::has_commitment(&env, &public_signals.commitment) {
panic_with_error!(&env, ContractError::DuplicateCommitment);
Comment on lines +40 to +45
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

duplicate_commitment_is_rejected cannot pass with this check order.

Once the first submission updates the root, replaying the same proof fails at RootMismatch before DuplicateCommitment. Either swap these branches or update the test/docs to make stale-root precedence explicit.

🔁 If duplicate should win, swap the checks
-        if current_root != public_signals.old_root.clone() {
-            panic_with_error!(&env, ContractError::RootMismatch);
-        }
-
         if storage::has_commitment(&env, &public_signals.commitment) {
             panic_with_error!(&env, ContractError::DuplicateCommitment);
         }
+
+        if current_root != public_signals.old_root.clone() {
+            panic_with_error!(&env, ContractError::RootMismatch);
+        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@gateway-contract/contracts/core_contract/src/lib.rs` around lines 40 - 45,
The current ordering checks current_root vs public_signals.old_root first and
panics with ContractError::RootMismatch, preventing DuplicateCommitment from
ever being reached; to make duplicate-commitment take precedence, move the
storage::has_commitment(&env, &public_signals.commitment) check (and its panic
ContractError::DuplicateCommitment) before the root comparison, i.e. perform the
duplicate check first in the function that references current_root and
public_signals.old_root, or alternatively update tests/docs to state that
RootMismatch has precedence if you prefer keeping the existing order.

}

pub fn resolve(env: Env, commitment: BytesN<32>) -> (Address, Option<u64>) {
let key = DataKey::Resolver(commitment);
let verifier = storage::get_verifier(&env)
.unwrap_or_else(|| panic_with_error!(&env, ContractError::NotInitialized));
let verifier_client = VerifierContractClient::new(&env, &verifier);
let is_valid = verifier_client.verify_proof(&proof, &public_signals);
if !is_valid {
panic_with_error!(&env, ContractError::InvalidProof);
Comment on lines +48 to +53
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

fd -t f "lib.rs" gateway-contract

Repository: Alien-Protocol/Alien-Gateway

Length of output: 287


🏁 Script executed:

wc -l gateway-contract/contracts/core_contract/src/lib.rs

Repository: Alien-Protocol/Alien-Gateway

Length of output: 126


🏁 Script executed:

head -80 gateway-contract/contracts/core_contract/src/lib.rs | cat -n

Repository: Alien-Protocol/Alien-Gateway

Length of output: 3082


🏁 Script executed:

rg "ContractError" gateway-contract/contracts/core_contract/src/lib.rs -A 2 -B 2

Repository: Alien-Protocol/Alien-Gateway

Length of output: 1402


🏁 Script executed:

rg "enum ContractError" gateway-contract -A 20

Repository: Alien-Protocol/Alien-Gateway

Length of output: 608


🏁 Script executed:

rg "verify_proof" gateway-contract -B 5 -A 5

Repository: Alien-Protocol/Alien-Gateway

Length of output: 2986


🏁 Script executed:

rg "VerifierContractClient" gateway-contract -B 2 -A 2

Repository: Alien-Protocol/Alien-Gateway

Length of output: 2096


🌐 Web query:

Soroban SDK contractclient cross-contract call error handling Result

💡 Result:

In Soroban Rust, generated ContractClient methods (and env.invoke_contract) panic on any cross-contract failure. To handle errors as a Result, use the try_ variants: client.try_method(...) or env.try_invoke_contract(...). (developers.stellar.org)

What try_ returns (nested Result)

A try_ call returns a nested result you can pattern-match:

match client.try_add(&x, &y) {
    Ok(Ok(v)) => { /* call succeeded, value decoded */ }
    Ok(Err(_conv_err)) => { /* call ok, but return value decode/conversion failed */ }
    Err(Ok(contract_err)) => { /* callee returned a spec-defined contract error */ }
    Err(Err(status)) => { /* host/system/unrecognized error (Status) */ }
}

This exact shape is shown in the Stellar cross-contract guide. (developers.stellar.org)

Simple fallback pattern

let v = client.try_add(&x, &y).unwrap_or(Ok(0)).unwrap();

This avoids panics and defaults to 0 if the call errors. (developers.stellar.org)

Making “callee errors” matchable (spec-defined)

If you want Err(Ok(contract_err)) to be meaningful, the called contract should define errors with #[contracterror] and return Result<_, Error> (or escalate with panic_with_error!), so callers can decode/handle them. (developers.stellar.org)

Citations:


Use try_verify_proof to map cross-contract invocation errors to ContractError.

The code calls verifier_client.verify_proof(...) without error handling. Soroban's generated ContractClient methods panic on any cross-contract failure (invalid address, contract trap, etc.). Replace with verifier_client.try_verify_proof(...) and map the nested Result to an appropriate ContractError variant so callers receive stable errors instead of raw invocation failures.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@gateway-contract/contracts/core_contract/src/lib.rs` around lines 48 - 53,
The current call uses VerifierContractClient::verify_proof which panics on
cross-contract failures; replace it with
VerifierContractClient::try_verify_proof(&proof, &public_signals) and handle the
nested Result by mapping invocation errors and false/ok values into
ContractError variants (e.g., map invocation Err to
ContractError::VerifierInvocationFailed or ContractError::NotInitialized as
appropriate and map a false/Ok(false) to ContractError::InvalidProof) so callers
receive stable ContractError values instead of raw panics; update the
surrounding code that currently unwraps storage::get_verifier and the is_valid
check to use the new try_* result mapping.

}

storage::store_commitment(&env, &public_signals.commitment);
storage::set_root(&env, &public_signals.new_root);

match env.storage().persistent().get::<DataKey, ResolveData>(&key) {
Some(data) => (data.wallet, data.memo),
None => panic_with_error!(&env, ResolverError::NotFound),
UsernameRegistered {
commitment: public_signals.commitment,
}
.publish(&env);
}
}

#[cfg(test)]
mod test;
pub fn get_root(env: Env) -> Option<BytesN<32>> {
storage::get_root(&env)
}

pub fn get_verifier(env: Env) -> Option<Address> {
storage::get_verifier(&env)
}

pub fn has_commitment(env: Env, commitment: BytesN<32>) -> bool {
storage::has_commitment(&env, &commitment)
}
}
41 changes: 41 additions & 0 deletions gateway-contract/contracts/core_contract/src/storage.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
use soroban_sdk::{contracttype, Address, BytesN, Env};

#[contracttype]
#[derive(Clone)]
pub enum DataKey {
Root,
Verifier,
Commitment(BytesN<32>),
}

pub fn is_initialized(env: &Env) -> bool {
env.storage().instance().has(&DataKey::Root) && env.storage().instance().has(&DataKey::Verifier)
}

pub fn get_root(env: &Env) -> Option<BytesN<32>> {
env.storage().instance().get(&DataKey::Root)
}

pub fn set_root(env: &Env, root: &BytesN<32>) {
env.storage().instance().set(&DataKey::Root, root);
}

pub fn get_verifier(env: &Env) -> Option<Address> {
env.storage().instance().get(&DataKey::Verifier)
}

pub fn set_verifier(env: &Env, verifier: &Address) {
env.storage().instance().set(&DataKey::Verifier, verifier);
}

pub fn has_commitment(env: &Env, commitment: &BytesN<32>) -> bool {
env.storage()
.persistent()
.has(&DataKey::Commitment(commitment.clone()))
}

pub fn store_commitment(env: &Env, commitment: &BytesN<32>) {
env.storage()
.persistent()
.set(&DataKey::Commitment(commitment.clone()), &true);
Comment on lines +15 to +40
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n gateway-contract/contracts/core_contract/src/storage.rs

Repository: Alien-Protocol/Alien-Gateway

Length of output: 1439


🏁 Script executed:

rg "extend_ttl|TTL|ttl" gateway-contract/contracts/core_contract/src/ -i

Repository: Alien-Protocol/Alien-Gateway

Length of output: 54


🏁 Script executed:

find gateway-contract/contracts/core_contract/src -name "*.rs" -type f

Repository: Alien-Protocol/Alien-Gateway

Length of output: 514


🏁 Script executed:

cat -n gateway-contract/contracts/core_contract/src/lib.rs

Repository: Alien-Protocol/Alien-Gateway

Length of output: 3091


🌐 Web query:

Soroban contract storage TTL extension requirements instance persistent storage

💡 Result:

Instance storage TTL extension (contract instance)

  • What gets extended: All instance storage is stored in a single contract instance ledger entry (one shared TTL), so extending instance TTL extends all instance keys, the contract instance, and (via the same call) the contract code entry. The code + instance are checked/extended independently, so one may bump while the other doesn’t. [1]
  • How it works (SDK): env.storage().instance().extend_ttl(threshold, extend_to) only changes TTL if current TTL < threshold, and then sets it so TTL becomes extend_to (otherwise it’s a no-op). [2]

Persistent storage TTL extension (per key)

  • What gets extended: Each persistent entry has its own TTL and must be extended individually (per storage key). [1]
  • How it works (SDK): env.storage().persistent().extend_ttl(&key, threshold, extend_to) only extends if that entry’s TTL < threshold, then sets TTL to extend_to. [2]

Extending TTL from outside the contract (transaction operation)

  • You can extend TTL using ExtendFootprintTTLOp, which extends the entries specified in the operation’s footprint so their TTL becomes at least extendTo ledgers from now. [1]

Sources: [1] Stellar Docs – Smart contract state archival; [2] soroban-sdk storage.rs source/docs on extend_ttl.


Extend TTL for long-lived storage entries to prevent state expiry.

Instance storage (Root and Verifier) and persistent storage (Commitments) require explicit TTL extension on Soroban—without it, these entries will expire and be archived. This breaks contract initialization state (Root and Verifier queries will fail) and disables duplicate protection (commitments can be replayed after archival). Add extend_ttl calls in the storage helpers: instance TTL in set_root and set_verifier, and persistent TTL per-key in store_commitment.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@gateway-contract/contracts/core_contract/src/storage.rs` around lines 15 -
40, The instance entries set by set_root and set_verifier and the per-key
persistent entry set by store_commitment must extend their TTLs to avoid Soroban
archiving; after calling env.storage().instance().set(&DataKey::Root, ...) and
env.storage().instance().set(&DataKey::Verifier, ...) add an instance extend_ttl
call for the same DataKey (Root and Verifier) with a long/maximum TTL, and after
env.storage().persistent().set(&DataKey::Commitment(commitment.clone()), &true)
call the persistent extend_ttl for that same
DataKey::Commitment(commitment.clone()) so each stored item’s TTL is refreshed
and won’t expire.

}
165 changes: 143 additions & 22 deletions gateway-contract/contracts/core_contract/src/test.rs
Original file line number Diff line number Diff line change
@@ -1,39 +1,160 @@
#![cfg(test)]

use crate::{Contract, ContractClient};
use soroban_sdk::testutils::Address as _;
use soroban_sdk::{Address, BytesN, Env};
use crate::errors::ContractError;
use crate::types::{Proof, PublicSignals};
use crate::{CoreContract, CoreContractClient};
Comment on lines +3 to +5
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for PublicSignals constructions with only old_root and new_root (no commitment)
rg -n -A 3 'PublicSignals\s*\{' gateway-contract/contracts/core_contract/src/test.rs | grep -v commitment

Repository: Alien-Protocol/Alien-Gateway

Length of output: 1668


🏁 Script executed:

# Check the actual PublicSignals definition(s) in types.rs
find gateway-contract -name "types.rs" -type f

Repository: Alien-Protocol/Alien-Gateway

Length of output: 295


🏁 Script executed:

# Look at the PublicSignals definitions and their contexts
rg -n "^pub struct PublicSignals" gateway-contract/contracts/core_contract/src/ -A 5

Repository: Alien-Protocol/Alien-Gateway

Length of output: 855


🏁 Script executed:

# Check the structure and section boundaries in test.rs to understand which lines belong to which tests
rg -n "^#\[test\]|^fn test_" gateway-contract/contracts/core_contract/src/test.rs

Repository: Alien-Protocol/Alien-Gateway

Length of output: 2513


🏁 Script executed:

# Look at the first few lines of new test code (around line 88) to see which PublicSignals variant
sed -n '85,100p' gateway-contract/contracts/core_contract/src/test.rs

Repository: Alien-Protocol/Alien-Gateway

Length of output: 583


🏁 Script executed:

# Look at test code around the old tests section (around line 162) to see which variant
sed -n '160,180p' gateway-contract/contracts/core_contract/src/test.rs

Repository: Alien-Protocol/Alien-Gateway

Length of output: 613


🏁 Script executed:

# Check if the 3-field PublicSignals (with commitment) is used anywhere in test.rs
rg -n "commitment.*:" gateway-contract/contracts/core_contract/src/test.rs | head -20

Repository: Alien-Protocol/Alien-Gateway

Length of output: 54


🏁 Script executed:

# Check if setup_with_root, commitment, and dummy_proof are defined in test.rs
rg -n "^fn setup_with_root|^fn commitment|^fn dummy_proof" gateway-contract/contracts/core_contract/src/test.rs

Repository: Alien-Protocol/Alien-Gateway

Length of output: 54


🏁 Script executed:

# Check if these functions are defined anywhere or if they're truly missing
rg -n "fn setup_with_root|fn commitment|fn dummy_proof" gateway-contract/contracts/core_contract/src/

Repository: Alien-Protocol/Alien-Gateway

Length of output: 54


Old tests at lines 162+ reference undefined functions and will break when the duplicate PublicSignals is removed.

The duplicate 2-field PublicSignals (at types.rs:40-43) is used exclusively by old tests at lines 162+. These tests also depend on missing helper functions: setup_with_root, commitment, and dummy_proof, which are not defined anywhere in the codebase. Once the duplicate definition is removed, these tests must be either removed or refactored to use the correct 3-field PublicSignals variant and provide the missing helpers.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@gateway-contract/contracts/core_contract/src/test.rs` around lines 3 - 5, Old
tests rely on a duplicate 2-field PublicSignals and undefined helpers
(setup_with_root, commitment, dummy_proof) so they will break after removing the
duplicate; update those tests (the ones around the original lines ~162+) to use
the correct 3-field PublicSignals variant from types.rs, and either implement
the missing helper functions (add setup_with_root to initialize contract state,
commitment to compute the expected commitment, and dummy_proof to return a valid
Proof/Proof-like placeholder) or remove the obsolete tests entirely; ensure any
test assertions reference the correct field names of the 3-field PublicSignals
and that CoreContract/CoreContractClient usages match their real APIs.

use soroban_sdk::testutils::Events as _;
use soroban_sdk::{
contract, contractimpl, contracttype, map, Address, BytesN, Env, Error, IntoVal, Map, Symbol,
Val,
};

fn setup_test(env: &Env) -> (ContractClient<'_>, BytesN<32>, Address) {
let contract_id = env.register(Contract, ());
let client = ContractClient::new(env, &contract_id);
let commitment = BytesN::from_array(env, &[7u8; 32]);
let wallet = Address::generate(env);
#[contract]
struct MockVerifierContract;

(client, commitment, wallet)
#[contracttype]
#[derive(Clone)]
enum MockVerifierDataKey {
ShouldVerify,
}

#[contractimpl]
impl MockVerifierContract {
pub fn set_should_verify(env: Env, should_verify: bool) {
env.storage()
.instance()
.set(&MockVerifierDataKey::ShouldVerify, &should_verify);
}

pub fn verify_proof(env: Env, proof: Proof, public_signals: PublicSignals) -> bool {
let should_verify = env
.storage()
.instance()
.get::<MockVerifierDataKey, bool>(&MockVerifierDataKey::ShouldVerify)
.unwrap_or(true);

should_verify
&& proof.a == public_signals.old_root
&& proof.b == public_signals.new_root
&& proof.c == public_signals.commitment
}
}

fn bytes(env: &Env, byte: u8) -> BytesN<32> {
BytesN::from_array(env, &[byte; 32])
}

fn offchain_register_fixture(env: &Env) -> (Proof, PublicSignals) {
let old_root = bytes(env, 0);
let new_root = bytes(env, 42);
let commitment = bytes(env, 7);

(
Proof {
a: old_root.clone(),
b: new_root.clone(),
c: commitment.clone(),
},
PublicSignals {
old_root,
new_root,
commitment,
},
)
}

fn setup(env: &Env) -> (Address, CoreContractClient<'_>, Address) {
let verifier_id = env.register(MockVerifierContract, ());
let verifier_client = MockVerifierContractClient::new(env, &verifier_id);
verifier_client.set_should_verify(&true);

let contract_id = env.register(CoreContract, ());
let client = CoreContractClient::new(env, &contract_id);
client.init(&verifier_id, &bytes(env, 0));

(contract_id, client, verifier_id)
}

fn assert_submit_error(
result: Result<
Result<(), soroban_sdk::ConversionError>,
Result<Error, soroban_sdk::InvokeError>,
>,
expected: ContractError,
) {
assert_eq!(result, Err(Ok(expected.into())));
}

#[test]
fn test_resolve_returns_none_when_no_memo() {
fn submit_proof_succeeds_and_updates_state() {
let env = Env::default();
let (client, commitment, wallet) = setup_test(&env);
let (contract_id, client, _) = setup(&env);
let (proof, public_signals) = offchain_register_fixture(&env);

client.register_resolver(&commitment, &wallet, &None);
client.submit_proof(&proof, &public_signals);

let (resolved_wallet, memo) = client.resolve(&commitment);
assert_eq!(resolved_wallet, wallet);
assert_eq!(memo, None);
assert_eq!(client.get_root(), Some(public_signals.new_root.clone()));
assert!(client.has_commitment(&public_signals.commitment));

let expected_event_data: Map<Symbol, Val> = map![
&env,
(
Symbol::new(&env, "commitment"),
public_signals.commitment.clone().into_val(&env)
)
];
assert_eq!(
env.events().all(),
soroban_sdk::vec![
&env,
(
contract_id,
(Symbol::new(&env, "username_registered"),).into_val(&env),
expected_event_data.into_val(&env),
)
]
);
}

#[test]
fn invalid_proof_is_rejected() {
let env = Env::default();
let (_, client, verifier_id) = setup(&env);
let verifier_client = MockVerifierContractClient::new(&env, &verifier_id);
verifier_client.set_should_verify(&false);

let (proof, public_signals) = offchain_register_fixture(&env);
let result = client.try_submit_proof(&proof, &public_signals);

assert_submit_error(result, ContractError::InvalidProof);
assert!(!client.has_commitment(&public_signals.commitment));
assert_eq!(client.get_root(), Some(public_signals.old_root));
}

#[test]
fn test_set_memo_and_resolve_flow() {
fn stale_root_is_rejected() {
let env = Env::default();
let (client, commitment, wallet) = setup_test(&env);
let (_, client, _) = setup(&env);
let (proof, mut public_signals) = offchain_register_fixture(&env);
public_signals.old_root = bytes(&env, 1);

let result = client.try_submit_proof(&proof, &public_signals);

assert_submit_error(result, ContractError::RootMismatch);
assert!(!client.has_commitment(&public_signals.commitment));
assert_eq!(client.get_root(), Some(bytes(&env, 0)));
}

#[test]
fn duplicate_commitment_is_rejected() {
let env = Env::default();
let (_, client, _) = setup(&env);
let (proof, public_signals) = offchain_register_fixture(&env);

client.submit_proof(&proof, &public_signals);

client.register_resolver(&commitment, &wallet, &None);
client.set_memo(&commitment, &4242u64);
let duplicate_result = client.try_submit_proof(&proof, &public_signals);

let (resolved_wallet, memo) = client.resolve(&commitment);
assert_eq!(resolved_wallet, wallet);
assert_eq!(memo, Some(4242u64));
assert_submit_error(duplicate_result, ContractError::DuplicateCommitment);
assert_eq!(client.get_root(), Some(public_signals.new_root));
}
Loading