Skip to main content

VirtualPool

The source code of the VirtualPool.vy contract can be found on GitHub. The contract is written with Vyper version 0.4.3.

Overview

The VirtualPool is a simplified, two-coin swap interface for a market. It lets users trade between the pool's two tokens without ever touching LP tokens. Quotes are provided via get_dy(i, j, in_amount) and swaps execute through exchange(i, j, in_amount, min_out, _for).

Under the hood, swaps are backed by a flash loan of the stablecoin from the Factory’s flash provider. For stable→asset, the contract borrows stablecoin, combines it with the user’s input to route into LP via the YieldBasis AMM, withdraws back to the pool’s coins, repays the flash, and delivers the asset to the recipient. For asset→stable, it borrows stablecoin, adds liquidity together with the user’s asset to mint LP, routes LP→stable via the AMM, repays the flash, and delivers the stablecoin. Any LP exposure is strictly transient during the call; users never receive LP.


Function Documentation

Public Variables

The VirtualPool contract exposes several public variables for reading contract state and configuration.

Constants

  • ROUNDING_DISCOUNT (uint256) - Rounding discount applied to stablecoin inputs (10^18 / 10^8)

Contract Addresses

  • FACTORY (Factory) - Factory contract address (immutable)
  • AMM (YbAMM) - YieldBasis AMM contract address (immutable)
  • POOL (Pool) - Curve pool contract address (immutable)
  • ASSET_TOKEN (ERC20) - Crypto asset token address (immutable)
  • STABLECOIN (ERC20) - Stablecoin token address (immutable)
  • IMPL (address) - Implementation address (immutable)

View Functions

  • coins(i: uint256) (ERC20) - Returns token address for index i (0=stablecoin, 1=crypto)

Data Structs

AMMState

  • collateral (uint256) - LP token amount held as collateral
  • debt (uint256) - Interest-accrued stablecoin debt
  • x0 (uint256) - Virtual balance for maintaining leverage

Events

TokenExchange (Emitted when tokens are exchanged through the VirtualPool)

  • buyer (address) - Address that initiated the exchange
  • sold_id (uint256) - Index of token sold (0=stablecoin, 1=asset)
  • tokens_sold (uint256) - Amount of tokens sold
  • bought_id (uint256) - Index of token bought (0=stablecoin, 1=asset)
  • tokens_bought (uint256) - Amount of tokens bought

exchange()

Description:
Executes swaps between stablecoins and crypto assets. Users can exchange one asset for another through the virtual pool mechanism, which uses flash loans to perform the swap without requiring users to hold LP tokens.

Returns:
uint256 - The actual amount of output tokens received after the swap.

Emits:
TokenExchange - Swap execution event with buyer address, token indices, and amounts.

InputTypeDescription
iuint256Input token index (0=stablecoin, 1=crypto)
juint256Output token index (must be opposite of i)
in_amountuint256Amount of input tokens to swap
min_outuint256Minimum acceptable output amount (slippage protection)
_foraddressRecipient address for output tokens (defaults to msg.sender)
📄 View Source Code
interface Flash:
def flashLoan(receiver: address, token: address, amount: uint256, data: Bytes[10**5]) -> bool: nonpayable
def supportedTokens(token: address) -> bool: view
def maxFlashLoan(token: address) -> uint256: view

ASSET_TOKEN: public(immutable(ERC20))
STABLECOIN: public(immutable(ERC20))

@external
@nonreentrant
def exchange(i: uint256, j: uint256, in_amount: uint256, min_out: uint256, _for: address = msg.sender) -> uint256:
assert (i == 0 and j == 1) or (i == 1 and j == 0)
flash: Flash = staticcall FACTORY.flash()

in_coin: ERC20 = [STABLECOIN, ASSET_TOKEN][i]
out_coin: ERC20 = [STABLECOIN, ASSET_TOKEN][j]

assert extcall in_coin.transferFrom(msg.sender, self, in_amount, default_return_value=True)

data: Bytes[128] = empty(Bytes[128])
data = abi_encode(i, in_amount)
extcall flash.flashLoan(self, STABLECOIN.address, staticcall flash.maxFlashLoan(STABLECOIN.address), data)

out_amount: uint256 = staticcall out_coin.balanceOf(self)
assert out_amount >= min_out, "Slippage"
assert extcall out_coin.transfer(_for, out_amount, default_return_value=True)

log TokenExchange(buyer=_for, sold_id=i, tokens_sold=in_amount, bought_id=j, tokens_bought=out_amount)
return out_amount

get_dy()

Description:
Calculates the expected output amount for a given input amount when swapping between stablecoins and crypto assets. This is a view function that applies fees and slippage calculations without executing the actual swap.

Returns:
uint256 - The expected output amount for the given input, accounting for fees and slippage.

InputTypeDescription
iuint256Input token index (0=stablecoin, 1=crypto)
juint256Output token index (must be opposite of i)
in_amountuint256Amount of input tokens to swap
📄 View Source Code
interface Pool:
def approve(_spender: address, _value: uint256) -> bool: nonpayable
def coins(i: uint256) -> ERC20: view
def balances(i: uint256) -> uint256: view
def calc_token_amount(amounts: uint256[2], deposit: bool) -> uint256: view
def add_liquidity(amounts: uint256[2], min_mint_amount: uint256) -> uint256: nonpayable
def remove_liquidity(amount: uint256, min_amounts: uint256[2]) -> uint256[2]: nonpayable
def totalSupply() -> uint256: view

FACTORY: public(immutable(Factory))
AMM: public(immutable(YbAMM))
POOL: public(immutable(Pool))
ASSET_TOKEN: public(immutable(ERC20))
STABLECOIN: public(immutable(ERC20))
ROUNDING_DISCOUNT: public(constant(uint256)) = 10**18 // 10**8
IMPL: public(immutable(address))

@external
@view
def get_dy(i: uint256, j: uint256, in_amount: uint256) -> uint256:
assert (i == 0 and j == 1) or (i == 1 and j == 0)
_in_amount: uint256 = in_amount
if i == 0:
_in_amount = in_amount * (10**18 - ROUNDING_DISCOUNT) // 10**18
return self._calculate(i, _in_amount, False)[0]

@internal
@view
def _calculate(i: uint256, in_amount: uint256, only_flash: bool) -> (uint256, uint256):
stables_in_pool: uint256 = staticcall POOL.balances(0)
out_amount: uint256 = 0

if i == 0:
state: AMMState = staticcall AMM.get_state()
pool_supply: uint256 = staticcall POOL.totalSupply()
fee: uint256 = staticcall AMM.fee()
r0fee: uint256 = stables_in_pool * (10**18 - fee) // pool_supply

# Solving quadratic eqn instead of calling the AMM b/c we have a special case
b: uint256 = state.x0 - state.debt + in_amount - r0fee * state.collateral // 10**18
D: uint256 = b**2 + 4 * state.collateral * r0fee // 10**18 * in_amount
flash_amount: uint256 = (isqrt(D) - b) // 2 # We received this withdrawing from the pool

if not only_flash:
crypto_in_pool: uint256 = staticcall POOL.balances(1)
out_amount = flash_amount * crypto_in_pool // stables_in_pool
# Withdrawal was ideally balanced
return out_amount, flash_amount

else:
crypto_in_pool: uint256 = staticcall POOL.balances(1)
flash_amount: uint256 = in_amount * stables_in_pool // crypto_in_pool
if not only_flash:
pool_supply: uint256 = staticcall POOL.totalSupply()
lp_amount: uint256 = pool_supply * in_amount // crypto_in_pool
out_amount = staticcall AMM.get_dy(1, 0, lp_amount) - flash_amount
return out_amount, flash_amount

onFlashLoan()

Description:
Flash loan callback function that executes the swap logic. This function is called by the flash loan provider after borrowing stablecoins, allowing the contract to perform the token exchange and repay the loan.

Returns:
No return value (void function).

InputTypeDescription
initiatoraddressContract that initiated the flash loan
tokenaddressToken borrowed (must be stablecoin)
total_flash_amountuint256Total amount borrowed including fees
feeuint256Flash loan fee amount
dataBytes[10**5]Encoded parameters (i, in_amount)
📄 View Source Code
interface Pool:
def approve(_spender: address, _value: uint256) -> bool: nonpayable
def coins(i: uint256) -> ERC20: view
def balances(i: uint256) -> uint256: view
def calc_token_amount(amounts: uint256[2], deposit: bool) -> uint256: view
def add_liquidity(amounts: uint256[2], min_mint_amount: uint256) -> uint256: nonpayable
def remove_liquidity(amount: uint256, min_amounts: uint256[2]) -> uint256[2]: nonpayable
def totalSupply() -> uint256: view

interface YbAMM:
def coins(i: uint256) -> ERC20: view
def get_dy(i: uint256, j: uint256, in_amount: uint256) -> uint256: view
def get_state() -> AMMState: view
def fee() -> uint256: view
def exchange(i: uint256, j: uint256, in_amount: uint256, min_out: uint256) -> uint256: nonpayable
def STABLECOIN() -> ERC20: view
def COLLATERAL() -> Pool: view

FACTORY: public(immutable(Factory))
AMM: public(immutable(YbAMM))
POOL: public(immutable(Pool))
ASSET_TOKEN: public(immutable(ERC20))
STABLECOIN: public(immutable(ERC20))
ROUNDING_DISCOUNT: public(constant(uint256)) = 10**18 // 10**8
IMPL: public(immutable(address))

@external
def onFlashLoan(initiator: address, token: address, total_flash_amount: uint256, fee: uint256, data: Bytes[10**5]):
assert initiator == self
assert token == STABLECOIN.address
assert msg.sender == (staticcall FACTORY.flash()).address, "Wrong caller"

# executor
i: uint256 = 0
in_amount: uint256 = 0
i, in_amount = abi_decode(data, (uint256, uint256))
flash_amount: uint256 = self._calculate(i, in_amount, True)[1]
in_coin: ERC20 = [STABLECOIN, ASSET_TOKEN][i]
out_coin: ERC20 = [STABLECOIN, ASSET_TOKEN][1-i]
repay_flash_amount: uint256 = total_flash_amount

if i == 0:
# stablecoin -> crypto exchange
# 1. Take flash loan
# 2. Use our stables + flash borrowed amount to swap to pool LP in AMM
# 3. Withdraw symmetrically from pool LP
# 4. Repay the flash loan
# 5. Send the crypto
lp_amount: uint256 = extcall AMM.exchange(0, 1, (in_amount * (10**18 - ROUNDING_DISCOUNT) // 10**18 + flash_amount), 0)
extcall POOL.remove_liquidity(lp_amount, [0, 0])
repay_flash_amount = staticcall STABLECOIN.balanceOf(self)

else:
# crypto -> stablecoin exchange
# 1. Take flash loan
# 2. Deposit taken stables + to Pool
# 3. Swap LP of the pool to stables
# 4. Repay flash loan
# 5. Send the rest to the user
lp_amount: uint256 = extcall POOL.add_liquidity([flash_amount, in_amount], 0)
extcall AMM.exchange(1, 0, lp_amount, 0)

assert extcall STABLECOIN.transfer(msg.sender, repay_flash_amount, default_return_value=True)