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

#[contractevent]
pub struct UsernameRegistered {
pub commitment: BytesN<32>,
}

#[contractevent]
pub struct MerkleRootUpdated {
pub old_root: BytesN<32>,
pub new_root: BytesN<32>,
}
#![allow(dead_code)]

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

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

#[cfg(test)]
mod test;

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

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

Expand All @@ -9,29 +22,63 @@ use soroban_sdk::{
use types::ResolveData;

#[contract]
pub struct Contract;
pub struct CoreContract;

#[contractclient(name = "VerifierContractClient")]
pub trait VerifierContract {
fn verify_proof(env: Env, proof: Proof, public_signals: PublicSignals) -> bool;
}

#[contracttype]
#[derive(Clone)]
pub enum DataKey {
Resolver(BytesN<32>),
fn current_merkle_root(env: &Env) -> BytesN<32> {
storage::get_merkle_root(env)
.unwrap_or_else(|| panic_with_error!(env, ContractError::NotInitialized))
}

fn update_merkle_root(env: &Env, old_root: BytesN<32>, new_root: BytesN<32>) {
storage::set_merkle_root(env, &new_root);

MerkleRootUpdated { old_root, new_root }.publish(env);
#[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_merkle_root(&env, &root);
}
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:

cd gateway-contract && find . -type f -name "*.rs" | head -20

Repository: Alien-Protocol/Alien-Gateway

Length of output: 896


🏁 Script executed:

cd gateway-contract && rg "fn init" --type rust -B 2 -A 15

Repository: Alien-Protocol/Alien-Gateway

Length of output: 2467


🏁 Script executed:

cd gateway-contract && rg "invoke_contract_check_host_approval|__check_auth" --type rust

Repository: Alien-Protocol/Alien-Gateway

Length of output: 54


🏁 Script executed:

cd gateway-contract && git log --oneline --all | head -20

Repository: Alien-Protocol/Alien-Gateway

Length of output: 1611


🏁 Script executed:

cd gateway-contract && find . -type f \( -name "*.toml" -o -name "*.md" \) | head -20

Repository: Alien-Protocol/Alien-Gateway

Length of output: 404


🏁 Script executed:

cd gateway-contract && sed -n '1,100p' contracts/core_contract/src/test.rs | grep -A 20 "fn setup"

Repository: Alien-Protocol/Alien-Gateway

Length of output: 766


🏁 Script executed:

cd gateway-contract && rg "fn setup" contracts/core_contract/src/test.rs -A 25

Repository: Alien-Protocol/Alien-Gateway

Length of output: 884


🏁 Script executed:

cd gateway-contract && cat -n contracts/core_contract/src/storage.rs | head -80

Repository: Alien-Protocol/Alien-Gateway

Length of output: 1558


🏁 Script executed:

cd gateway-contract && rg "is_initialized\|initialization" contracts/core_contract/src/ -B 2 -A 3

Repository: Alien-Protocol/Alien-Gateway

Length of output: 54


🏁 Script executed:

cd gateway-contract && cat contracts/core_contract/Cargo.toml | grep -A 10 "soroban"

Repository: Alien-Protocol/Alien-Gateway

Length of output: 187


🏁 Script executed:

cd gateway-contract && rg "contractimpl|contract_fn|soroban_sdk" contracts/core_contract/src/lib.rs | head -20

Repository: Alien-Protocol/Alien-Gateway

Length of output: 186


🏁 Script executed:

cd gateway-contract && find . -type f -name "*.rs" -exec grep -l "deploy\|register" {} \; | head -10

Repository: Alien-Protocol/Alien-Gateway

Length of output: 195


🏁 Script executed:

cd gateway-contract && cat -n contracts/core_contract/src/lib.rs | sed -n '1,70p'

Repository: Alien-Protocol/Alien-Gateway

Length of output: 2839


🏁 Script executed:

cd gateway-contract && rg "require_auth\|require\|auth" contracts/core_contract/src/lib.rs

Repository: Alien-Protocol/Alien-Gateway

Length of output: 54


🏁 Script executed:

cd gateway-contract && find . -type f -name "*.rs" -path "*/factory*" -o -name "*deploy*" | xargs cat 2>/dev/null | head -100

Repository: Alien-Protocol/Alien-Gateway

Length of output: 207


🏁 Script executed:

cd gateway-contract && cat contracts/core_contract/core.md 2>/dev/null | head -150

Repository: Alien-Protocol/Alien-Gateway

Length of output: 54


🏁 Script executed:

cd gateway-contract && cat readme.md 2>/dev/null | grep -A 20 -i "init\|deploy\|setup"

Repository: Alien-Protocol/Alien-Gateway

Length of output: 54


🏁 Script executed:

cd gateway-contract && rg "fn init" contracts/ -B 5 -A 20 --type rust | grep -E "(init|pub|auth|require)" | head -30

Repository: Alien-Protocol/Alien-Gateway

Length of output: 1143


🏁 Script executed:

cd gateway-contract && web_search "Soroban public contract function authorization require_auth"

Repository: Alien-Protocol/Alien-Gateway

Length of output: 122


🏁 Script executed:

cd gateway-contract && find . -type f \( -name "*.md" -o -name "*.txt" \) -exec grep -l "init\|deploy" {} \;

Repository: Alien-Protocol/Alien-Gateway

Length of output: 54


🌐 Web query:

Soroban contract require_auth authorization public functions

💡 Result:

In Soroban, any exported contract function (often called “public”) is callable by anyone unless your code explicitly enforces authorization. Soroban does not auto-restrict access based on “public/private”; you enforce permissions by calling require_auth (or require_auth_for_args) on the Address that must approve the action. [1][9]

How require_auth works (what it guarantees)

  • some_address.require_auth() tells the Soroban host: “this invocation must be authorized by some_address”.
  • The host then handles signature verification + replay prevention (you don’t manually verify signatures in the contract). [9]
  • If some_address is a contract address (account abstraction), the host calls that contract’s __check_auth hook to validate the authorization. [1]

Pattern for “public” functions that must be protected

Call require_auth() at the start of the function for the party whose rights are being exercised:

pub fn withdraw(env: Env, from: Address, amount: i128) {
    from.require_auth();          // must be authorized by `from`
    // ... mutate balances / transfer tokens ...
}

This is the core pattern used in Soroban auth examples. [2]

require_auth_for_args (more specific authorization)

Use require_auth_for_args(...) when you want the signature to be bound to specific arguments / intent, which enables safer delegation patterns (e.g., delegated signers / smart accounts authorizing particular calls). [3]

Cross-contract calls and “sub-invocation” authorization

If your contract calls another contract that does require_auth, Soroban’s auth framework supports authorizing those sub-contract calls as part of the same authorized invocation tree (you generally don’t need special wiring beyond using require_auth). [1]

Practical “when do I require auth?” rule of thumb

  • Read-only access to a user’s data: usually no require_auth needed.
  • Mutating a user’s state in a way that isn’t strictly beneficial (e.g., decreasing balance / transferring out): you should require_auth. [1]

Sources: Stellar authorization docs and examples. [1][2][3][9]


🏁 Script executed:

cd gateway-contract && cat -n contracts/core_contract/src/lib.rs | sed -n '30,80p'

Repository: Alien-Protocol/Alien-Gateway

Length of output: 2224


🏁 Script executed:

cd gateway-contract && rg "TODO\|FIXME\|XXX\|HACK\|NOTE" contracts/core_contract/src/lib.rs

Repository: Alien-Protocol/Alien-Gateway

Length of output: 54


🏁 Script executed:

cd gateway-contract && git log --all --oneline -- contracts/core_contract/src/lib.rs | head -10

Repository: Alien-Protocol/Alien-Gateway

Length of output: 474


🏁 Script executed:

cd gateway-contract && git show d864384 | grep -A 5 -B 5 "init\|auth" | head -40

Repository: Alien-Protocol/Alien-Gateway

Length of output: 2151


🏁 Script executed:

cd gateway-contract && git show d864384 | grep -A 50 "init\|Entry point" | head -80

Repository: Alien-Protocol/Alien-Gateway

Length of output: 4103


🏁 Script executed:

cd gateway-contract && git show d864384 | grep -B 5 -A 5 "^| \`init\`"

Repository: Alien-Protocol/Alien-Gateway

Length of output: 54


🏁 Script executed:

cd gateway-contract && git show d864384 | grep -C 10 "core_contract" | grep -A 30 "Entry point"

Repository: Alien-Protocol/Alien-Gateway

Length of output: 2951


🏁 Script executed:

cd gateway-contract && git show d864384:contracts/core_contract/src/lib.rs 2>/dev/null | head -100

Repository: Alien-Protocol/Alien-Gateway

Length of output: 54


🏁 Script executed:

cd gateway-contract && rg "fn init\|pub fn " contracts/core_contract/src/lib.rs | head -15

Repository: Alien-Protocol/Alien-Gateway

Length of output: 54


🏁 Script executed:

cd gateway-contract && git log --all --oneline --diff-filter=A -- contracts/core_contract/src/lib.rs

Repository: Alien-Protocol/Alien-Gateway

Length of output: 183


🏁 Script executed:

cd gateway-contract && wc -l contracts/core_contract/src/lib.rs && grep -c "pub fn" contracts/core_contract/src/lib.rs

Repository: Alien-Protocol/Alien-Gateway

Length of output: 111


🏁 Script executed:

cd gateway-contract && cat -n contracts/core_contract/src/lib.rs | grep "pub fn"

Repository: Alien-Protocol/Alien-Gateway

Length of output: 430


Add require_auth() to gate initialization to a trusted deployer.

This entrypoint lacks authorization checks, so any caller can invoke init() first and permanently set both the verifier and initial merkle root. If deployment and initialization are separate transactions, a frontrunner can anchor arbitrary state before the intended owner. Call require_auth() on a trusted deployer address, or ensure initialization is atomic with contract deployment.

🤖 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 38 - 45,
The init() entrypoint must require authorization before mutating state: call
env.require_auth(...) at the start of init to restrict who can initialize
(either require_auth(&env, &verifier) if the verifier is the trusted deployer,
or add an explicit deployer Address parameter and call
env.require_auth(&deployer)); keep the existing storage::is_initialized check
and ContractError::AlreadyInitialized panic, and only call storage::set_verifier
and storage::set_merkle_root after the require_auth check succeeds.


pub fn submit_proof(env: Env, proof: Proof, public_signals: PublicSignals) {
let current_root = current_merkle_root(&env);

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

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

storage::store_commitment(&env, &public_signals.commitment);
update_merkle_root(&env, current_root, public_signals.new_root.clone());

UsernameRegistered {
commitment: public_signals.commitment,
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 +98,19 @@ impl Contract {
Some(data) => (data.wallet, data.memo),
None => panic_with_error!(&env, ResolverError::NotFound),
}
.publish(&env);
}

pub fn get_merkle_root(env: Env) -> BytesN<32> {
current_merkle_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
44 changes: 44 additions & 0 deletions gateway-contract/contracts/core_contract/src/storage.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
use soroban_sdk::{contracttype, Address, BytesN, Env};

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

pub fn is_initialized(env: &Env) -> bool {
env.storage().persistent().has(&DataKey::CurrentMerkleRoot)
&& env.storage().instance().has(&DataKey::Verifier)
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

Fail closed on partial initialization state.

is_initialized() currently returns false unless both keys exist, but only one missing key is enough to make a second init() dangerous. If the verifier entry ever disappears while the persistent root remains, init() can overwrite the anchored root outside the proof flow. At minimum, treat either key as “already initialized”; ideally keep the verifier in the same durability class as the root.

🛠️ Minimal fail-closed fix
 pub fn is_initialized(env: &Env) -> bool {
     env.storage().persistent().has(&DataKey::CurrentMerkleRoot)
-        && env.storage().instance().has(&DataKey::Verifier)
+        || env.storage().instance().has(&DataKey::Verifier)
 }

Also applies to: 26-31

🤖 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 11 -
13, The current is_initialized function uses a logical AND so it returns false
if either DataKey::CurrentMerkleRoot or DataKey::Verifier is missing, which
allows init() to run when one of the two is present — change the check in
is_initialized (function name: is_initialized) to treat the presence of either
key as “already initialized” (use logical OR between
env.storage().persistent().has(&DataKey::CurrentMerkleRoot) and
env.storage().instance().has(&DataKey::Verifier)) so init() will fail-closed;
additionally consider moving the Verifier entry to persistent storage (make
DataKey::Verifier stored via env.storage().persistent()) so both items share
durability to avoid asymmetric deletion risks.

}

pub fn get_merkle_root(env: &Env) -> Option<BytesN<32>> {
env.storage().persistent().get(&DataKey::CurrentMerkleRoot)
}

pub fn set_merkle_root(env: &Env, root: &BytesN<32>) {
env.storage()
.persistent()
.set(&DataKey::CurrentMerkleRoot, 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);
}
Loading
Loading