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:
- Receives tokens from the user
- Calculates LP tokens to mint
- Updates pool reserves
- Mints LP tokens to the user
- Updates price oracles
Burn
Removes liquidity from the pool:
#[opcode(2)]
Burn
This function:
- Burns user's LP tokens
- Calculates proportional token amounts
- Transfers tokens to the user
- Updates pool reserves
- 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:
- Validates the swap parameters
- Transfers tokens to the recipient
- Calls back if data is provided (flash swap)
- Validates the constant product formula
- 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 poolreserve1
: 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.