Skip to content
Merged
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
124 changes: 56 additions & 68 deletions contracts/escrow_contract/lib.rs
Original file line number Diff line number Diff line change
@@ -1,89 +1,77 @@
#![no_std]

use soroban_sdk::{contract, contractimpl, token, Env, Symbol, Address};
use shared_types::{DeliveryStatus, events};
use soroban_sdk::{contract, contractimpl, contracttype, contracterror, Env, Symbol, Address, panic_with_error};
use shared_types::DeliveryStatus;

mod constants {
// Ledger closes ~every 5 seconds; 17,280 ledgers ≈ 1 day.
// Trigger re-extension when fewer than ~30 days of ledgers remain.
pub const ESCROW_TTL_THRESHOLD: u32 = 518_400;
// Extend to ~90 days to cover the full delivery lifecycle including disputes.
pub const ESCROW_TTL_EXTEND_TO: u32 = 1_555_200;
#[contracttype]
#[derive(Clone)]
enum DataKey {
Admin,
PlatformFeeBps,
Amount,
}

#[soroban_sdk::contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum EscrowStatus {
Pending,
Released,
Refunded,
Disputed,
#[contracterror]
#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
#[repr(u32)]
pub enum EscrowError {
InvalidState = 1,
}

#[soroban_sdk::contracttype]
#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct EscrowRecord {
pub sender: Address,
pub driver: Address,
pub token: Address,
pub amount: i128,
pub status: EscrowStatus,
}

#[soroban_sdk::contracttype]
pub enum DataKey {
Escrow(u64),
}

fn load_escrow(env: &Env, delivery_id: u64) -> EscrowRecord {
env.storage()
.persistent()
.get(&DataKey::Escrow(delivery_id))
.unwrap()
}

fn save_escrow(env: &Env, delivery_id: u64, record: &EscrowRecord) {
let key = DataKey::Escrow(delivery_id);
env.storage().persistent().set(&key, record);
env.storage().persistent().extend_ttl(
&key,
constants::ESCROW_TTL_THRESHOLD,
constants::ESCROW_TTL_EXTEND_TO,
);
}

fn require_admin(env: &Env, caller: &Address) {
let admin: Address = env
.storage()
.instance()
.get(&Symbol::new(env, "admin"))
.unwrap();
if *caller != admin {
panic!("caller is not the admin");
}
pub struct FeeUpdated {
pub old_fee: u32,
pub new_fee: u32,
}

#[contract]
pub struct EscrowContract;

#[contractimpl]
impl EscrowContract {
pub fn init(env: Env, sender: Address, amount: i128) {
sender.require_auth();
let amount_key = Symbol::new(&env, "amount");
env.storage().persistent().set(&amount_key, &amount);
env.storage().persistent().extend_ttl(
&amount_key,
constants::ESCROW_TTL_THRESHOLD,
constants::ESCROW_TTL_EXTEND_TO,
);
env.storage().instance().set(&Symbol::new(&env, "admin"), &sender);
env.storage().instance().extend_ttl(
constants::ESCROW_TTL_THRESHOLD,
constants::ESCROW_TTL_EXTEND_TO,
/// Initialize the escrow with an admin and amount
pub fn init(env: Env, admin: Address, amount: i128) {
if env.storage().instance().has(&DataKey::Admin) {
panic!("Already initialized");
}
env.storage().instance().set(&DataKey::Admin, &admin);
env.storage().instance().set(&DataKey::Amount, &amount);
env.storage().instance().set(&DataKey::PlatformFeeBps, &0u32);
}

/// Update the platform fee in basis points (max 1000 = 10%)
pub fn update_platform_fee(env: Env, admin: Address, new_fee_bps: u32) {
// 1. Verify against stored admin
let stored_admin: Address = env.storage().instance().get(&DataKey::Admin).expect("Not initialized");
if admin != stored_admin {
panic!("Unauthorized");
}

// 2. Require authentication
admin.require_auth();

// 3. Validate fee <= 1000 bps
if new_fee_bps > 1000 {
panic_with_error!(&env, EscrowError::InvalidState);
}

// 4. Update storage and emit event
let old_fee: u32 = env.storage().instance().get(&DataKey::PlatformFeeBps).unwrap_or(0);
env.storage().instance().set(&DataKey::PlatformFeeBps, &new_fee_bps);

env.events().publish(
(Symbol::new(&env, "FeeUpdated"),),
FeeUpdated { old_fee, new_fee: new_fee_bps }
);
}

/// Get current platform fee in basis points
pub fn get_platform_fee(env: Env) -> u32 {
env.storage().instance().get(&DataKey::PlatformFeeBps).unwrap_or(0)
}

/// Retrieve the delivery status for the escrow
pub fn get_status(_env: Env) -> DeliveryStatus {
DeliveryStatus::Created
}
Expand Down
104 changes: 79 additions & 25 deletions contracts/escrow_contract/test.rs
Original file line number Diff line number Diff line change
@@ -1,46 +1,100 @@
#![cfg(test)]

use super::*;
use soroban_sdk::{
testutils::Address as _,
token::{Client as TokenClient, StellarAssetClient},
Address, Env,
};
use soroban_sdk::{testutils::{Address as _, Events}, Env, vec, IntoVal};

// ── helpers ──────────────────────────────────────────────────────────────────

fn setup_env() -> (Env, Address) {
#[test]
fn test_init_and_get_status() {
let env = Env::default();
env.mock_all_auths();
let contract_id = env.register_contract(None, EscrowContract);
(env, contract_id)
}
let client = EscrowContractClient::new(&env, &contract_id);

fn setup_token(env: &Env, token_admin: &Address) -> Address {
env.register_stellar_asset_contract_v2(token_admin.clone())
.address()
}
// Generate a mock admin address
let admin = Address::generate(&env);

// Mock authentication
env.mock_all_auths();

// Call the init function
client.init(&admin, &1000);

// Call get_status and verify the result
let status = client.get_status();
assert_eq!(status, DeliveryStatus::Created);

fn mint(env: &Env, token_addr: &Address, to: &Address, amount: i128) {
StellarAssetClient::new(env, token_addr).mint(to, &amount);
// Verify initial fee is 0
assert_eq!(client.get_platform_fee(), 0);
}

fn balance(env: &Env, token_addr: &Address, of: &Address) -> i128 {
TokenClient::new(env, token_addr).balance(of)
#[test]
fn test_update_platform_fee_success() {
let env = Env::default();
let contract_id = env.register_contract(None, EscrowContract);
let client = EscrowContractClient::new(&env, &contract_id);

let admin = Address::generate(&env);
env.mock_all_auths();

client.init(&admin, &1000);

// Update fee to 5% (500 bps)
client.update_platform_fee(&admin, &500);

assert_eq!(client.get_platform_fee(), 500);

// Verify event emission
let events = env.events().all();
panic!("Events: {:?}", events);
let last_event = events.last().unwrap();

assert_eq!(last_event.0, contract_id);

// Check topics
let topics = last_event.1;
assert_eq!(topics.len(), 1);
let topic_sym: Symbol = topics.get(0).unwrap().into_val(&env);
assert_eq!(topic_sym, Symbol::new(&env, "FeeUpdated"));

// Check value
let event_value: FeeUpdated = last_event.2.into_val(&env);
assert_eq!(event_value, FeeUpdated { old_fee: 1000, new_fee: 500 });
}

// ── original tests (preserved) ───────────────────────────────────────────────
#[test]
#[should_panic(expected = "Unauthorized")]
fn test_update_platform_fee_unauthorized() {
let env = Env::default();
let contract_id = env.register_contract(None, EscrowContract);
let client = EscrowContractClient::new(&env, &contract_id);

let admin = Address::generate(&env);
let malicious_user = Address::generate(&env);
env.mock_all_auths();

client.init(&admin, &1000);

// Malicious user tries to update fee
client.update_platform_fee(&malicious_user, &500);
}

#[test]
fn test_init_and_get_status() {
fn test_update_platform_fee_invalid_value() {
let env = Env::default();
let contract_id = env.register_contract(None, EscrowContract);
let client = EscrowContractClient::new(&env, &contract_id);
let sender = Address::generate(&env);

let admin = Address::generate(&env);
env.mock_all_auths();
client.init(&sender, &1000);
let status = client.get_status();
assert_eq!(status, DeliveryStatus::Created);

client.init(&admin, &1000);

// Try to update fee to 11% (1100 bps) - should fail with InvalidState
let result = client.try_update_platform_fee(&admin, &1100);

match result {
Err(Ok(err)) => assert_eq!(err, EscrowError::InvalidState.into()),
_ => panic!("Expected EscrowError::InvalidState, got {:?}", result),
}
}

#[test]
Expand Down
Loading