justsomegeeks / enzyme-balancer

7 stars 1 forks source link

Implement Lend #6

Open JulissaDantes opened 3 years ago

JulissaDantes commented 3 years ago

This ticket is to keep track of documentation and development to implement the lending function of our adapter.

When we say Lending we mean the user lends the money to the Balancer pool.

jrhite commented 3 years ago

Check this out as a starting point of how we might wire everything together for Lend/Redeem: https://gist.github.com/buendiadas/2435c56880cd5b81d711c5163cf49f71

JulissaDantes commented 3 years ago

Documentation:

Understanding the lending function

This is the function we will use

  /// @dev Helper to add liquidity
    function __balancerV2Lend(
        bytes32 _poolId,
        address _sender,
        address _recipient,
        IBalancerV2Vault.JoinPoolRequest memory _request
    ) internal {
        IBalancerV2Vault(BALANCER_V2_VAULT).joinPool(_poolId, _sender, _recipient, _request);
    }

The request param struct looks like this:

  struct JoinPoolRequest {
        IBalancerV2Asset[] assets;
        uint256[] maxAmountsIn;
        bytes userData;
        bool fromInternalBalance;

}

The IBalancerV2Asset represents an empty interface used to represent either ERC20-conforming token contracts or ETH (using the zero address sentinel value). We're just relying on the fact that interface can be used to declare new address-like types. The join pool implementations looks like this on the balancer repo:

 function joinPool(
        bytes32 poolId,
        address sender,
        address recipient,
        JoinPoolRequest memory request
    ) external payable override whenNotPaused {
        // This function doesn't have the nonReentrant modifier: it is applied to `_joinOrExit` instead.

        // Note that `recipient` is not actually payable in the context of a join - we cast it because we handle both
        // joins and exits at once.
        _joinOrExit(PoolBalanceChangeKind.JOIN, poolId, sender, payable(recipient), _toPoolBalanceChange(request));
    }

The actual function is calling is:

 /**
     * @dev Implements both `joinPool` and `exitPool`, based on `kind`.
     */
    function _joinOrExit(
        PoolBalanceChangeKind kind,
        bytes32 poolId,
        address sender,
        address payable recipient,
        PoolBalanceChange memory change
    ) private nonReentrant withRegisteredPool(poolId) authenticateFor(sender) {
        // This function uses a large number of stack variables (poolId, sender and recipient, balances, amounts, fees,
        // etc.), which leads to 'stack too deep' issues. It relies on private functions with seemingly arbitrary
        // interfaces to work around this limitation.

        InputHelpers.ensureInputLengthMatch(change.assets.length, change.limits.length);

        // We first check that the caller passed the Pool's registered tokens in the correct order, and retrieve the
        // current balance for each.
        IERC20[] memory tokens = _translateToIERC20(change.assets);
        bytes32[] memory balances = _validateTokensAndGetBalances(poolId, tokens);

        // The bulk of the work is done here: the corresponding Pool hook is called, its final balances are computed,
        // assets are transferred, and fees are paid.
        (
            bytes32[] memory finalBalances,
            uint256[] memory amountsInOrOut,
            uint256[] memory paidProtocolSwapFeeAmounts
        ) = _callPoolBalanceChange(kind, poolId, sender, recipient, change, balances);

        // All that remains is storing the new Pool balances.
        PoolSpecialization specialization = _getPoolSpecialization(poolId);
        if (specialization == PoolSpecialization.TWO_TOKEN) {
            _setTwoTokenPoolCashBalances(poolId, tokens[0], finalBalances[0], tokens[1], finalBalances[1]);
        } else if (specialization == PoolSpecialization.MINIMAL_SWAP_INFO) {
            _setMinimalSwapInfoPoolBalances(poolId, tokens, finalBalances);
        } else {
            // PoolSpecialization.GENERAL
            _setGeneralPoolBalances(poolId, finalBalances);
        }

        bool positive = kind == PoolBalanceChangeKind.JOIN; // Amounts in are positive, out are negative
        emit PoolBalanceChanged(
            poolId,
            sender,
            tokens,
            // We can unsafely cast to int256 because balances are actually stored as uint112
            _unsafeCastToInt256(amountsInOrOut, positive),
            paidProtocolSwapFeeAmounts
        );
    }
jrhite commented 3 years ago

@JulissaDantes

Good question. I was just facing an issue with this interface. It wouldn't compile with Solidity 0.6.12 when I was trying to return an array of them.

If you check the Balancer docs and code you'll find the interface there: IAsset.sol. But you won't find any implementations because it's just to wrap ERC20 addresses with and (I think) provide some ETH convenience functionality since ETH isn't an ERC20 itself. I don't fully understand yet it though.

Example usage: IAsset a = IAsset(...some_erc20_addr_);

To get it to compile on our end I have a branch that I updated the balancer adapter code in. I changed all the IBalancerV2Asset[] to be address[] and that compiles. I still haven't tested the call in our contract to Balancers contract yet though.

JulissaDantes commented 3 years ago

Documentation: Another useful example of the enzyme consumming the lending function of an external adapter is the AAVE Adapter, so Im leaving here the adapter contract, the adapters tests, and the testutils.

JulissaDantes commented 3 years ago

Why do we use the BPT price inside the Lending function?

The JoinPoolRequest includes a field called userData which contains this information:

 joinRequest = {
        assets: basePoolTokens.addresses,
        maxAmountsIn: [0, fp(1)],
        userData: WeightedPoolEncoder.joinExactTokensInForBPTOut([0, fp(1)], 0),
        fromInternalBalance: false,
      };

*That code snippet was taken from a balancer tests calling the joinPool function that we use on the lending process. The joinExactTokensInForBPTOut function goes like this(also taken from balancer repo):

/**
   * Encodes the userData parameter for joining a WeightedPool with exact token inputs
   * @param amountsIn - the amounts each of token to deposit in the pool as liquidity
   * @param minimumBPT - the minimum acceptable BPT to receive in return for deposited tokens
   */
  static joinExactTokensInForBPTOut = (amountsIn: BigNumberish[], minimumBPT: BigNumberish): string =>
    defaultAbiCoder.encode(
      ['uint256', 'uint256[]', 'uint256'],
      [WeightedPoolJoinKind.EXACT_TOKENS_IN_FOR_BPT_OUT, amountsIn, minimumBPT]
    );

My opinion: In order to compute minimumBPT we need to know the BPT price, and the other place where we need it is after joinning the pool to compute how much to send the recipient on the function _callPoolBalanceChange, but that is called inside the vault and doesnt require the BPT price. But honestly I still don't see where would we need it.

JulissaDantes commented 3 years ago

Response from balancer: Q: Hi, how do I compute the minimumBPT when trying to fill the userData of the JoinPoolRequest? R: you should look at the total amount of BPT that exists for that pool, the amount of tokens in the pool, the amount you're looking to input, and some slippage tolerance Q: Hi, and after I get all that does do I compute the minimun BPT? and just to confirm, when you say amount of tokens in that pool you mean there are 2 weth and 1 dai the amount of tokens will be 3? R: If you're doing a single asset join with DAI, it would look something like...

expectedBPT = totalBPT/totalDAI * amountInDAI

minimumBPT= slippageFactor* expectedBPT where totalBPTis the total amount of BPT that exists for that pool, and totalDAIis the total amount of DAI in the pool amountInDAIis the amount of DAI you're putting into the pool

JulissaDantes commented 3 years ago

Useful link to Balancer errors: https://github.com/balancer-labs/balancer-v2-monorepo/blob/c18ff2686c61a8cbad72cdcfc65e9b11476fdbc3/pkg/balancer-js/src/utils/errors.ts