feat: Add execution for curve
- Add CurveSwapStructEncoder and tests - Add CurveSwapExecutorExposed and tests - Add needed interfaces #time 0m #time 0m #time 0m
This commit is contained in:
4
.github/workflows/evm.yml
vendored
4
.github/workflows/evm.yml
vendored
@@ -31,7 +31,7 @@ jobs:
|
|||||||
- name: Run Forge build
|
- name: Run Forge build
|
||||||
run: |
|
run: |
|
||||||
forge --version
|
forge --version
|
||||||
forge build --sizes
|
forge build --sizes --via-ir
|
||||||
id: build
|
id: build
|
||||||
|
|
||||||
- name: Run Forge format check
|
- name: Run Forge format check
|
||||||
@@ -42,7 +42,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Run Forge tests
|
- name: Run Forge tests
|
||||||
run: |
|
run: |
|
||||||
forge test -vvv
|
forge test -vvv --via-ir
|
||||||
id: test
|
id: test
|
||||||
env:
|
env:
|
||||||
ETH_RPC_URL: ${{ secrets.ETH_RPC_URL }}
|
ETH_RPC_URL: ${{ secrets.ETH_RPC_URL }}
|
||||||
2
.github/workflows/swap-encoders.yaml
vendored
2
.github/workflows/swap-encoders.yaml
vendored
@@ -12,7 +12,7 @@ env:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
tests:
|
tests:
|
||||||
uses: propeller-heads/propeller-protocol-lib/.github/workflows/python-tests.yaml@dc/ENG-3545-make-encoders-lib
|
uses: propeller-heads/propeller-protocol-lib/.github/workflows/python-tests.yaml@main
|
||||||
|
|
||||||
formatting:
|
formatting:
|
||||||
name: Formatting
|
name: Formatting
|
||||||
|
|||||||
174
evm/src/curve/CurveSwapExecutor.sol
Normal file
174
evm/src/curve/CurveSwapExecutor.sol
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
// SPDX-License-Identifier: UNLICENCED
|
||||||
|
pragma solidity ^0.8.0;
|
||||||
|
|
||||||
|
import "../interfaces/ISwapExecutor.sol";
|
||||||
|
import "./interfaces/ICurvePool.sol";
|
||||||
|
import "./interfaces/ICurvePoolNoReturn.sol";
|
||||||
|
import "./interfaces/ICurveCryptoPool.sol";
|
||||||
|
import "./interfaces/ICurvePoolNoReturn.sol";
|
||||||
|
import "./interfaces/ICurvePoolWithReturn.sol";
|
||||||
|
import {
|
||||||
|
IERC20,
|
||||||
|
SafeERC20
|
||||||
|
} from "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol";
|
||||||
|
import "src/libraries/EfficientERC20.sol";
|
||||||
|
|
||||||
|
interface IWETH is IERC20 {
|
||||||
|
function deposit() external payable;
|
||||||
|
|
||||||
|
function withdraw(uint256) external;
|
||||||
|
}
|
||||||
|
|
||||||
|
contract CurveSwapExecutor is ISwapExecutor, ISwapExecutorErrors {
|
||||||
|
using EfficientERC20 for IERC20;
|
||||||
|
|
||||||
|
IWETH private constant weth =
|
||||||
|
IWETH(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);
|
||||||
|
address private constant ETH = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE;
|
||||||
|
|
||||||
|
function _decodeParams(bytes calldata data)
|
||||||
|
internal
|
||||||
|
pure
|
||||||
|
returns (
|
||||||
|
IERC20 tokenOut,
|
||||||
|
address target,
|
||||||
|
address receiver,
|
||||||
|
uint8 poolType,
|
||||||
|
int128 i,
|
||||||
|
int128 j,
|
||||||
|
bool tokenApprovalNeeded
|
||||||
|
)
|
||||||
|
{
|
||||||
|
tokenOut = IERC20(address(bytes20(data[0:20])));
|
||||||
|
target = address(bytes20(data[20:40]));
|
||||||
|
receiver = address(bytes20(data[40:60]));
|
||||||
|
poolType = uint8(data[60]);
|
||||||
|
i = int128(uint128(uint8(data[61])));
|
||||||
|
j = int128(uint128(uint8(data[62])));
|
||||||
|
tokenApprovalNeeded = data[63] != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function swap(uint256 amountIn, bytes calldata data)
|
||||||
|
external
|
||||||
|
payable
|
||||||
|
returns (uint256 res)
|
||||||
|
{
|
||||||
|
(
|
||||||
|
IERC20 tokenOut,
|
||||||
|
address target,
|
||||||
|
address receiver,
|
||||||
|
uint8 poolType,
|
||||||
|
int128 i,
|
||||||
|
int128 j,
|
||||||
|
bool tokenApprovalNeeded
|
||||||
|
) = _decodeParams(data);
|
||||||
|
|
||||||
|
// Approve the token for the pool's address if `tokenApprovalNeeded` is
|
||||||
|
// true
|
||||||
|
if (tokenApprovalNeeded) {
|
||||||
|
address tokenIn;
|
||||||
|
// pool type 6 has a different function signature to get the coins
|
||||||
|
if (poolType == 6) {
|
||||||
|
tokenIn = ICurvePoolNoReturn(target).underlying_coins(int128(i));
|
||||||
|
} else {
|
||||||
|
tokenIn = ICurvePool(target).coins(uint256(uint128(i)));
|
||||||
|
}
|
||||||
|
IERC20(tokenIn).forceApprove(target, type(uint256).max);
|
||||||
|
}
|
||||||
|
if (poolType == 0) {
|
||||||
|
// simple exchange with int128
|
||||||
|
// e.g. AAVE, EURS
|
||||||
|
res = ICurvePoolWithReturn(target).exchange(i, j, amountIn, 0);
|
||||||
|
if (receiver != address(this)) {
|
||||||
|
tokenOut.safeTransfer(receiver, res);
|
||||||
|
}
|
||||||
|
} else if (poolType == 1) {
|
||||||
|
// simple exchange with int128 but no amountOut,
|
||||||
|
// e.g. BUSD, HBTC, PAX, renBTC, sBTC, SUSD, USDT, Y, 3pool
|
||||||
|
uint256 tokenOutBalanceBeforeSwap =
|
||||||
|
tokenOut.balanceOf(address(this));
|
||||||
|
ICurvePoolNoReturn(target).exchange(i, j, amountIn, 0);
|
||||||
|
uint256 tokenOutBalanceAfterSwap = tokenOut.balanceOf(address(this));
|
||||||
|
res = tokenOutBalanceAfterSwap - tokenOutBalanceBeforeSwap;
|
||||||
|
if (receiver != address(this)) {
|
||||||
|
tokenOut.safeTransfer(receiver, res);
|
||||||
|
}
|
||||||
|
} else if (poolType == 3) {
|
||||||
|
// tricrypto case
|
||||||
|
uint256 tokenOutBalanceBeforeSwap =
|
||||||
|
tokenOut.balanceOf(address(this));
|
||||||
|
ICurveCryptoPool(target).exchange(
|
||||||
|
uint256(uint128(i)),
|
||||||
|
uint256(uint128(j)),
|
||||||
|
amountIn,
|
||||||
|
0,
|
||||||
|
false //TODO: Check if we can call the entrypoint without
|
||||||
|
// 'use_eth' as it's false by default.
|
||||||
|
);
|
||||||
|
uint256 tokenOutBalanceAfterSwap = tokenOut.balanceOf(address(this));
|
||||||
|
res = tokenOutBalanceAfterSwap - tokenOutBalanceBeforeSwap;
|
||||||
|
if (receiver != address(this)) {
|
||||||
|
tokenOut.safeTransfer(receiver, res);
|
||||||
|
}
|
||||||
|
} else if (poolType == 4) {
|
||||||
|
// (payable) ether based stableswaps - so far no liquidity
|
||||||
|
// e.g. sETH, stETH, rETH, etc
|
||||||
|
ICurveCryptoPool pool = ICurveCryptoPool(target);
|
||||||
|
if (pool.coins(uint256(uint128(i))) == ETH) {
|
||||||
|
weth.withdraw(amountIn);
|
||||||
|
res = pool.exchange{value: amountIn}(i, j, amountIn, 0);
|
||||||
|
} else {
|
||||||
|
res = pool.exchange(i, j, amountIn, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pool.coins(uint256(uint128(j))) == ETH) {
|
||||||
|
weth.deposit{value: res}();
|
||||||
|
}
|
||||||
|
if (receiver != address(this)) {
|
||||||
|
tokenOut.safeTransfer(receiver, res);
|
||||||
|
}
|
||||||
|
} else if (poolType == 5) {
|
||||||
|
// metapool or lending pool interface using int128
|
||||||
|
// e.g. AAVE
|
||||||
|
res = ICurvePoolWithReturn(target).exchange_underlying(
|
||||||
|
i, j, amountIn, 0
|
||||||
|
);
|
||||||
|
if (receiver != address(this)) {
|
||||||
|
tokenOut.safeTransfer(receiver, res);
|
||||||
|
}
|
||||||
|
} else if (poolType == 6) {
|
||||||
|
// metapool or lending pool interface using int128 no amountOut
|
||||||
|
// returned
|
||||||
|
// e.g. Y, Compound
|
||||||
|
uint256 tokenOutBalanceBeforeSwap =
|
||||||
|
tokenOut.balanceOf(address(this));
|
||||||
|
ICurvePoolNoReturn(target).exchange_underlying(i, j, amountIn, 0);
|
||||||
|
uint256 tokenOutBalanceAfterSwap = tokenOut.balanceOf(address(this));
|
||||||
|
res = tokenOutBalanceAfterSwap - tokenOutBalanceBeforeSwap;
|
||||||
|
if (receiver != address(this)) {
|
||||||
|
tokenOut.safeTransfer(receiver, res);
|
||||||
|
}
|
||||||
|
} else if (poolType == 7) {
|
||||||
|
// cryptov2 pool with two tokens
|
||||||
|
// e.g. LDO/ETH
|
||||||
|
res = ICurvePoolWithReturn(target).exchange(
|
||||||
|
uint256(uint128(i)),
|
||||||
|
uint256(uint128(j)),
|
||||||
|
amountIn,
|
||||||
|
0,
|
||||||
|
false,
|
||||||
|
receiver
|
||||||
|
);
|
||||||
|
} else if (poolType == 8) {
|
||||||
|
// cryptov2 two tokens not factory pools ETH/CRV and ETH/CVX
|
||||||
|
res = ICurvePoolWithReturn(target).exchange(
|
||||||
|
uint256(uint128(i)), uint256(uint128(j)), amountIn, 0, false
|
||||||
|
);
|
||||||
|
if (receiver != address(this)) {
|
||||||
|
tokenOut.safeTransfer(receiver, res);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
revert UnknownPoolType(poolType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
26
evm/src/curve/interfaces/ICurveCryptoPool.sol
Normal file
26
evm/src/curve/interfaces/ICurveCryptoPool.sol
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
pragma solidity >=0.4.0;
|
||||||
|
|
||||||
|
interface ICurveCryptoPool {
|
||||||
|
function get_dy(uint256 i, uint256 j, uint256 dx)
|
||||||
|
external
|
||||||
|
view
|
||||||
|
returns (uint256);
|
||||||
|
|
||||||
|
// tricrypto
|
||||||
|
function exchange(
|
||||||
|
uint256 i,
|
||||||
|
uint256 j,
|
||||||
|
uint256 dx,
|
||||||
|
uint256 min_dy,
|
||||||
|
bool use_eth
|
||||||
|
) external payable;
|
||||||
|
|
||||||
|
// eth accepting pools
|
||||||
|
function exchange(int128 i, int128 j, uint256 dx, uint256 min_dy)
|
||||||
|
external
|
||||||
|
payable
|
||||||
|
returns (uint256);
|
||||||
|
|
||||||
|
function coins(uint256 i) external view returns (address);
|
||||||
|
}
|
||||||
174
evm/src/curve/interfaces/ICurvePool.sol
Normal file
174
evm/src/curve/interfaces/ICurvePool.sol
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
pragma solidity >=0.4.0;
|
||||||
|
|
||||||
|
interface ICurvePool {
|
||||||
|
function initialize(
|
||||||
|
string memory _name,
|
||||||
|
string memory _symbol,
|
||||||
|
address _coin,
|
||||||
|
uint256 _decimals,
|
||||||
|
uint256 _A,
|
||||||
|
uint256 _fee,
|
||||||
|
address _admin
|
||||||
|
) external;
|
||||||
|
function decimals() external view returns (uint256);
|
||||||
|
function transfer(address _to, uint256 _value) external returns (bool);
|
||||||
|
function transferFrom(address _from, address _to, uint256 _value)
|
||||||
|
external
|
||||||
|
returns (bool);
|
||||||
|
function approve(address _spender, uint256 _value)
|
||||||
|
external
|
||||||
|
returns (bool);
|
||||||
|
function get_previous_balances()
|
||||||
|
external
|
||||||
|
view
|
||||||
|
returns (uint256[2] memory);
|
||||||
|
function get_balances() external view returns (uint256[2] memory);
|
||||||
|
function get_twap_balances(
|
||||||
|
uint256[2] memory _first_balances,
|
||||||
|
uint256[2] memory _last_balances,
|
||||||
|
uint256 _time_elapsed
|
||||||
|
) external view returns (uint256[2] memory);
|
||||||
|
function get_price_cumulative_last()
|
||||||
|
external
|
||||||
|
view
|
||||||
|
returns (uint256[2] memory);
|
||||||
|
function admin_fee() external view returns (uint256);
|
||||||
|
function A() external view returns (uint256);
|
||||||
|
function A_precise() external view returns (uint256);
|
||||||
|
function get_virtual_price() external view returns (uint256);
|
||||||
|
function calc_token_amount(uint256[2] memory _amounts, bool _is_deposit)
|
||||||
|
external
|
||||||
|
view
|
||||||
|
returns (uint256);
|
||||||
|
function calc_token_amount(
|
||||||
|
uint256[2] memory _amounts,
|
||||||
|
bool _is_deposit,
|
||||||
|
bool _previous
|
||||||
|
) external view returns (uint256);
|
||||||
|
function add_liquidity(uint256[2] memory _amounts, uint256 _min_mint_amount)
|
||||||
|
external
|
||||||
|
returns (uint256);
|
||||||
|
function add_liquidity(
|
||||||
|
uint256[2] memory _amounts,
|
||||||
|
uint256 _min_mint_amount,
|
||||||
|
address _receiver
|
||||||
|
) external returns (uint256);
|
||||||
|
function get_dy(int128 i, int128 j, uint256 dx)
|
||||||
|
external
|
||||||
|
view
|
||||||
|
returns (uint256);
|
||||||
|
function get_dy(int128 i, int128 j, uint256 dx, uint256[2] memory _balances)
|
||||||
|
external
|
||||||
|
view
|
||||||
|
returns (uint256);
|
||||||
|
function get_dy_underlying(int128 i, int128 j, uint256 dx)
|
||||||
|
external
|
||||||
|
view
|
||||||
|
returns (uint256);
|
||||||
|
function get_dy_underlying(
|
||||||
|
int128 i,
|
||||||
|
int128 j,
|
||||||
|
uint256 dx,
|
||||||
|
uint256[2] memory _balances
|
||||||
|
) external view returns (uint256);
|
||||||
|
function exchange(uint256 i, uint256 j, uint256 dx, uint256 min_dy)
|
||||||
|
external;
|
||||||
|
function exchange(
|
||||||
|
uint256 i,
|
||||||
|
uint256 j,
|
||||||
|
uint256 dx,
|
||||||
|
uint256 min_dy,
|
||||||
|
address _receiver
|
||||||
|
) external;
|
||||||
|
function exchange(int128 i, int128 j, uint256 dx, uint256 min_dy)
|
||||||
|
external;
|
||||||
|
function exchange(
|
||||||
|
int128 i,
|
||||||
|
int128 j,
|
||||||
|
uint256 dx,
|
||||||
|
uint256 min_dy,
|
||||||
|
address _receiver
|
||||||
|
) external;
|
||||||
|
function exchange_underlying(
|
||||||
|
uint256 i,
|
||||||
|
uint256 j,
|
||||||
|
uint256 dx,
|
||||||
|
uint256 min_dy
|
||||||
|
) external;
|
||||||
|
function exchange_underlying(
|
||||||
|
uint256 i,
|
||||||
|
uint256 j,
|
||||||
|
uint256 dx,
|
||||||
|
uint256 min_dy,
|
||||||
|
address _receiver
|
||||||
|
) external;
|
||||||
|
function exchange_underlying(int128 i, int128 j, uint256 dx, uint256 min_dy)
|
||||||
|
external;
|
||||||
|
function exchange_underlying(
|
||||||
|
int128 i,
|
||||||
|
int128 j,
|
||||||
|
uint256 dx,
|
||||||
|
uint256 min_dy,
|
||||||
|
address _receiver
|
||||||
|
) external;
|
||||||
|
function remove_liquidity(
|
||||||
|
uint256 _burn_amount,
|
||||||
|
uint256[2] memory _min_amounts
|
||||||
|
) external returns (uint256[2] memory);
|
||||||
|
function remove_liquidity(
|
||||||
|
uint256 _burn_amount,
|
||||||
|
uint256[2] memory _min_amounts,
|
||||||
|
address _receiver
|
||||||
|
) external returns (uint256[2] memory);
|
||||||
|
function remove_liquidity_imbalance(
|
||||||
|
uint256[2] memory _amounts,
|
||||||
|
uint256 _max_burn_amount
|
||||||
|
) external returns (uint256);
|
||||||
|
function remove_liquidity_imbalance(
|
||||||
|
uint256[2] memory _amounts,
|
||||||
|
uint256 _max_burn_amount,
|
||||||
|
address _receiver
|
||||||
|
) external returns (uint256);
|
||||||
|
function calc_withdraw_one_coin(uint256 _burn_amount, int128 i)
|
||||||
|
external
|
||||||
|
view
|
||||||
|
returns (uint256);
|
||||||
|
function calc_withdraw_one_coin(
|
||||||
|
uint256 _burn_amount,
|
||||||
|
int128 i,
|
||||||
|
bool _previous
|
||||||
|
) external view returns (uint256);
|
||||||
|
function remove_liquidity_one_coin(
|
||||||
|
uint256 _burn_amount,
|
||||||
|
int128 i,
|
||||||
|
uint256 _min_received
|
||||||
|
) external returns (uint256);
|
||||||
|
function remove_liquidity_one_coin(
|
||||||
|
uint256 _burn_amount,
|
||||||
|
int128 i,
|
||||||
|
uint256 _min_received,
|
||||||
|
address _receiver
|
||||||
|
) external returns (uint256);
|
||||||
|
function ramp_A(uint256 _future_A, uint256 _future_time) external;
|
||||||
|
function stop_ramp_A() external;
|
||||||
|
function admin_balances(uint256 i) external view returns (uint256);
|
||||||
|
function withdraw_admin_fees() external;
|
||||||
|
function admin() external view returns (address);
|
||||||
|
function coins(uint256 arg0) external view returns (address);
|
||||||
|
function balances(uint256 arg0) external view returns (uint256);
|
||||||
|
function fee() external view returns (uint256);
|
||||||
|
function block_timestamp_last() external view returns (uint256);
|
||||||
|
function initial_A() external view returns (uint256);
|
||||||
|
function future_A() external view returns (uint256);
|
||||||
|
function initial_A_time() external view returns (uint256);
|
||||||
|
function future_A_time() external view returns (uint256);
|
||||||
|
function name() external view returns (string memory);
|
||||||
|
function symbol() external view returns (string memory);
|
||||||
|
function balanceOf(address arg0) external view returns (uint256);
|
||||||
|
function allowance(address arg0, address arg1)
|
||||||
|
external
|
||||||
|
view
|
||||||
|
returns (uint256);
|
||||||
|
function totalSupply() external view returns (uint256);
|
||||||
|
}
|
||||||
22
evm/src/curve/interfaces/ICurvePoolNoReturn.sol
Normal file
22
evm/src/curve/interfaces/ICurvePoolNoReturn.sol
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
pragma solidity >=0.4.0;
|
||||||
|
|
||||||
|
interface ICurvePoolNoReturn {
|
||||||
|
function get_dy(int128 i, int128 j, uint256 dx)
|
||||||
|
external
|
||||||
|
view
|
||||||
|
returns (uint256);
|
||||||
|
|
||||||
|
function get_dy_underlying(int128 i, int128 j, uint256 dx)
|
||||||
|
external
|
||||||
|
view
|
||||||
|
returns (uint256);
|
||||||
|
|
||||||
|
function exchange(int128 i, int128 j, uint256 dx, uint256 min_dy)
|
||||||
|
external;
|
||||||
|
|
||||||
|
function exchange_underlying(int128 i, int128 j, uint256 dx, uint256 min_dy)
|
||||||
|
external;
|
||||||
|
function coins(int128 arg0) external view returns (address);
|
||||||
|
function underlying_coins(int128 arg0) external view returns (address);
|
||||||
|
}
|
||||||
39
evm/src/curve/interfaces/ICurvePoolWithReturn.sol
Normal file
39
evm/src/curve/interfaces/ICurvePoolWithReturn.sol
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
pragma solidity >=0.4.0;
|
||||||
|
|
||||||
|
interface ICurvePoolWithReturn {
|
||||||
|
function get_dy(int128 i, int128 j, uint256 dx)
|
||||||
|
external
|
||||||
|
view
|
||||||
|
returns (uint256);
|
||||||
|
|
||||||
|
function get_dy_underlying(int128 i, int128 j, uint256 dx)
|
||||||
|
external
|
||||||
|
view
|
||||||
|
returns (uint256);
|
||||||
|
|
||||||
|
function exchange(int128 i, int128 j, uint256 dx, uint256 min_dy)
|
||||||
|
external
|
||||||
|
returns (uint256);
|
||||||
|
|
||||||
|
function exchange_underlying(int128 i, int128 j, uint256 dx, uint256 min_dy)
|
||||||
|
external
|
||||||
|
returns (uint256);
|
||||||
|
|
||||||
|
function exchange(
|
||||||
|
uint256 i,
|
||||||
|
uint256 j,
|
||||||
|
uint256 dx,
|
||||||
|
uint256 min_dy,
|
||||||
|
bool use_eth,
|
||||||
|
address receiver
|
||||||
|
) external returns (uint256);
|
||||||
|
|
||||||
|
function exchange(
|
||||||
|
uint256 i,
|
||||||
|
uint256 j,
|
||||||
|
uint256 dx,
|
||||||
|
uint256 min_dy,
|
||||||
|
bool use_eth
|
||||||
|
) external returns (uint256);
|
||||||
|
}
|
||||||
295
evm/src/libraries/EfficientERC20.sol
Normal file
295
evm/src/libraries/EfficientERC20.sol
Normal file
@@ -0,0 +1,295 @@
|
|||||||
|
// SPDX-License-Identifier: UNLICENSED
|
||||||
|
pragma solidity ^0.8.0;
|
||||||
|
|
||||||
|
import "openzeppelin-contracts/contracts/interfaces/IERC20.sol";
|
||||||
|
import "openzeppelin-contracts/contracts/utils/Address.sol";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @title Propellerheads Safe ERC20 Transfer Library
|
||||||
|
* @author PropellerHeads Developers
|
||||||
|
* @dev Gas-efficient version of Openzeppelin's SafeERC20 contract.
|
||||||
|
* This is a mix between SafeERC20 and GPv2SafeERC20 libraries. It
|
||||||
|
* provides efficient transfers optimised for router contracts, while
|
||||||
|
* keeping the Openzeppelins compatibility for approvals.
|
||||||
|
*/
|
||||||
|
library EfficientERC20 {
|
||||||
|
using Address for address;
|
||||||
|
|
||||||
|
error TransferFailed(uint256 balance, uint256 amount);
|
||||||
|
error TransferFromFailed(uint256 balance, uint256 amount);
|
||||||
|
|
||||||
|
bytes4 private constant _balanceOfSelector = hex"70a08231";
|
||||||
|
bytes4 private constant _transferSelector = hex"a9059cbb";
|
||||||
|
|
||||||
|
/// @dev Wrapper around a call to the ERC20 function `transfer` that reverts
|
||||||
|
/// also when the token returns `false`.
|
||||||
|
function safeTransfer(IERC20 token, address to, uint256 value) internal {
|
||||||
|
// solhint-disable-next-line no-inline-assembly
|
||||||
|
assembly {
|
||||||
|
let freeMemoryPointer := mload(0x40)
|
||||||
|
mstore(freeMemoryPointer, _transferSelector)
|
||||||
|
mstore(
|
||||||
|
add(freeMemoryPointer, 4),
|
||||||
|
and(to, 0xffffffffffffffffffffffffffffffffffffffff)
|
||||||
|
)
|
||||||
|
mstore(add(freeMemoryPointer, 36), value)
|
||||||
|
|
||||||
|
if iszero(call(gas(), token, 0, freeMemoryPointer, 68, 0, 0)) {
|
||||||
|
returndatacopy(0, 0, returndatasize())
|
||||||
|
revert(0, returndatasize())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!getLastTransferResult(token)) {
|
||||||
|
uint256 balance = token.balanceOf(address(this));
|
||||||
|
revert TransferFailed(balance, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Transfers the callers balance - 1. This effectively leaves dust on
|
||||||
|
* the contract
|
||||||
|
* which will lead to more gas efficient transfers in the future.
|
||||||
|
*/
|
||||||
|
function transferBalanceLeavingDust(IERC20 token, address to) internal {
|
||||||
|
uint256 amount;
|
||||||
|
assembly {
|
||||||
|
// Load free memory pointer
|
||||||
|
let input := mload(0x40)
|
||||||
|
// Prepare call data: function selector (4 bytes) + contract address
|
||||||
|
// (32 bytes)
|
||||||
|
mstore(input, _balanceOfSelector)
|
||||||
|
mstore(add(input, 0x04), address())
|
||||||
|
|
||||||
|
// Call 'balanceOf' function and store result in 'amount'
|
||||||
|
let success := staticcall(gas(), token, input, 0x24, input, 0x20)
|
||||||
|
|
||||||
|
if iszero(success) {
|
||||||
|
// Get the size of the returned error message and forward it
|
||||||
|
let returnSize := returndatasize()
|
||||||
|
returndatacopy(input, 0, returnSize)
|
||||||
|
revert(input, returnSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
amount := sub(mload(input), 1)
|
||||||
|
|
||||||
|
// Prepare call data: function selector (4 bytes) + to (32 bytes) +
|
||||||
|
// amount (32 bytes)
|
||||||
|
mstore(input, _transferSelector)
|
||||||
|
mstore(add(input, 0x04), to)
|
||||||
|
mstore(add(input, 0x24), amount)
|
||||||
|
|
||||||
|
if iszero(call(gas(), token, 0, input, 0x44, 0, 0)) {
|
||||||
|
returndatacopy(0, 0, returndatasize())
|
||||||
|
revert(0, returndatasize())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!getLastTransferResult(token)) {
|
||||||
|
uint256 balance = token.balanceOf(address(this));
|
||||||
|
revert TransferFailed(balance, amount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Wrapper around a call to the ERC20 function `transferFrom` that
|
||||||
|
* reverts also when the token returns `false`.
|
||||||
|
*/
|
||||||
|
function safeTransferFrom(
|
||||||
|
IERC20 token,
|
||||||
|
address from,
|
||||||
|
address to,
|
||||||
|
uint256 value
|
||||||
|
) internal {
|
||||||
|
bytes4 selector_ = token.transferFrom.selector;
|
||||||
|
|
||||||
|
// solhint-disable-next-line no-inline-assembly
|
||||||
|
assembly {
|
||||||
|
let freeMemoryPointer := mload(0x40)
|
||||||
|
mstore(freeMemoryPointer, selector_)
|
||||||
|
mstore(
|
||||||
|
add(freeMemoryPointer, 4),
|
||||||
|
and(from, 0xffffffffffffffffffffffffffffffffffffffff)
|
||||||
|
)
|
||||||
|
mstore(
|
||||||
|
add(freeMemoryPointer, 36),
|
||||||
|
and(to, 0xffffffffffffffffffffffffffffffffffffffff)
|
||||||
|
)
|
||||||
|
mstore(add(freeMemoryPointer, 68), value)
|
||||||
|
|
||||||
|
if iszero(call(gas(), token, 0, freeMemoryPointer, 100, 0, 0)) {
|
||||||
|
returndatacopy(0, 0, returndatasize())
|
||||||
|
revert(0, returndatasize())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!getLastTransferResult(token)) {
|
||||||
|
uint256 balance = token.balanceOf(address(this));
|
||||||
|
revert TransferFailed(balance, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Deprecated. This function has issues similar to the ones found in
|
||||||
|
* {IERC20-approve}, and its usage is discouraged.
|
||||||
|
*
|
||||||
|
* Whenever possible, use {safeIncreaseAllowance} and
|
||||||
|
* {safeDecreaseAllowance} instead.
|
||||||
|
*/
|
||||||
|
function safeApprove(IERC20 token, address spender, uint256 value)
|
||||||
|
internal
|
||||||
|
{
|
||||||
|
// safeApprove should only be called when setting an initial allowance,
|
||||||
|
// or when resetting it to zero. To increase and decrease it, use
|
||||||
|
// 'safeIncreaseAllowance' and 'safeDecreaseAllowance'
|
||||||
|
require(
|
||||||
|
(value == 0) || (token.allowance(address(this), spender) == 0),
|
||||||
|
"SafeERC20: approve from non-zero to non-zero allowance"
|
||||||
|
);
|
||||||
|
_callOptionalReturn(
|
||||||
|
token,
|
||||||
|
abi.encodeWithSelector(token.approve.selector, spender, value)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Set the calling contract's allowance toward `spender` to `value`. If
|
||||||
|
* `token` returns no value,
|
||||||
|
* non-reverting calls are assumed to be successful. Meant to be used with
|
||||||
|
* tokens that require the approval
|
||||||
|
* to be set to zero before setting it to a non-zero value, such as USDT.
|
||||||
|
*/
|
||||||
|
function forceApprove(IERC20 token, address spender, uint256 value)
|
||||||
|
internal
|
||||||
|
{
|
||||||
|
bytes memory approvalCall =
|
||||||
|
abi.encodeCall(token.approve, (spender, value));
|
||||||
|
|
||||||
|
if (!_callOptionalReturnBool(token, approvalCall)) {
|
||||||
|
_callOptionalReturn(
|
||||||
|
token, abi.encodeCall(token.approve, (spender, 0))
|
||||||
|
);
|
||||||
|
_callOptionalReturn(token, approvalCall);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Imitates a Solidity high-level call (i.e. a regular function call to
|
||||||
|
* a contract), relaxing the requirement
|
||||||
|
* on the return value: the return value is optional (but if data is
|
||||||
|
* returned, it must not be false).
|
||||||
|
* @param token The token targeted by the call.
|
||||||
|
* @param data The call data (encoded using abi.encode or one of its
|
||||||
|
* variants).
|
||||||
|
*/
|
||||||
|
function _callOptionalReturn(IERC20 token, bytes memory data) private {
|
||||||
|
// We need to perform a low level call here, to bypass Solidity's return
|
||||||
|
// data size checking mechanism, since
|
||||||
|
// we're implementing it ourselves. We use {Address-functionCall} to
|
||||||
|
// perform this call, which verifies that
|
||||||
|
// the target address contains contract code and also asserts for
|
||||||
|
// success in the low-level call.
|
||||||
|
|
||||||
|
bytes memory returndata = address(token).functionCall(data);
|
||||||
|
if (returndata.length > 0) {
|
||||||
|
// Return data is optional
|
||||||
|
require(
|
||||||
|
abi.decode(returndata, (bool)),
|
||||||
|
"SafeERC20: ERC20 operation did not succeed"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Imitates a Solidity high-level call (i.e. a regular function call to
|
||||||
|
* a contract), relaxing the requirement
|
||||||
|
* on the return value: the return value is optional (but if data is
|
||||||
|
* returned, it must not be false).
|
||||||
|
* @param token The token targeted by the call.
|
||||||
|
* @param data The call data (encoded using abi.encode or one of its
|
||||||
|
* variants).
|
||||||
|
*
|
||||||
|
* This is a variant of {_callOptionalReturn} that silently catches all
|
||||||
|
* reverts and returns a bool instead.
|
||||||
|
*/
|
||||||
|
function _callOptionalReturnBool(IERC20 token, bytes memory data)
|
||||||
|
private
|
||||||
|
returns (bool)
|
||||||
|
{
|
||||||
|
// We need to perform a low level call here, to bypass Solidity's return
|
||||||
|
// data size checking mechanism, since
|
||||||
|
// we're implementing it ourselves. We cannot use {Address-functionCall}
|
||||||
|
// here since this should return false
|
||||||
|
// and not revert is the subcall reverts.
|
||||||
|
|
||||||
|
(bool success, bytes memory returndata) = address(token).call(data);
|
||||||
|
return success
|
||||||
|
&& (returndata.length == 0 || abi.decode(returndata, (bool)))
|
||||||
|
&& address(token).code.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @dev Verifies that the last return was a successful `transfer*` call.
|
||||||
|
/// This is done by checking that the return data is either empty, or
|
||||||
|
/// is a valid ABI encoded boolean.
|
||||||
|
function getLastTransferResult(IERC20 token)
|
||||||
|
private
|
||||||
|
view
|
||||||
|
returns (bool success)
|
||||||
|
{
|
||||||
|
// NOTE: Inspecting previous return data requires assembly. Note that
|
||||||
|
// we write the return data to memory 0 in the case where the return
|
||||||
|
// data size is 32, this is OK since the first 64 bytes of memory are
|
||||||
|
// reserved by Solidy as a scratch space that can be used within
|
||||||
|
// assembly blocks.
|
||||||
|
// <https://docs.soliditylang.org/en/v0.7.6/internals/layout_in_memory.html>
|
||||||
|
// solhint-disable-next-line no-inline-assembly
|
||||||
|
assembly {
|
||||||
|
/// @dev Revert with an ABI encoded Solidity error with a message
|
||||||
|
/// that fits into 32-bytes.
|
||||||
|
///
|
||||||
|
/// An ABI encoded Solidity error has the following memory layout:
|
||||||
|
///
|
||||||
|
/// ------------+----------------------------------
|
||||||
|
/// byte range | value
|
||||||
|
/// ------------+----------------------------------
|
||||||
|
/// 0x00..0x04 | selector("Error(string)")
|
||||||
|
/// 0x04..0x24 | string offset (always 0x20)
|
||||||
|
/// 0x24..0x44 | string length
|
||||||
|
/// 0x44..0x64 | string value, padded to 32-bytes
|
||||||
|
function revertWithMessage(length, message) {
|
||||||
|
mstore(0x00, "\x08\xc3\x79\xa0")
|
||||||
|
mstore(0x04, 0x20)
|
||||||
|
mstore(0x24, length)
|
||||||
|
mstore(0x44, message)
|
||||||
|
revert(0x00, 0x64)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch returndatasize()
|
||||||
|
// Non-standard ERC20 transfer without return.
|
||||||
|
case 0 {
|
||||||
|
// NOTE: When the return data size is 0, verify that there
|
||||||
|
// is code at the address. This is done in order to maintain
|
||||||
|
// compatibility with Solidity calling conventions.
|
||||||
|
// <https://docs.soliditylang.org/en/v0.7.6/control-structures.html#external-function-calls>
|
||||||
|
if iszero(extcodesize(token)) {
|
||||||
|
revertWithMessage(20, "GPv2: not a contract")
|
||||||
|
}
|
||||||
|
|
||||||
|
success := 1
|
||||||
|
}
|
||||||
|
// Standard ERC20 transfer returning boolean success value.
|
||||||
|
case 32 {
|
||||||
|
returndatacopy(0, 0, returndatasize())
|
||||||
|
|
||||||
|
// NOTE: For ABI encoding v1, any non-zero value is accepted
|
||||||
|
// as `true` for a boolean. In order to stay compatible with
|
||||||
|
// OpenZeppelin's `SafeERC20` library which is known to work
|
||||||
|
// with the existing ERC20 implementation we care about,
|
||||||
|
// make sure we return success for any non-zero return value
|
||||||
|
// from the `transfer*` call.
|
||||||
|
success := iszero(iszero(mload(0)))
|
||||||
|
}
|
||||||
|
default { revertWithMessage(31, "GPv2: malformed transfer result") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
303
evm/test/CurveSwapExecutor.t.sol
Normal file
303
evm/test/CurveSwapExecutor.t.sol
Normal file
@@ -0,0 +1,303 @@
|
|||||||
|
// SPDX-License-Identifier: UNLICENSED
|
||||||
|
pragma solidity ^0.8.13;
|
||||||
|
|
||||||
|
import "./SwapExecutor.t.sol";
|
||||||
|
import "../src/curve/CurveSwapExecutor.sol";
|
||||||
|
|
||||||
|
contract CurveSwapExecutorExposed is CurveSwapExecutor {
|
||||||
|
function decodeParams(bytes calldata data)
|
||||||
|
external
|
||||||
|
pure
|
||||||
|
returns (
|
||||||
|
IERC20 tokenOut,
|
||||||
|
address target,
|
||||||
|
address receiver,
|
||||||
|
uint8 poolType,
|
||||||
|
int128 i,
|
||||||
|
int128 j,
|
||||||
|
bool tokenApprovalNeeded
|
||||||
|
)
|
||||||
|
{
|
||||||
|
return _decodeParams(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
contract CurveSwapExecutorPayable is CurveSwapExecutor {
|
||||||
|
receive() external payable {}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ILendingPool {
|
||||||
|
function deposit(
|
||||||
|
address asset,
|
||||||
|
uint256 amount,
|
||||||
|
address onBehalfOf,
|
||||||
|
uint16 referralCode
|
||||||
|
) external;
|
||||||
|
|
||||||
|
function withdraw(address asset, uint256 amount, address to)
|
||||||
|
external
|
||||||
|
returns (uint256);
|
||||||
|
}
|
||||||
|
|
||||||
|
contract TestCurveSwapExecutor is SwapExecutorTest {
|
||||||
|
CurveSwapExecutor swapMethod;
|
||||||
|
address swapMethodAddress;
|
||||||
|
// type 0 pool
|
||||||
|
address aDAI_ADDR = 0x028171bCA77440897B824Ca71D1c56caC55b68A3;
|
||||||
|
address aUSDC_ADDR = 0xBcca60bB61934080951369a648Fb03DF4F96263C;
|
||||||
|
IERC20 aDAI = IERC20(aDAI_ADDR);
|
||||||
|
IERC20 aUSDC = IERC20(aUSDC_ADDR);
|
||||||
|
address AAVE_POOL = 0xDeBF20617708857ebe4F679508E7b7863a8A8EeE;
|
||||||
|
|
||||||
|
// type 1 - 3pool
|
||||||
|
IERC20 DAI = IERC20(DAI_ADDR);
|
||||||
|
IERC20 USDC = IERC20(USDC_ADDR);
|
||||||
|
address THREE_POOL = 0xbEbc44782C7dB0a1A60Cb6fe97d0b483032FF1C7;
|
||||||
|
|
||||||
|
// type 3 - tricrypto case
|
||||||
|
IERC20 WETH = IERC20(WETH_ADDR);
|
||||||
|
IERC20 WBTC = IERC20(WBTC_ADDR);
|
||||||
|
address TRICRYPTO_POOL = 0xD51a44d3FaE010294C616388b506AcdA1bfAAE46;
|
||||||
|
|
||||||
|
// type 4 - stETH
|
||||||
|
address stETH_ADDR = 0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84;
|
||||||
|
IERC20 stETH = IERC20(stETH_ADDR);
|
||||||
|
address stETH_POOL = 0xDC24316b9AE028F1497c275EB9192a3Ea0f67022;
|
||||||
|
|
||||||
|
// type 5 - LUSD
|
||||||
|
address LUSD_ADDR = 0x5f98805A4E8be255a32880FDeC7F6728C6568bA0;
|
||||||
|
IERC20 LUSD = IERC20(LUSD_ADDR);
|
||||||
|
IERC20 USDT = IERC20(USDT_ADDR);
|
||||||
|
address LUSD_POOL = 0xEd279fDD11cA84bEef15AF5D39BB4d4bEE23F0cA;
|
||||||
|
|
||||||
|
// type 6 - compound
|
||||||
|
address CPOOL = 0xA2B47E3D5c44877cca798226B7B8118F9BFb7A56;
|
||||||
|
|
||||||
|
// type 7
|
||||||
|
address LDO_POOL = 0x9409280DC1e6D33AB7A8C6EC03e5763FB61772B5;
|
||||||
|
IERC20 LDO = IERC20(LDO_ADDR);
|
||||||
|
|
||||||
|
// type 8
|
||||||
|
address CRV_POOL = 0x8301AE4fc9c624d1D396cbDAa1ed877821D7C511;
|
||||||
|
IERC20 CRV = IERC20(CRV_ADDR);
|
||||||
|
|
||||||
|
function setUp() public {
|
||||||
|
//Fork
|
||||||
|
uint256 forkBlock = 16000000;
|
||||||
|
vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock);
|
||||||
|
|
||||||
|
//Setup
|
||||||
|
swapMethod = new CurveSwapExecutor();
|
||||||
|
swapMethodAddress = address(swapMethod);
|
||||||
|
vm.makePersistent(swapMethodAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
// foundry deal doesn't work with the atokens:
|
||||||
|
// https://github.com/foundry-rs/forge-std/issues/140
|
||||||
|
function dealAaveDai() internal {
|
||||||
|
deal(DAI_ADDR, swapMethodAddress, 100_000 * 10 ** 18);
|
||||||
|
ILendingPool aave =
|
||||||
|
ILendingPool(0x7d2768dE32b0b80b7a3454c06BdAc94A69DDc7A9);
|
||||||
|
|
||||||
|
vm.startPrank(swapMethodAddress);
|
||||||
|
DAI.approve(address(aave), type(uint256).max);
|
||||||
|
aave.deposit(DAI_ADDR, 100_000 * 10 ** 18, swapMethodAddress, 0);
|
||||||
|
vm.stopPrank();
|
||||||
|
}
|
||||||
|
|
||||||
|
function testSwapType0() public {
|
||||||
|
dealAaveDai();
|
||||||
|
IERC20[] memory tokens = twoTokens(aDAI_ADDR, aUSDC_ADDR);
|
||||||
|
uint256 expAmountOut = 999647;
|
||||||
|
address receiver = bob;
|
||||||
|
bytes memory data =
|
||||||
|
getDataCurve(tokens[1], AAVE_POOL, receiver, 1, 0, 1, true);
|
||||||
|
uint256 amountOut = swapMethod.swap(10 ** 18, data);
|
||||||
|
|
||||||
|
uint256 finalBalance = aUSDC.balanceOf(receiver);
|
||||||
|
assertGe(finalBalance, expAmountOut);
|
||||||
|
assertEq(amountOut, expAmountOut);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3pool
|
||||||
|
function testSwapType1() public {
|
||||||
|
deal(DAI_ADDR, swapMethodAddress, 10_000 * 10 ** 18);
|
||||||
|
IERC20[] memory tokens = twoTokens(DAI_ADDR, USDC_ADDR);
|
||||||
|
uint256 expAmountOut = 999963;
|
||||||
|
address receiver = bob;
|
||||||
|
|
||||||
|
bytes memory data =
|
||||||
|
getDataCurve(tokens[1], THREE_POOL, receiver, 1, 0, 1, true);
|
||||||
|
|
||||||
|
uint256 amountOut = swapMethod.swap(10 ** 18, data);
|
||||||
|
|
||||||
|
uint256 finalBalance = USDC.balanceOf(receiver);
|
||||||
|
assertGe(finalBalance, expAmountOut);
|
||||||
|
assertEq(amountOut, expAmountOut);
|
||||||
|
}
|
||||||
|
|
||||||
|
// tricrypto
|
||||||
|
function testSwapType3() public {
|
||||||
|
deal(USDT_ADDR, swapMethodAddress, 10_000 * 10 ** 6);
|
||||||
|
IERC20[] memory tokens = twoTokens(USDT_ADDR, WBTC_ADDR);
|
||||||
|
uint256 expAmountOut = 60232482;
|
||||||
|
address receiver = bob;
|
||||||
|
|
||||||
|
bytes memory data =
|
||||||
|
getDataCurve(tokens[1], TRICRYPTO_POOL, receiver, 3, 0, 1, true);
|
||||||
|
|
||||||
|
uint256 amountOut = swapMethod.swap(10_000 * 10 ** 6, data);
|
||||||
|
|
||||||
|
uint256 finalBalance = WBTC.balanceOf(receiver);
|
||||||
|
assertGe(finalBalance, expAmountOut);
|
||||||
|
assertEq(amountOut, expAmountOut);
|
||||||
|
}
|
||||||
|
|
||||||
|
// stETH/ETH pool
|
||||||
|
function testSwapType4() public {
|
||||||
|
CurveSwapExecutorPayable swapMethodPayable =
|
||||||
|
new CurveSwapExecutorPayable();
|
||||||
|
address swapMethodPayableAddress = address(swapMethodPayable);
|
||||||
|
deal(WETH_ADDR, swapMethodPayableAddress, 100 * 10 ** 18);
|
||||||
|
IERC20[] memory tokens = twoTokens(WETH_ADDR, stETH_ADDR);
|
||||||
|
uint256 expAmountOut = 1011264689661846353;
|
||||||
|
bytes memory data = getDataCurve(
|
||||||
|
tokens[1], stETH_POOL, swapMethodPayableAddress, 4, 0, 1, false
|
||||||
|
);
|
||||||
|
|
||||||
|
vm.prank(swapMethodPayableAddress);
|
||||||
|
uint256 amountOut = swapMethodPayable.swap(10 ** 18, data);
|
||||||
|
|
||||||
|
uint256 finalBalance = stETH.balanceOf(swapMethodPayableAddress);
|
||||||
|
assertGe(finalBalance, expAmountOut);
|
||||||
|
// There is something weird with
|
||||||
|
// stETH that it gives me 1 Wei more here sometimes
|
||||||
|
assertGe(amountOut, expAmountOut);
|
||||||
|
|
||||||
|
// part 2 swap back stETH
|
||||||
|
tokens = twoTokens(stETH_ADDR, WETH_ADDR);
|
||||||
|
expAmountOut = 988069860569702379;
|
||||||
|
address receiver = bob;
|
||||||
|
|
||||||
|
data = getDataCurve(tokens[1], stETH_POOL, receiver, 4, 1, 0, true);
|
||||||
|
uint256 initialBalance = WETH.balanceOf(receiver);
|
||||||
|
|
||||||
|
amountOut = swapMethodPayable.swap(10 ** 18, data);
|
||||||
|
|
||||||
|
finalBalance = WETH.balanceOf(receiver) - initialBalance;
|
||||||
|
assertGe(finalBalance, expAmountOut);
|
||||||
|
assertEq(amountOut, expAmountOut);
|
||||||
|
}
|
||||||
|
|
||||||
|
// // metapool - LUSD
|
||||||
|
function testSwapType5() public {
|
||||||
|
deal(LUSD_ADDR, swapMethodAddress, 10_000 * 10 ** 18);
|
||||||
|
IERC20[] memory tokens = twoTokens(LUSD_ADDR, USDT_ADDR);
|
||||||
|
uint256 expAmountOut = 1035119;
|
||||||
|
address receiver = bob;
|
||||||
|
|
||||||
|
bytes memory data =
|
||||||
|
getDataCurve(tokens[1], LUSD_POOL, receiver, 5, 0, 3, true);
|
||||||
|
|
||||||
|
uint256 amountOut = swapMethod.swap(10 ** 18, data);
|
||||||
|
|
||||||
|
uint256 finalBalance = USDT.balanceOf(receiver);
|
||||||
|
assertGe(finalBalance, expAmountOut);
|
||||||
|
assertEq(amountOut, expAmountOut);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compound
|
||||||
|
function testSwapType6() public {
|
||||||
|
deal(DAI_ADDR, swapMethodAddress, 10_000 * 10 ** 18);
|
||||||
|
IERC20[] memory tokens = twoTokens(DAI_ADDR, USDC_ADDR);
|
||||||
|
uint256 expAmountOut = 999430;
|
||||||
|
address receiver = bob;
|
||||||
|
|
||||||
|
bytes memory data =
|
||||||
|
getDataCurve(tokens[1], CPOOL, receiver, 6, 0, 1, true);
|
||||||
|
|
||||||
|
uint256 amountOut = swapMethod.swap(10 ** 18, data);
|
||||||
|
|
||||||
|
uint256 finalBalance = USDC.balanceOf(receiver);
|
||||||
|
assertGe(finalBalance, expAmountOut);
|
||||||
|
assertEq(amountOut, expAmountOut);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Curve v2
|
||||||
|
function testSwapType7() public {
|
||||||
|
vm.rollFork(17_000_000); //change block because this pool wasn't
|
||||||
|
// deployed at block 16M
|
||||||
|
uint256 amountIn = 10 ** 18;
|
||||||
|
uint256 expAmountOut = 743676671921315909289;
|
||||||
|
address receiver = bob;
|
||||||
|
deal(WETH_ADDR, swapMethodAddress, amountIn);
|
||||||
|
bytes memory data = abi.encodePacked(
|
||||||
|
getDataCurve(LDO, LDO_POOL, receiver, 7, 0, 1, true), receiver
|
||||||
|
);
|
||||||
|
|
||||||
|
uint256 amountOut = swapMethod.swap(amountIn, data);
|
||||||
|
|
||||||
|
uint256 finalBalance = LDO.balanceOf(bob);
|
||||||
|
assertGe(finalBalance, expAmountOut);
|
||||||
|
assertEq(amountOut, expAmountOut);
|
||||||
|
}
|
||||||
|
// Curve v2 2 token not factory pool
|
||||||
|
|
||||||
|
function testSwapType8() public {
|
||||||
|
vm.rollFork(17_000_000); //change block because this pool wasn't
|
||||||
|
// deployed at block 16M
|
||||||
|
uint256 amountIn = 10 ** 18;
|
||||||
|
uint256 expAmountOut = 1831110768300490995125;
|
||||||
|
address receiver = bob;
|
||||||
|
deal(WETH_ADDR, swapMethodAddress, amountIn);
|
||||||
|
bytes memory data = abi.encodePacked(
|
||||||
|
getDataCurve(CRV, CRV_POOL, receiver, 8, 0, 1, true), receiver
|
||||||
|
);
|
||||||
|
|
||||||
|
uint256 amountOut = swapMethod.swap(amountIn, data);
|
||||||
|
|
||||||
|
uint256 finalBalance = CRV.balanceOf(bob);
|
||||||
|
assertGe(finalBalance, expAmountOut);
|
||||||
|
assertEq(amountOut, expAmountOut);
|
||||||
|
}
|
||||||
|
|
||||||
|
function testDecodeParams() public {
|
||||||
|
CurveSwapExecutorExposed swapMethodExposed =
|
||||||
|
new CurveSwapExecutorExposed();
|
||||||
|
|
||||||
|
//Logic
|
||||||
|
bytes memory data = getDataCurve(LDO, LDO_POOL, bob, 7, 0, 1, true);
|
||||||
|
(
|
||||||
|
IERC20 tokenOut,
|
||||||
|
address target,
|
||||||
|
address receiver,
|
||||||
|
uint8 poolType,
|
||||||
|
int128 i,
|
||||||
|
int128 j,
|
||||||
|
bool tokenApprovalNeeded
|
||||||
|
) = swapMethodExposed.decodeParams(data);
|
||||||
|
|
||||||
|
//Assertions
|
||||||
|
assertEq(address(tokenOut), LDO_ADDR);
|
||||||
|
assertEq(address(target), LDO_POOL);
|
||||||
|
assertEq(address(receiver), bob);
|
||||||
|
assertEq(poolType, 7);
|
||||||
|
assertEq(i, 0);
|
||||||
|
assertEq(j, 1);
|
||||||
|
assertEq(tokenApprovalNeeded, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDataCurve(
|
||||||
|
IERC20 tokenOut,
|
||||||
|
address pool,
|
||||||
|
address receiver,
|
||||||
|
uint8 poolType,
|
||||||
|
uint8 i,
|
||||||
|
uint8 j,
|
||||||
|
bool tokenApprovalNeeded
|
||||||
|
) internal pure returns (bytes memory data) {
|
||||||
|
data = abi.encodePacked(
|
||||||
|
tokenOut, pool, receiver, poolType, i, j, tokenApprovalNeeded
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
98
propeller-swap-encoders/propeller_swap_encoders/curve.py
Normal file
98
propeller-swap-encoders/propeller_swap_encoders/curve.py
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
import enum
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from core.encoding.interface import EncodingContext, SwapStructEncoder
|
||||||
|
from core.type_aliases import Address
|
||||||
|
from eth_abi.packed import encode_abi_packed
|
||||||
|
from eth_utils import to_checksum_address
|
||||||
|
|
||||||
|
curve_config = {
|
||||||
|
# curve pool type 4
|
||||||
|
"eth_stable_pools": [
|
||||||
|
"0xA96A65c051bF88B4095Ee1f2451C2A9d43F53Ae2",
|
||||||
|
"0xF9440930043eb3997fc70e1339dBb11F341de7A8",
|
||||||
|
"0xa1F8A6807c402E4A15ef4EBa36528A3FED24E577",
|
||||||
|
"0xBfAb6FA95E0091ed66058ad493189D2cB29385E6",
|
||||||
|
"0x94B17476A93b3262d87B9a326965D1E91f9c13E7",
|
||||||
|
],
|
||||||
|
# curve pool type 7
|
||||||
|
"v2_eth_pools": [
|
||||||
|
"0x9409280DC1e6D33AB7A8C6EC03e5763FB61772B5",
|
||||||
|
"0x5FAE7E604FC3e24fd43A72867ceBaC94c65b404A",
|
||||||
|
"0x0f3159811670c117c372428D4E69AC32325e4D0F",
|
||||||
|
"0x838af967537350D2C44ABB8c010E49E32673ab94",
|
||||||
|
"0xC26b89A667578ec7b3f11b2F98d6Fd15C07C54ba",
|
||||||
|
"0x6bfE880Ed1d639bF80167b93cc9c56a39C1Ba2dC",
|
||||||
|
"0x0E9B5B092caD6F1c5E6bc7f89Ffe1abb5c95F1C2",
|
||||||
|
"0x21410232B484136404911780bC32756D5d1a9Fa9",
|
||||||
|
"0xfB8814D005C5f32874391e888da6eB2fE7a27902",
|
||||||
|
"0xe0e970a99bc4F53804D8145beBBc7eBc9422Ba7F",
|
||||||
|
"0x6e314039f4C56000F4ebb3a7854A84cC6225Fb92",
|
||||||
|
"0xf861483fa7E511fbc37487D91B6FAa803aF5d37c",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class CurvePoolType(enum.IntEnum):
|
||||||
|
"""
|
||||||
|
Represents different swap logics of curve pools. For more details, please see
|
||||||
|
CurveSwapMethodV1 in defibot-contracts repository.
|
||||||
|
"""
|
||||||
|
|
||||||
|
simple = 0
|
||||||
|
simple_no_amount = 1
|
||||||
|
tricrypto = 3
|
||||||
|
eth_stableswap = 4
|
||||||
|
underlying = 5
|
||||||
|
underlying_no_amount = 6
|
||||||
|
crypto_v2 = 7
|
||||||
|
crypto_v2_2_tokens_not_factory = 8
|
||||||
|
|
||||||
|
|
||||||
|
curve_v2_pool_type_mapping: dict[str, CurvePoolType] = {
|
||||||
|
"tricrypto2_non_factory": CurvePoolType.tricrypto,
|
||||||
|
"two_token_factory": CurvePoolType.crypto_v2,
|
||||||
|
"two_token_non_factory": CurvePoolType.crypto_v2_2_tokens_not_factory,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class CurveSwapStructEncoder(SwapStructEncoder):
|
||||||
|
eth_stable_pools: list[str] = curve_config["eth_stable_pools"]
|
||||||
|
v2_eth_pools = curve_config["v2_eth_pools"]
|
||||||
|
|
||||||
|
def encode_swap_struct(
|
||||||
|
self, swap: dict[str, Any], receiver: Address, encoding_context: EncodingContext
|
||||||
|
) -> bytes:
|
||||||
|
|
||||||
|
pool_type = swap["pool_type"]
|
||||||
|
if pool_type == "CurveSimulatedPoolState":
|
||||||
|
curve_pool_type = (
|
||||||
|
CurvePoolType.tricrypto
|
||||||
|
if swap["protocol_specific_attrs"]["is_curve_tricrypto"]
|
||||||
|
else CurvePoolType.simple_no_amount
|
||||||
|
)
|
||||||
|
elif to_checksum_address(swap["pool_id"]) in self.v2_eth_pools:
|
||||||
|
curve_pool_type = CurvePoolType.crypto_v2
|
||||||
|
elif to_checksum_address(swap["pool_id"]) in self.eth_stable_pools:
|
||||||
|
curve_pool_type = CurvePoolType.eth_stableswap
|
||||||
|
else:
|
||||||
|
curve_pool_type = (
|
||||||
|
curve_v2_pool_type_mapping[
|
||||||
|
swap["protocol_specific_attrs"]["curve_v2_pool_type"]
|
||||||
|
]
|
||||||
|
if pool_type == "CurveV2PoolState"
|
||||||
|
else CurvePoolType.simple_no_amount
|
||||||
|
)
|
||||||
|
|
||||||
|
return encode_abi_packed(
|
||||||
|
["address", "address", "address", "uint8", "uint8", "uint8", "bool"],
|
||||||
|
[
|
||||||
|
swap["buy_token"].address,
|
||||||
|
swap["pool_id"],
|
||||||
|
receiver,
|
||||||
|
curve_pool_type,
|
||||||
|
swap["pool_tokens"].index(swap["sell_token"]),
|
||||||
|
swap["pool_tokens"].index(swap["buy_token"]),
|
||||||
|
swap["token_approval_needed"],
|
||||||
|
],
|
||||||
|
)
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
from core.encoding.interface import EncodingContext
|
||||||
|
from core.models.evm.ethereum_token import EthereumToken
|
||||||
|
|
||||||
|
from propeller_swap_encoders.curve import CurveSwapStructEncoder
|
||||||
|
|
||||||
|
WETH = EthereumToken(
|
||||||
|
symbol="WETH",
|
||||||
|
address="0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
|
||||||
|
decimals=18,
|
||||||
|
gas=0,
|
||||||
|
)
|
||||||
|
USDT = EthereumToken(
|
||||||
|
symbol="USDT", address="0xdAC17F958D2ee523a2206206994597C13D831ec7", decimals=6
|
||||||
|
)
|
||||||
|
WBTC = EthereumToken(
|
||||||
|
symbol="WBTC", address="0x2260fac5e5542a773aa44fbcfedf7c193bc2c599", decimals=8
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_encode_curve_v2():
|
||||||
|
bob = "0x000000000000000000000000000000000000007B"
|
||||||
|
|
||||||
|
swap = {
|
||||||
|
"pool_id": "0xD51a44d3FaE010294C616388b506AcdA1bfAAE46",
|
||||||
|
"sell_token": USDT,
|
||||||
|
"buy_token": WETH,
|
||||||
|
"split": 0,
|
||||||
|
"sell_amount": 0,
|
||||||
|
"buy_amount": 100,
|
||||||
|
"token_approval_needed": False,
|
||||||
|
"pool_tokens": (USDT, WBTC, WETH),
|
||||||
|
"pool_type": "CurveV2PoolState",
|
||||||
|
"protocol_specific_attrs": {
|
||||||
|
"curve_v2_pool_type": "tricrypto2_non_factory",
|
||||||
|
"is_curve_tricrypto": None,
|
||||||
|
"quote": None,
|
||||||
|
"pool_fee": None,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
curve_encoder = CurveSwapStructEncoder()
|
||||||
|
encoded = curve_encoder.encode_swap_struct(
|
||||||
|
swap, receiver=bob, encoding_context=EncodingContext()
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
encoded.hex()
|
||||||
|
==
|
||||||
|
# buy token
|
||||||
|
"c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"
|
||||||
|
# pool address
|
||||||
|
"d51a44d3fae010294c616388b506acda1bfaae46"
|
||||||
|
# receiver
|
||||||
|
"000000000000000000000000000000000000007b"
|
||||||
|
# pool type (tricrypto = 3)
|
||||||
|
"03"
|
||||||
|
# i (sell token index)
|
||||||
|
"00"
|
||||||
|
# j (buy token index)
|
||||||
|
"02"
|
||||||
|
# token_approval_needed
|
||||||
|
"00"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_encode_curve_v1():
|
||||||
|
bob = "0x000000000000000000000000000000000000007B"
|
||||||
|
swap = {
|
||||||
|
"pool_id": "bebc44782c7db0a1a60cb6fe97d0b483032ff1c7",
|
||||||
|
"sell_token": USDT,
|
||||||
|
"buy_token": WETH,
|
||||||
|
"split": 0,
|
||||||
|
"sell_amount": 0,
|
||||||
|
"buy_amount": 100,
|
||||||
|
"token_approval_needed": False,
|
||||||
|
"pool_tokens": (USDT, WBTC, WETH),
|
||||||
|
"pool_type": "CurveV1PoolState",
|
||||||
|
"protocol_specific_attrs": {
|
||||||
|
"curve_v2_pool_type": None,
|
||||||
|
"is_curve_tricrypto": None,
|
||||||
|
"quote": None,
|
||||||
|
"pool_fee": 1000000,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
curve_encoder = CurveSwapStructEncoder()
|
||||||
|
encoded = curve_encoder.encode_swap_struct(
|
||||||
|
swap, receiver=bob, encoding_context=EncodingContext()
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
encoded.hex()
|
||||||
|
==
|
||||||
|
# buy token
|
||||||
|
"c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"
|
||||||
|
# pool address
|
||||||
|
"bebc44782c7db0a1a60cb6fe97d0b483032ff1c7"
|
||||||
|
# receiver
|
||||||
|
"000000000000000000000000000000000000007b"
|
||||||
|
# pool type (simple_no_amount = 1)
|
||||||
|
"01"
|
||||||
|
# i (sell token index)
|
||||||
|
"00"
|
||||||
|
# j (buy token index)
|
||||||
|
"02"
|
||||||
|
# token_approval_needed
|
||||||
|
"00"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_encode_curve_evm_crypto_pool():
|
||||||
|
bob = "0x000000000000000000000000000000000000007B"
|
||||||
|
swap = {
|
||||||
|
"pool_id": "bebc44782c7db0a1a60cb6fe97d0b483032ff1c7",
|
||||||
|
"sell_token": USDT,
|
||||||
|
"buy_token": WETH,
|
||||||
|
"split": 0,
|
||||||
|
"sell_amount": 0,
|
||||||
|
"buy_amount": 100,
|
||||||
|
"token_approval_needed": False,
|
||||||
|
"pool_tokens": (USDT, WBTC, WETH),
|
||||||
|
"pool_type": "CurveSimulatedPoolState",
|
||||||
|
"protocol_specific_attrs": {
|
||||||
|
"curve_v2_pool_type": None,
|
||||||
|
"is_curve_tricrypto": True,
|
||||||
|
"quote": None,
|
||||||
|
"pool_fee": None,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
curve_encoder = CurveSwapStructEncoder()
|
||||||
|
encoded = curve_encoder.encode_swap_struct(
|
||||||
|
swap, receiver=bob, encoding_context=EncodingContext()
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
encoded.hex()
|
||||||
|
==
|
||||||
|
# buy token
|
||||||
|
"c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"
|
||||||
|
# pool address
|
||||||
|
"bebc44782c7db0a1a60cb6fe97d0b483032ff1c7"
|
||||||
|
# receiver
|
||||||
|
"000000000000000000000000000000000000007b"
|
||||||
|
# pool type (tricrypto = 3)
|
||||||
|
"03"
|
||||||
|
# i (sell token index)
|
||||||
|
"00"
|
||||||
|
# j (buy token index)
|
||||||
|
"02"
|
||||||
|
# token_approval_needed
|
||||||
|
"00"
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user