Withdraw process with the Liquidity Handler

Example: A walkthrough of a withdrawal from IbAlluoUSD to USDC

Step 1: User calls withdraw with 100 USDC and withdraw is called from the handler to send funds directly to the user.

/// @notice  Withdraws accuratel
/// @dev When called, immediately check for new interest index. Then find the adjusted amount in IbAlluo tokens
///      Then burn appropriate amount of IbAlluo tokens to receive asset token
/// @param _targetToken Asset token
/// @param _amount Amount (parsed 10**18) in asset value

function withdrawTo(
    address _recipient,
    address _targetToken,
    uint256 _amount
) public {
    updateRatio();
    uint256 adjustedAmount = (_amount * multiplier) / growingRatio;
    _burn(_msgSender(), adjustedAmount);
    ILiquidityHandler handler = ILiquidityHandler(liquidityHandler);
    if (supportedTokens.contains(_targetToken) == false) {
        (address liquidToken,) = ILiquidityHandler(liquidityHandler).getAdapterCoreTokensFromIbAlluo(address(this));
        // This just is used to revert if there is no active route.
        require(IExchange(exchangeAddress).buildRoute(liquidToken, _targetToken).length > 0, "!Supported");
        handler.withdraw(
        _recipient,
        liquidToken,
        _amount,
        _targetToken
        );
    } else {
        handler.withdraw(
        _recipient,
        _targetToken,
        _amount
        );
    }

    emit TransferAssetValue(_msgSender(), address(0), adjustedAmount, _amount, growingRatio);
    emit BurnedForWithdraw(_msgSender(), adjustedAmount);
}

Step 2: When withdraw is called for 100 USDC, carry out checks and either withdraw funds directly or add the user to queue.

  1. If the buffer can be used to meet the withdrawal, simply call withdraw on the adapter to send 100 USDC tokens directly to the user.

  2. If there is not enough funds in the buffer OR there is an existing queue of withdrawals, add the user to the queue until there are enough funds to meet it.

LiquidityHandler.sol
 /** @notice Called by ibAlluo, withdraws tokens from the adapter.
 * @dev Attempt to withdraw. If there are insufficient funds, you are added to the queue.
 ** @param _user Address of depositor
 ** @param _token Address of token (USDC, DAI, USDT...)
 ** @param _amount Amount of tokens in 10**18
 */
function withdraw(
    address _user,
    address _token,
    uint256 _amount
) external whenNotPaused onlyRole(DEFAULT_ADMIN_ROLE) {
    uint256 inAdapter = getAdapterAmount(msg.sender);

    WithdrawalSystem storage withdrawalSystem = ibAlluoToWithdrawalSystems[
        msg.sender
    ];
    if (inAdapter >= _amount && withdrawalSystem.totalWithdrawalAmount == 0) {
        uint256 adapterId = ibAlluoToAdapterId.get(msg.sender);
        address adapter = adapterIdsToAdapterInfo[adapterId].adapterAddress;
        IHandlerAdapter(adapter).withdraw(_user, _token, _amount);
        emit WithdrawalSatisfied(
            msg.sender,
            _user,
            _token,
            _amount,
            0,
            block.timestamp
        );
    } else {
        uint256 lastWithdrawalRequest = withdrawalSystem
            .lastWithdrawalRequest;
        withdrawalSystem.lastWithdrawalRequest++;
        withdrawalSystem.withdrawals[
            lastWithdrawalRequest + 1
        ] = Withdrawal({
            user: _user,
            token: _token,
            amount: _amount,
            time: block.timestamp
        });
        withdrawalSystem.totalWithdrawalAmount += _amount;
        emit AddedToQueue(
            msg.sender,
            _user,
            _token,
            _amount,
            lastWithdrawalRequest + 1,
            block.timestamp
        );
    }
}

Step 2.5: If the user wanted a withdrawal in a token not supported by the IbAlluo (i.e WETH for IbAlluoUSD), carry out the withdrawal and do a swap through the Alluo Exchange before sending to the user

//// Same function as above but overload for case when you want to withdraw in a different token native to an ibAlluo.
// For example: Withdraw USDC from ibAlluoEth.
function withdraw(
    address _user,
    address _token,
    uint256 _amount,
    address _outputToken
) external whenNotPaused onlyRole(DEFAULT_ADMIN_ROLE) {
    uint256 inAdapter = getAdapterAmount(msg.sender);

    WithdrawalSystem storage withdrawalSystem = ibAlluoToWithdrawalSystems[
        msg.sender
    ];
    if (
        inAdapter >= _amount && withdrawalSystem.totalWithdrawalAmount == 0
    ) {
        uint256 adapterId = ibAlluoToAdapterId.get(msg.sender);
        address adapter = adapterIdsToAdapterInfo[adapterId].adapterAddress;
        if (_token != _outputToken) {
            IHandlerAdapter(adapter).withdraw(address(this), _token, _amount);
            _withdrawThroughExchange(_token, _outputToken, _amount, _user);
        } else {
            IHandlerAdapter(adapter).withdraw(_user, _token, _amount);
        }
        emit WithdrawalSatisfied(
            msg.sender,
            _user,
            _token,
            _amount,
            0,
            block.timestamp
        );
    } else {
        require(
            _token == _outputToken,
            "Handler: Only supported tokens"
        );
        uint256 lastWithdrawalRequest = withdrawalSystem
            .lastWithdrawalRequest;
        withdrawalSystem.lastWithdrawalRequest++;
        withdrawalSystem.withdrawals[
            lastWithdrawalRequest + 1
        ] = Withdrawal({
            user: _user,
            token: _token,
            amount: _amount,
            time: block.timestamp
        });
        withdrawalSystem.totalWithdrawalAmount += _amount;
        emit AddedToQueue(
            msg.sender,
            _user,
            _token,
            _amount,
            lastWithdrawalRequest + 1,
            block.timestamp
        );
    }
}

// Here, we are directly swapping tokens and then sending these tokens 
function _withdrawThroughExchange(
    address _inputToken,
    address _targetToken,
    uint256 _amount18,
    address _user
) internal {
    uint256 amountinInputTokens = (_amount18 *
        10**ERC20Upgradeable(_inputToken).decimals()) / 10**18;
    IERC20Upgradeable(_inputToken).safeIncreaseAllowance(
        exchangeAddress,
        amountinInputTokens
    );
    uint256 amountinTargetTokens = IExchange(exchangeAddress).exchange(
        _inputToken,
        _targetToken,
        amountinInputTokens,
        0
    );
    IERC20Upgradeable(_targetToken).safeTransfer(_user, amountinTargetTokens);
}

Note, if there is insufficient funds in the buffer and the user wanted to withdraw in a non-supported token (i.e WETH in IbAlluoUSD withdrawals), instead of being added to the queue, the transaction will revert with the require block in line 34 in the code snippet above. This is because of fluctuating token prices over time and meeting withdrawals in a non native token at a later date may result in users receiving significantly less tokens than they initially expected.

Step 3: When withdraw is called on the adapter, it sends the tokens directly to the user.

USDCurveAdapter.sol
/// @notice When called by liquidity handler, withdraws funds from liquidity pool
/// @dev It checks against arbitragers attempting to exploit spreads in stablecoins. 
/// @param _user Recipient address
/// @param _token Deposit token address (eg. USDC)
/// @param _amount  Amount to be withdrawn in 10*18
function withdraw (address _user, address _token, uint256 _amount ) external onlyRole(DEFAULT_ADMIN_ROLE) {
    
    uint256[3] memory amounts;
    address liquidToken = ICurvePoolUSD(curvePool).underlying_coins(liquidTokenIndex);
    uint256 amount = _amount / 10**(18 - IERC20Metadata(liquidToken).decimals());
    amounts[liquidTokenIndex] = amount;
    
    if(_token == liquidToken){
        ICurvePoolUSD(curvePool).remove_liquidity_imbalance(
            amounts, 
            _amount * (10000 + slippage) / 10000,
            true
        );
        IERC20(_token).safeTransfer(_user, amount);
    }
    else{
        // We want to be save agains arbitragers so at any withraw contract checks 
        // how much will be burned curveLp by withrawing this amount in token with most liquidity
        // and passes this burned amount to get tokens
        uint256 toBurn = ICurvePoolUSD(curvePool).calc_token_amount(amounts, false);
        uint256 minAmountOut = _amount / 10**(18 - IERC20Metadata(_token).decimals());
        uint256 toUser = ICurvePoolUSD(curvePool).remove_liquidity_one_coin(
                toBurn, 
                int128(indexes[_token]), 
                minAmountOut * (10000 - slippage) / 10000,
                true
            );
        IERC20(_token).safeTransfer(_user, toUser);
    }
}
    

Last updated