Skip to content
Open
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
29 changes: 29 additions & 0 deletions .github/workflows/sdk_tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: SDK Tests

on:
push:
branches: ["main"]
pull_request:
branches: ["main"]

jobs:
sdk-tests:
runs-on: ubuntu-latest
defaults:
run:
working-directory: zk/sdk

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20

- name: Install dependencies
run: npm install

- name: Run SDK tests
run: npm test
12 changes: 12 additions & 0 deletions gateway-contract/contracts/core_contract/core.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Core Contract Notes

## SMT Root Sequencing Requirement

`register_resolver` enforces strict root sequencing:

- `public_signals.old_root` must exactly equal the current on-chain SMT root.
- A successful `register_resolver` updates the on-chain root to `public_signals.new_root`.
- Any later call reusing the pre-update root is rejected as stale.

This replay protection prevents re-submitting proofs against an already-consumed root. In tests, the stale replay path is asserted to panic with `Error(Contract, #4)` (`StaleRoot`).

37 changes: 37 additions & 0 deletions gateway-contract/contracts/core_contract/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,43 @@ fn test_register_resolver_stale_root_fails() {
client.register_resolver(&caller, &hash, &dummy_proof(&env), &signals);
}

#[test]
#[should_panic(expected = "Error(Contract, #4)")]
fn test_register_resolver_stale_root_after_first_registration() {
let env = Env::default();
env.mock_all_auths();
let (_, client, initial_root) = setup_with_root(&env);

let caller = Address::generate(&env);
let first_hash = commitment(&env, 70);
let first_new_root = BytesN::from_array(&env, &[71u8; 32]);

// First registration succeeds and advances SMT root.
client.register_resolver(
&caller,
&first_hash,
&dummy_proof(&env),
&PublicSignals {
old_root: initial_root.clone(),
new_root: first_new_root,
},
);

let second_hash = commitment(&env, 72);
let second_new_root = BytesN::from_array(&env, &[73u8; 32]);

// Replay with the original old_root must fail as stale.
client.register_resolver(
&caller,
&second_hash,
&dummy_proof(&env),
&PublicSignals {
old_root: initial_root,
new_root: second_new_root,
},
);
}

#[test]
#[should_panic(expected = "Error(Contract, #6)")]
fn test_resolve_stellar_no_address_linked_when_not_set() {
Expand Down
15 changes: 15 additions & 0 deletions gateway-contract/contracts/escrow_contract/src/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -165,4 +165,19 @@ impl Events {
}
.publish(env);
}

/// Emits a DEPOSIT event with topics (symbol!("DEPOSIT"), commitment)
/// and data (owner, amount, new_balance).
pub fn deposit(
env: &Env,
commitment: BytesN<32>,
owner: Address,
amount: i128,
new_balance: i128,
) {
env.events().publish(
(symbol_short!("DEPOSIT"), commitment),
(owner, amount, new_balance),
);
}
}
33 changes: 33 additions & 0 deletions gateway-contract/contracts/escrow_contract/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,39 @@ impl EscrowContract {
Events::vault_crt(&env, commitment, token, owner);
}

/// Deposits tokens into an active vault.
///
/// The vault owner must authorize the call. Funds are transferred from the
/// owner to this contract and reflected in the vault's tracked balance.
///
/// ### Errors
/// - `InvalidAmount`: If `amount <= 0`.
/// - `VaultNotFound`: If the vault does not exist.
/// - `VaultInactive`: If the vault has been cancelled/inactivated.
pub fn deposit(env: Env, commitment: BytesN<32>, amount: i128) {
if amount <= 0 {
panic_with_error!(&env, EscrowError::InvalidAmount);
}

let config = read_vault_config(&env, &commitment)
.unwrap_or_else(|| panic_with_error!(&env, EscrowError::VaultNotFound));
config.owner.require_auth();

let mut state = read_vault_state(&env, &commitment)
.unwrap_or_else(|| panic_with_error!(&env, EscrowError::VaultNotFound));
if !state.is_active {
panic_with_error!(&env, EscrowError::VaultInactive);
}

let token_client = token::Client::new(&env, &config.token);
token_client.transfer(&config.owner, &env.current_contract_address(), &amount);
Comment on lines +130 to +131
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
# Find all needless-borrow candidates for current_contract_address.
rg -n --type rust '&env\.current_contract_address\(\)'

Repository: Alien-Protocol/Alien-Gateway

Length of output: 869


🏁 Script executed:

# First, let's look at the escrow_contract lib.rs around line 131 to understand the context
sed -n '125,135p' gateway-contract/contracts/escrow_contract/src/lib.rs

Repository: Alien-Protocol/Alien-Gateway

Length of output: 507


🏁 Script executed:

# Let's also check the full scope of the transfer calls and token client setup
sed -n '125,155p' gateway-contract/contracts/escrow_contract/src/lib.rs

Repository: Alien-Protocol/Alien-Gateway

Length of output: 1340


🏁 Script executed:

# Check what token module is being used - look for imports
head -50 gateway-contract/contracts/escrow_contract/src/lib.rs

Repository: Alien-Protocol/Alien-Gateway

Length of output: 1862


🏁 Script executed:

# Check if there's any custom token module definition in the codebase
fd -e rs | xargs rg -l 'impl.*transfer' | head -20

Repository: Alien-Protocol/Alien-Gateway

Length of output: 54


🏁 Script executed:

# Look for soroban-sdk version in Cargo.toml to understand what version is being used
find . -name "Cargo.toml" -exec grep -H "soroban-sdk" {} \;

Repository: Alien-Protocol/Alien-Gateway

Length of output: 1049


🏁 Script executed:

# Check the token client type to understand what transfer expects
rg -A5 'token::Client::new' gateway-contract/contracts/escrow_contract/src/lib.rs

Repository: Alien-Protocol/Alien-Gateway

Length of output: 1028


🏁 Script executed:

# Search for soroban-sdk documentation or check what Address type is
# Let's look for any type definitions or imports that might give us clues
rg 'Address' gateway-contract/contracts/escrow_contract/src/lib.rs -B2 -A2 | head -30

Repository: Alien-Protocol/Alien-Gateway

Length of output: 1330


🌐 Web query:

soroban-sdk token::Client transfer method signature Address parameter

💡 Result:

The transfer method signature of soroban_sdk::token::TokenClient is: rust pub fn transfer( &self, from: &Address, to: impl Into, amount: &i128, ) The first parameter is from: &Address, which specifies the source address holding the tokens to be transferred. The second parameter to accepts values convertible into MuxedAddress (including Address), representing the destination. This is confirmed in the official Soroban SDK documentation for TokenClient.

Citations:


🏁 Script executed:

# Let's check if there are compilation errors by looking at CI logs or checking other transfer patterns
# First, check all token_client.transfer calls in the codebase to see patterns
rg -B2 -A2 'token_client\.transfer\(' gateway-contract/contracts/

Repository: Alien-Protocol/Alien-Gateway

Length of output: 2066


🏁 Script executed:

# Let's check if this matches a Soroban SDK pattern issue
# Look at how Address is typically used as a parameter
rg 'to: &env' gateway-contract/ -A1 -B1

Repository: Alien-Protocol/Alien-Gateway

Length of output: 54


Remove needless borrow in token transfer call.

Line 131 should pass env.current_contract_address() directly instead of borrowing it. The transfer method's second parameter accepts impl Into<MuxedAddress>, which expects an owned Address value, not a reference.

Proposed fix
-        token_client.transfer(&config.owner, &env.current_contract_address(), &amount);
+        token_client.transfer(&config.owner, env.current_contract_address(), &amount);
🧰 Tools
🪛 GitHub Actions: Checks

[error] 131-131: Clippy failed: needless_borrows_for_generic_args — the borrowed expression implements the required traits. Help suggests replacing &env.current_contract_address() usage with env.current_contract_address().

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

In `@gateway-contract/contracts/escrow_contract/src/lib.rs` around lines 130 -
131, The token transfer call currently passes a borrowed address; update the
call in Escrow contract so token::Client::transfer receives
env.current_contract_address() by value (not &env.current_contract_address()),
i.e., remove the needless borrow when invoking token_client.transfer with
&config.owner, env.current_contract_address(), &amount so the second parameter
matches the transfer signature that accepts impl Into<MuxedAddress>.


state.balance += amount;
write_vault_state(&env, &commitment, &state);
Comment on lines +133 to +134
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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify whether balance updates in escrow use checked arithmetic.
rg -n --type rust 'balance\s*[\+\-]=|checked_add\(|checked_sub\(' gateway-contract/contracts/escrow_contract/src

Repository: Alien-Protocol/Alien-Gateway

Length of output: 524


🏁 Script executed:

cat -n gateway-contract/contracts/escrow_contract/src/lib.rs | sed -n '125,145p'

Repository: Alien-Protocol/Alien-Gateway

Length of output: 1043


🏁 Script executed:

cat -n gateway-contract/contracts/escrow_contract/src/lib.rs | sed -n '185,205p'

Repository: Alien-Protocol/Alien-Gateway

Length of output: 790


🏁 Script executed:

cat -n gateway-contract/contracts/escrow_contract/src/lib.rs | sed -n '400,415p'

Repository: Alien-Protocol/Alien-Gateway

Length of output: 647


🏁 Script executed:

rg -n "struct.*State|balance.*:" gateway-contract/contracts/escrow_contract/src --type rust -A 3 | head -30

Repository: Alien-Protocol/Alien-Gateway

Length of output: 2120


🏁 Script executed:

cat -n gateway-contract/contracts/escrow_contract/src/lib.rs | sed -n '370,410p'

Repository: Alien-Protocol/Alien-Gateway

Length of output: 1864


🏁 Script executed:

cat -n gateway-contract/contracts/escrow_contract/src/lib.rs | sed -n '100,138p'

Repository: Alien-Protocol/Alien-Gateway

Length of output: 1852


Fix unchecked addition overflow in vault balance deposit.

Line 133 uses unchecked addition (state.balance += amount). Even though amount > 0 is validated, the addition can overflow when state.balance approaches i128::MAX. For example: if state.balance = i128::MAX - 1 and amount = 10, the operation overflows and silently wraps, corrupting the balance.

Use checked arithmetic to detect and handle overflow:

Proposed fix
-        state.balance += amount;
+        state.balance = state
+            .balance
+            .checked_add(amount)
+            .unwrap_or_else(|| panic_with_error!(&env, EscrowError::InvalidAmount));
         write_vault_state(&env, &commitment, &state);
📝 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
state.balance += amount;
write_vault_state(&env, &commitment, &state);
state.balance = state
.balance
.checked_add(amount)
.unwrap_or_else(|| panic_with_error!(&env, EscrowError::InvalidAmount));
write_vault_state(&env, &commitment, &state);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@gateway-contract/contracts/escrow_contract/src/lib.rs` around lines 133 -
134, The unchecked addition state.balance += amount can overflow; replace it
with a checked add and handle the overflow path: compute new_balance =
state.balance.checked_add(amount) and if Some(v) set state.balance = v and call
write_vault_state(&env, &commitment, &state), otherwise return/abort with an
explicit error (e.g., Err/contract trap or a specific VaultBalanceOverflow
error) so the deposit is rejected; update the handler that currently mutates
state.balance and ensure the overflow branch does not call write_vault_state.


Events::deposit(&env, commitment, config.owner, amount, state.balance);
}

/// Schedules a payment from one vault to another.
///
/// Funds are reserved in the source vault immediately upon scheduling.
Expand Down
96 changes: 96 additions & 0 deletions gateway-contract/contracts/escrow_contract/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,102 @@ fn read_vault(env: &Env, contract_id: &Address, id: &BytesN<32>) -> VaultState {
})
}

#[test]
fn test_deposit_success() {
let env = Env::default();
env.mock_all_auths();
let (contract_id, client, token, _token_admin, from, _to) = setup_test(&env);

let owner = Address::generate(&env);
create_vault(&env, &contract_id, &from, &owner, &token, 0);

let token_admin_client = StellarAssetClient::new(&env, &token);
token_admin_client.mint(&owner, &500);

client.deposit(&from, &200);

env.as_contract(&contract_id, || {
let state: VaultState = env
.storage()
.persistent()
.get(&DataKey::VaultState(from.clone()))
.unwrap();
assert_eq!(state.balance, 200);
});

let token_client = TokenClient::new(&env, &token);
assert_eq!(token_client.balance(&owner), 300);
assert_eq!(token_client.balance(&contract_id), 200);

let deposit_events = env
.events()
.all()
.iter()
.filter(|(event_contract, _, _)| event_contract == &contract_id)
.count();
assert_eq!(deposit_events, 1);
}

#[test]
fn test_deposit_zero_amount_rejected() {
let env = Env::default();
env.mock_all_auths();
let (contract_id, client, token, _token_admin, from, _to) = setup_test(&env);

create_vault(
&env,
&contract_id,
&from,
&Address::generate(&env),
&token,
0,
);

let result = client.try_deposit(&from, &0);
assert!(matches!(
result,
Err(Ok(err)) if err == Error::from_contract_error(EscrowError::InvalidAmount as u32)
));
}

#[test]
#[should_panic]
fn test_deposit_non_owner_panics() {
let env = Env::default();
let (contract_id, client, token, _token_admin, from, _to) = setup_test(&env);

let owner = Address::generate(&env);
create_vault(&env, &contract_id, &from, &owner, &token, 0);

client.deposit(&from, &10);
}

#[test]
fn test_deposit_inactive_vault_rejected() {
let env = Env::default();
env.mock_all_auths();
let (contract_id, client, token, _token_admin, from, _to) = setup_test(&env);

let owner = Address::generate(&env);
create_vault(&env, &contract_id, &from, &owner, &token, 0);

env.as_contract(&contract_id, || {
let state = VaultState {
balance: 0,
is_active: false,
};
env.storage()
.persistent()
.set(&DataKey::VaultState(from.clone()), &state);
});

let result = client.try_deposit(&from, &10);
assert!(matches!(
result,
Err(Ok(err)) if err == Error::from_contract_error(EscrowError::VaultInactive as u32)
));
}

Comment on lines +88 to +183
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

PR objective mismatch: required SMT stale-root replay test/docs are not covered in this diff.

Issue #211 acceptance requires test_register_resolver_stale_root_after_first_registration plus core.md sequencing docs (core contract), but this changed segment adds escrow deposit tests instead.

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

In `@gateway-contract/contracts/escrow_contract/src/test.rs` around lines 88 -
183, This diff adds escrow deposit tests but misses the required SMT stale-root
replay acceptance items: add the test function named
test_register_resolver_stale_root_after_first_registration that reproduces the
stale-root replay scenario against the core contract (exercise register_resolver
/ resolve flows and assert the contract rejects a stale-root replay after the
first successful registration), and also update the core.md sequencing docs to
describe the expected registration ordering and stale-root replay protections;
implement the test using the same test harness conventions (Env, mock_all_auths,
contract client helpers) as other tests and ensure assertions compare the
returned Error::from_contract_error variant for the stale-root case.

#[test]
fn test_schedule_payment_success() {
let env = Env::default();
Expand Down
9 changes: 9 additions & 0 deletions zk/sdk/jest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
const config = {
preset: "ts-jest",
testEnvironment: "node",
roots: ["<rootDir>/src"],
testMatch: ["**/__tests__/**/*.test.ts"],
clearMocks: true,
};

export default config;
2 changes: 1 addition & 1 deletion zk/sdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@
"dependencies": {
"circomlibjs": "^0.1.7"
}
}
}
11 changes: 11 additions & 0 deletions zk/sdk/src/__tests__/smoke.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import * as sdk from "../index";

declare const describe: (name: string, fn: () => void) => void;
declare const it: (name: string, fn: () => void) => void;
declare const expect: (value: unknown) => { toBeDefined: () => void };

describe("zk sdk smoke", () => {
it("imports index without throwing", () => {
expect(sdk).toBeDefined();
});
});
Loading