Full working code: The complete source code for this example is available in
example-contracts/pool/WIT Interface
Copy
Ask AI
package root:component;
world root {
include kontor:built-in/built-in;
use kontor:built-in/context.{view-context, proc-context, signer};
use kontor:built-in/error.{error};
use kontor:built-in/foreign.{contract-address};
use kontor:built-in/numbers.{integer, decimal};
record deposit-result {
lp-shares: integer,
deposit-a: integer,
deposit-b: integer,
}
record withdraw-result {
amount-a: integer,
amount-b: integer,
}
export init: func(ctx: borrow<proc-context>);
export re-init: func(ctx: borrow<proc-context>, token-a: contract-address, amount-a: integer, token-b: contract-address, amount-b: integer, fee: integer) -> result<integer, error>;
export fee: func(ctx: borrow<view-context>) -> integer;
export balance: func(ctx: borrow<view-context>, acc: string) -> option<integer>;
export transfer: func(ctx: borrow<proc-context>, to: string, n: integer) -> result<_, error>;
export token-balance: func(ctx: borrow<view-context>, token: contract-address) -> result<integer, error>;
export quote-deposit: func(ctx: borrow<view-context>, amount-a: integer, amount-b: integer) -> result<deposit-result, error>;
export deposit: func(ctx: borrow<proc-context>, amount-a: integer, amount-b: integer) -> result<deposit-result, error>;
export quote-withdraw: func(ctx: borrow<view-context>, shares: integer) -> result<withdraw-result, error>;
export withdraw: func(ctx: borrow<proc-context>, shares: integer) -> result<withdraw-result, error>;
export swap: func(ctx: borrow<proc-context>, token-in: contract-address, amount-in: integer, min-out: integer) -> result<integer, error>;
export quote-swap: func(ctx: borrow<view-context>, token-in: contract-address, amount-in: integer) -> result<integer, error>;
}
Rust Implementation
Copy
Ask AI
use stdlib::*;
contract!(name = "pool");
interface!(name = "token_dyn", path = "../token/contract/wit");
#[derive(Clone, StorageRoot)]
struct PoolStorage {
pub token_a: ContractAddress,
pub token_b: ContractAddress,
pub fee_bps: Integer,
pub lp_total_supply: Integer,
pub lp_ledger: Map<String, Integer>,
pub custodian: String,
}
impl PoolStorage {
pub fn new(
ctx: &ProcContext,
token_a: ContractAddress,
amount_a: Integer,
token_b: ContractAddress,
amount_b: Integer,
fee_bps: Integer,
) -> Result<Self, Error> {
validate_amount(amount_a)?;
validate_amount(amount_b)?;
let lp_shares = (amount_a * amount_b).sqrt()?;
let custodian = ctx.contract_signer().to_string();
let pool = PoolStorage {
token_a: token_a.clone(),
token_b: token_b.clone(),
fee_bps,
lp_total_supply: lp_shares,
lp_ledger: Map::new(&[(ctx.signer().to_string(), lp_shares)]),
custodian: custodian.clone(),
};
token_dyn::transfer(&token_a, ctx.signer(), &custodian, amount_a)?;
token_dyn::transfer(&token_b, ctx.signer(), &custodian, amount_b)?;
Ok(pool)
}
}
fn token_out(ctx: &impl ReadContext, token_in: &ContractAddress) -> Result<ContractAddress, Error> {
let token_a = ctx.model().token_a();
let token_b = ctx.model().token_b();
if token_in == &token_a {
Ok(token_b)
} else if token_in == &token_b {
Ok(token_a)
} else {
Err(Error::Message(format!("token {} not in pair", token_in)))
}
}
fn validate_amount(amount: Integer) -> Result<(), Error> {
// 0 < amount < sqrt(MAX_INT)
if amount <= Integer::default() || amount > "340_282_366_920_938_463_463_374_607_431".into() {
return Err(Error::Message("bad amount".to_string()));
}
Ok(())
}
fn calc_swap_result(
amount_in: Integer,
bal_in: Integer,
bal_out: Integer,
fee_bps: Integer,
) -> Result<Integer, Error> {
validate_amount(amount_in)?;
validate_amount(bal_in)?;
validate_amount(bal_out)?;
// input amount less fee, round down
let bps_in_100pct = 10000.into();
let in_less_fee = amount_in * (bps_in_100pct - fee_bps) / bps_in_100pct;
let new_bal_in = bal_in + in_less_fee;
validate_amount(new_bal_in)?;
// calculate output amount from delta in output-token balance, round down
let k = bal_in * bal_out;
Ok((bal_out * new_bal_in - k) / new_bal_in)
}
fn quote_swap(
ctx: &impl ReadContext,
token_in: &ContractAddress,
amount_in: Integer,
) -> Result<Integer, Error> {
let custodian = ctx.model().custodian();
let bal_in = token_dyn::balance(token_in, &custodian).unwrap_or_default();
let bal_out = token_dyn::balance(&token_out(ctx, token_in)?, &custodian).unwrap_or_default();
calc_swap_result(amount_in, bal_in, bal_out, ctx.model().fee_bps())
}
fn quote_deposit(
ctx: &impl ReadContext,
amount_a: Integer,
amount_b: Integer,
) -> Result<DepositResult, Error> {
validate_amount(amount_a)?;
validate_amount(amount_b)?;
let token_a = ctx.model().token_a();
let token_b = ctx.model().token_b();
let lp_supply = ctx.model().lp_total_supply();
let custodian = ctx.model().custodian();
let bal_a = token_dyn::balance(&token_a, &custodian).unwrap_or_default();
let bal_b = token_dyn::balance(&token_b, &custodian).unwrap_or_default();
let lp_shares = if amount_a * bal_b < amount_b * bal_a {
amount_a * lp_supply / bal_a
} else {
amount_b * lp_supply / bal_b
};
let supply_minus_one = lp_supply - 1.into();
Ok(DepositResult {
deposit_a: (lp_shares * bal_a + supply_minus_one) / lp_supply, // round up
deposit_b: (lp_shares * bal_b + supply_minus_one) / lp_supply, // round up
lp_shares,
})
}
fn quote_withdraw(ctx: &impl ReadContext, shares: Integer) -> Result<WithdrawResult, Error> {
validate_amount(shares)?;
let token_a = ctx.model().token_a();
let token_b = ctx.model().token_b();
let lp_supply = ctx.model().lp_total_supply();
let custodian = ctx.model().custodian();
let bal_a = token_dyn::balance(&token_a, &custodian).unwrap_or_default();
let bal_b = token_dyn::balance(&token_b, &custodian).unwrap_or_default();
Ok(WithdrawResult {
amount_a: shares * bal_a / lp_supply,
amount_b: shares * bal_b / lp_supply,
})
}
impl Guest for Pool {
// Dummy implementation for testing purposes.
fn init(ctx: &ProcContext) {
PoolStorage {
token_a: ContractAddress {
name: "".to_string(),
height: 0,
tx_index: 0,
},
token_b: ContractAddress {
name: "".to_string(),
height: 0,
tx_index: 0,
},
lp_ledger: Map::default(),
lp_total_supply: 0.into(),
fee_bps: 0.into(),
custodian: "".to_string(),
}
.init(ctx);
}
// This represents the production init function.
// Only for local testing purposes.
fn re_init(
ctx: &ProcContext,
token_a: ContractAddress,
amount_a: Integer,
token_b: ContractAddress,
amount_b: Integer,
fee: Integer,
) -> Result<Integer, Error> {
PoolStorage::new(ctx, token_a, amount_a, token_b, amount_b, fee)?.init(ctx);
Ok(ctx.model().lp_total_supply())
}
fn fee(ctx: &ViewContext) -> Integer {
ctx.model().fee_bps()
}
fn balance(ctx: &ViewContext, acc: String) -> Option<Integer> {
ctx.model().lp_ledger().get(acc)
}
fn transfer(ctx: &ProcContext, to: String, n: Integer) -> Result<(), Error> {
let from = ctx.signer().to_string();
let ledger = ctx.model().lp_ledger();
let from_balance = ledger.get(&from).unwrap_or_default();
let to_balance = ledger.get(&to).unwrap_or_default();
if from_balance < n {
return Err(Error::Message("insufficient funds".to_string()));
}
ledger.set(from, from_balance - n);
ledger.set(to, to_balance + n);
Ok(())
}
fn token_balance(ctx: &ViewContext, token: ContractAddress) -> Result<Integer, Error> {
token_out(ctx, &token)?;
Ok(token_dyn::balance(&token, &ctx.model().custodian()).unwrap_or_default())
}
fn quote_deposit(
ctx: &ViewContext,
amount_a: Integer,
amount_b: Integer,
) -> Result<DepositResult, Error> {
quote_deposit(ctx, amount_a, amount_b)
}
fn deposit(
ctx: &ProcContext,
amount_a: Integer,
amount_b: Integer,
) -> Result<DepositResult, Error> {
let res = quote_deposit(ctx, amount_a, amount_b)?;
let ledger = ctx.model().lp_ledger();
let custodian = ctx.model().custodian();
let user = ctx.signer().to_string();
let bal = ledger.get(&user).unwrap_or_default();
ledger.set(user, bal + res.lp_shares);
ctx.model().set_lp_total_supply(ctx.model().lp_total_supply() + res.lp_shares);
token_dyn::transfer(
&ctx.model().token_a(),
ctx.signer(),
&custodian,
res.deposit_a,
)?;
token_dyn::transfer(
&ctx.model().token_b(),
ctx.signer(),
&custodian,
res.deposit_b,
)?;
Ok(res)
}
fn quote_withdraw(ctx: &ViewContext, shares: Integer) -> Result<WithdrawResult, Error> {
quote_withdraw(ctx, shares)
}
fn withdraw(ctx: &ProcContext, shares: Integer) -> Result<WithdrawResult, Error> {
let res = quote_withdraw(ctx, shares)?;
let ledger = ctx.model().lp_ledger();
let user = ctx.signer().to_string();
let total = ctx.model().lp_total_supply();
let bal = ledger.get(&user).unwrap_or_default();
if total < shares {
return Err(Error::Message("insufficient total supply".to_string()));
}
if bal < shares {
return Err(Error::Message("insufficient share balance".to_string()));
}
ledger.set(user.clone(), bal - shares);
ctx.model().set_lp_total_supply(total - shares);
token_dyn::transfer(
&ctx.model().token_a(),
ctx.contract_signer(),
&user,
res.amount_a,
)?;
token_dyn::transfer(
&ctx.model().token_b(),
ctx.contract_signer(),
&user,
res.amount_b,
)?;
Ok(res)
}
fn quote_swap(
ctx: &ViewContext,
token_in: ContractAddress,
amount_in: Integer,
) -> Result<Integer, Error> {
quote_swap(ctx, &token_in, amount_in)
}
fn swap(
ctx: &ProcContext,
token_in: ContractAddress,
amount_in: Integer,
min_out: Integer,
) -> Result<Integer, Error> {
let token_out = token_out(ctx, &token_in)?;
let amount_out = quote_swap(ctx, &token_in, amount_in)?;
if amount_out < min_out {
return Err(Error::Message(format!(
"amount out ({}) below minimum",
amount_out
)));
}
token_dyn::transfer(
&token_in,
ctx.signer(),
&ctx.model().custodian(),
amount_in,
)?;
token_dyn::transfer(
&token_out,
ctx.contract_signer(),
&ctx.signer().to_string(),
amount_out,
)?;
Ok(amount_out)
}
}
Testing
Copy
Ask AI
#[cfg(test)]
mod tests {
use testlib::*;
interface!(name = "pool");
interface!(name = "token_a", path = "../token/contract/wit");
interface!(name = "token_b", path = "../token/contract/wit");
#[testlib::test]
async fn test_contract() -> Result<()> {
let admin = runtime.identity().await?;
let minter = runtime.identity().await?;
let pool = runtime.publish(&admin, "pool").await?;
let token_a = runtime.publish(&admin, "token").await?;
let token_b = runtime.publish(&admin, "token").await?;
token_a::mint(runtime, &token_a, &minter, 1000.into()).await??;
token_b::mint(runtime, &token_b, &minter, 1000.into()).await??;
token_a::transfer(runtime, &token_a, &minter, &admin, 100.into()).await??;
token_b::transfer(runtime, &token_b, &minter, &admin, 500.into()).await??;
let res = pool::re_init(
runtime,
&pool,
&admin,
token_a.clone(),
100.into(),
token_b.clone(),
500.into(),
0.into(),
)
.await?;
assert_eq!(res, Ok(223.into()));
let bal_a = pool::token_balance(runtime, &pool, token_a.clone()).await?;
assert_eq!(bal_a, Ok(100.into()));
let bal_b = pool::token_balance(runtime, &pool, token_b.clone()).await?;
assert_eq!(bal_b, Ok(500.into()));
let k1 = bal_a.unwrap() * bal_b.unwrap();
let res = pool::quote_swap(runtime, &pool, token_a.clone(), 10.into()).await?;
assert_eq!(res, Ok(45.into()));
let res = pool::quote_swap(runtime, &pool, token_a.clone(), 100.into()).await?;
assert_eq!(res, Ok(250.into()));
let res = pool::quote_swap(runtime, &pool, token_a.clone(), 1000.into()).await?;
assert_eq!(res, Ok(454.into()));
let res = pool::swap(runtime, &pool, &minter, token_a.clone(), 10.into(), 46.into()).await?;
assert!(res.is_err()); // below minimum
let res = pool::swap(runtime, &pool, &minter, token_a.clone(), 10.into(), 45.into()).await?;
assert_eq!(res, Ok(45.into()));
let bal_a = pool::token_balance(runtime, &pool, token_a.clone()).await?;
let bal_b = pool::token_balance(runtime, &pool, token_b.clone()).await?;
let k2 = bal_a.unwrap() * bal_b.unwrap();
assert!(k2 >= k1);
let res = pool::quote_swap(runtime, &pool, token_b.clone(), 45.into()).await?;
assert_eq!(res, Ok(9.into()));
let res = pool::swap(runtime, &pool, &minter, token_b.clone(), 45.into(), 0.into()).await?;
assert_eq!(res, Ok(9.into()));
let bal_a = pool::token_balance(runtime, &pool, token_a.clone()).await?;
let bal_b = pool::token_balance(runtime, &pool, token_b.clone()).await?;
let k3 = bal_a.unwrap() * bal_b.unwrap();
assert!(k3 >= k2);
// use token interface to transfer shares
let res = token::balance(runtime, &pool, &admin).await?;
assert_eq!(res, Some(223.into()));
let res = token::balance(runtime, &pool, &minter).await?;
assert_eq!(res, None);
token::transfer(runtime, &pool, &admin, &minter, 23.into()).await??;
let res = token::balance(runtime, &pool, &admin).await?;
assert_eq!(res, Some(200.into()));
let res = token::balance(runtime, &pool, &minter).await?;
assert_eq!(res, Some(23.into()));
Ok(())
}
}