diff --git a/.github/workflows/evm.yml b/.github/workflows/evm.yml index e3d0ddd9..a2429d30 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 5378523e..d246eb6e 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 00000000..b1105570 --- /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 00000000..ff9ea533 --- /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 00000000..2e378747 --- /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 00000000..b5602d62 --- /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 00000000..bfe4fcd2 --- /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 00000000..45b9f240 --- /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 00000000..ef655d08 --- /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 00000000..ac324726 --- /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 00000000..f10ac5fb --- /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" + ) diff --git a/substreams/Cargo.lock b/substreams/Cargo.lock index 7c0e418f..fbc22c5e 100644 --- a/substreams/Cargo.lock +++ b/substreams/Cargo.lock @@ -210,7 +210,7 @@ dependencies = [ [[package]] name = "ethereum-balancer" -version = "0.2.0" +version = "0.2.1" dependencies = [ "anyhow", "bytes", diff --git a/substreams/crates/tycho-substreams/src/lib.rs b/substreams/crates/tycho-substreams/src/lib.rs index 0eef870f..888080e0 100644 --- a/substreams/crates/tycho-substreams/src/lib.rs +++ b/substreams/crates/tycho-substreams/src/lib.rs @@ -4,6 +4,7 @@ pub mod balances; pub mod contract; mod mock_store; pub mod models; +#[allow(clippy::too_long_first_doc_paragraph)] mod pb; pub mod prelude { diff --git a/substreams/ethereum-balancer/Cargo.toml b/substreams/ethereum-balancer/Cargo.toml index 9bc359ae..8b9ba802 100644 --- a/substreams/ethereum-balancer/Cargo.toml +++ b/substreams/ethereum-balancer/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ethereum-balancer" -version = "0.2.0" +version = "0.2.1" edition = "2021" [lib] diff --git a/substreams/ethereum-balancer/src/modules.rs b/substreams/ethereum-balancer/src/modules.rs index f0e1abb7..d85102d0 100644 --- a/substreams/ethereum-balancer/src/modules.rs +++ b/substreams/ethereum-balancer/src/modules.rs @@ -90,10 +90,10 @@ pub fn map_relative_balances( } } } else if let Some(ev) = abi::vault::events::Swap::match_and_decode(vault_log.log) { - let component_id = format!("0x{}", hex::encode(&ev.pool_id[..20])); + let component_id = format!("0x{}", hex::encode(ev.pool_id)); if store - .get_last(format!("pool:{}", component_id)) + .get_last(format!("pool:{}", &component_id[..42])) .is_some() { deltas.extend_from_slice(&[ diff --git a/substreams/ethereum-balancer/substreams.yaml b/substreams/ethereum-balancer/substreams.yaml index dfd8c30b..0bbb66b6 100644 --- a/substreams/ethereum-balancer/substreams.yaml +++ b/substreams/ethereum-balancer/substreams.yaml @@ -1,7 +1,7 @@ specVersion: v0.1.0 package: name: "ethereum_balancer" - version: v0.2.0 + version: v0.2.1 protobuf: files: diff --git a/substreams/ethereum-curve/README.md b/substreams/ethereum-curve/README.md index 2bcc2437..772e8c8c 100644 --- a/substreams/ethereum-curve/README.md +++ b/substreams/ethereum-curve/README.md @@ -24,3 +24,28 @@ contains the following fields: - `attributes`: A nested object of key to value that represents attributes. Please see the included 3 examples for `3pool`, `steth`, and `tricrypto2`. + +## Open tasks + +### Add underlying tokens in metapools + +Currently, metapools are not working properly due to the way we override token balances. +The issue arises because when we modify token balances, we end up changing the token contract code and storage. +This issue will be resolved once we implement a flexible method to adjust token balances without affecting the contract’s functionality. +We will also need to index additional contract such as the base pool lp token. + +### Handle rebasing, ERC4644 and others special kind of tokens + +At the moment, we are unable to manage certain types of tokens, such as rebasing tokens or ERC4644 tokens, because they have unique behavior or specific logic that complicates simulations. +To handle these tokens properly, we will likely need to use the dynamic contract indexer (DCI), which can track and index the full state of the token contract, allowing us to deal with their complexities effectively. + +## Static Attributes + +| name | type | description | +| ------------ | ----- | ----------------------------------------------------------------------------------------------------------- | +| pool_type | str | A unique identifier per pool type. Set depending on the factory. | +| name | str | A string representing the name of the pool, set if there is one. | +| factory_name | str | A string representing the name of the factory that created the pool. "na" if the pool was manually created. | +| factory | bytes | The address of the factory that created the pool. "0x000..." if the pool was manually created. | +| lp_token | bytes | The pool lp token, set if the lp token is not the pool itself | +| base_pool | bytes | The base pool related to this pool, set only for metapools. | diff --git a/substreams/ethereum-curve/integration_test.tycho.yaml b/substreams/ethereum-curve/integration_test.tycho.yaml index b579c9ab..b7442c54 100644 --- a/substreams/ethereum-curve/integration_test.tycho.yaml +++ b/substreams/ethereum-curve/integration_test.tycho.yaml @@ -16,7 +16,7 @@ tests: - "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48" - "0x6b175474e89094c44da98b954eedeac495271d0f" static_attributes: - factory_name: "0x6e61" # na + factory_name: "0x4e41" # NA name: "0x33706f6f6c" # 3pool factory: "0x307830303030303030303030303030303030303030303030303030303030303030303030303030303030" # 0x0000000000000000000000000000000000000000 creation_tx: "0x20793bbf260912aae189d5d261ff003c9b9166da8191d8f9d63ff1c7722f3ac6" @@ -34,7 +34,7 @@ tests: static_attributes: factory: "0x307830303030303030303030303030303030303030303030303030303030303030303030303030303030" # 0x0000000000000000000000000000000000000000 name: "0x7374657468" # steth - factory_name: "0x6e61" # na + factory_name: "0x4e41" # NA creation_tx: "0xfac67ecbd423a5b915deff06045ec9343568edaec34ae95c43d35f2c018afdaa" skip_simulation: false @@ -50,7 +50,7 @@ tests: - "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2" static_attributes: factory: "0x307830303030303030303030303030303030303030303030303030303030303030303030303030303030" # 0x0000000000000000000000000000000000000000 - factory_name: "0x6e61" # na + factory_name: "0x4e41" # NA name: "0x74726963727970746f32" # tricrypto2 creation_tx: "0xdafb6385ed988ce8aacecfe1d97b38ea5e60b1ebce74d2423f71ddd621680138" skip_simulation: false @@ -68,7 +68,7 @@ tests: - "0x57ab1ec28d129707052df4df418d58a2d46d5f51" static_attributes: factory: "0x307830303030303030303030303030303030303030303030303030303030303030303030303030303030" # 0x0000000000000000000000000000000000000000 - factory_name: "0x6e61" # na + factory_name: "0x4e41" # NA name: "0x73757364" # susd creation_tx: "0x51aca4a03a395de8855fa2ca59b7febe520c2a223e69c502066162f7c1a95ec2" skip_simulation: false @@ -84,7 +84,7 @@ tests: - "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48" static_attributes: name: "0x6672617875736463" # fraxusdc - factory_name: "0x6e61" # na + factory_name: "0x4e41" # NA factory: "0x307830303030303030303030303030303030303030303030303030303030303030303030303030303030" # 0x0000000000000000000000000000000000000000 creation_tx: "0x1f4254004ce9e19d4eb742ee5a69d30f29085902d976f73e97c44150225ef775" skip_simulation: false @@ -106,7 +106,7 @@ tests: static_attributes: factory: "0x307836613863626564373536383034623136653035653734316564616264356362353434616532316266" # 0x6a8cbed756804b16e05e741edabd5cb544ae21bf factory_name: "0x63727970746f5f737761705f6e675f666163746f7279" # crypto_swap_ng_factory - name: "0x757364652d75736463" # usde-usdc + name: "0x555344652d55534443" # USDe-USDC pool_type: "0x706c61696e5f706f6f6c" # plain_pool creation_tx: "0x6f4438aa1785589e2170599053a0cdc740d8987746a4b5ad9614b6ab7bb4e550" skip_simulation: false @@ -125,7 +125,7 @@ tests: - "0xa5588f7cdf560811710a2d82d3c9c99769db1dcb" static_attributes: factory_name: "0x63727970746f5f737761705f6e675f666163746f7279" # crypto_swap_ng_factory - name: "0x646f6c612f667261787079757364" # dola/fraxpyusd + name: "0x444f4c412f465241585059555344" # DOLA/FRAXPYUSD pool_type: "0x6d657461706f6f6c" # metapool base_pool: "0x307861353538386637636466353630383131373130613264383264336339633939373639646231646362" # 0xa5588f7cdf560811710a2d82d3c9c99769db1dcb factory: "0x307836613863626564373536383034623136653035653734316564616264356362353434616532316266" # 0x6a8cbed756804b16e05e741edabd5cb544ae21bf @@ -145,7 +145,7 @@ tests: - "0x6c3ea9036406852006290770BEdFcAbA0e23A0e8" - "0x3175Df0976dFA876431C2E9eE6Bc45b65d3473CC" static_attributes: - name: "0x70617970616c667261786270" # paypalfraxbp + name: "0x50415950414c465241584250" # PAYPALFRAXBP factory_name: "0x6d6574615f706f6f6c5f666163746f7279" # meta_pool_factory base_pool: "0x307864636566393638643431366134316364616330656438373032666163383132386136343234316132" # 0xdcfe968d416ac0ed8702fac8128a64241a2 factory: "0x307862396663313537333934616638303461333537383133346136353835633064633963633939306434" # 0xb9fc157394af804a3578134a6585c0dcc993099d @@ -165,7 +165,7 @@ tests: - "0xe9633C52f4c8B7BDeb08c4A7fE8a5c1B84AFCf67" - "0x77E06c9eCCf2E797fd462A92B6D7642EF85b0A44" static_attributes: - name: "0x77737474616f2f7774616f" # wsttao/wtao + name: "0x77737454414f2f7754414f" # wstTAO/wTAO factory: "0x307862396663313537333934616638303461333537383133346136353835633064633963633939306434" # 0xb9fc157394af804a3578134a6585c0dcc993099d factory_name: "0x6d6574615f706f6f6c5f666163746f7279" # meta_pool_factory pool_type: "0x706c61696e5f706f6f6c" # plain_pool @@ -176,7 +176,7 @@ tests: - "0xe9633C52f4c8B7BDeb08c4A7fE8a5c1B84AFCf67" - "0x77E06c9eCCf2E797fd462A92B6D7642EF85b0A44" static_attributes: - name: "0x77737474616f2f7774616f" # wsttao/wtao + name: "0x77737454414f2f7754414f" # wstTAO/wTAO factory: "0x307862396663313537333934616638303461333537383133346136353835633064633963633939306434" # 0xb9fc157394af804a3578134a6585c0dcc993099d factory_name: "0x6d6574615f706f6f6c5f666163746f7279" # meta_pool_factory pool_type: "0x706c61696e5f706f6f6c" # plain_pool @@ -188,16 +188,16 @@ tests: start_block: 19162590 stop_block: 19163633 expected_components: - - id: "0x71db3764d6841d8b01dc27c0fd4a66a8a34b2be0" #TODO: ADD TEST THAT USE WETH + - id: "0x71db3764d6841d8b01dc27c0fd4a66a8a34b2be0" tokens: - "0x04c154b66cb340f3ae24111cc767e0184ed00cc6" - "0x4591dbff62656e7859afe5e45f6f47d3669fbb28" static_attributes: - name: "0x343030303030" # 400000 + name: "0x70784554482f6d6b555344" # pxETH/mkUS pool_type: "0x63727970746f5f706f6f6c" # crypto_pool factory: "0x307866313830353662626433323065393661343865336662663862633036313332323533316161633939" # 0xf18056bbd320e96a48e3fb8bc061322531aacc99 factory_name: "0x63727970746f5f706f6f6c5f666163746f7279" # crypto_pool_factory - lp_token: "0x6ade6971ca3d90990c30d39c78b0736c7166e07b" # 0x6ade6971ca3d90990c30d39c78b0736c7166e07b + lp_token: "0x6ade6971ca3d90990c30d39c78b0534c7166e07b" # 0x6ade6971ca3d90990c30d39c78b0534c7166e07b creation_tx: "0xa89c09a7e0dfd84f3a294b8df4f33cc4a623e6d52deee357457afe2591ea596f" skip_simulation: false - id: "0x6c9Fe53cC13b125d6476E5Ce2b76983bd5b7A112" @@ -205,16 +205,16 @@ tests: - "0x35fA164735182de50811E8e2E824cFb9B6118ac2" - "0xf951E335afb289353dc249e82926178EaC7DEd78" static_attributes: - name: "0x343030303030" # 400000 + name: "0x654554482f7377455448" # eETH/swETH pool_type: "0x63727970746f5f706f6f6c" # crypto_pool factory: "0x307866313830353662626433323065393661343865336662663862633036313332323533316161633939" # 0xf18056bbd320e96a48e3fb8bc061322531aacc99 factory_name: "0x63727970746f5f706f6f6c5f666163746f7279" # crypto_pool_factory - lp_token: "0x94c4eba4f4b97be8d778f8c27027d676270e87a6" # 0x94c4eba4f4b97be8d778f8c27027d676270e87a6 + lp_token: "0x94c4eba4f4b97be8d758f8c27027d656270e87a6" # 0x94c4eba4f4b97be8d758f8c27027d656270e87a6 creation_tx: "0xa5b13d50c56242f7994b8e1339032bb4c6f9ac3af3054d4eae3ce9e32e3c1a50" skip_simulation: true # Reason: this pool has no liquidity at stop_block # CryptoPool factory 0xF18056Bbd320E96A48e3Fbf8bC061322531aac99 - with ETH - - name: test_cryptopool_factory + - name: test_cryptopool_factory_with_eth start_block: 19278886 stop_block: 19278926 expected_components: @@ -223,7 +223,7 @@ tests: - "0x0000000000000000000000000000000000000000" - "0x55296f69f40Ea6d20E478533C15A6B08B654E758" static_attributes: - name: "0x343030303030" # 400000 + name: "0x4554482f58594f" # ETH/XYO pool_type: "0x63727970746f5f706f6f6c" # crypto_pool factory: "0x307866313830353662626433323065393661343865336662663862633036313332323533316161633939" # 0xf18056bbd320e96a48e3fb8bc061322531aacc99 factory_name: "0x63727970746f5f706f6f6c5f666163746f7279" # crypto_pool_factory @@ -247,7 +247,7 @@ tests: static_attributes: factory: "0x307830633065356632666630666631386133626539623833353633353033393235366463346234393633" # 0x0c0e5f2ff0ff18a3be9b8356335039256dc4b4963 factory_name: "0x74726963727970746f5f666163746f7279" # tricrypto_factory - name: "0x74726963727970746f75736463" # tricrypto_usdc + name: "0x54726963727970746f55534443" # TricryptoUSDC pool_type: "0x74726963727970746f" # tricrypto creation_tx: "0x2bd59c19f993b83729fb23498f897a58567c6f0b3ee2f00613ba515a7b19fe23" skip_simulation: false @@ -267,7 +267,7 @@ tests: factory: "0x307839386565383531613030616265653064393564303863663463613262646365333261656161663766" # 0x98ee851a00abee0d95d08cf4ca2bdce32aea7f7f pool_type: "0x74776f63727970746f" # twocrypto factory_name: "0x74776f63727970746f5f666163746f7279" # twocrypto_factory - name: "0x7577752f77657468" # uwu/weth + name: "0x5577552f57455448" # UwU/WETH creation_tx: "0x61d563e2627437da172fdd60ab54e5cc955fcb75829fd819486e857bac31cad2" skip_simulation: false @@ -283,7 +283,7 @@ tests: - "0xf939E0A03FB07F59A73314E73794Be0E57ac1b4E" - "0xdAC17F958D2ee523a2206206994597C13D831ec7" static_attributes: - name: "0x6372767573642f75736474" # crvusd/usdt + name: "0x6372765553442f55534454" # crvUSD/USDT pool_type: "0x706c61696e5f706f6f6c" # plain_pool factory: "0x307834663838343661653933383062393064326537316435653364303432646666336537656262343064" # 0x4f8846ae9380b90d2e71d5e3d042dff3e7ebb40d factory_name: "0x737461626c655f737761705f666163746f7279" # stable_swap_factory @@ -307,10 +307,10 @@ tests: - "0x853d955aCEf822Db058eb8505911ED77F175b99e" - "0x6c3F90f043a72FA612cbac8115EE7e52BDe6E490" static_attributes: - factory_name: "0x6d6574615f706f6f6c5f666163746f7279" # meta_pool_factory + factory_name: "0x6d6574615f706f6f6c5f666163746f72795f6f6c64" # meta_pool_factory_old base_pool: "0x307862656263343437383263376462306131613630636236666539376430623438333033326666316337" # 0xbebc44782c7db0a1a60cb6fe97d0b483032ff1c7 factory: "0x307830393539313538623630343064333264303463333031613732636266643662333965323163396165" # 0x0959158b6040d32d04c301a72cbfd6b39e21c9ae pool_type: "0x6d657461706f6f6c" # metapool - name: "0x66726178" # frax + name: "0x46726178" # Frax creation_tx: "0x1f2a0d4e1c1eca594bd7f27f9952480ccda422c3453e0c5074a63aa46a2ed628" skip_simulation: true # Reason: this pool calls `totalSupply()` on the LP token during simulation. But this token is overridden and doesn't have anything for totalSupply diff --git a/substreams/ethereum-curve/src/pool_factories.rs b/substreams/ethereum-curve/src/pool_factories.rs index a52e249c..4c994eab 100644 --- a/substreams/ethereum-curve/src/pool_factories.rs +++ b/substreams/ethereum-curve/src/pool_factories.rs @@ -84,6 +84,9 @@ pub fn address_map( let pool_added = abi::crypto_pool_factory::events::CryptoPoolDeployed::match_and_decode(log)?; + let pool_name = abi::crypto_pool_factory::functions::DeployPool::match_and_decode(call) + .map_or("none".to_string(), |call| call.name); + let tokens = swap_weth_for_eth(pool_added.coins.into()); let component_id = &call.return_data[12..]; @@ -110,7 +113,7 @@ pub fn address_map( }, Attribute { name: "name".into(), - value: pool_added.a.to_string().into(), + value: pool_name.into(), change: ChangeType::Creation.into(), }, Attribute { @@ -392,7 +395,7 @@ pub fn address_map( }, Attribute { name: "factory_name".into(), - value: "meta_pool_factory".into(), + value: "meta_pool_factory_old".into(), change: ChangeType::Creation.into(), }, Attribute { diff --git a/testing/src/runner/models.py b/testing/src/runner/models.py index 6e15d027..a4711a80 100644 --- a/testing/src/runner/models.py +++ b/testing/src/runner/models.py @@ -30,7 +30,9 @@ def convert_tokens_to_hexbytes(cls, v): @validator("static_attributes", pre=True, always=True) def convert_static_attributes_to_hexbytes(cls, v): - return {k: HexBytes(v[k].lower()) for k in v} if v else {} + if v: + return {k: v[k] if isinstance(v[k], HexBytes) else HexBytes(v[k].lower()) for k in v} + return {} @validator("creation_tx", pre=True, always=True) def convert_creation_tx_to_hexbytes(cls, v):