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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,5 @@ coverage/
.agents/
issue.md
COMMIT.md
contracts/target
target
115 changes: 72 additions & 43 deletions contracts/multi_hop_swap/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
#![no_std]
use soroban_sdk::{contract, contractimpl, contracttype, symbol_short, vec, Env, Address, Vec};
use soroban_sdk::{contract, contractimpl, contracttype, contractclient, symbol_short, vec, Env, Address, Vec, token};

/// One leg of a swap route.
#[contracttype]
#[derive(Clone)]
pub struct Hop {
pub pool: Address,
pub token_in: Address,
pub token_out: Address,
pub amount_in: i128,
pub min_amount_out: i128,
}
Expand All @@ -15,79 +17,106 @@ pub struct Hop {
#[derive(Clone)]
pub struct HopResult {
pub pool: Address,
pub token_in: Address,
pub token_out: Address,
pub amount_in: i128,
pub amount_out: i128,
}

/// Persistent pool stateprice ratio stored as (numer, denom).
#[contracttype]
#[derive(Clone)]
pub enum PoolKey {
Price(Address),
Reserve(Address),
/// Pool interface traitdownstream pools must implement this.
#[contractclient(name = "PoolClient")]
pub trait PoolTrait {
/// Execute a swap on this pool.
/// Returns the actual amount out.
fn swap(env: Env, to: Address, token_in: Address, token_out: Address, amount_in: i128, min_amount_out: i128) -> i128;
}

#[contract]
pub struct MultiHopSwap;

#[contractimpl]
impl MultiHopSwap {
/// Seed a pool with an initial price ratio so swaps have something to read.
pub fn seed_pool(env: Env, pool: Address, numer: i128, denom: i128) {
env.storage().persistent().set(&PoolKey::Price(pool.clone()), &(numer, denom));
env.storage().persistent().set(&PoolKey::Reserve(pool), &(numer * 1_000_i128));
}

/// Execute a multi-hop swap across `hops` pools.
/// Each hop: reads pool price, computes output, writes updated reserve, emits event.
/// Returns the final amount out.
/// Each hop: transfers tokens to pool, calls pool's swap, transfers output to next hop (or caller at end).
/// Returns results for each hop.
pub fn swap(env: Env, caller: Address, hops: Vec<Hop>) -> Vec<HopResult> {
caller.require_auth();

let mut results = vec![&env];
let mut running_amount = 0_i128;

for hop in hops.iter() {
let (numer, denom): (i128, i128) = env
.storage()
.persistent()
.get(&PoolKey::Price(hop.pool.clone()))
.unwrap_or((1, 1));

let reserve: i128 = env
.storage()
.persistent()
.get(&PoolKey::Reserve(hop.pool.clone()))
.unwrap_or(1_000_000);
if hops.is_empty() {
panic!("no hops provided");
}

// constant-product style output: out = in * numer / denom
let amount_out = hop.amount_in * numer / denom;
let mut results = vec![&env];
let mut current_amount = 0_i128;
let contract_address = env.current_contract_address();

// Process each hop
for (i, hop) in hops.iter().enumerate() {
let amount_in = if i == 0 {
// First hop: pull tokens from caller
let token_in_client = token::Client::new(&env, &hop.token_in);
token_in_client.transfer(&caller, &contract_address, &hop.amount_in);
hop.amount_in
} else {
// Subsequent hops: use output from previous hop
current_amount
};

// Transfer tokens to pool
let token_in_client = token::Client::new(&env, &hop.token_in);
token_in_client.transfer(&contract_address, &hop.pool, &amount_in);

// Call pool to perform swap
let pool_client = PoolClient::new(&env, &hop.pool);
let amount_out = pool_client.swap(
&contract_address,
&hop.token_in,
&hop.token_out,
&amount_in,
&hop.min_amount_out,
);

// Validate slippage
if amount_out < hop.min_amount_out {
panic!("slippage exceeded");
}

// update reserve
let new_reserve = reserve - amount_out;
env.storage()
.persistent()
.set(&PoolKey::Reserve(hop.pool.clone()), &new_reserve);
// Record result
results.push_back(HopResult {
pool: hop.pool.clone(),
token_in: hop.token_in.clone(),
token_out: hop.token_out.clone(),
amount_in,
amount_out,
});

// Update current amount for next hop
current_amount = amount_out;

// Emit event
env.events().publish(
(symbol_short!("hop"), hop.pool.clone()),
(hop.amount_in, amount_out),
(hop.token_in.clone(), hop.token_out.clone(), amount_in, amount_out),
);

running_amount += amount_out;
results.push_back(HopResult { pool: hop.pool.clone(), amount_out });
}

// one final storage write to record cumulative output
// Transfer final output to caller
let last_hop = hops.last().unwrap();
let token_out_client = token::Client::new(&env, &last_hop.token_out);
token_out_client.transfer(&contract_address, &caller, &current_amount);

// Record last output amount in instance storage for convenience
env.storage()
.instance()
.set(&symbol_short!("last_out"), &running_amount);
.set(&symbol_short!("last_out"), &current_amount);

results
}

/// Returns the last output amount recorded.
pub fn get_last_out(env: Env) -> Option<i128> {
env.storage().instance().get(&symbol_short!("last_out"))
}
}

mod test;
Loading
Loading