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
16 changes: 16 additions & 0 deletions Timebased Escrows/contracts/escrow/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
[package]
name = "escrow"
version = "0.0.0"
edition = "2021"
publish = false

[lib]
crate-type = ["cdylib"]

[dependencies]
soroban-sdk = { workspace = true }
soroban-token-sdk = { workspace = true }

[dev-dependencies]
soroban-sdk = { workspace = true, features = ["testutils"] }
soroban-token-sdk = { workspace = true }
210 changes: 210 additions & 0 deletions Timebased Escrows/contracts/escrow/src/contract.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
use soroban_sdk::{contract, contractimpl, Address, BytesN, Env, Map, String, Symbol, Val, Vec};

use crate::core::{DisputeManager, EscrowManager, MilestoneManager};
use crate::error::ContractError;
use crate::events::handler::{
ChgEsc, DisEsc, DisputeResolved, EscrowDisputed, ExtTtlEvt, FundEsc, InitEsc,
MilestoneApproved, MilestoneStatusChanged, WithdrawMemo,
};
use crate::storage::types::{AddressBalance, Escrow};

#[contract]
pub struct EscrowContract;

#[contractimpl]
impl EscrowContract {
pub fn __constructor() {}

pub fn deploy(
env: Env,
deployer: Address,
wasm_hash: BytesN<32>,
salt: BytesN<32>,
init_fn: Symbol,
init_args: Vec<Val>,
constructor_args: Vec<Val>,
) -> (Address, Val) {
if deployer != env.current_contract_address() {
deployer.require_auth();
}

let deployed_address = env
.deployer()
.with_address(deployer, salt)
.deploy_v2(wasm_hash, constructor_args);

let res: Val = env.invoke_contract(&deployed_address, &init_fn, init_args);
(deployed_address, res)
}

////////////////////////
// Escrow /////
////////////////////////

pub fn initialize_escrow(e: &Env, escrow_properties: Escrow) -> Result<Escrow, ContractError> {
let initialized_escrow = EscrowManager::initialize_escrow(e, escrow_properties)?;
InitEsc {
escrow: initialized_escrow.clone(),
}
.publish(e);
Ok(initialized_escrow)
}

pub fn fund_escrow(
e: &Env,
signer: Address,
expected_escrow: Escrow,
amount: i128,
) -> Result<(), ContractError> {
EscrowManager::fund_escrow(e, &signer, &expected_escrow, amount)?;
FundEsc { signer, amount }.publish(e);
Ok(())
}

pub fn release_funds(
e: &Env,
release_signer: Address,
trustless_work_address: Address,
) -> Result<(), ContractError> {
EscrowManager::release_funds(e, &release_signer, &trustless_work_address)?;
DisEsc { release_signer }.publish(e);
Ok(())
}

pub fn withdraw_with_memo(e: &Env) -> Result<(), ContractError> {
let escrow = EscrowManager::get_escrow(e)?;
EscrowManager::withdraw_with_memo(e, &e.current_contract_address())?;
WithdrawMemo {
amount: escrow.withdrawal_state.pending_receiver_amount,
receiver: escrow.roles.receiver,
memo: escrow.receiver_memo,
}
.publish(e);
Ok(())
}
Comment on lines +74 to +84
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

Memo verification path unclear; function appears permissionless.

  • Where is the check that the transaction’s memo matches the stored receiver memo? The contract method doesn’t accept a memo parameter and I don’t see a call that reads an L1 memo from Env.

  • If on-chain memo access isn’t supported, the contract cannot verify memos; enforcement must live in the client/backend. In that case, update docs/tests to reflect “memo required by client, not verified on-chain.”

  • Authorization: anyone can call withdraw_with_memo here. Confirm EscrowManager::withdraw_with_memo enforces:

    • fees_released == true,
    • pending_receiver_amount > 0,
    • receiver_memo.is_some() && not empty,
    • idempotency (subsequent calls revert),
    • sets flags.released = true and pending to 0 after transfer.

If not already enforced, add these checks in the manager/validators.


🌐 Web query:

Can a Soroban contract access the Stellar transaction memo (e.g., via soroban_sdk::Env) to verify it on-chain?
If yes, what API is used and in which Soroban protocol/sdk version?

💡 Result:

Short answer: No — a Soroban contract cannot directly read the Stellar transaction memo from env (there is no Env API that returns the transaction memo). The memo can be used in Soroban authorization credentials (CAP‑0064 adds a txMemo field for SorobanAddressCredentialsV2 and the host validates it), but that memo-checking happens in the host/authorization layer, not by the contract code via soroban_sdk::Env. [1][2][3]

Sources

  • Soroban Env / SDK docs (Env API shows ledger/auth context access but no tx memo accessor). [1]
  • CAP‑0064 (adds txMemo to Soroban credentials; host validates tx memo as part of authorization). [2]
  • Relayer / docs note: memos are not supported on Soroban contract operations (attempting to include a memo with invoke_contract/create_contract/upload_wasm results in an error). [3]

References

  1. soroban_sdk Env documentation (API listing). [https://docs.rs/soroban-sdk/latest/soroban_sdk/struct.Env.html]
  2. CAP‑0064 in stellar-protocol (txMemo in SorobanAddressCredentialsV2). [https://github.com/stellar/stellar-protocol/blob/master/core/cap-0064.md]
  3. Relayer / docs statement that memos are not supported for Soroban contract operations. [https://docs.openzeppelin.com/relayer/1.1.x/stellar]


---

🏁 Script executed:

```shell
#!/bin/bash
set -eux
# locate contract.rs
find . -type f -name 'contract.rs'
# locate EscrowManager definitions
rg -nP 'struct\s+EscrowManager\b' -g '*.rs'
rg -nP 'impl\s+EscrowManager\b' -g '*.rs'
# locate withdraw_with_memo definition
rg -nP 'fn\s+withdraw_with_memo\b' -g '*.rs'

Length of output: 1055


🏁 Script executed:

#!/bin/bash
# Extract EscrowManager::withdraw_with_memo implementation for inspection
sed -n '140,260p' "Timebased Escrows/contracts/escrow/src/core/escrow.rs"

Length of output: 1215


🏁 Script executed:

#!/bin/bash
# Locate and show context around validate_withdrawal_conditions definition
rg -nP -C5 'fn\s+validate_withdrawal_conditions' -g '*.rs'

Length of output: 1057


🏁 Script executed:

#!/bin/bash
sed -n '1,200p' "Timebased Escrows/contracts/escrow/src/core/validators/escrow.rs"

Length of output: 4321


Cannot verify tx memo on-chain; enforce off-chain and restrict withdrawal to receiver

Soroban Env has no API for reading the Stellar transaction memo—memo validation must happen in your client/backend and update docs/tests to reflect “memo required off-chain, not verified on-chain.” The existing validate_withdrawal_conditions already enforces fees_released, pending_receiver_amount > 0, non-empty stored memo, idempotency, and state reset. To ensure only the designated receiver can trigger withdrawal, add an Env::caller() == escrow.roles.receiver check in withdraw_with_memo.


pub fn update_escrow(
e: &Env,
plataform_address: Address,
escrow_properties: Escrow,
) -> Result<Escrow, ContractError> {
let updated_escrow = EscrowManager::change_escrow_properties(
e,
&plataform_address,
escrow_properties.clone(),
)?;
ChgEsc {
platform: plataform_address,
engagement_id: escrow_properties.engagement_id.clone(),
}
.publish(e);
Ok(updated_escrow)
}
Comment on lines +86 to +102
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

Typo: plataform_address → platform_address (public API).

Public API misspelling propagates across clients/tests.

-    pub fn update_escrow(
-        e: &Env,
-        plataform_address: Address,
-        escrow_properties: Escrow,
-    ) -> Result<Escrow, ContractError> {
-        let updated_escrow = EscrowManager::change_escrow_properties(
-            e,
-            &plataform_address,
-            escrow_properties.clone(),
-        )?;
-        ChgEsc {
-            platform: plataform_address,
+    pub fn update_escrow(
+        e: &Env,
+        platform_address: Address,
+        escrow_properties: Escrow,
+    ) -> Result<Escrow, ContractError> {
+        let updated_escrow = EscrowManager::change_escrow_properties(
+            e,
+            &platform_address,
+            escrow_properties.clone(),
+        )?;
+        ChgEsc {
+            platform: platform_address,
             engagement_id: escrow_properties.engagement_id.clone(),
         }
         .publish(e);
         Ok(updated_escrow)
     }

Run to find other occurrences:


🏁 Script executed:

#!/bin/bash
rg -nP 'plataform_address|Plataform' -C2

Length of output: 6034


Rename all occurrences of plataform_address to platform_address across public APIs and core modules

  • Misspelling appears in:
    • Timebased Escrows/contracts/escrow/src/contract.rs (update_escrow)
    • Timebased Escrows/contracts/engagement/src/contract.rs (change_escrow_properties)
    • Timebased Escrows/contracts/engagement/src/core/escrow.rs (parameter, comparisons, auth)
    • Identical occurrences in Price-Oracle-Contract/contracts/engagement/...
  • This is a breaking change to your public API—update all function signatures, call sites, events, client code and tests in both repositories.
  • Use an automated rename/refactor tool or IDE to ensure consistency and avoid missing any references.
🤖 Prompt for AI Agents
In Timebased Escrows/contracts/escrow/src/contract.rs around lines 86 to 102,
the identifier `plataform_address` is misspelled and must be renamed to
`platform_address` across public APIs and core modules; update the function
signature, parameter name, local references, event fields, and any call sites to
use `platform_address`, then propagate the same rename to Timebased
Escrows/contracts/engagement/src/contract.rs, Timebased
Escrows/contracts/engagement/src/core/escrow.rs and the identical locations in
Price-Oracle-Contract (including function signatures, comparisons, auth checks,
event structs, clients and tests); perform the change with an automated
rename/refactor tool or IDE to ensure all references (including public API docs,
exported symbols, and tests) are updated consistently, run compilation and
tests, and adjust any external client code to match the new API name.


pub fn get_escrow(e: &Env) -> Result<Escrow, ContractError> {
EscrowManager::get_escrow(e)
}

pub fn get_escrow_by_contract_id(
e: &Env,
contract_id: Address,
) -> Result<Escrow, ContractError> {
EscrowManager::get_escrow_by_contract_id(e, &contract_id)
}

pub fn get_multiple_escrow_balances(
e: &Env,
addresses: Vec<Address>,
) -> Result<Vec<AddressBalance>, ContractError> {
EscrowManager::get_multiple_escrow_balances(e, addresses)
}

////////////////////////
// Admin / TTL /////
////////////////////////

pub fn extend_contract_ttl(
e: &Env,
platform_address: Address,
ledgers_to_extend: u32,
) -> Result<(), ContractError> {
platform_address.require_auth();

let escrow = EscrowManager::get_escrow(e)?;
if platform_address != escrow.roles.platform_address {
return Err(ContractError::OnlyPlatformAddressExecuteThisFunction);
}

let min_ledgers = 1u32;
e.storage()
.instance()
.extend_ttl(min_ledgers, ledgers_to_extend);

ExtTtlEvt {
platform: platform_address,
ledgers_to_extend,
}
.publish(e);

Ok(())
}

////////////////////////
// Milestones /////
////////////////////////

pub fn change_milestone_status(
e: Env,
milestone_index: i128,
new_status: String,
new_evidence: Option<String>,
service_provider: Address,
) -> Result<(), ContractError> {
let escrow = MilestoneManager::change_milestone_status(
&e,
milestone_index,
new_status,
new_evidence,
service_provider,
)?;
MilestoneStatusChanged { escrow }.publish(&e);
Ok(())
}

pub fn approve_milestone(
e: Env,
milestone_index: i128,
approver: Address,
) -> Result<(), ContractError> {
let escrow =
MilestoneManager::change_milestone_approved_flag(&e, milestone_index, approver)?;
MilestoneApproved { escrow }.publish(&e);
Ok(())
}

////////////////////////
// Disputes /////
////////////////////////

pub fn resolve_dispute(
e: Env,
dispute_resolver: Address,
trustless_work_address: Address,
distributions: Map<Address, i128>,
) -> Result<(), ContractError> {
let escrow = DisputeManager::resolve_dispute(
&e,
dispute_resolver,
trustless_work_address,
distributions,
)?;
DisputeResolved { escrow }.publish(&e);
Ok(())
}

pub fn dispute_escrow(e: Env, signer: Address) -> Result<(), ContractError> {
let escrow = DisputeManager::dispute_escrow(&e, signer)?;
EscrowDisputed { escrow }.publish(&e);
Ok(())
}
}
94 changes: 94 additions & 0 deletions Timebased Escrows/contracts/escrow/src/core/dispute.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
use soroban_sdk::token::Client as TokenClient;
use soroban_sdk::{Address, Env, Map};

use crate::core::escrow::EscrowManager;
use crate::error::ContractError;
use crate::modules::{
fee::{FeeCalculator, FeeCalculatorTrait},
math::{BasicArithmetic, BasicMath},
};
Comment on lines +6 to +9
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

Fix rounding/overdraft and overflow in fee pro‑rata distribution.

  • Problem: fee_share uses integer division; sum(fee_shares) ≤ total_fees. Because fees are transferred first, sum(net_amounts) can exceed the remaining balance, causing a transfer to fail (insufficient balance) or leave dust if order changes.
  • Problem: amount * total_fees can overflow i128 for large balances.
  • Fix: Distribute from the net pot (receiver_amount) using SafeMath::safe_mul_div, track assigned sum, and send the deterministic remainder to the last beneficiary. This guarantees fees + net == total without overflow.

Apply:

@@
-use crate::modules::{
-    fee::{FeeCalculator, FeeCalculatorTrait},
-    math::{BasicArithmetic, BasicMath},
-};
+use crate::modules::{
+    fee::{FeeCalculator, FeeCalculatorTrait},
+    math::{BasicArithmetic, BasicMath, SafeMath},
+};
@@
-        let fee_result = FeeCalculator::calculate_standard_fees(total, escrow.platform_fee)?;
-        let total_fees =
-            BasicMath::safe_add(fee_result.trustless_work_fee, fee_result.platform_fee)?;
+        let fee_result = FeeCalculator::calculate_standard_fees(total, escrow.platform_fee)?;
+        let total_fees =
+            BasicMath::safe_add(fee_result.trustless_work_fee, fee_result.platform_fee)?;
+        let net_pot = fee_result.receiver_amount;
@@
-        for (addr, amount) in distributions.iter() {
-            if amount <= 0 {
-                continue;
-            }
-            let fee_share = (amount * (total_fees as i128)) / total;
-            let net_amount = amount - fee_share;
-            if net_amount > 0 {
-                token_client.transfer(&contract_address, &addr, &net_amount);
-            }
-        }
+        // Pro‑rata net distribution with deterministic remainder allocation
+        let mut assigned: i128 = 0;
+        let mut last_addr: Option<Address> = None;
+        for (addr, amount) in distributions.iter() {
+            if amount <= 0 {
+                continue;
+            }
+            last_addr = Some(addr.clone());
+            let share = SafeMath::safe_mul_div(amount, net_pot, total)?;
+            if share > 0 {
+                token_client.transfer(&contract_address, &addr, &share);
+                assigned = BasicMath::safe_add(assigned, share)?;
+            }
+        }
+        let remainder = BasicMath::safe_sub(net_pot, assigned)?;
+        if remainder > 0 {
+            if let Some(addr) = last_addr {
+                token_client.transfer(&contract_address, &addr, &remainder);
+            }
+        }

Rationale anchored by existing primitives (SafeMath::safe_mul_div, BasicMath). Based on provided snippets.

Also applies to: 47-50, 66-75

🤖 Prompt for AI Agents
In Timebased Escrows/contracts/escrow/src/core/dispute.rs around lines 6-9 (and
also apply same change to ranges 47-50 and 66-75), fix the fee pro‑rata
distribution by computing fee shares from the net pot (receiver_amount) rather
than gross amount and by using SafeMath::safe_mul_div to avoid i128 overflow;
compute each beneficiary's fee_share = SafeMath::safe_mul_div(receiver_amount,
beneficiary_weight, total_weight), accumulate assigned_sum and for the final
beneficiary set fee_share = receiver_amount - assigned_sum to absorb any
rounding remainder so that sum(fee_shares) + sum(net_amounts) == total, and
replace any direct amount * total_fees multiplication with safe_mul_div calls
and bounds-checked arithmetic to prevent overdraft and overflow.

use crate::storage::types::{DataKey, Escrow};

use super::validators::dispute::{
validate_dispute_flag_change_conditions, validate_dispute_resolution_conditions,
};

pub struct DisputeManager;

impl DisputeManager {
pub fn resolve_dispute(
e: &Env,
dispute_resolver: Address,
trustless_work_address: Address,
distributions: Map<Address, i128>,
) -> Result<Escrow, ContractError> {
dispute_resolver.require_auth();
let mut escrow = EscrowManager::get_escrow(e)?;
let contract_address = e.current_contract_address();

let token_client = TokenClient::new(&e, &escrow.trustline.address);
let current_balance = token_client.balance(&contract_address);

let mut total: i128 = 0;
for (_addr, amount) in distributions.iter() {
if amount <= 0 {
return Err(ContractError::AmountsToBeTransferredShouldBePositive);
}
total = BasicMath::safe_add(total, amount)?;
}

validate_dispute_resolution_conditions(
&escrow,
&dispute_resolver,
current_balance,
total,
)?;

let fee_result = FeeCalculator::calculate_standard_fees(total, escrow.platform_fee)?;
let total_fees =
BasicMath::safe_add(fee_result.trustless_work_fee, fee_result.platform_fee)?;

if fee_result.trustless_work_fee > 0 {
token_client.transfer(
&contract_address,
&trustless_work_address,
&fee_result.trustless_work_fee,
);
}
Comment on lines +51 to +57
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

Validate the trustless_work fee recipient; do not take it from the caller.

trustless_work_address is supplied by the invoker and not validated against a canonical/configured address. A (compromised) dispute_resolver could redirect TW fees. Persist this address at init (e.g., in instance storage/roles) and enforce equality here, or hardcode via contract config.

Follow‑up: If you already store TW address elsewhere, please wire it here and reject mismatches.

Would you like me to draft a storage key (e.g., DataKey::TrustlessWorkAddress) change and migration?


🏁 Script executed:

#!/bin/bash
# Locate and display dispute.rs context
FILE=$(fd --hidden 'dispute.rs' -t f | head -n1)
sed -n '1,100p' "$FILE"
# Search all occurrences of trustless_work_address
rg -n 'trustless_work_address' -C3 .

Length of output: 46561


🏁 Script executed:

rg -n "enum DataKey" -C3 "Timebased Escrows/contracts/escrow/src/storage/types.rs"

Length of output: 98


🏁 Script executed:

#!/bin/bash
rg -n "pub struct Escrow" -C3 "Timebased Escrows/contracts/escrow/src/core/escrow.rs"

Length of output: 206


Enforce a configured trustless_work_address rather than using the caller’s value.
Define a storage key (e.g. DataKey::TrustlessWorkAddress), persist the canonical TW address at init or via a setter, and in resolve_dispute reject calls where the passed trustless_work_address does not equal the stored value.

if fee_result.platform_fee > 0 {
token_client.transfer(
&contract_address,
&escrow.roles.platform_address,
&fee_result.platform_fee,
);
}

for (addr, amount) in distributions.iter() {
if amount <= 0 {
continue;
}
let fee_share = (amount * (total_fees as i128)) / total;
let net_amount = amount - fee_share;
if net_amount > 0 {
token_client.transfer(&contract_address, &addr, &net_amount);
}
}

escrow.flags.resolved = true;
escrow.flags.disputed = false;
e.storage().instance().set(&DataKey::Escrow, &escrow);

Ok(escrow)
}

pub fn dispute_escrow(e: &Env, signer: Address) -> Result<Escrow, ContractError> {
signer.require_auth();
let mut escrow = EscrowManager::get_escrow(e)?;
validate_dispute_flag_change_conditions(&escrow, &signer)?;

escrow.flags.disputed = true;
e.storage().instance().set(&DataKey::Escrow, &escrow);

Ok(escrow)
}
}
Loading