Skip to content
This repository has been archived by the owner on Nov 19, 2023. It is now read-only.

ShadowForce - Lack of check for liquidator address when deleveraging result in either griefing or loss of fund #134

Closed
sherlock-admin opened this issue May 15, 2023 · 0 comments
Labels
Duplicate A valid issue that is a duplicate of an issue with `Has Duplicates` label Medium A valid Medium severity issue Reward A payout will be made for this issue

Comments

@sherlock-admin
Copy link
Contributor

sherlock-admin commented May 15, 2023

ShadowForce

high

Lack of check for liquidator address when deleveraging result in either griefing or loss of fund

Summary

Lack of check for liquidator address when deleveraging result in loss of fund

Vulnerability Detail

When we are liquidating asset we are calling the function authenticate-deleverage check

    function deleverageAccount(
        address account,
        address vault,
        address liquidator,
        uint16 currencyIndex,
        int256 depositUnderlyingInternal
    ) external payable nonReentrant override returns (
        uint256 vaultSharesToLiquidator,
        int256 depositAmountPrimeCash
    ) {
        require(currencyIndex < 3);
        (
            VaultConfig memory vaultConfig,
            VaultAccount memory vaultAccount,
            VaultState memory vaultState
        ) = _authenticateDeleverage(account, vault, liquidator);

this is calling

    /// @notice Authenticates a call to the deleverage method
    function _authenticateDeleverage(
        address account,
        address vault,
        address liquidator
    ) private returns (
        VaultConfig memory vaultConfig,
        VaultAccount memory vaultAccount,
        VaultState memory vaultState
    ) {
        // Do not allow invalid accounts to liquidate
        requireValidAccount(liquidator);
        require(liquidator != vault);

        // Cannot liquidate self, if a vault needs to deleverage itself as a whole it has other methods 
        // in VaultAction to do so.
        require(account != msg.sender);
        require(account != liquidator);

        vaultConfig = VaultConfiguration.getVaultConfigStateful(vault);
        require(vaultConfig.getFlag(VaultConfiguration.DISABLE_DELEVERAGE) == false);

        // Authorization rules for deleveraging
        if (vaultConfig.getFlag(VaultConfiguration.ONLY_VAULT_DELEVERAGE)) {
            require(msg.sender == vault);
        } else {
            require(msg.sender == liquidator);
        }

        vaultAccount = VaultAccountLib.getVaultAccount(account, vaultConfig);

if this check pass, in deleverageAccount, we are calling depositUnderlyingExternal function, this is an important

        uint16 currencyId = vaultConfig.borrowCurrencyId;
        if (currencyIndex == 1) currencyId = vaultConfig.secondaryBorrowCurrencies[0];
        else if (currencyIndex == 2) currencyId = vaultConfig.secondaryBorrowCurrencies[1];

        Token memory token = TokenHandler.getUnderlyingToken(currencyId);
        // Excess ETH is returned to the liquidator natively
        (/* */, depositAmountPrimeCash) = TokenHandler.depositUnderlyingExternal(
            liquidator, currencyId, token.convertToExternal(depositUnderlyingInternal), pr, false 
        );

which calling the code below, this is a important because it either use the ETH sent along with this transaciton or pull fund from liquidator account

    /// @notice Deposits an amount of underlying tokens to mint prime cash
    /// @param account account to transfer tokens from
    /// @param currencyId the associated currency id
    /// @param _underlyingExternalDeposit the amount of underlying tokens to deposit
    /// @param primeRate the current accrued prime rate
    /// @param returnNativeTokenWrapped if true, return excess msg.value ETH payments as WETH
    /// @return actualTransferExternal the actual amount of tokens transferred in external precision
    /// @return netPrimeSupplyChange the amount of prime supply created
    function depositUnderlyingExternal(
        address account,
        uint16 currencyId,
        int256 _underlyingExternalDeposit,
        PrimeRate memory primeRate,
        bool returnNativeTokenWrapped
    ) internal returns (int256 actualTransferExternal, int256 netPrimeSupplyChange) {
        uint256 underlyingExternalDeposit = _underlyingExternalDeposit.toUint();
        if (underlyingExternalDeposit == 0) return (0, 0);

        Token memory underlying = getUnderlyingToken(currencyId);
        if (underlying.tokenType == TokenType.Ether) {
            // Underflow checked above
            if (underlyingExternalDeposit < msg.value) {
                // Transfer any excess ETH back to the account
                GenericToken.transferNativeTokenOut(
                    account, msg.value - underlyingExternalDeposit, returnNativeTokenWrapped
                );
            } else {
                require(underlyingExternalDeposit == msg.value, "ETH Balance");
            }

            actualTransferExternal = _underlyingExternalDeposit;
        } else {
            // In the case of deposits, we use a balance before and after check
            // to ensure that we record the proper balance change.
            actualTransferExternal = GenericToken.safeTransferIn(
                underlying.tokenAddress, account, underlyingExternalDeposit
            ).toInt();
        }

        netPrimeSupplyChange = _postTransferPrimeCashUpdate(
            account, currencyId, actualTransferExternal, underlying, primeRate
        );
    }

what is the account parameter, account parameter is the liquidator

// Cannot liquidate self, if a vault needs to deleverage itself as a whole it has other methods 
// in VaultAction to do so.
require(account != msg.sender);
require(account != liquidator);

vaultConfig = VaultConfiguration.getVaultConfigStateful(vault);
require(vaultConfig.getFlag(VaultConfiguration.DISABLE_DELEVERAGE) == false);

// Authorization rules for deleveraging
if (vaultConfig.getFlag(VaultConfiguration.ONLY_VAULT_DELEVERAGE)) {
	require(msg.sender == vault);
} else {
	require(msg.sender == liquidator);
}

if (vaultConfig.getFlag(VaultConfiguration.ONLY_VAULT_DELEVERAGE)) is on,

we are checking

if (vaultConfig.getFlag(VaultConfiguration.ONLY_VAULT_DELEVERAGE)) {
	require(msg.sender == vault);
} else {
	require(msg.sender == liquidator);
}

if vaultConfig.getFlag(VaultConfiguration.ONLY_VAULT_DELEVERAGE) is flag return false, we are checking

require(msg.sender == liquidator);

this looks right because if the underlyinig token is not ETH, token is pulled from liquidator account

but if vaultConfig.getFlag(VaultConfiguration.ONLY_VAULT_DELEVERAGE) is flag return true, we only check

require(msg.sender == vault);

but are not checking which account is liquidator

liquidator can just reject the deleverage by removing the allowance

OR in the worst case, as long as the liquidator given allowance to the smart contract, the liquidator is pulled from his account to cover the debt, which is result in loss of fund

Impact

Lack of check for liquidator address when deleveraging result in either griefing or loss of fund

Code Snippet

https://github.com/notional-finance/contracts-v2/blob/b20a45c912785fab5f2b62992e5260f44dbae197/contracts/external/actions/VaultLiquidationAction.sol#L63

https://github.com/notional-finance/contracts-v2/blob/b20a45c912785fab5f2b62992e5260f44dbae197/contracts/external/actions/VaultLiquidationAction.sol#L90

Tool used

Manual Review

Recommendation

even if we check msg.sender is vault, we have to implement some sort of check to validate the liquidator address

Duplicate of #215

@github-actions github-actions bot added Medium A valid Medium severity issue Duplicate A valid issue that is a duplicate of an issue with `Has Duplicates` label labels May 19, 2023
@sherlock-admin sherlock-admin added the Reward A payout will be made for this issue label May 29, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Duplicate A valid issue that is a duplicate of an issue with `Has Duplicates` label Medium A valid Medium severity issue Reward A payout will be made for this issue
Projects
None yet
Development

No branches or pull requests

1 participant