Skip to main content

Pool Contract

Pool contracts are the core trading venues in the Oyl AMM protocol. Each pool represents a single trading pair and contains the reserves, trading logic, and liquidity management for that pair.

Contract Overview

Each Pool contract:

  • Holds Reserves: Stores tokens for the trading pair
  • Executes Swaps: Implements the constant product formula
  • Manages Liquidity: Mints and burns LP tokens
  • Tracks Prices: Maintains price oracles
  • Collects Fees: Accumulates trading fees for liquidity providers

Core Functions

Pool Initialization

InitPool

Initializes a new pool with the trading pair:

#[opcode(0)]
InitPool {
alkane_a: AlkaneId, // First token in the pair
alkane_b: AlkaneId, // Second token in the pair
factory: AlkaneId, // Factory contract address
}

This function:

  • Sets up the token pair
  • Links to the factory contract
  • Initializes reserves to zero
  • Prepares the pool for liquidity provision

Liquidity Management

AddLiquidity

Adds liquidity to the pool:

#[opcode(1)]
AddLiquidity

This function:

  1. Receives tokens from the user
  2. Calculates LP tokens to mint
  3. Updates pool reserves
  4. Mints LP tokens to the user
  5. Updates price oracles

Burn

Removes liquidity from the pool:

#[opcode(2)]
Burn

This function:

  1. Burns user's LP tokens
  2. Calculates proportional token amounts
  3. Transfers tokens to the user
  4. Updates pool reserves
  5. Updates price oracles

Trading

Swap

Executes a token swap:

#[opcode(3)]
Swap {
amount_0_out: u128, // Amount of token0 to send out
amount_1_out: u128, // Amount of token1 to send out
to: AlkaneId, // Recipient address
data: Vec<u128>, // Additional data (for flash swaps)
}

Warning: This is a low-level function that should generally not be called directly. Use the Factory contract's swap functions instead.

The swap function:

  1. Validates the swap parameters
  2. Transfers tokens to the recipient
  3. Calls back if data is provided (flash swap)
  4. Validates the constant product formula
  5. Updates reserves and price oracles

Information Queries

GetReserves

Returns the current token reserves:

#[opcode(97)]
#[returns(u128, u128)]
GetReserves

Returns (reserve0, reserve1) where:

  • reserve0: Amount of token0 in the pool
  • reserve1: Amount of token1 in the pool

GetPriceCumulativeLast

Returns cumulative prices for oracle functionality:

#[opcode(98)]
#[returns(u128, u128)]
GetPriceCumulativeLast

Returns cumulative prices used for TWAP calculations. Note that the price is returned with 128 bits for the integer and 128 bits for the decimal.

use ruint::Uint;
pub type U256 = Uint<256, 4>;

// Create a storage wrapper for U256 to implement ByteView
#[derive(Clone, Copy, PartialEq, Eq)]
pub struct StorableU256(pub U256);

impl ByteView for StorableU256 {
fn from_bytes(v: Vec<u8>) -> Self {
assert!(v.len() == 32, "Expected a byte vector of length 32.");
// Convert bytes to U256 using from_le_bytes
let mut bytes_array = [0u8; 32];
bytes_array.copy_from_slice(&v);
StorableU256(U256::from_le_bytes(bytes_array))
}

fn to_bytes(&self) -> Vec<u8> {
self.0.to_le_bytes::<32>().to_vec()
}

fn maximum() -> Self {
StorableU256(U256::MAX)
}

fn zero() -> Self {
StorableU256(U256::ZERO)
}
}

impl From<U256> for StorableU256 {
fn from(value: U256) -> Self {
StorableU256(value)
}
}

impl From<StorableU256> for U256 {
fn from(value: StorableU256) -> Self {
value.0
}
}

// assuming data is the response from the rpc from calling GetPriceCumulativeLast
let p0: U256 = StorableU256::from_bytes(data[0..32].to_vec()).into();
let p1: U256 = StorableU256::from_bytes(data[32..64].to_vec()).into();

println!(
"{:?}.{:?}",
p0 >> U256::from(PRECISION), // integer portion
p0 & U256::from(u128::MAX) // decimal portion
);
println!(
"{:?}.{:?}",
p1 >> U256::from(PRECISION), // integer portion
p1 & U256::from(u128::MAX) // decimal portion
);

GetName

Returns the pool's name:

#[opcode(99)]
#[returns(String)]
GetName

PoolDetails

Returns comprehensive pool information:

#[opcode(999)]
#[returns(Vec<u8>)]
PoolDetails

Example script to decode the output

#[derive(Default)]
pub struct PoolInfo {
pub token_a: AlkaneId,
pub token_b: AlkaneId,
pub reserve_a: u128,
pub reserve_b: u128,
pub total_supply: u128,
pub pool_name: String,
}

impl PoolInfo {
pub fn try_to_vec(&self) -> Vec<u8> {
let mut bytes = Vec::new();

// token_a: 32 bytes (16 bytes block + 16 bytes tx)
bytes.extend_from_slice(&self.token_a.block.to_le_bytes());
bytes.extend_from_slice(&self.token_a.tx.to_le_bytes());

// token_b: 32 bytes (16 bytes block + 16 bytes tx)
bytes.extend_from_slice(&self.token_b.block.to_le_bytes());
bytes.extend_from_slice(&self.token_b.tx.to_le_bytes());

// reserves and total_supply: 16 bytes each
bytes.extend_from_slice(&self.reserve_a.to_le_bytes());
bytes.extend_from_slice(&self.reserve_b.to_le_bytes());
bytes.extend_from_slice(&self.total_supply.to_le_bytes());

// Add the pool name
let name_bytes = self.pool_name.as_bytes();
// Add the length of the name as a u32
bytes.extend_from_slice(&(name_bytes.len() as u32).to_le_bytes());
// Add the name bytes
bytes.extend_from_slice(name_bytes);

bytes
}

pub fn from_vec(bytes: &[u8]) -> Result<Self> {
// Minimum size: 32 (token_a) + 32 (token_b) + 16 (reserve_a) + 16 (reserve_b) + 16 (total_supply) + 4 (name_length) = 116 bytes
if bytes.len() < 116 {
return Err(anyhow!("Invalid bytes length for PoolInfo"));
}

let mut offset = 0;

// Read token_a (32 bytes: 16 bytes block + 16 bytes tx)
let token_a_block = u128::from_le_bytes(bytes[offset..offset + 16].try_into()?);
offset += 16;
let token_a_tx = u128::from_le_bytes(bytes[offset..offset + 16].try_into()?);
offset += 16;

// Read token_b (32 bytes: 16 bytes block + 16 bytes tx)
let token_b_block = u128::from_le_bytes(bytes[offset..offset + 16].try_into()?);
offset += 16;
let token_b_tx = u128::from_le_bytes(bytes[offset..offset + 16].try_into()?);
offset += 16;

// Read reserve_a (16 bytes for u128)
let reserve_a = u128::from_le_bytes(bytes[offset..offset + 16].try_into()?);
offset += 16;

// Read reserve_b (16 bytes for u128)
let reserve_b = u128::from_le_bytes(bytes[offset..offset + 16].try_into()?);
offset += 16;

// Read total_supply (16 bytes for u128)
let total_supply = u128::from_le_bytes(bytes[offset..offset + 16].try_into()?);
offset += 16;

// Read pool_name length (4 bytes for u32)
let name_length = u32::from_le_bytes(bytes[offset..offset + 4].try_into()?) as usize;
offset += 4;

// Check if we have enough bytes for the name
if bytes.len() < offset + name_length {
return Err(anyhow!("Invalid bytes length for pool_name"));
}

// Read pool_name
let pool_name = String::from_utf8(bytes[offset..offset + name_length].to_vec())?;

Ok(PoolInfo {
token_a: AlkaneId {
block: token_a_block,
tx: token_a_tx,
},
token_b: AlkaneId {
block: token_b_block,
tx: token_b_tx,
},
reserve_a,
reserve_b,
total_supply,
pool_name,
})
}
}

fn _get_pool_info(&self, pool: AlkaneId) -> Result<PoolInfo> {
let cellpack = Cellpack {
target: pool,
inputs: vec![999],
};
let response = self.call(&cellpack, &AlkaneTransferParcel(vec![]), self.fuel())?;
Ok(PoolInfo::from_vec(&response.data)?)
}

Fee Management

CollectFees

Collects accumulated fees:

#[opcode(10)]
CollectFees {}

This function allows authorized parties to collect protocol fees from the pool.

Pool States

Empty Pool

  • No liquidity provided
  • Reserves are zero
  • Cannot execute swaps
  • Waiting for initial liquidity

Active Pool

  • Has liquidity from providers
  • Can execute swaps
  • Generates fees
  • Price oracles are active

Imbalanced Pool

  • Significant price deviation from external markets
  • Arbitrage opportunities available
  • Higher price impact for trades

Liquidity Provider Tokens

LP Token Mechanics

LP tokens represent ownership in the pool:

LP_tokens = sqrt(amount0 * amount1) - 1000  // For first liquidity provision
LP_tokens = min(amount0 * total_supply / reserve0, amount1 * total_supply / reserve1) // For subsequent provisions

LP Token Value

The value of LP tokens increases over time due to fees:

token0_per_LP = reserve0 / total_LP_supply
token1_per_LP = reserve1 / total_LP_supply

Price Oracle Integration

Price Tracking

The pool automatically tracks:

  • Current spot prices
  • Cumulative prices over time
  • Last update timestamp

Oracle Updates

Price oracles update on every:

  • Swap transaction
  • Liquidity addition
  • Liquidity removal

Pool contracts are the heart of the Oyl AMM protocol, implementing the core trading and liquidity management functionality that enables decentralized exchange.