Yeti Finance contest - WatchPug's results

Portfolio borrowing. 11x leverage. 0% interest.

General Information

Platform: Code4rena

Start Date: 16/12/2021

Pot Size: $100,000 USDC

Total HM: 21

Participants: 25

Period: 7 days

Judge: alcueca

Total Solo HM: 12

Id: 66

League: ETH

Yeti Finance

Findings Distribution

Researcher Performance

Rank: 5/25

Findings: 5

Award: $9,204.87

🌟 Selected for report: 14

🚀 Solo Findings: 1

Findings Information

🌟 Selected for report: defsec

Also found by: WatchPug, hyh

Labels

bug
duplicate
2 (Med Risk)

Awards

430.3487 USDC - $430.35

External Links

Handle

WatchPug

Vulnerability details

https://github.com/code-423n4/2021-12-yetifinance/blob/5f5bf61209b722ba568623d8446111b1ea5cb61c/packages/contracts/contracts/PriceFeed.sol#L578-L589

function _badChainlinkResponse(ChainlinkResponse memory _response) internal view returns (bool) {
    // Check for response call reverted
    if (!_response.success) {return true;}
    // Check for an invalid roundId that is 0
    if (_response.roundId == 0) {return true;}
    // Check for an invalid timeStamp that is 0, or in the future
    if (_response.timestamp == 0 || _response.timestamp > block.timestamp) {return true;}
    // Check for non-positive price
    if (_response.answer <= 0) {return true;}

    return false;
}

On PriceFeed.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:

function _badChainlinkResponse(ChainlinkResponse memory _response) internal view returns (bool) {
    // Check for response call reverted
    if (!_response.success) {return true;}
    // Check for an invalid roundId that is 0
    if (_response.roundId == 0) {return true;}
    // Check for stale price
    if (_response.answeredInRound < _response.roundID) {return true;}
    // Check for an invalid timeStamp that is 0, or in the future
    if (_response.timestamp == 0 || _response.timestamp > block.timestamp) {return true;}
    // Check for non-positive price
    if (_response.answer <= 0) {return true;}

    return false;
}

#0 - kingyetifinance

2022-01-05T18:27:41Z

Duplicate #91

Findings Information

🌟 Selected for report: cmichel

Also found by: WatchPug, kenzo, pauliax

Labels

bug
duplicate
3 (High Risk)

Awards

968.2846 USDC - $968.28

External Links

Handle

WatchPug

Vulnerability details

https://github.com/code-423n4/2021-12-yetifinance/blob/5f5bf61209b722ba568623d8446111b1ea5cb61c/packages/contracts/contracts/AssetWrappers/WJLP/WJLP.sol#L126-L139

function wrap(uint _amount, address _from, address _to, address _rewardOwner) external override {
        JLP.transferFrom(_from, address(this), _amount);
        JLP.approve(address(_MasterChefJoe), _amount);

        // stake LP tokens in Trader Joe's.
        // In process of depositing, all this contract's
        // accumulated JOE rewards are sent into this contract
        _MasterChefJoe.deposit(_poolPid, _amount);

        // update user reward tracking
        _userUpdate(_rewardOwner, _amount, true);
        _mint(_to, _amount);
    }

Funds are transferred from the from parameter, and the output tokens are transferred to the to parameter, both passed by the caller without proper access control.

Impact

This issue allows anyone to call wrap() and steal almost all their wallet balances for all the users who have approved the contract before.

PoC

  1. Alice approved JLP token with the max amount to the WJLP contract
  2. Bob calls wrap() with _from = Alice's address, and _to = Bob's address, get WJLP.

Recommendation

Consider replacing from with msg.sender:

function wrap(uint _amount, address _to, address _rewardOwner) external override {
        JLP.transferFrom(msg.sender, address(this), _amount);
        JLP.approve(address(_MasterChefJoe), _amount);

        // stake LP tokens in Trader Joe's.
        // In process of depositing, all this contract's
        // accumulated JOE rewards are sent into this contract
        _MasterChefJoe.deposit(_poolPid, _amount);

        // update user reward tracking
        _userUpdate(_rewardOwner, _amount, true);
        _mint(_to, _amount);
    }

#0 - kingyetifinance

2022-01-07T06:47:20Z

Duplicate #58

#1 - alcueca

2022-01-15T06:41:54Z

Duplicate #208

Findings Information

🌟 Selected for report: WatchPug

Labels

bug
3 (High Risk)

Awards

5312.9473 USDC - $5,312.95

External Links

Handle

WatchPug

Vulnerability details

https://github.com/code-423n4/2021-12-yetifinance/blob/5f5bf61209b722ba568623d8446111b1ea5cb61c/packages/contracts/contracts/TroveManagerLiquidations.sol#L409-L409

    _updateWAssetsRewardOwner(collsToUpdate, _borrower, yetiFinanceTreasury);

In _liquidateNormalMode(), WAsset rewards for collToRedistribute will accrue to Yeti Finance Treasury, However, if a borrower wrap WJLP and set _rewardOwner to other address, _updateWAssetsRewardOwner() will fail due to failure of IWAsset(token).updateReward().

https://github.com/code-423n4/2021-12-yetifinance/blob/5f5bf61209b722ba568623d8446111b1ea5cb61c/packages/contracts/contracts/AssetWrappers/WJLP/WJLP.sol#L126-L138

function wrap(uint _amount, address _from, address _to, address _rewardOwner) external override {
    JLP.transferFrom(_from, address(this), _amount);
    JLP.approve(address(_MasterChefJoe), _amount);

    // stake LP tokens in Trader Joe's.
    // In process of depositing, all this contract's
    // accumulated JOE rewards are sent into this contract
    _MasterChefJoe.deposit(_poolPid, _amount);

    // update user reward tracking
    _userUpdate(_rewardOwner, _amount, true);
    _mint(_to, _amount);
}

PoC

  1. Alice wrap() some JLP to WJLP and set _rewardOwner to another address;
  2. Alice deposited WJLP as a collateral asset and borrowed the max amount of YUSD;
  3. When the liquidator tries to call batchLiquidateTroves() when Alice defaulted, the transaction will fail.

Recommendation

Consider checking if the user have sufficient reward amount to the balance of collateral in BorrowerOperations.sol#_transferCollateralsIntoActivePool().

#0 - 0xtruco

2022-01-12T21:06:16Z

Duplicate #136 and is more specific about the exact error. For issue 136, had to extrapolate to find the real error there, and this issue is a better description.

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