Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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,
}
6 changes: 6 additions & 0 deletions gateway-contract/contracts/core_contract/src/events.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
use soroban_sdk::{contractevent, BytesN};

#[contractevent]
pub struct UsernameRegistered {
pub commitment: BytesN<32>,
}
#![allow(dead_code)]

use soroban_sdk::{symbol_short, Symbol};
Expand Down
74 changes: 62 additions & 12 deletions gateway-contract/contracts/core_contract/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
#![no_std]

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

Expand All @@ -8,30 +12,56 @@ use soroban_sdk::{
};
use types::ResolveData;

#[contract]
pub struct Contract;
#[cfg(test)]
mod test;

#[contracttype]
#[derive(Clone)]
pub enum DataKey {
Resolver(BytesN<32>),
}
use soroban_sdk::{contract, contractclient, contractimpl, panic_with_error, Address, BytesN, Env};
Comment on lines +8 to +11
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

Remove duplicate imports causing compile errors.

Line 16 re-imports symbols already imported in lines 9-11. The pipeline reports E0252 for each duplicate. Consolidate into a single import statement and add only the missing contractclient.

🐛 Proposed fix — single consolidated import
 #[cfg(test)]
 mod test;

-use soroban_sdk::{contract, contractclient, contractimpl, panic_with_error, Address, BytesN, Env};
+use soroban_sdk::{contract, contractclient, contractimpl, panic_with_error, Address, BytesN, Env};

Combined with the earlier fix (removing lines 9-11), this becomes the sole import.

🧰 Tools
🪛 GitHub Actions: Smart Contracts CI

[error] 16-16: Rust compile error E0252: the macro name contract is defined/imported multiple times. Compiler suggests removing the unnecessary import.


[error] 16-16: Rust compile error E0252: the macro name contractimpl is defined/imported multiple times. Compiler suggests removing the unnecessary import.


[error] 16-16: Rust compile error E0252: the macro name panic_with_error is defined/imported multiple times. Compiler suggests removing the unnecessary import.


[error] 16-16: Rust compile error E0252: the type name Address is defined/imported multiple times. Compiler suggests removing the unnecessary import.


[error] 16-16: Rust compile error E0252: the type name BytesN is defined/imported multiple times. Compiler suggests removing the unnecessary import.


[error] 16-16: Rust compile error E0252: the type name Env is defined/imported multiple times. Compiler suggests removing the unnecessary import.


[warning] 16-16: Rust warning: unused imports contract, contractimpl, panic_with_error, Address, BytesN, and Env (#[warn(unused_imports)]).

🤖 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 13 - 16,
Remove the duplicated import blocks and consolidate into a single use statement
that includes the missing symbol contractclient; specifically, eliminate the
redundant import lines that re-import contract, contractimpl, panic_with_error,
Address, BytesN, and Env, and replace them with one consolidated import
containing contract, contractclient, contractimpl, panic_with_error, Address,
BytesN, and Env so only one use declaration remains (locate the import(s) around
the top of the file where symbols like contract, contractimpl, panic_with_error,
Address, BytesN, and Env are currently brought into scope).


pub use crate::errors::ContractError;
pub use crate::events::UsernameRegistered;
pub use crate::types::{Proof, PublicSignals};

#[contract]
pub struct CoreContract;

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

#[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 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));

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.

}

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.

pub fn set_memo(env: Env, commitment: BytesN<32>, memo_id: u64) {
let key = DataKey::Resolver(commitment);
let mut data = env
Expand All @@ -51,6 +81,26 @@ impl Contract {
Some(data) => (data.wallet, data.memo),
None => panic_with_error!(&env, ResolverError::NotFound),
}

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

UsernameRegistered {
commitment: public_signals.commitment,
}
.publish(&env);
}

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)
}
}

Expand Down
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.

}
17 changes: 14 additions & 3 deletions gateway-contract/contracts/core_contract/src/types.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,20 @@
use soroban_sdk::{contracttype, BytesN};
use soroban_sdk::{contracttype, Address, Symbol};

#[contracttype]
#[derive(Clone)]
pub struct AddressMetadata {
pub label: Symbol,
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Proof {
pub a: BytesN<32>,
pub b: BytesN<32>,
pub c: BytesN<32>,
}

#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PublicSignals {
pub old_root: BytesN<32>,
pub new_root: BytesN<32>,
pub commitment: BytesN<32>,
}

#[contracttype]
Expand Down
Loading