pool_state_updates.py
Pool related state update functions:
ActionDecoder's decoded actions (signals, in cadCAD lore) get passed here into s_update_pool
, which redirects to the actual state update function like s_swap_exact_amount_in
. The redirection is performed via a dict lookup:
pool_operation_mappings = {
JoinSwapExternAmountInInput: s_join_swap_extern_amount_in,
JoinParamsInput: s_join_pool,
SwapExactAmountInInput: s_swap_exact_amount_in,
SwapExactAmountOutInput: s_swap_exact_amount_out,
ExitPoolInput: s_exit_pool,
ExitSwapPoolAmountInInput: s_exit_swap_pool_amount_in,
ExitSwapPoolExternAmountOutInput: s_exit_swap_extern_amount_out
}
Each state update function follows the same structure:
Parse input from the action and get variables from current
pool
state (related token balances)Apply related Balancer Math
Update fees if applicable
Update pool balances
Return pool so action decoder can signal updates
Some sanity checks are made, following BPool smart contracts. In REPLAY_OUTPUT
mode no sanity checks are performed:
def s_join_pool_plot_output(params, step, history, current_state, input_params: JoinParamsInput, output_params: JoinParamsOutput) -> dict:
pool = current_state['pool'].copy()
pool_amount_out = input_params.pool_amount_out
for token in output_params.tokens_in:
pool['tokens'][token.symbol].balance += token.amount
pool['pool_shares'] += pool_amount_out
return pool
Min/Max limits in prices and token amounts are not included since the model won't late stuck transactions or tx reordering like the actual BPool contract.
Swaps
def s_swap_exact_amount_in(params, step, history, current_state, input_params: SwapExactAmountInInput,
output_params: SwapExactAmountInOutput) -> dict:
pool = current_state['pool'].copy()
# Parse action params
token_in_symbol = input_params.token_in.symbol
token_amount_in = input_params.token_in.amount
token_out = input_params.min_token_out.symbol
pool_token_in = pool['tokens'][token_in_symbol]
swap_fee = pool['swap_fee']
if not pool_token_in.bound:
raise Exception('ERR_NOT_BOUND')
out_record = pool['tokens'][token_out]
if not out_record.bound:
raise Exception('ERR_NOT_BOUND')
if token_amount_in > Decimal(pool_token_in.balance) * MAX_IN_RATIO:
raise Exception("ERR_MAX_IN_RATIO")
swap_result = BalancerMath.calc_out_given_in(
token_balance_in=pool_token_in.balance,
token_weight_in=Decimal(pool_token_in.denorm_weight),
token_balance_out=out_record.balance,
token_weight_out=Decimal(out_record.denorm_weight),
token_amount_in=token_amount_in,
swap_fee=Decimal(swap_fee)
)
_, pool = s_pool_update_fee(pool, {token_in_symbol: swap_result.fee})
pool_in_balance = pool_token_in.balance + token_amount_in
pool_token_in.balance = pool_in_balance
pool_out_balance = out_record.balance - swap_result.result
out_record.balance = pool_out_balance
return pool
Join Multi Asset
def s_join_pool(params, step, history, current_state, input_params: JoinParamsInput, output_params: JoinParamsOutput) -> dict:
"""
Join a pool by providing liquidity for all token_symbols.
"""
pool = current_state['pool'].copy()
# tokens_in is a suggestion. The real fixed input is pool_amount_out - how many pool shares does the user want.
# tokens_in will then be recalculated and that value used instead.
pool_amount_out = input_params.pool_amount_out
ratio = pool_amount_out / Decimal(pool['pool_shares'])
if ratio == Decimal('0'):
raise Exception("ERR_MATH_APPROX")
for token in output_params.tokens_in:
amount_expected = token.amount
symbol = token.symbol
amount = ratio * pool['tokens'][symbol].balance
if amount != amount_expected and VERBOSE:
print("WARNING: calculated that user should get {} {} but input specified that he should get {} {} instead".format(amount, symbol,
amount_expected,
symbol))
pool['tokens'][symbol].balance += amount
pool['pool_shares'] += pool_amount_out
return pool
Join Single Asset
def s_join_swap_extern_amount_in(params, step, history, current_state, input_params: JoinSwapExternAmountInInput,
output_params: JoinSwapExternAmountInOutput) -> dict:
"""
Join a pool by providing liquidity for a single token_symbol.
"""
pool = current_state['pool'].copy()
tokens_in_symbol = input_params.token_in.symbol
token_in_amount = input_params.token_in.amount
pool_amount_out_expected = output_params.pool_amount_out
swap_fee = pool['swap_fee']
total_weight = calculate_total_denorm_weight(pool)
join_swap = BalancerMath.calc_pool_out_given_single_in(
token_balance_in=Decimal(pool['tokens'][tokens_in_symbol].balance),
token_weight_in=Decimal(pool['tokens'][tokens_in_symbol].denorm_weight),
pool_supply=Decimal(pool['pool_shares']),
total_weight=total_weight,
token_amount_in=token_in_amount,
swap_fee=Decimal(swap_fee)
)
_, pool = s_pool_update_fee(pool, {tokens_in_symbol: join_swap.fee})
pool_amount_out = join_swap.result
if pool_amount_out != pool_amount_out_expected and VERBOSE:
print(
"WARNING: calculated that user should get {} pool shares but input specified that he should get {} pool shares instead.".format(
pool_amount_out, pool_amount_out_expected))
pool['pool_shares'] = Decimal(pool['pool_shares']) + pool_amount_out
pool['tokens'][tokens_in_symbol].balance += token_in_amount
return pool
Single Asset Exit
def s_exit_swap_extern_amount_out(params, step, history, current_state, input_params: ExitSwapPoolExternAmountOutInput,
output_params: ExitSwapPoolExternAmountOutOutput) -> dict:
"""
Exit a pool by withdrawing liquidity for a single token_symbol.
"""
pool = current_state['pool'].copy()
swap_fee = pool['swap_fee']
token_out_symbol = input_params.token_out.symbol
# Check that all tokens_out are bound
if not pool['tokens'][token_out_symbol].bound:
raise Exception("ERR_NOT_BOUND")
# Check that user is not trying to withdraw too many tokens
token_amount_out = input_params.token_out.amount
if token_amount_out > Decimal(pool['tokens'][token_out_symbol].balance) * MAX_OUT_RATIO:
raise Exception("ERR_MAX_OUT_RATIO")
total_weight = calculate_total_denorm_weight(pool)
exit_swap = BalancerMath.calc_pool_in_given_single_out(
token_balance_out=Decimal(pool['tokens'][token_out_symbol].balance),
token_weight_out=Decimal(pool['tokens'][token_out_symbol].denorm_weight),
pool_supply=Decimal(pool['pool_shares']),
total_weight=Decimal(total_weight),
token_amount_out=token_amount_out,
swap_fee=Decimal(swap_fee)
)
pool_amount_in = exit_swap.result
_, pool = s_pool_update_fee(pool, {token_out_symbol: exit_swap.fee})
if pool_amount_in == 0:
raise Exception("ERR_MATH_APPROX")
if pool_amount_in != output_params.pool_amount_in and VERBOSE:
print(
"WARNING: calculated that pool should get {} pool shares but input specified that pool should get {} pool shares instead".format(
pool_amount_in, output_params.pool_amount_in))
# Decrease token_symbol (give it to user)
pool['tokens'][token_out_symbol].balance -= token_amount_out
# Burn the user's incoming pool shares - exit fee
exit_fee = pool_amount_in * EXIT_FEE
pool['pool_shares'] = Decimal(pool['pool_shares']) - pool_amount_in - exit_fee
return pool
Exit Multi Asset
def s_exit_pool(params, step, history, current_state, input_params: ExitPoolInput, output_params: ExitPoolOutput) -> dict:
"""
Exit a pool by withdrawing liquidity for all token_symbol.
"""
pool = current_state['pool'].copy()
pool_shares = Decimal(pool['pool_shares'])
pool_amount_in = input_params.pool_amount_in
# Current Balancer implementation has 0 exit fees, but we are leaving this to generalize
exit_fee = pool_amount_in * Decimal(EXIT_FEE)
pool_amount_in_afer_exit_fee = pool_amount_in - exit_fee
ratio = pool_amount_in_afer_exit_fee / pool_shares
pool['pool_shares'] = pool_shares - pool_amount_in_afer_exit_fee
for token_symbol in pool['tokens']:
token_amount_out = ratio * pool['tokens'][token_symbol].balance
if token_amount_out == Decimal('0'):
raise Exception("ERR_MATH_APPROX")
pool['tokens'][token_symbol].balance -= token_amount_out
return pool
Last updated