@external
@payable
@nonreentrant('lock')
def exchange(
_route: address[11],
_swap_params: uint256[5][5],
_amount: uint256,
_expected: uint256,
_pools: address[5]=empty(address[5]),
_receiver: address=msg.sender
) -> uint256:
"""
@notice Performs up to 5 swaps in a single transaction.
@dev Routing and swap params must be determined off-chain. This
functionality is designed for gas efficiency over ease-of-use.
@param _route Array of [initial token, pool or zap, token, pool or zap, token, ...]
The array is iterated until a pool address of 0x00, then the last
given token is transferred to `_receiver`
@param _swap_params Multidimensional array of [i, j, swap type, pool_type, n_coins] where
i is the index of input token
j is the index of output token
The swap_type should be:
1. for `exchange`,
2. for `exchange_underlying`,
3. for underlying exchange via zap: factory stable metapools with lending base pool `exchange_underlying`
and factory crypto-meta pools underlying exchange (`exchange` method in zap)
4. for coin -> LP token "exchange" (actually `add_liquidity`),
5. for lending pool underlying coin -> LP token "exchange" (actually `add_liquidity`),
6. for LP token -> coin "exchange" (actually `remove_liquidity_one_coin`)
7. for LP token -> lending or fake pool underlying coin "exchange" (actually `remove_liquidity_one_coin`)
8. for ETH <-> WETH, ETH -> stETH or ETH -> frxETH, stETH <-> wstETH, frxETH <-> sfrxETH, ETH -> wBETH
9. for SNX swaps (sUSD, sEUR, sETH, sBTC)
pool_type: 1 - stable, 2 - crypto, 3 - tricrypto, 4 - llamma
n_coins is the number of coins in pool
@param _amount The amount of input token (`_route[0]`) to be sent.
@param _expected The minimum amount received after the final swap.
@param _pools Array of pools for swaps via zap contracts. This parameter is only needed for swap_type = 3.
@param _receiver Address to transfer the final output token to.
@return Received amount of the final output token.
"""
input_token: address = _route[0]
output_token: address = empty(address)
amount: uint256 = _amount
# validate / transfer initial token
if input_token == ETH_ADDRESS:
assert msg.value == amount
else:
assert msg.value == 0
assert ERC20(input_token).transferFrom(msg.sender, self, amount, default_return_value=True)
for i in range(1, 6):
# 5 rounds of iteration to perform up to 5 swaps
swap: address = _route[i*2-1]
pool: address = _pools[i-1] # Only for Polygon meta-factories underlying swap (swap_type == 6)
output_token = _route[i*2]
params: uint256[5] = _swap_params[i-1] # i, j, swap_type, pool_type, n_coins
if not self.is_approved[input_token][swap]:
assert ERC20(input_token).approve(swap, max_value(uint256), default_return_value=True, skip_contract_check=True)
self.is_approved[input_token][swap] = True
eth_amount: uint256 = 0
if input_token == ETH_ADDRESS:
eth_amount = amount
# perform the swap according to the swap type
if params[2] == 1:
if params[3] == 1: # stable
StablePool(swap).exchange(convert(params[0], int128), convert(params[1], int128), amount, 0, value=eth_amount)
else: # crypto, tricrypto or llamma
if input_token == ETH_ADDRESS or output_token == ETH_ADDRESS:
CryptoPoolETH(swap).exchange(params[0], params[1], amount, 0, True, value=eth_amount)
else:
CryptoPool(swap).exchange(params[0], params[1], amount, 0)
elif params[2] == 2:
if params[3] == 1: # stable
StablePool(swap).exchange_underlying(convert(params[0], int128), convert(params[1], int128), amount, 0, value=eth_amount)
else: # crypto or tricrypto
CryptoPool(swap).exchange_underlying(params[0], params[1], amount, 0, value=eth_amount)
elif params[2] == 3: # SWAP IS ZAP HERE !!!
if params[3] == 1: # stable
LendingBasePoolMetaZap(swap).exchange_underlying(pool, convert(params[0], int128), convert(params[1], int128), amount, 0)
else: # crypto or tricrypto
use_eth: bool = input_token == ETH_ADDRESS or output_token == ETH_ADDRESS
CryptoMetaZap(swap).exchange(pool, params[0], params[1], amount, 0, use_eth, value=eth_amount)
elif params[2] == 4:
if params[4] == 2:
amounts: uint256[2] = [0, 0]
amounts[params[0]] = amount
StablePool2Coins(swap).add_liquidity(amounts, 0, value=eth_amount)
elif params[4] == 3:
amounts: uint256[3] = [0, 0, 0]
amounts[params[0]] = amount
StablePool3Coins(swap).add_liquidity(amounts, 0, value=eth_amount)
elif params[4] == 4:
amounts: uint256[4] = [0, 0, 0, 0]
amounts[params[0]] = amount
StablePool4Coins(swap).add_liquidity(amounts, 0, value=eth_amount)
elif params[4] == 5:
amounts: uint256[5] = [0, 0, 0, 0, 0]
amounts[params[0]] = amount
StablePool5Coins(swap).add_liquidity(amounts, 0, value=eth_amount)
elif params[2] == 5:
amounts: uint256[3] = [0, 0, 0]
amounts[params[0]] = amount
LendingStablePool3Coins(swap).add_liquidity(amounts, 0, True, value=eth_amount) # example: aave on Polygon
elif params[2] == 6:
if params[3] == 1: # stable
StablePool(swap).remove_liquidity_one_coin(amount, convert(params[1], int128), 0)
else: # crypto or tricrypto
CryptoPool(swap).remove_liquidity_one_coin(amount, params[1], 0) # example: atricrypto3 on Polygon
elif params[2] == 7:
LendingStablePool3Coins(swap).remove_liquidity_one_coin(amount, convert(params[1], int128), 0, True) # example: aave on Polygon
elif params[2] == 8:
if input_token == ETH_ADDRESS and output_token == WETH_ADDRESS:
WETH(swap).deposit(value=amount)
elif input_token == WETH_ADDRESS and output_token == ETH_ADDRESS:
WETH(swap).withdraw(amount)
elif input_token == ETH_ADDRESS and output_token == STETH_ADDRESS:
stETH(swap).submit(0x0000000000000000000000000000000000000000, value=amount)
elif input_token == ETH_ADDRESS and output_token == FRXETH_ADDRESS:
frxETHMinter(swap).submit(value=amount)
elif input_token == STETH_ADDRESS and output_token == WSTETH_ADDRESS:
wstETH(swap).wrap(amount)
elif input_token == WSTETH_ADDRESS and output_token == STETH_ADDRESS:
wstETH(swap).unwrap(amount)
elif input_token == FRXETH_ADDRESS and output_token == SFRXETH_ADDRESS:
sfrxETH(swap).deposit(amount, self)
elif input_token == SFRXETH_ADDRESS and output_token == FRXETH_ADDRESS:
sfrxETH(swap).redeem(amount, self, self)
elif input_token == ETH_ADDRESS and output_token == WBETH_ADDRESS:
wBETH(swap).deposit(0xeCb456EA5365865EbAb8a2661B0c503410e9B347, value=amount)
else:
raise "Swap type 8 is only for ETH <-> WETH, ETH -> stETH or ETH -> frxETH, stETH <-> wstETH, frxETH <-> sfrxETH, ETH -> wBETH"
elif params[2] == 9:
Synthetix(swap).exchangeAtomically(self.snx_currency_keys[input_token], amount, self.snx_currency_keys[output_token], SNX_TRACKING_CODE, 0)
else:
raise "Bad swap type"
# update the amount received
if output_token == ETH_ADDRESS:
amount = self.balance
else:
amount = ERC20(output_token).balanceOf(self)
# sanity check, if the routing data is incorrect we will have a 0 balance and that is bad
assert amount != 0, "Received nothing"
# check if this was the last swap
if i == 5 or _route[i*2+1] == empty(address):
break
# if there is another swap, the output token becomes the input for the next round
input_token = output_token
amount -= 1 # Change non-zero -> non-zero costs less gas than zero -> non-zero
assert amount >= _expected, "Slippage"
# transfer the final token to the receiver
if output_token == ETH_ADDRESS:
raw_call(_receiver, b"", value=amount)
else:
assert ERC20(output_token).transfer(_receiver, amount, default_return_value=True)
log Exchange(msg.sender, _receiver, _route, _swap_params, _pools, _amount, amount)
return amount