Backd contest - WatchPug's results

Maximize the power of your assets and start earning yield

General Information

Platform: Code4rena

Start Date: 21/04/2022

Pot Size: $100,000 USDC

Total HM: 18

Participants: 60

Period: 7 days

Judge: gzeon

Total Solo HM: 10

Id: 112

League: ETH

Backd

Findings Distribution

Researcher Performance

Rank: 3/60

Findings: 7

Award: $12,502.17

🌟 Selected for report: 3

🚀 Solo Findings: 3

Findings Information

Labels

bug
duplicate
2 (Med Risk)
reviewed

Awards

70.085 USDC - $70.08

External Links

Lines of code

https://github.com/code-423n4/2022-04-backd/blob/c856714a50437cb33240a5964b63687c9876275b/backd/contracts/pool/EthPool.sol#L30

Vulnerability details

Since the introduction of transfer(), it has typically been recommended by the security community because it helps guard against reentrancy attacks. This guidance made sense under the assumption that gas costs wouldn’t change. It's now recommended that transfer() and send() be avoided, as gas costs can and will change and reentrancy guard is more commonly used.

Any smart contract that uses transfer() is taking a hard dependency on gas costs by forwarding a fixed amount of gas: 2300.

Impact

Recommendation

It's recommended to stop using transfer() and switch to using call() instead.

See: https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable/blob/v4.5.2/contracts/utils/AddressUpgradeable.sol#L44-L65

Consider using OpenZeppelin's AddressUpgradeable#sendValue().

https://github.com/code-423n4/2022-04-backd/blob/c856714a50437cb33240a5964b63687c9876275b/backd/contracts/pool/EthPool.sol#L29-L31

    function _doTransferOut(address payable to, uint256 amount) internal override {
        to.transfer(amount);
    }

Can be changed to:

    function _doTransferOut(address payable to, uint256 amount) internal override {
          AddressUpgradeable.sendValue(to, amount);
    }

https://github.com/code-423n4/2022-04-backd/blob/c856714a50437cb33240a5964b63687c9876275b/backd/contracts/vault/VaultReserve.sol#L72-L87

    function withdraw(address token, uint256 amount) external override onlyVault returns (bool) {
        require(canWithdraw(msg.sender), Error.RESERVE_ACCESS_EXCEEDED);
        uint256 accountBalance = _balances[msg.sender][token];
        require(accountBalance >= amount, Error.INSUFFICIENT_BALANCE);

        _balances[msg.sender][token] -= amount;
        _lastWithdrawal[msg.sender] = block.timestamp;

        if (token == address(0)) {
            payable(msg.sender).transfer(amount);
        } else {
            IERC20(token).safeTransfer(msg.sender, amount);
        }
        emit Withdraw(msg.sender, token, amount);
        return true;
    }

Can be changed to:

https://github.com/code-423n4/2022-04-backd/blob/c856714a50437cb33240a5964b63687c9876275b/backd/contracts/vault/VaultReserve.sol#L72-L87

    function withdraw(address token, uint256 amount) external override onlyVault returns (bool) {
        require(canWithdraw(msg.sender), Error.RESERVE_ACCESS_EXCEEDED);
        uint256 accountBalance = _balances[msg.sender][token];
        require(accountBalance >= amount, Error.INSUFFICIENT_BALANCE);

        _balances[msg.sender][token] -= amount;
        _lastWithdrawal[msg.sender] = block.timestamp;

        if (token == address(0)) {
            AddressUpgradeable.sendValue(payable(msg.sender), amount);
        } else {
            IERC20(token).safeTransfer(msg.sender, amount);
        }
        emit Withdraw(msg.sender, token, amount);
        return true;
    }

#0 - chase-manning

2022-04-28T11:38:16Z

Duplicate of #52

Findings Information

Labels

bug
duplicate
2 (Med Risk)
resolved
sponsor confirmed
reviewed

Awards

58.8714 USDC - $58.87

External Links

Lines of code

https://github.com/code-423n4/2022-04-backd/blob/c856714a50437cb33240a5964b63687c9876275b/backd/contracts/oracles/ChainlinkUsdWrapper.sol#L63-L66

Vulnerability details

https://github.com/code-423n4/2022-04-backd/blob/c856714a50437cb33240a5964b63687c9876275b/backd/contracts/oracles/ChainlinkUsdWrapper.sol#L63-L66

    function _ethPrice() private view returns (int256) {
        (, int256 answer, , , ) = _ethOracle.latestRoundData();
        return answer;
    }

On ChainlinkUsdWrapper.sol, we are using latestRoundData, but there is no check if the return value indicates stale data. This could lead to stale prices according to the Chainlink documentation:

Recommendation

Consider adding missing checks for stale data.

For example:

(uint80 roundID, int256 answer, , uint256 updatedAt, uint80 answeredInRound) = _ethOracle.latestRoundData();
require(answer > 0, "Chainlink price <= 0"); 
require(answeredInRound >= roundID, "Stale price");
require(updatedAt != 0, "Round not complete");

#0 - gzeoneth

2022-05-07T20:56:44Z

Duplicate of #17

#1 - chase-manning

2022-05-11T14:57:18Z

Findings Information

🌟 Selected for report: shenwilly

Also found by: StyxRave, WatchPug, pauliax

Labels

bug
duplicate
2 (Med Risk)
reviewed

Awards

703.5062 USDC - $703.51

External Links

Lines of code

https://github.com/code-423n4/2022-04-backd/blob/c856714a50437cb33240a5964b63687c9876275b/backd/contracts/strategies/StrategySwapper.sol#L287-L289

Vulnerability details

In swapAllForWeth() and swapAllWethForToken, when using tokens with decimals larger than 18, the txs will revert due to underflow at _decimalMultiplier.

https://github.com/code-423n4/2022-04-backd/blob/c856714a50437cb33240a5964b63687c9876275b/backd/contracts/strategies/StrategySwapper.sol#L287-L289

function _decimalMultiplier(address token_) internal view returns (uint256) {
    return 10**(18 - IERC20Full(token_).decimals());
}

#0 - chase-manning

2022-04-28T12:01:09Z

Duplicate of #49

Findings Information

🌟 Selected for report: WatchPug

Labels

bug
2 (Med Risk)
resolved
sponsor confirmed
reviewed

Awards

3860.1163 USDC - $3,860.12

External Links

Lines of code

https://github.com/compound-finance/compound-protocol/blob/v2.8.1/contracts/CEther.sol#L44-L47

Vulnerability details

https://github.com/compound-finance/compound-protocol/blob/v2.8.1/contracts/CEther.sol#L44-L47

function mint() external payable {
    (uint err,) = mintInternal(msg.value);
    requireNoError(err, "mint failed");
}

mint() for native cToken (CEther) does not have any parameters, as the Function Selector is based on the function name with the parenthesised list of parameter types, when you add a nonexisting parameter, the Function Selector will be incorrect.

https://github.com/code-423n4/2022-04-backd/blob/c856714a50437cb33240a5964b63687c9876275b/backd/interfaces/vendor/CTokenInterfaces.sol#L316

    function mint(uint256 mintAmount) external payable virtual returns (uint256);

The current implementation uses the same CToken interface for both CEther and CErc20 in topUp(), and function mint(uint256 mintAmount) is a nonexisting function for CEther.

As a result, the native token topUp() always revert.

https://github.com/code-423n4/2022-04-backd/blob/c856714a50437cb33240a5964b63687c9876275b/backd/contracts/actions/topup/handlers/CompoundHandler.sol#L57-L70

    CToken ctoken = cTokenRegistry.fetchCToken(underlying);
    uint256 initialTokens = ctoken.balanceOf(address(this));

    address addr = account.addr();

    if (repayDebt) {
        amount -= _repayAnyDebt(addr, underlying, amount, ctoken);
        if (amount == 0) return true;
    }

    uint256 err;
    if (underlying == address(0)) {
        err = ctoken.mint{value: amount}(amount);
    }

See also:

#0 - chase-manning

2022-04-28T11:31:52Z

Duplicate of #125

#1 - gzeoneth

2022-05-07T20:51:25Z

Not a duplicate.

#2 - samwerner

2022-05-27T13:44:36Z

Findings Information

🌟 Selected for report: WatchPug

Labels

bug
2 (Med Risk)
sponsor confirmed
reviewed

Awards

3860.1163 USDC - $3,860.12

External Links

Lines of code

https://github.com/code-423n4/2022-04-backd/blob/c856714a50437cb33240a5964b63687c9876275b/backd/interfaces/vendor/CTokenInterfaces.sol#L355-L358

Vulnerability details

https://github.com/code-423n4/2022-04-backd/blob/c856714a50437cb33240a5964b63687c9876275b/backd/interfaces/vendor/CTokenInterfaces.sol#L355-L358

function repayBorrowBehalf(address borrower, uint256 repayAmount)
        external
        payable
        returns (uint256);

repayBorrowBehalf() for native cToken (CEther) will return nothing, while the current CEthInterface interface defines the returns as (uint256).

As a result, ether.repayBorrowBehalf() will always revert

https://github.com/code-423n4/2022-04-backd/blob/c856714a50437cb33240a5964b63687c9876275b/backd/contracts/actions/topup/handlers/CompoundHandler.sol#L117-L118

    CEther cether = CEther(address(ctoken));
    err = cether.repayBorrowBehalf{value: debt}(account);

Ref:

methodCEtherCErc20
mint()reverterror code
redeem()error codeerror code
repayBorrow()reverterror code
repayBorrowBehalf()reverterror code

Findings Information

🌟 Selected for report: WatchPug

Labels

bug
2 (Med Risk)
sponsor confirmed
reviewed

Awards

3860.1163 USDC - $3,860.12

External Links

Lines of code

https://github.com/code-423n4/2022-04-backd/blob/c856714a50437cb33240a5964b63687c9876275b/backd/interfaces/vendor/CTokenInterfaces.sol#L345

Vulnerability details

https://github.com/code-423n4/2022-04-backd/blob/c856714a50437cb33240a5964b63687c9876275b/backd/interfaces/vendor/CTokenInterfaces.sol#L345

 function mint() external payable returns (uint256);

mint() for native cToken (CEther) will return nothing, while the current CEthInterface interface defines the returns as (uint256).

In the current implementation, the interface for CToken is used for both CEther and CErc20.

As a result, the transaction will revert with the error: function returned an unexpected amount of data when topUp() with the native token (ETH).

https://github.com/code-423n4/2022-04-backd/blob/c856714a50437cb33240a5964b63687c9876275b/backd/contracts/actions/topup/handlers/CompoundHandler.sol#L57-L70

    CToken ctoken = cTokenRegistry.fetchCToken(underlying);
    uint256 initialTokens = ctoken.balanceOf(address(this));

    address addr = account.addr();

    if (repayDebt) {
        amount -= _repayAnyDebt(addr, underlying, amount, ctoken);
        if (amount == 0) return true;
    }

    uint256 err;
    if (underlying == address(0)) {
        err = ctoken.mint{value: amount}(amount);
    }

Ref:

methodCEtherCErc20
mint()reverterror code
redeem()error codeerror code
repayBorrow()reverterror code
repayBorrowBehalf()reverterror code

Awards

89.3504 USDC - $89.35

Labels

bug
G (Gas Optimization)
resolved
reviewed

External Links

[S]: Suggested optimation, save a decent amount of gas without compromising readability;

[M]: Minor optimation, the amount of gas saved is minor, change when you see fit;

[N]: Non-preferred, the amount of gas saved is at cost of readability, only apply when gas saving is a top priority.

[S] Cache array length in for loops can save gas

Reading array length at each iteration of the loop takes 6 gas (3 for mload and 3 to place memory_offset) in the stack.

Caching the array length in the stack saves around 3 gas per iteration.

Instances include:

  • CompoundHandler.sol#_getAccountBorrowsAndSupply()

https://github.com/code-423n4/2022-04-backd/blob/c856714a50437cb33240a5964b63687c9876275b/backd/contracts/actions/topup/handlers/CompoundHandler.sol#L135-L170

[S] Avoid unnecessary storage read can save gas

https://github.com/code-423n4/2022-04-backd/blob/c856714a50437cb33240a5964b63687c9876275b/backd/contracts/StakerVault.sol#L360-L389

    function unstakeFor(
        address src,
        address dst,
        uint256 amount
    ) public override returns (bool) {
        ILiquidityPool pool = controller.addressProvider().getPoolForToken(token);
        uint256 allowance_ = _allowances[src][msg.sender];
        require(
            src == msg.sender || allowance_ >= amount || address(pool) == msg.sender,
            Error.UNAUTHORIZED_ACCESS
        );
        require(balances[src] >= amount, Error.INSUFFICIENT_BALANCE);
        address lpGauge = currentAddresses[_LP_GAUGE];
        if (lpGauge != address(0)) {
            ILpGauge(lpGauge).userCheckpoint(src);
        }
        uint256 oldBal = IERC20(token).balanceOf(address(this));

        if (src != dst) {
            pool.handleLpTokenTransfer(src, dst, amount);
        }

        IERC20(token).safeTransfer(dst, amount);

        uint256 unstaked = oldBal - IERC20(token).balanceOf(address(this));

        if (src != msg.sender && allowance_ != type(uint256).max && address(pool) != msg.sender) {
            // update allowance
            _allowances[src][msg.sender] -= unstaked;
        }
        // ...
    }

L388 can use allowance_ - unstaked directly to avoid unnecessary storage read of _allowances[src][msg.sender] to save some gas.

Recommendation

Change to:

    function unstakeFor(
        address src,
        address dst,
        uint256 amount
    ) public override returns (bool) {
        ILiquidityPool pool = controller.addressProvider().getPoolForToken(token);
        uint256 allowance_ = _allowances[src][msg.sender];
        require(
            src == msg.sender || allowance_ >= amount || address(pool) == msg.sender,
            Error.UNAUTHORIZED_ACCESS
        );
        require(balances[src] >= amount, Error.INSUFFICIENT_BALANCE);
        address lpGauge = currentAddresses[_LP_GAUGE];
        if (lpGauge != address(0)) {
            ILpGauge(lpGauge).userCheckpoint(src);
        }
        uint256 oldBal = IERC20(token).balanceOf(address(this));

        if (src != dst) {
            pool.handleLpTokenTransfer(src, dst, amount);
        }

        IERC20(token).safeTransfer(dst, amount);

        uint256 unstaked = oldBal - IERC20(token).balanceOf(address(this));

        if (src != msg.sender && allowance_ != type(uint256).max && address(pool) != msg.sender) {
            // update allowance
            _allowances[src][msg.sender] = allowance_ - unstaked;
        }
        // ...
    }

[S] Using immutable variable can save gas

https://github.com/code-423n4/2022-04-backd/blob/c856714a50437cb33240a5964b63687c9876275b/backd/contracts/StakerVault.sol#L45

    address public token;

https://github.com/code-423n4/2022-04-backd/blob/c856714a50437cb33240a5964b63687c9876275b/backd/contracts/StakerVault.sol#L65-L67

    function initialize(address _token) external override initializer {
        token = _token;
    }

Considering that token will never change, changing it to immutable variable instead of storage variable can save gas.

[M] Setting uint variables to 0 is redundant

Setting uint variables to 0 is redundant as they default to 0.

https://github.com/code-423n4/2022-04-backd/blob/c856714a50437cb33240a5964b63687c9876275b/backd/contracts/pool/LiquidityPool.sol#L483

        uint256 currentFeeRatio = 0;

https://github.com/code-423n4/2022-04-backd/blob/c856714a50437cb33240a5964b63687c9876275b/backd/contracts/actions/topup/handlers/CompoundHandler.sol#L135

        for (uint256 i = 0; i < assets.length; i++) 
AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax © 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter