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 collateraldebt
(uint256
) - Interest-accrued stablecoin debtx0
(uint256
) - Virtual balance for maintaining leverage
Events
TokenExchange (Emitted when tokens are exchanged through the VirtualPool)
buyer
(address
) - Address that initiated the exchangesold_id
(uint256
) - Index of token sold (0=stablecoin, 1=asset)tokens_sold
(uint256
) - Amount of tokens soldbought_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.
Input | Type | Description |
---|---|---|
i | uint256 | Input token index (0=stablecoin, 1=crypto) |
j | uint256 | Output token index (must be opposite of i ) |
in_amount | uint256 | Amount of input tokens to swap |
min_out | uint256 | Minimum acceptable output amount (slippage protection) |
_for | address | Recipient 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.
Input | Type | Description |
---|---|---|
i | uint256 | Input token index (0=stablecoin, 1=crypto) |
j | uint256 | Output token index (must be opposite of i ) |
in_amount | uint256 | Amount 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).
Input | Type | Description |
---|---|---|
initiator | address | Contract that initiated the flash loan |
token | address | Token borrowed (must be stablecoin) |
total_flash_amount | uint256 | Total amount borrowed including fees |
fee | uint256 | Flash loan fee amount |
data | Bytes[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)