Wise Lending - shealtielanz's results

Decentralized liquidity market that allows users to supply crypto assets and start earning a variable APY from borrowers.

General Information

Platform: Code4rena

Start Date: 21/02/2024

Pot Size: $200,000 USDC

Total HM: 22

Participants: 36

Period: 19 days

Judge: Trust

Total Solo HM: 12

Id: 330

League: ETH

Wise Lending

Findings Distribution

Researcher Performance

Rank: 31/36

Findings: 1

Award: $159.94

🌟 Selected for report: 0

🚀 Solo Findings: 0

Findings Information

🌟 Selected for report: Dup1337

Also found by: 0x11singh99, NentoR, cheatc0d3, nonseodion, serial-coder, shealtielanz

Labels

bug
grade-b
edited-by-warden
QA (Quality Assurance)
sufficient quality report
Q-06

Awards

159.9366 USDC - $159.94

External Links

L1 - Using block.timestamp as deadline can be problematic.

Summary

On the call to _getTokensUniV3() the swap performed in the call uses block.stamp as the deadline for the transaction, the issue here is that due to MEV, miners can simply hold such a transaction on the TX queue and execute it whenever it might be profitable to them, which might also in loss to the contract due to slippage issues. https://github.com/code-423n4/2024-02-wise-lending/blob/79186b243d8553e66358c05497e5ccfd9488b5e2/contracts/PowerFarms/PendlePowerFarm/PendlePowerFarmLeverageLogic.sol#L274C1-L287C6

    function _getTokensUniV3(uint256 _amountIn, uint256 _minOutAmount, address _tokenIn, address _tokenOut)
        internal
        returns (uint256)
    {

       // @audit deadline params.
        return UNISWAP_V3_ROUTER.exactInputSingle(
            IUniswapV3.ExactInputSingleParams({
                tokenIn: _tokenIn,
                tokenOut: _tokenOut,
                fee: UNISWAP_V3_FEE,
                recipient: address(this),
                deadline: block.timestamp,
                amountIn: _amountIn,
                amountOutMinimum: _minOutAmount,
                sqrtPriceLimitX96: 0
            })
        );
    }

Suggestion

Allow the caller to specify a preferred deadline for the TX to be executed.

L2 - PendlePowerFarmLeverageLogic doesn't adhere to EIP-3156 specs completely

Summary

As per the EIP-3156 Specs

If successful, onFlashLoan MUST return the keccak256 hash of “ERC3156FlashBorrower.onFlashLoan”.

To trust that the value of data is genuine, in addition to the check in point 1, it is recommended to verify that the initiator belongs to a group of trusted addresses. Trusting the lender and the initiator is enough to trust that the contents of data are genuine.

In the contracts function, it neither returns the hash nor checks that the initiator was from a verified source though it doesn't safety checks against the consequences it must adhere to EIP-3156, if it intends to use a flash loan.

https://github.com/code-423n4/2024-02-wise-lending/blob/79186b243d8553e66358c05497e5ccfd9488b5e2/contracts/PowerFarms/PendlePowerFarm/PendlePowerFarmLeverageLogic.sol#L59C1-L75C10

    function receiveFlashLoan(
        IERC20[] memory _flashloanToken,
        uint256[] memory _flashloanAmounts,
        uint256[] memory _feeAmounts,
        bytes memory _userData
    )
        external
    {
        if (allowEnter == false) {
            revert AccessDenied();
        }

        allowEnter = false;

        if (_flashloanToken.length == 0) {
            revert InvalidParam();
        }

Suggestion

Adhere to EIP-3156 Specs as in the link I provided above.

L3 - Stealth donation attack is possible in PendlePowerFarmToken.

on the call to withdrawExactAmount() the shares to be burned from a user using the previewBurnShares() function. https://github.com/code-423n4/2024-02-wise-lending/blob/79186b243d8553e66358c05497e5ccfd9488b5e2/contracts/PowerFarms/PendlePowerFarmController/PendlePowerFarmToken.sol#L478C2-L492C6

    function previewBurnShares(
        uint256 _underlyingAssetAmount,
        uint256 _underlyingLpAssetsCurrent
    )
        public
        view
        returns (uint256)
    {
        uint256 product = _underlyingAssetAmount
            * totalSupply();


        return product % _underlyingLpAssetsCurrent == 0
            ? product / _underlyingLpAssetsCurrent
            : product / _underlyingLpAssetsCurrent + 1;
    }

It ensures that the shares to be burned are rounded up to safeguard the protocol, but this safety check can be used against it, via stealth donation attack, where the attacker can purposefully lose funds to the protocol(as it rounds up the shares and burns from the caller) to drive the price of the LP tokens up and use it as collateral in another pool. Such an example has happened live to Wiselending before - Read up -> https://twitter.com/danielvf/status/1746303616778981402?s=46

Suggestion.

Use a similar calculation as used in WiseLending MainHelper.sol to calculate the shares here:

   function _calculateShares(uint256 _product, uint256 _pseudo, bool _maxSharePrice) private pure returns (uint256) {
        return _maxSharePrice == true ? _product / _pseudo + 1 : _product / _pseudo - 1;
    }

L4 - Allow users to specify a min amount out when depositing and withdrawing from PendlePowerFarmToken.sol.

The functions :

Suggestion

Allow users to specify a minimum amount to receive from the specified functions.

L5 - values gotten from uniSwap::slot0() are easily manipulated.

Summary

In PendleTokenCustomOracle.sol it uses the value gotten from uniswap.slot0() to derive the latestAnswer() the issue here is that the values gotten from the uni swap function are highly manipulatable by an attack and can be used to wreck the protocol if such a function is used to determine the price of an asset. https://github.com/code-423n4/2024-02-wise-lending/blob/79186b243d8553e66358c05497e5ccfd9488b5e2/contracts/DerivativeOracles/PendleTokenCustomOracle.sol#L98C1-L104C45

//@audit values gotten from uniswap slot zero are easily manipulated
    function latestAnswer() public view returns (uint256 amountOutForOneTokenIn) {
        (, int24 tick,,,,,) = IUniswapV3Pool(uniPool).slot0(); //@audit-isse

        address tokenIn = targetToken;
        uint128 amountIn = uint128(globalAmountIn);

        address tokenOut = targetToken == token1 ? token0 : token1;

        amountOutForOneTokenIn = OracleLibrary.getQuoteAtTick(tick, amountIn, tokenIn, tokenOut);
    }

Suggestion

Use TWAP rather than uniSwap.solt0().

Info1 - contracts that have been destroyed will pass the token.code.length > 0 check.

In CallOptionalReturn.sol the function used by the transfer helpers _callOptionalReturn() check that the token actual exist with the check token.code.length > 0 https://github.com/code-423n4/2024-02-wise-lending/blob/79186b243d8553e66358c05497e5ccfd9488b5e2/contracts/TransferHub/CallOptionalReturn.sol#L35C1-L38C6

    function _callOptionalReturn(address token, bytes memory data) internal returns (bool call) {
        (bool success, bytes memory returndata) = token.call(data);

        bool results = returndata.length == 0 || abi.decode(returndata, (bool));

        if (success == false) {
            revert();
        }

        call = success && results && token.code.length > 0;//@audit
    }

The issue here is that contracts that have been self-destroyed will pass that check, for example, Upgradeable tokens when the logic is transferred to another address, if the owner destroys the logic of the past contract, the check will still pass for such a call thereby and allowing a caller to perform malicious actions in the protocol.

POC

pragma solidity ^0.8.17;

import "forge-std/Test.sol";
import "forge-std/console.sol";

contract ForloopTest is Test {

    SelfDestructExample public self;
    address payable me = payable(address(this));

    function setUp() public {
        self = new SelfDestructExample(me);
    }

    function testLoop(uint256 x) public {
        assertEq(loop._toString(x), loop.toString(x));
    }

    function testContract() public {
        bool check = activate();
        console.log("here boolie", check);
        self.destroyAndSend();
        console.log("here boolie", check);
    }

    function activate() public returns (bool) {
        address _self = address(self);
        return _self.code.length > 0;
    }
}

contract SelfDestructExample {
    address payable public beneficiary;

    constructor(address payable _beneficiary) {
        beneficiary = _beneficiary;
    }

    function destroyAndSend() public {
        require(msg.sender == beneficiary, "You are not the beneficiary");
        selfdestruct(beneficiary);
    }
}

Info2 - extcodesize() check against zero isn't always safe.

In OracleHelper.sol the function _checkFunctionExistence() checks contract actual exists. https://github.com/code-423n4/2024-02-wise-lending/blob/79186b243d8553e66358c05497e5ccfd9488b5e2/contracts/WiseOracleHub/OracleHelper.sol#L68C1-L76C10

assembly { size := extcodesize(_tokenAggregator) } if (size == 0) {//@audit checks before the call return false; }

However, this check is insufficient to determine if an address has existing code. According to EIP-1052 https://eips.ethereum.org/EIPS/eip-1052.

The EXTCODEHASH of an precompiled contract is either c5d246... or 0.

Info3 - Use a maths library when dealing with calculations.

Lots of raw maths calculations are used all over wise lending contracts, and maths in solidity can lead to certain behaviors like rounding and truncation. Sample : https://github.com/code-423n4/2024-02-wise-lending/blob/79186b243d8553e66358c05497e5ccfd9488b5e2/contracts/WiseOracleHub/OracleHelper.sol#L119C1-L129C6

    function latestResolverTwap(address _tokenAddress) public view returns (uint256) {
        UniTwapPoolInfo memory uniTwapPoolInfoStruct = uniTwapPoolInfo[_tokenAddress];

        if (uniTwapPoolInfoStruct.isUniPool == true) {
            return _getTwapPrice(_tokenAddress, uniTwapPoolInfoStruct.oracle)
                / 10 ** (_decimalsWETH - decimals(_tokenAddress));
        }

        return _getTwapDerivatePrice(_tokenAddress, uniTwapPoolInfoStruct)
            / 10 ** (_decimalsWETH - decimals(_tokenAddress));
    }

It is best to make use of a safeMaths or decimal library to ensure against rounding and truncation errors.

Info4 - Calculations using PRECISION_FACTOR_YEAR will not always be accurate.

The PRECISION_FACTOR_YEAR is a multiple of 1e18 and 365 days: https://github.com/code-423n4/2024-02-wise-lending/blob/79186b243d8553e66358c05497e5ccfd9488b5e2/contracts/PowerFarms/PendlePowerFarmController/PendlePowerFarmToken.sol#L59C1-L59C87

uint256 internal constant PRECISION_FACTOR_YEAR = PRECISION_FACTOR_E18 * ONE_YEAR;

The issue here is that it is used in multiple calculations in the different contracts, however, it doesn't put to context Leap years and during such a period it could affect the calculations on the contracts as leap years come and go from time to time.

Info5 - Delete functions that you don't intend to use.

There are functions that are specified to be deleted by the protocol however such functions haven't been deleted and might lead to issues in the future. Sample: https://github.com/code-423n4/2024-02-wise-lending/blob/79186b243d8553e66358c05497e5ccfd9488b5e2/contracts/WiseOracleHub/WiseOracleHub.sol#L95C1-L117C6


    // @TODO: Delete later, keep for backward compatibility //@audit
    function getTokensInUSD(address _tokenAddress, uint256 _tokenAmount) external view returns (uint256) {
        uint8 tokenDecimals = _tokenDecimals[_tokenAddress];

        return _decimalsETH < tokenDecimals
            ? _tokenAmount * latestResolver(_tokenAddress) / 10 ** decimals(_tokenAddress)
                / 10 ** (tokenDecimals - _decimalsETH)
            : _tokenAmount * 10 ** (_decimalsETH - tokenDecimals) * latestResolver(_tokenAddress)
                / 10 ** decimals(_tokenAddress);
    }

https://github.com/code-423n4/2024-02-wise-lending/blob/79186b243d8553e66358c05497e5ccfd9488b5e2/contracts/WiseOracleHub/WiseOracleHub.sol#L170C1-L192C6

   // @TODO: Delete later, keep for backward compatibility//@audit
    function getTokensFromUSD(address _tokenAddress, uint256 _usdValue) external view returns (uint256) {
        uint8 tokenDecimals = _tokenDecimals[_tokenAddress];

        return _decimalsETH < tokenDecimals
            ? _usdValue * 10 ** (tokenDecimals - _decimalsETH) * 10 ** decimals(_tokenAddress)
                / latestResolver(_tokenAddress)
            : _usdValue * 10 ** decimals(_tokenAddress) / latestResolver(_tokenAddress)
                / 10 ** (_decimalsETH - tokenDecimals);
    }

R1 - Rounding issues arise during shares and amount calculation in PendlePowerFarmToken.

refactor the previewAmountWithdrawShares() to ensure tightly against precision loss, so the user doesn't get a lesser amount transferred to them if the denominator is slightly bigger than the individual numerators. https://github.com/code-423n4/2024-02-wise-lending/blob/79186b243d8553e66358c05497e5ccfd9488b5e2/contracts/PowerFarms/PendlePowerFarmController/PendlePowerFarmToken.sol#L465C1-L477C1

   function previewAmountWithdrawShares(uint256 _shares, uint256 _underlyingLpAssetsCurrent)
        public
        view
        returns (uint256)
    {
        return (_shares * ((_underlyingLpAssetsCurrent * 1e18) / totalSupply())) / 1e18;
    }

#0 - c4-pre-sort

2024-03-17T10:46:39Z

GalloDaSballo marked the issue as sufficient quality report

#1 - trust1995

2024-03-26T11:10:49Z

L2 is invalid - the specs discuss onReceiveFlashloan() while warden points to receiveFlashloan() function. L5 is useless suggestion without explaining the context in which slot0 is used. Having said that, meets the bar for B grade.

#2 - c4-judge

2024-03-26T11:10:53Z

trust1995 marked the issue as grade-b

#3 - shealtielanz

2024-03-27T14:09:03Z

hi @trust1995 you're correct but this still qualifies for a higher grade can you please check

#4 - trust1995

2024-03-27T14:38:10Z

I've checked and confirmed my position.

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