Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Integrate 1inch #28

Closed
bout3fiddy opened this issue Jul 12, 2024 · 0 comments · Fixed by #27
Closed

Integrate 1inch #28

bout3fiddy opened this issue Jul 12, 2024 · 0 comments · Fixed by #27
Assignees

Comments

@bout3fiddy
Copy link
Collaborator

Background

Currently aggregators face steep uphill battles integrating Curve fully. This is because of the following conditions:

  1. Curve AMMs use more complicated maths that are not easy for integrators to integrate (which means the one who does integrate fully has good alfa)
  2. Newer and Newer factories make things quite challenging to integrate.

This has led to newer pools being slow to integrate of popular aggregator api(s) such as 1inch.

Current integration and its challenges

1inch currently uses an on-chain spot price oracle with the following abi:

https://github.com/1inch/spot-price-aggregator/blob/master/contracts/oracles/CurveOracle.sol

for (uint256 i = 0; i < REGISTRIES_COUNT && index < MAX_POOLS; i++) {
    uint256 registryIndex = 0;
    address pool = registries[i].find_pool_for_coins(address(srcToken), address(dstToken), registryIndex);
    while (pool != address(0) && index < MAX_POOLS) {
        index++;
        // call `get_coin_indices` and set (srcTokenIndex, dstTokenIndex, isUnderlying) variables
        bool isUnderlying;
        int128 srcTokenIndex;
        int128 dstTokenIndex;
        (bool success, bytes memory data) = address(registries[i]).staticcall(abi.encodeWithSelector(ICurveRegistry.get_coin_indices.selector, pool, address(srcToken), address(dstToken)));
        if (success && data.length >= 64) {
            if (
                registryTypes[i] == CurveRegistryType.CRYPTOSWAP_REGISTRY ||
                registryTypes[i] == CurveRegistryType.CRYPTOPOOL_FACTORY ||
                registryTypes[i] == CurveRegistryType.CURVE_TRICRYPTO_FACTORY
            ) {
                (srcTokenIndex, dstTokenIndex) = abi.decode(data, (int128, int128));
            } else {
                // registryTypes[i] == CurveRegistryType.MAIN_REGISTRY ||
                // registryTypes[i] == CurveRegistryType.METAPOOL_FACTORY ||
                // registryTypes[i] == CurveRegistryType.METAREGISTRY ||
                // registryTypes[i] == CurveRegistryType.CRVUSD_PLAIN_POOLS
                (srcTokenIndex, dstTokenIndex, isUnderlying) = abi.decode(data, (int128, int128, bool));
            }
        } else {
            pool = registries[i].find_pool_for_coins(address(srcToken), address(dstToken), ++registryIndex);
            continue;
        }

        if (!isUnderlying) {
            info = FunctionSelectorsInfo({
                balanceFunc: ICurveRegistry.get_balances.selector,
                dyFuncInt128: ICurveSwapInt128.get_dy.selector,
                dyFuncUint256: ICurveSwapUint256.get_dy.selector
            });
        } else {
            info = FunctionSelectorsInfo({
                balanceFunc: ICurveRegistry.get_underlying_balances.selector,
                dyFuncInt128: ICurveSwapInt128.get_dy_underlying.selector,
                dyFuncUint256: ICurveSwapUint256.get_dy_underlying.selector
            });
        }

        // call `balanceFunc` (`get_balances` or `get_underlying_balances`) and decode results
        uint256[] memory balances;
        (success, data) = address(registries[i]).staticcall(abi.encodeWithSelector(info.balanceFunc, pool));
        if (success && data.length >= 64) {
            // registryTypes[i] == CurveRegistryType.MAIN_REGISTRY ||
            // registryTypes[i] == CurveRegistryType.CRYPTOSWAP_REGISTRY ||
            // registryTypes[i] == CurveRegistryType.METAREGISTRY
            uint256 length = 8;
            if (registryTypes[i] == CurveRegistryType.METAPOOL_FACTORY ||
                registryTypes[i] == CurveRegistryType.CRVUSD_PLAIN_POOLS) {
                length = 4;
            } else if (registryTypes[i] == CurveRegistryType.CURVE_TRICRYPTO_FACTORY) {
                length = 3;
            } else if (registryTypes[i] == CurveRegistryType.CRYPTOPOOL_FACTORY) {
                length = 2;
            }

            assembly ("memory-safe") { // solhint-disable-line no-inline-assembly
                balances := data
                mstore(balances, length)
            }
        } else {
            pool = registries[i].find_pool_for_coins(address(srcToken), address(dstToken), ++registryIndex);
            continue;
        }

        uint256 w = (balances[uint128(srcTokenIndex)] * balances[uint128(dstTokenIndex)]).sqrt();
        uint256 b0 = balances[uint128(srcTokenIndex)] / 10000;
        uint256 b1 = balances[uint128(dstTokenIndex)] / 10000;

        if (b0 != 0 && b1 != 0) {
            (success, data) = pool.staticcall(abi.encodeWithSelector(info.dyFuncInt128, srcTokenIndex, dstTokenIndex, b0));
            if (!success || data.length < 32) {
                (success, data) = pool.staticcall(abi.encodeWithSelector(info.dyFuncUint256, uint128(srcTokenIndex), uint128(dstTokenIndex), b0));
            }
            if (success && data.length >= 32) {  // vyper could return redundant bytes
                b1 = abi.decode(data, (uint256));
                ratesAndWeights.append(OraclePrices.OraclePrice(Math.mulDiv(b1, 1e18, b0), w));
            }
        }
        pool = registries[i].find_pool_for_coins(address(srcToken), address(dstToken), ++registryIndex);
    }

There's a lot of if-else branches here that need to be added by the integrator. This gets quite challenging over time to update, which needs to be done ad-hoc.

Proposed Solution

The method that 1inch relies on is the getRate method with the following signature:

function getRate(IERC20 srcToken, IERC20 dstToken, IERC20 connector, uint256 thresholdFilter) external view override returns (uint256 rate, uint256 weight)

This is utilised by 1inch to get some sort of a weighting and a rate output for a source token and destination token.

We've come to realise that 1inch could greatly benefit with Curve core handling the integration. Instead of providing a single pool and rate value (and weight), the on-chain api contract could fetch a list of quotes from a list of pool contracts. This contract would need to call a working MetaRegistry in each chain. The flow would be as follows:

  1. Rate contract calls metaregistry to fetch a list of coins for coin_a and coin_b via: metaregistry.find_pools_for_coins
  2. Rate contract gets coin indices from metaregistry.get_coin_indices
  3. If pool is stableswap (check if pool has gamma parameter], then step 2 returns is_underlying as True in the Tuple output.
  4. The rate contract calls get_dy or get_dy_underlying for each pool and coin indices list.
  5. The rate contract compiles this into a list of quotes defined as follows:
MAX_COINS: constant(uint256) = 8

struct Quote:
    # i and j values to be fed into either `exchange_underlying` or `exchange` method
    # user can infer whether to use int128 or uint256 from `pool_type` and `exchange_underlying`
    # from `is_underlying`.
    source_token_index: uint256
    dest_token_index: uint256

    # Pool which gives the requested quote
    pool: address

    # Balances are necessary for aggregators to know if pool is liquid
    balances: DynArray[uint256, MAX_COINS]

    # Quoted output amount
    amount_out: address
    
    # If is_underlying, pool is a stableswap metapool and you can call. 
    # exchange_underlying with the indices directly on the pool contract.
    is_underlying: bool   

    # 0 for stableswap, 1 for cryptoswap, 2 for LLAMMA.
    #   If 0, pool needs uint128 as coin index inputs.
    pool_type: uint8       

    
@external
@view
def getRate(source_token: address, dstToken: address, amount_in: uint256) -> DynArray[Quote, MAX_QUOTES]:
    ...

The main concern faced here is: there should not be an out-of-gas error while doing multicalls for a large array of coins: gas needs to be a consideration.

@bout3fiddy bout3fiddy self-assigned this Jul 12, 2024
@bout3fiddy bout3fiddy linked a pull request Jul 12, 2024 that will close this issue
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

1 participant