From a6caf84f55abf47d24b62f3c545cf1c48fe976a6 Mon Sep 17 00:00:00 2001 From: Diana Carvalho Date: Thu, 5 Sep 2024 13:13:09 +0100 Subject: [PATCH] feat: Add execution for curve - Add CurveSwapStructEncoder and tests - Add CurveSwapExecutorExposed and tests - Add needed interfaces #time 0m #time 0m #time 0m --- .github/workflows/evm.yml | 4 +- .github/workflows/swap-encoders.yaml | 2 +- evm/src/curve/CurveSwapExecutor.sol | 174 ++++++++++ evm/src/curve/interfaces/ICurveCryptoPool.sol | 26 ++ evm/src/curve/interfaces/ICurvePool.sol | 174 ++++++++++ .../curve/interfaces/ICurvePoolNoReturn.sol | 22 ++ .../curve/interfaces/ICurvePoolWithReturn.sol | 39 +++ evm/src/libraries/EfficientERC20.sol | 295 +++++++++++++++++ evm/test/CurveSwapExecutor.t.sol | 303 ++++++++++++++++++ .../propeller_swap_encoders/curve.py | 98 ++++++ .../tests/test_curve.py | 148 +++++++++ 11 files changed, 1282 insertions(+), 3 deletions(-) create mode 100644 evm/src/curve/CurveSwapExecutor.sol create mode 100644 evm/src/curve/interfaces/ICurveCryptoPool.sol create mode 100644 evm/src/curve/interfaces/ICurvePool.sol create mode 100644 evm/src/curve/interfaces/ICurvePoolNoReturn.sol create mode 100644 evm/src/curve/interfaces/ICurvePoolWithReturn.sol create mode 100644 evm/src/libraries/EfficientERC20.sol create mode 100644 evm/test/CurveSwapExecutor.t.sol create mode 100644 propeller-swap-encoders/propeller_swap_encoders/curve.py create mode 100644 propeller-swap-encoders/propeller_swap_encoders/tests/test_curve.py diff --git a/.github/workflows/evm.yml b/.github/workflows/evm.yml index e3d0ddd..a2429d3 100644 --- a/.github/workflows/evm.yml +++ b/.github/workflows/evm.yml @@ -31,7 +31,7 @@ jobs: - name: Run Forge build run: | forge --version - forge build --sizes + forge build --sizes --via-ir id: build - name: Run Forge format check @@ -42,7 +42,7 @@ jobs: - name: Run Forge tests run: | - forge test -vvv + forge test -vvv --via-ir id: test env: ETH_RPC_URL: ${{ secrets.ETH_RPC_URL }} \ No newline at end of file diff --git a/.github/workflows/swap-encoders.yaml b/.github/workflows/swap-encoders.yaml index 5378523..d246eb6 100644 --- a/.github/workflows/swap-encoders.yaml +++ b/.github/workflows/swap-encoders.yaml @@ -12,7 +12,7 @@ env: jobs: 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: name: Formatting diff --git a/evm/src/curve/CurveSwapExecutor.sol b/evm/src/curve/CurveSwapExecutor.sol new file mode 100644 index 0000000..b110557 --- /dev/null +++ b/evm/src/curve/CurveSwapExecutor.sol @@ -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); + } + } +} diff --git a/evm/src/curve/interfaces/ICurveCryptoPool.sol b/evm/src/curve/interfaces/ICurveCryptoPool.sol new file mode 100644 index 0000000..ff9ea53 --- /dev/null +++ b/evm/src/curve/interfaces/ICurveCryptoPool.sol @@ -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); +} diff --git a/evm/src/curve/interfaces/ICurvePool.sol b/evm/src/curve/interfaces/ICurvePool.sol new file mode 100644 index 0000000..2e37874 --- /dev/null +++ b/evm/src/curve/interfaces/ICurvePool.sol @@ -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); +} diff --git a/evm/src/curve/interfaces/ICurvePoolNoReturn.sol b/evm/src/curve/interfaces/ICurvePoolNoReturn.sol new file mode 100644 index 0000000..b5602d6 --- /dev/null +++ b/evm/src/curve/interfaces/ICurvePoolNoReturn.sol @@ -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); +} diff --git a/evm/src/curve/interfaces/ICurvePoolWithReturn.sol b/evm/src/curve/interfaces/ICurvePoolWithReturn.sol new file mode 100644 index 0000000..bfe4fcd --- /dev/null +++ b/evm/src/curve/interfaces/ICurvePoolWithReturn.sol @@ -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); +} diff --git a/evm/src/libraries/EfficientERC20.sol b/evm/src/libraries/EfficientERC20.sol new file mode 100644 index 0000000..45b9f24 --- /dev/null +++ b/evm/src/libraries/EfficientERC20.sol @@ -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. + // + // 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. + // + 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") } + } + } +} diff --git a/evm/test/CurveSwapExecutor.t.sol b/evm/test/CurveSwapExecutor.t.sol new file mode 100644 index 0000000..ef655d0 --- /dev/null +++ b/evm/test/CurveSwapExecutor.t.sol @@ -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 + ); + } +} diff --git a/propeller-swap-encoders/propeller_swap_encoders/curve.py b/propeller-swap-encoders/propeller_swap_encoders/curve.py new file mode 100644 index 0000000..ac32472 --- /dev/null +++ b/propeller-swap-encoders/propeller_swap_encoders/curve.py @@ -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"], + ], + ) diff --git a/propeller-swap-encoders/propeller_swap_encoders/tests/test_curve.py b/propeller-swap-encoders/propeller_swap_encoders/tests/test_curve.py new file mode 100644 index 0000000..f10ac5f --- /dev/null +++ b/propeller-swap-encoders/propeller_swap_encoders/tests/test_curve.py @@ -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" + )