FXSwap Pool
This reference covers the verified public ABI of deployed FXSwap v2.1.0d pools. Unless stated otherwise, amounts are raw token or LP-token units, indices are uint256, and the only valid coin indices are 0 and 1.
For transaction sequencing and examples, start with Integrating FXSwap. Protocols holding LP positions should read Building on FXSwap. For recentering and parameter interactions, see Mechanism and Parameter Design. For the refuel lifecycle, see FXSwap Refuels.
Deployed version and source
Twocrypto.vyThe callable pool used for this reference is the YieldBasis WBTC pool at 0xD9FF…8373. Its verified similar-match source has SHA-256 5b4c3e0cf8a23c0e16d5d3c4d0a2d06ebd39220fa71c6300a72dec5159b3dfad and was compiled with Vyper 0.4.3.
The source is closest to curvefi/twocrypto-ng@387fbe5, with deployment-specific constructor values for the Views address, Math address, and initial admin_fee.
The live pool currently returns Views 0x3504…D31f, Math 0x7983…2e51, and factory 0x98EE…AF7F. Read VIEW(), MATH(), and factory() from the target pool because periphery addresses and implementations may differ.
This page inventories every deployed FXSwap entry point. It documents the swap and LP-token methods locally even where their call shapes remain compatible with earlier two-coin pools, so an FXSwap integration does not depend on another AMM reference.
All entries on this page are available in deployed v2.1.0d unless a version boundary says otherwise. View methods have no caller restriction. State-changing user methods are permissionless and nonpayable; token allowance, balance, index, receiver, cap, invariant, and slippage guards still apply. The Admin controls table identifies the factory-admin-only methods. Event names are listed with the state-changing method or group and defined in Events.
FXSwap-specific source walkthroughs
These focused excerpts come from the verified deployed source identified above. They explain the refuel and recentering behavior that is unique to FXSwap; the complete public interface follows them.
Refuel creation and supply accounting
The refuel overload calculates shares, enforces the zero receiver and configured cap, then increases aggregate supply without minting an account balance:
if donation:
assert receiver == empty(address), "nonzero receiver"
new_donation_shares: uint256 = self.donation_shares + d_token
assert (
new_donation_shares * PRECISION // (token_supply + d_token)
<= self.donation_shares_max_ratio
), "donation above cap!"
self.donation_shares = new_donation_shares
self.totalSupply += d_token
log Donation(donor=msg.sender, token_amounts=amounts_received)
The deployed user-owned-supply getter is therefore:
@external
@view
def user_supply() -> uint256:
return self.totalSupply - self.donation_shares
Unlocking and protection
The internal availability calculation linearly unlocks the outstanding shares, then damps them while protection remains active:
elapsed: uint256 = block.timestamp - self.last_donation_release_ts
unlocked_shares: uint256 = min(
donation_shares,
donation_shares * elapsed // self.donation_duration
)
protection_factor: uint256 = 0
expiry: uint256 = self.donation_protection_expiry_ts
if expiry > block.timestamp:
protection_factor = min(
(expiry - block.timestamp) * PRECISION
// self.donation_protection_period,
PRECISION,
)
return unlocked_shares * (PRECISION - protection_factor) // PRECISION
Regular LP additions can extend the protection expiry in proportion to their relative size, capped at one configured protection period. Overlapping refuels instead shift last_donation_release_ts to preserve shares already unlocked. See FXSwap Refuels for the complete lifecycle and the limits of public state inspection.
Recenter, burn, and depletion
The pool only proposes a new center when the normalized distance between price_oracle and price_scale exceeds the effective adjustment step:
adjustment_step: uint256 = max(rebalancing_params[1], norm // 5)
if norm > adjustment_step:
p_new: uint256 = (
price_scale * (norm - adjustment_step)
+ adjustment_step * price_oracle
) // norm
It calculates the virtual price at p_new and finds the smallest available refuel-share burn that can restore the permitted target, capped by currently available shares:
tweaked_supply: uint256 = 10**18 * new_xcp // goal_vp
donation_shares_to_burn = min(
total_supply - tweaked_supply,
donation_shares,
)
new_virtual_price = (
10**18 * new_xcp
// (total_supply - donation_shares_to_burn)
)
The new price_scale is stored only if the post-burn virtual price remains above the contract's acceptance thresholds. A burn reduces both donation_shares and totalSupply and shifts the release timestamp to preserve the remaining unlock state.
Dynamic-fee calculation
_fee(xp) builds a normalized balance indicator that is 1e18 near balance and tends toward zero with imbalance. fee_gamma shapes that indicator, then the contract interpolates between the 1e10-precision bounds:
B = PRECISION * N_COINS**N_COINS * xp[0] // B * xp[1] // B
B = fee_gamma * B // (fee_gamma * B // 10**18 + 10**18 - B)
return (mid_fee * B + out_fee * (10**18 - B)) // 10**18
The names in this shortened excerpt correspond to the values unpacked from packed_fee_params. Use fee() for current pool state and fee_calc(xp) only with correctly normalized balances.
Exchange Methods
| Signature | Mutability | Returns | Behavior |
|---|---|---|---|
exchange(uint256 i, uint256 j, uint256 dx, uint256 min_dy) | nonpayable | uint256 | Pulls input from caller; sends output to caller |
exchange(uint256 i, uint256 j, uint256 dx, uint256 min_dy, address receiver) | nonpayable | uint256 | Pulls input from caller; sends output to receiver |
exchange_received(uint256 i, uint256 j, uint256 dx, uint256 min_dy) | nonpayable | uint256 | Uses input already transferred to the pool; sends output to caller |
exchange_received(uint256 i, uint256 j, uint256 dx, uint256 min_dy, address receiver) | nonpayable | uint256 | Pre-transfer flow with explicit receiver |
get_dy(uint256 i, uint256 j, uint256 dx) | view | uint256 | Exact-input quote after the dynamic fee |
get_dx(uint256 i, uint256 j, uint256 dy) | view | uint256 | Approximate required input using five iterations |
get_dx(uint256 i, uint256 j, uint256 dy, uint256 n_iter) | view | uint256 | Approximate required input with an explicit iteration count |
i and j must differ. Swap methods revert when token transfer fails or the output is below min_dy. exchange_received requires the caller to have transferred at least dx input tokens to the pool; keep the transfer and call atomic. get_dx is comparatively expensive and is an estimate, not an exact-output execution method.
Successful swaps emit TokenExchange.
exchange
exchange pulls dx from the caller with transferFrom, applies the current dynamic fee, checks that output is at least min_dy, and transfers raw coins(j) units to the receiver. The caller must approve the pool for coins(i).
The return value and TokenExchange.tokens_bought are the realized raw output amount. TokenExchange.tokens_sold records the amount actually received, which can differ from requested dx for unusual token behavior.
exchange_received
exchange_received uses the surplus token balance already transferred to the pool instead of calling transferFrom. It accounts for the full observed surplus when that surplus is at least dx; the realized sold amount can therefore exceed the minimum declared dx.
The transfer and call must be atomic. A separate transaction exposes the prefunded tokens to another caller, who can direct the output to their own receiver.
get_dy and get_dx
get_dy is the normal exact-input quote and includes the dynamic fee. get_dx iteratively estimates the input for a target output; it does not provide an exact-output execution path. Both wrappers call the pool's current Views contract.
Adding Liquidity
| Signature | Mutability | Returns | Behavior |
|---|---|---|---|
add_liquidity(uint256[2] amounts, uint256 min_mint_amount) | nonpayable | uint256 | Mints LP tokens to caller |
add_liquidity(uint256[2] amounts, uint256 min_mint_amount, address receiver) | nonpayable | uint256 | Mints LP tokens to receiver |
add_liquidity(uint256[2] amounts, uint256 min_mint_amount, address receiver, bool donation) | nonpayable | uint256 | With donation=true, creates refuel shares rather than a user position |
calc_token_amount(uint256[2] amounts, bool deposit) | view | uint256 | With deposit=true, estimates LP tokens minted, including fee |
amounts is ordered as [coins(0), coins(1)] and uses raw token units. At least one amount must be non-zero. The call pulls both non-zero amounts from the caller, reverts if no shares can be created or the result is below min_mint_amount, and emits AddLiquidity.
Refuel overload
The four-argument add_liquidity overload uses immutable contract terminology:
| Parameter | Meaning |
|---|---|
amounts | Raw amounts of coins(0) and coins(1) pulled from the caller |
min_mint_amount | Minimum refuel shares that may be created |
receiver | Must be the zero address when donation=true; no user receives the shares |
donation | Set to true to add a refuel |
The return value is the number of refuel shares created. The call increases totalSupply() and donation_shares(), emits Donation, and does not increase any account's balanceOf. The refuel path applies its own cap, unlock, protection, and fee logic described in Refuels.
Removing Liquidity
| Signature | Mutability | Returns | Behavior |
|---|---|---|---|
remove_liquidity(uint256 amount, uint256[2] min_amounts) | nonpayable | uint256[2] | Proportional withdrawal to caller |
remove_liquidity(uint256 amount, uint256[2] min_amounts, address receiver) | nonpayable | uint256[2] | Proportional withdrawal to receiver |
remove_liquidity_fixed_out(uint256 token_amount, uint256 i, uint256 amount_i, uint256 min_amount_j) | nonpayable | uint256 | Burns token_amount, withdraws exactly amount_i of coin i, and returns the coin 1-i amount |
remove_liquidity_fixed_out(uint256 token_amount, uint256 i, uint256 amount_i, uint256 min_amount_j, address receiver) | nonpayable | uint256 | Fixed-out withdrawal to receiver |
remove_liquidity_one_coin(uint256 lp_token_amount, uint256 i, uint256 min_amount) | nonpayable | uint256 | Single-coin withdrawal to caller |
remove_liquidity_one_coin(uint256 lp_token_amount, uint256 i, uint256 min_amount, address receiver) | nonpayable | uint256 | Single-coin withdrawal to receiver |
calc_token_amount(uint256[2] amounts, bool deposit) | view | uint256 | With deposit=false, estimates LP tokens burned, including fee |
calc_withdraw_fixed_out(uint256 lp_token_amount, uint256 i, uint256 amount_i) | view | uint256 | Estimates coin 1-i for a fixed-out withdrawal |
calc_withdraw_one_coin(uint256 lp_token_amount, uint256 i) | view | uint256 | Estimates single-coin output |
All minimum amounts are raw token units. Proportional withdrawal emits RemoveLiquidity; fixed-out withdrawal emits RemoveLiquidityImbalance; one-coin withdrawal emits RemoveLiquidityOne.
Refuel state
| Signature | Mutability | Returns | Unit / meaning |
|---|---|---|---|
user_supply() | view | uint256 | totalSupply() - donation_shares() |
donation_shares() | view | uint256 | Total outstanding refuel shares |
donation_shares_max_ratio() | view | uint256 | Maximum refuel-share ratio, 1e18 precision |
donation_duration() | view | uint256 | Linear unlock duration, seconds |
last_donation_release_ts() | view | uint256 | Release-schedule timestamp |
donation_protection_expiry_ts() | view | uint256 | Active protection expiry, Unix seconds |
donation_protection_period() | view | uint256 | Protection period, seconds |
donation_protection_lp_threshold() | view | uint256 | LP threshold, 1e18 precision |
There is no public getter that separates the exact locked, unlocked, protected, and immediately burnable shares.
Price Scaling and Oracles
| Signature | Mutability | Returns | Unit / meaning |
|---|---|---|---|
price_scale() | view | uint256 | Internal center price of coin 1 in coin 0, 1e18 |
price_oracle() | view | uint256 | EMA target price of coin 1 in coin 0, 1e18 |
last_prices() | view | uint256 | Last observed normalized price, 1e18 |
last_timestamp() | view | uint256 | Last oracle timestamp, Unix seconds |
lp_price() | view | uint256 | LP-token price estimate, 1e18 |
get_virtual_price() | view | uint256 | Current virtual-price calculation, 1e18 |
virtual_price() | view | uint256 | Cached internal virtual price, 1e18 |
ma_time() | view | uint256 | Approximate EMA half-life, seconds |
All three price getters express the price of coins(1) in coins(0) with 1e18 precision. Read FXSwap Oracles for update behavior, recentering, and manipulation boundaries.
Fees and Profits
| Signature | Mutability | Returns | Unit / meaning |
|---|---|---|---|
fee() | view | uint256 | Current dynamic fee, 1e10 |
mid_fee() | view | uint256 | Lower dynamic-fee bound, 1e10 |
out_fee() | view | uint256 | Upper dynamic-fee bound, 1e10 |
fee_gamma() | view | uint256 | Fee-transition parameter, 1e18 |
admin_fee() | view | uint256 | Share of profit assigned to admin, 1e10 |
fee_calc(uint256[2] xp) | view | uint256 | Dynamic fee for normalized balances, 1e10 |
calc_token_fee(uint256[2] amounts, uint256[2] xp) | view | uint256 | Liquidity fee calculation |
calc_token_fee(uint256[2] amounts, uint256[2] xp, bool donation) | view | uint256 | Liquidity fee with explicit refuel flag |
calc_token_fee(uint256[2] amounts, uint256[2] xp, bool donation, bool deposit) | view | uint256 | Liquidity fee with explicit refuel and deposit flags |
xcp_profit() | view | uint256 | Current profit-accounting value, 1e18 |
xcp_profit_a() | view | uint256 | Profit value at the prior admin claim, 1e18 |
xp is the two-coin normalized balance vector, not the raw balances. For parameter interactions, see Mechanism and Parameter Design.
Parameters
| Signature | Mutability | Returns | Unit / meaning |
|---|---|---|---|
A() | view | uint256 | Amplification parameter in contract precision |
gamma() | view | uint256 | Compatibility value; unused by the FXSwap invariant |
precisions() | view | uint256[2] | Token precision multipliers |
allowed_extra_profit() | view | uint256 | Recentring profit buffer, 1e18 |
adjustment_step() | view | uint256 | Minimum recentering step, 1e18 |
ma_time() | view | uint256 | Approximate EMA half-life, seconds |
LP Token Methods
The pool is its own 18-decimal ERC-20 LP token:
| Signature | Mutability | Returns |
|---|---|---|
name() | view | string |
symbol() | view | string |
decimals() | view | uint8 |
version() | view | string |
totalSupply() | view | uint256 |
balanceOf(address owner) | view | uint256 |
allowance(address owner, address spender) | view | uint256 |
approve(address spender, uint256 value) | nonpayable | bool |
transfer(address to, uint256 value) | nonpayable | bool |
transferFrom(address from, address to, uint256 value) | nonpayable | bool |
totalSupply() includes refuel shares. Use user_supply() when an application needs the user-owned LP supply.
Contract Info
| Signature | Mutability | Returns | Meaning |
|---|---|---|---|
coins(uint256 i) | view | address | Token address at index i |
balances(uint256 i) | view | uint256 | Internally accounted raw token balance |
factory() | view | address | Factory and admin source |
admin() | view | address | Admin read through the factory |
fee_receiver() | view | address | Fee receiver read through the factory |
MATH() | view | address | Current Math contract |
VIEW() | view | address | Current Views contract |
D() | view | uint256 | Cached invariant |
initial_A_gamma() | view | uint256 | Packed ramp start |
future_A_gamma() | view | uint256 | Packed ramp target |
initial_A_gamma_time() | view | uint256 | Ramp start timestamp |
future_A_gamma_time() | view | uint256 | Ramp end timestamp |
packed_fee_params() | view | uint256 | Packed dynamic-fee configuration |
packed_rebalancing_params() | view | uint256 | Packed recentering configuration |
Prefer decoded getters such as mid_fee(), out_fee(), fee_gamma(), allowed_extra_profit(), adjustment_step(), and ma_time() instead of unpacking values in application code.
Admin controls
Every method below is nonpayable and checks the factory-provided admin:
| Signature | Guard / effect |
|---|---|
ramp_A_gamma(uint256 future_A, uint256 future_gamma, uint256 future_time) | Starts a bounded ramp lasting at least the contract minimum |
stop_ramp_A_gamma() | Stops an active ramp at current values |
apply_new_parameters(uint256 mid_fee, uint256 out_fee, uint256 fee_gamma, uint256 allowed_extra_profit, uint256 adjustment_step, uint256 ma_exp_time) | Updates validated fee and recentering parameters; the last argument is the internal EMA exponent denominator, approximately half_life / ln(2) |
set_donation_duration(uint256 duration) | Requires duration > 0; emits SetDonationDuration |
set_donation_protection_params(uint256 period, uint256 threshold, uint256 max_shares_ratio) | Requires positive values; ratios use 1e18 precision; emits SetDonationProtection |
set_admin_fee(uint256 admin_fee) | Requires a value at or below the contract maximum; 1e10 precision; emits SetAdminFee |
set_periphery(address views, address math) | Updates either or both periphery addresses; at least one must be non-zero; emits SetPeriphery |
apply_new_parameters uses compatibility-preserving sentinel behavior: out-of-range values for an individual field retain that field's current value, while combinations such as mid_fee > out_fee still revert. Read the emitted NewParameters event or decoded getters after the transaction instead of assuming every requested value was applied. NewParameters.ma_time is the internal exponent parameter; ma_time() returns its approximate half-life in seconds.
set_donation_parameters, POLICY, allowlist methods, and reserved_profit_fraction occur in a later development design. They are not part of deployed v2.1.0d and are intentionally absent from this reference.
Events
event TokenExchange(
address indexed buyer,
uint256 sold_id,
uint256 tokens_sold,
uint256 bought_id,
uint256 tokens_bought,
uint256 fee,
uint256 price_scale
);
event AddLiquidity(
address indexed provider,
address indexed receiver,
uint256[2] token_amounts,
uint256 fee,
uint256 token_supply,
uint256 price_scale
);
event Donation(address indexed donor, uint256[2] token_amounts);
event RemoveLiquidity(address indexed provider, uint256[2] token_amounts, uint256 token_supply);
event RemoveLiquidityOne(
address indexed provider,
uint256 token_amount,
uint256 coin_index,
uint256 coin_amount,
uint256 approx_fee,
uint256 packed_price_scale
);
event RemoveLiquidityImbalance(
address indexed provider,
uint256 lp_token_amount,
uint256[2] token_amounts,
uint256 approx_fee,
uint256 price_scale
);
event NewParameters(
uint256 mid_fee,
uint256 out_fee,
uint256 fee_gamma,
uint256 allowed_extra_profit,
uint256 adjustment_step,
uint256 ma_time
);
event RampAgamma(
uint256 initial_A,
uint256 future_A,
uint256 initial_gamma,
uint256 future_gamma,
uint256 initial_time,
uint256 future_time
);
event StopRampA(uint256 current_A, uint256 current_gamma, uint256 time);
event ClaimAdminFee(address indexed admin, uint256[2] tokens);
event SetDonationDuration(uint256 duration);
event SetDonationProtection(
uint256 donation_protection_period,
uint256 donation_protection_lp_threshold,
uint256 donation_shares_max_ratio
);
event SetAdminFee(uint256 admin_fee);
event SetPeriphery(address views, address math);
event Transfer(address indexed sender, address indexed receiver, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
Donation is the immutable deployed event name for a refuel. Its token amounts and all swap/liquidity event amounts are raw token units. Decode events by pool address and versioned ABI.