Yeti Finance contest - hyh'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: 4/25

Findings: 6

Award: $10,797.78

🌟 Selected for report: 10

πŸš€ Solo Findings: 2

Findings Information

🌟 Selected for report: hyh

Labels

bug
3 (High Risk)
sponsor confirmed

Awards

5312.9473 USDC - $5,312.95

External Links

Handle

hyh

Vulnerability details

Impact

The condition isn't checked now as the whole balance is used instead of the Yeti tokens bought back from the market. As it's not checked, the amount added to effectiveYetiTokenBalance during rebase can exceed the actual amount of the Yeti tokens owned by the contract. As the before check amount is calculated as the contract net worth, it can be fixed by immediate buy back, but it will not be the case.

The deficit of Yeti tokens can materialize in net worth terms as well if Yeti tokens price will raise compared to the last used one. In this case users will be cumulatively accounted with the amount of tokens that cannot be actually withdrawn from the contract, as its net holdings will be less then total users’ claims. In other words, the contract will be in default if enough users claim after that.

Proof of Concept

Now the whole balance amount is used instead of the amount bought back from market.

Rebasing amount is added to effectiveYetiTokenBalance, so it should be limited by extra Yeti tokens, not the whole balance: https://github.com/code-423n4/2021-12-yetifinance/blob/main/packages/contracts/contracts/YETI/sYETIToken.sol#L247

It looks like only extra tokens should be used for the check, i.e. yetiToken.balance - effectiveYetiTokenBalance.

Now:

function rebase() external { ... uint256 yetiTokenBalance = yetiToken.balanceOf(address(this)); uint256 valueOfContract = _getValueOfContract(yetiTokenBalance); uint256 additionalYetiTokenBalance = ... if (yetiTokenBalance < additionalYetiTokenBalance) { additionalYetiTokenBalance = yetiTokenBalance; } effectiveYetiTokenBalance = effectiveYetiTokenBalance.add(additionalYetiTokenBalance); ... function _getValueOfContract(uint _yetiTokenBalance) internal view returns (uint256) { uint256 adjustedYetiTokenBalance = _yetiTokenBalance.sub(effectiveYetiTokenBalance); uint256 yusdTokenBalance = yusdToken.balanceOf(address(this)); return div(lastBuybackPrice.mul(adjustedYetiTokenBalance), (1e18)).add(yusdTokenBalance); }

As the _getValueOfContract function isn't used elsewhere, the logic can be simplified. To be:

function rebase() external { ... uint256 adjustedYetiTokenBalance = (yetiToken.balanceOf(address(this))).sub(effectiveYetiTokenBalance); uint256 valueOfContract = _getValueOfContract(adjustedYetiTokenBalance); uint256 additionalYetiTokenBalance = ... if (additionalYetiTokenBalance > adjustedYetiTokenBalance) { additionalYetiTokenBalance = adjustedYetiTokenBalance; } effectiveYetiTokenBalance = effectiveYetiTokenBalance.add(additionalYetiTokenBalance); ... function _getValueOfContract(uint _adjustedYetiTokenBalance) internal view returns (uint256) { uint256 yusdTokenBalance = yusdToken.balanceOf(address(this)); return div(lastBuybackPrice.mul(_adjustedYetiTokenBalance), (1e18)).add(yusdTokenBalance); }

#0 - kingyetifinance

2022-01-05T06:44:09Z

@LilYeti:

This is the logic for the fix which we have already done:

if (yetiTokenBalance - effectiveYetiTokenBalance < additionalYetiTokenBalance)

Will look into this again before confirming as fixed to see if it is the same as the suggested error.

Findings Information

🌟 Selected for report: defsec

Also found by: WatchPug, hyh

Labels

bug
duplicate
2 (Med Risk)
disagree with severity

Awards

430.3487 USDC - $430.35

External Links

Handle

hyh

Vulnerability details

Impact

Stale 'carried over' price can be used for liquidations. This can cause various types of malfunctions and manipulated liquidations.

For example, if a portfolio consists of two inversely correlated assets, which move in opposite directions most of the times, and portfolio owner uses that knowledge to borrow against such a composition. Now first asset price is stale, second one is updated and it is in decline, so the link between assets looks to be broken and the system treats the portfolio as under collateralized, while in reality the inverse correlation holds, first asset market price rose to fully compensate the decline of the second asset price, and portfolio value isn't declined and no liquidation should take place.

Proof of Concept

Prices used in liquidations are obtained from PriceFeed contract, call sequence is as follows: Trove and Pool functions -> Whitelist.getValueVC -> Whitelist.getPrice -> PriceFeed.fetchPrice_v -> PriceFeed._getCurrentChainlinkResponse

PriceFeed determines that the price is usable via _badChainlinkResponse function. _badChainlinkResponse checks that timestamp is valid (not 0, not in the future), but do not check that it is recent: https://github.com/code-423n4/2021-12-yetifinance/blob/main/packages/contracts/contracts/PriceFeed.sol#L578

Moreover, _getCurrentChainlinkResponse function which provides core price structure used, ignores answeredInRound returned by ChainLink in the first place: https://github.com/code-423n4/2021-12-yetifinance/blob/main/packages/contracts/contracts/PriceFeed.sol#L756

Chainlink feed has two properties that can help determine if the answer is actual (price is fresh): updatedAt time https://docs.chain.link/docs/faq/#what-is-the-difference-between-the-data-feed-properties-updatedat-and-answeredinround and answeredInRound: https://docs.chain.link/docs/faq/#how-can-i-check-if-the-answer-to-a-round-is-being-carried-over-from-a-previous-round

This way ChainLink roundId isn't checked in PriceFeed contract used for price discovery and carried over price is treated as fresh in all VC related computations.

Liquidation mechanics is a core system functionality which is based on current market price observations. It is crucial to ensure that these observations are sound.

Best practice is to check that answeredInRound is equal to roundId and this way the price obtained is fresh and can be used for decision making. It's advised to save answeredInRound and check it against roundId returned, and use the price in PriceFeed response only if rounds match, treating it as stale otherwise.

It is advised to allow state changing operations only when the price is recent, i.e. it is better to reject an operation than to process it using stale parameter.

#0 - kingyetifinance

2022-01-07T08:08:28Z

@LilYeti : Duplicate #91 . Thorough suggestion though, severity recommended medium due to rarity of this case.

#1 - alcueca

2022-01-15T16:17:14Z

Medium Severity is a good fit, because the assets are not a direct risk, something else needs to happen first.

Findings Information

🌟 Selected for report: hyh

Labels

bug
2 (Med Risk)
sponsor confirmed

Awards

1593.8842 USDC - $1,593.88

External Links

Handle

hyh

Vulnerability details

Impact

Transactions will not be reverted on failed transfer call, setting system state as if it was successful. This will lead to wrong state accounting down the road with a wide spectrum of possible consequences.

Proof of Concept

_safeJoeTransfer do not check for JOE.transfer call success: https://github.com/code-423n4/2021-12-yetifinance/blob/main/packages/contracts/contracts/AssetWrappers/WJLP/WJLP.sol#L268

_safeJoeTransfer is called by _sendJoeReward, which is used in reward claiming.

JOE token use transfer from OpenZeppelin ERC20: https://github.com/traderjoe-xyz/joe-core/blob/main/contracts/JoeToken.sol#L9

Which does return success code: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/ERC20.sol#L113

Trader Joe also uses checked transfer when dealing with JOE tokens: https://github.com/traderjoe-xyz/joe-core/blob/main/contracts/MasterChefJoeV3.sol#L102

Also, unwrapFor do not check for JLP.transfer call success: https://github.com/code-423n4/2021-12-yetifinance/blob/main/packages/contracts/contracts/AssetWrappers/WJLP/WJLP.sol#L166

Add a require() check for the success of JOE transfer in _safeJoeTransfer function and create and use a similar function with the same check for JLP token transfers

Findings Information

🌟 Selected for report: csanuragjain

Also found by: hyh, jayjonah8

Labels

bug
duplicate
2 (Med Risk)
disagree with severity

Awards

430.3487 USDC - $430.35

External Links

Handle

hyh

Vulnerability details

Impact

User's rewards can be partial lost as system will write down all current rewards as transferred, while actual transfer amount can be smaller. WJLP will be still accounted for the remaining part of rewards by Trader Joe, but there will be no link to the user to whom these rewards should be accounted for in WJLP.

Proof of Concept

WJLP's _sendJoeReward calls _safeJoeTransfer to send unclaimedJOEReward and unconditionally set unclaimedJOEReward to 0: https://github.com/code-423n4/2021-12-yetifinance/blob/main/packages/contracts/contracts/AssetWrappers/WJLP/WJLP.sol#L229

While _safeJoeTransfer will send less if there is not enough balance at the moment: https://github.com/code-423n4/2021-12-yetifinance/blob/main/packages/contracts/contracts/AssetWrappers/WJLP/WJLP.sol#L268

Trader Joe system ('an external system we are dealing with' in general if this to be an example contract) can malfunction and send less than what's due or avoid sending rewards altogether. As _MasterChefJoe.withdraw is called without withdraw call result check of any kind, this will go unnoticed and user's balance will be decreased by more than was actually transferred to a user. In other words, part of users' rewards will be lost.

Now:

function _sendJoeReward(address _rewardOwner, address _to) internal { userInfo[_rewardOwner].unclaimedJOEReward = 0; _safeJoeTransfer(_to, joeToSend); ... function _safeJoeTransfer(address _to, uint256 _amount) internal { uint256 joeBal = JOE.balanceOf(address(this)); if (_amount > joeBal) { JOE.transfer(_to, joeBal); } else { JOE.transfer(_to, _amount); } }

To be:

function _sendJoeReward(address _rewardOwner, address _to) internal { uint256 joeSent = _safeJoeTransfer(_to, joeToSend); userInfo[_rewardOwner].unclaimedJOEReward = joeToSend - joeSent; ... function _safeJoeTransfer(address _to, uint256 _amount) internal returns (uint256) { uint256 toSend = JOE.balanceOf(address(this)); if (toSend > _amount) { toSend = _amount; } bool success = JOE.transfer(_to, toSend); require(success, "JOE transfer failed"); return toSend; }

JLP safe transfer function can approach token balance availability and transfer result in the similar manner.

#0 - kingyetifinance

2022-01-05T06:14:12Z

@LilYeti: #61 duplicate: and probably severity 1 since this will lose only dust.

#1 - alcueca

2022-01-15T06:38:57Z

Duplicate of #137

Findings Information

🌟 Selected for report: hyh

Labels

bug
1 (Low Risk)
sponsor disputed

Awards

531.2947 USDC - $531.29

External Links

Handle

hyh

Vulnerability details

Impact

Running initialization function second time setting another token address can break the system.

Proof of Concept

Both contracts govern liquidity pools that are linked to specific tokens and use setParams to initialize token address. This has to be done only once, i.e. reinitialization shouldn't be allowed.

Unipool.setParams https://github.com/code-423n4/2021-12-yetifinance/blob/main/packages/contracts/contracts/LPRewards/Unipool.sol#L95

Pool2Unipool.setParams https://github.com/code-423n4/2021-12-yetifinance/blob/main/packages/contracts/contracts/LPRewards/Pool2Unipool.sol#L95

Also, token addresses can be immutable to save gas.

Disallow running setParams twice, following initializer pattern, and make both token variables immutable.

#0 - kingyetifinance

2022-01-05T05:38:14Z

@LilYeti: Only owner and is revoked after being called.

#1 - alcueca

2022-01-16T20:54:11Z

In the case of Pool2Unipool.sol, _renounceOwnership is called in setRewards, not setParams. While it might be that the contract is not usable until setRewards is called, setParams can indeed be called several times. Incorrect as to spec.

Findings Information

🌟 Selected for report: defsec

Also found by: 0x1f8b, Jujic, WatchPug, broccolirob, certora, cmichel, csanuragjain, hyh, jayjonah8, kenzo, robee, sirhashalot

Labels

bug
duplicate
1 (Low Risk)

Awards

11.5426 USDC - $11.54

External Links

Handle

hyh

Vulnerability details

Impact

Transactions will not be reverted on failed transfer call, setting system state as if it was successful. Namely, user collateral tokens will be removed from Stability Pool account, while actual funds will not be sent fully. This way user funds will be locked within the system.

Proof of Concept

_sendGainsToDepositor do not check for collateral assets transfer call success:

https://github.com/code-423n4/2021-12-yetifinance/blob/main/packages/contracts/contracts/StabilityPool.sol#L947

The function is used for tokens StabilityPool deposit and withdraw: StabilityPool.provideToSP, withdrawFromSP -> _sendGainsToDepositor.

https://github.com/code-423n4/2021-12-yetifinance/blob/main/packages/contracts/contracts/StabilityPool.sol#L368

https://github.com/code-423n4/2021-12-yetifinance/blob/main/packages/contracts/contracts/StabilityPool.sol#L419

Add a require() check for the success of collateral token transfers

#0 - kingyetifinance

2022-01-05T06:20:02Z

@LilYeti: safe transfer issue duplicate issue #1

#1 - kingyetifinance

2022-01-10T06:09:11Z

#2 - alcueca

2022-01-15T07:32:23Z

Duplicate of #94

Findings Information

🌟 Selected for report: hyh

Labels

bug
1 (Low Risk)
sponsor confirmed

Awards

531.2947 USDC - $531.29

External Links

Handle

hyh

Vulnerability details

Impact

On calling with arrays of different lengths various malfunctions are possible as the arrays are used as given. withdrawColl outcome will not be as expected by a caller.

Proof of Concept

_adjustTrove doesn't check for array lengths and all other array providing usages of this function do check them before usage.

BorrowerOperations.withdrawColl doesn't: https://github.com/code-423n4/2021-12-yetifinance/blob/main/packages/contracts/contracts/BorrowerOperations.sol#L373

Add the check:

Now:

params._collsOut = _collsOut; params._amountsOut = _amountsOut;

To be:

require(_collsOut.length == _amountsOut.length); params._collsOut = _collsOut; params._amountsOut = _amountsOut;

#0 - 0xtruco

2022-01-11T09:57:52Z

Findings Information

🌟 Selected for report: hyh

Labels

bug
1 (Low Risk)
sponsor acknowledged

Awards

531.2947 USDC - $531.29

External Links

Handle

hyh

Vulnerability details

Impact

If there be any emergency with system contracts or outside infrastructure, there is no way to temporary stop the operations.

Proof of Concept

BorrowerOperations: https://github.com/code-423n4/2021-12-yetifinance/blob/main/packages/contracts/contracts/BorrowerOperations.sol

TroveManager: https://github.com/code-423n4/2021-12-yetifinance/blob/main/packages/contracts/contracts/TroveManager.sol

Make user facing contracts, first of all BorrowerOperations and TroveManager, pausable.

For example, by using OpenZeppelin's approach: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/security/Pausable.sol

It will be prudent to perform a deeper check to draw an extensive list of endpoints that require to be paused if something unexpected happens.

#0 - kingyetifinance

2022-01-05T05:48:46Z

@LilYeti: Known issue but unsure if we were going to implement this to make it less decentralized.

Findings Information

🌟 Selected for report: hyh

Also found by: heiho1

Labels

bug
0 (Non-critical)
disagree with severity
sponsor confirmed

Awards

239.0826 USDC - $239.08

External Links

Handle

hyh

Vulnerability details

Proof of Concept

_requireValidRouterParams and _requireRouterAVAXIndicesInOrder functions along with IYetiRouter interface are unused:

_requireValidRouterParams https://github.com/code-423n4/2021-12-yetifinance/blob/main/packages/contracts/contracts/BorrowerOperations.sol#L1053

_requireRouterAVAXIndicesInOrder https://github.com/code-423n4/2021-12-yetifinance/blob/main/packages/contracts/contracts/BorrowerOperations.sol#L1067

IYetiRouter import and the interface itself: https://github.com/code-423n4/2021-12-yetifinance/blob/main/packages/contracts/contracts/BorrowerOperations.sol#L12 https://github.com/code-423n4/2021-12-yetifinance/blob/main/packages/contracts/contracts/Interfaces/IYetiRouter.sol

If it is not meant to be implemented further consider removal to enhance code readability and size

#0 - kingyetifinance

2022-01-05T05:54:55Z

@LilYeti: Should be gas optimization for unused functions.

#1 - 0xtruco

2022-01-11T10:05:08Z

#2 - 0xtruco

2022-02-04T05:11:49Z

@alcueca Reason for this to be a low severity for internal function that was not used?

#3 - alcueca

2022-02-04T06:06:55Z

Right, this would be a non-critical. Apologies.

Findings Information

🌟 Selected for report: hyh

Labels

bug
1 (Low Risk)
disagree with severity
sponsor confirmed

Awards

531.2947 USDC - $531.29

External Links

Handle

hyh

Vulnerability details

Impact

Comment is misleading, now stating the opposite of the implemented logic.

Proof of Concept

Comment states that tokens added are not less than amount repurchased, while it should be not more: https://github.com/code-423n4/2021-12-yetifinance/blob/main/packages/contracts/contracts/YETI/sYETIToken.sol#L246

Now:

// Ensure that the amount of YETI tokens effectively added is >= the amount we have repurchased. if (yetiTokenBalance < additionalYetiTokenBalance) { additionalYetiTokenBalance = yetiTokenBalance; }

To be:

// Ensure that the amount of YETI tokens effectively added is <= the amount we have repurchased. if (yetiTokenBalance < additionalYetiTokenBalance) { additionalYetiTokenBalance = yetiTokenBalance; }

#0 - kingyetifinance

2022-01-05T05:57:13Z

@LilYeti: If they were suggesting our logic here was wrong, then they are correct. It should actually be

if (yetiTokenBalance - effectiveYetiTokenBalance < additionalYetiTokenBalance) {

This was pointed out by another of our independent auditors. If this is to be awarded for that bug as well then it is actually probably a medium error. However for just the comment which is what this issue references, it is non consequential error level 0 for readability.

#1 - kingyetifinance

2022-01-05T06:45:13Z

Actually probably just severity 0, it was not intended. Real bug reported here: #121

#2 - 0xtruco

2022-01-11T10:28:02Z

#3 - alcueca

2022-01-15T17:01:19Z

Errors in comments are low severity.

Findings Information

🌟 Selected for report: hyh

Also found by: Ruhum, WatchPug, cmichel

Labels

bug
1 (Low Risk)
sponsor confirmed

Awards

96.8285 USDC - $96.83

External Links

Handle

hyh

Vulnerability details

Impact

WJLP set configuration variables via setAddresses initialize function that has no access controls, so whenever it is being run not atomically with contract creation it can be front run by an attacker. The fix is to redeploy the contracts.

Proof of Concept

WJLP.setAddresses: https://github.com/code-423n4/2021-12-yetifinance/blob/main/packages/contracts/contracts/AssetWrappers/WJLP/WJLP.sol#L102

WJLP.constructor: https://github.com/code-423n4/2021-12-yetifinance/blob/main/packages/contracts/contracts/AssetWrappers/WJLP/WJLP.sol#L82

a. Either set access rights in the constructor and restrict initialize access b. Or run setAddresses atomically along with contract construction each time

It is also advised to check for zero addressed supplied by a caller both in constructor and setAddresses. Misconfiguration with zero address also leads to redeployment.

#0 - kingyetifinance

2022-01-05T06:06:23Z

@LilYeti: Since we may have many of these, extra checks would be good. Will likely add Ownable.

Findings Information

🌟 Selected for report: hyh

Labels

bug
1 (Low Risk)
sponsor confirmed

Awards

531.2947 USDC - $531.29

External Links

Handle

hyh

Vulnerability details

Impact

Core system logic can break up if enumeration structure be updated.

Proof of Concept

BorrowerOperations and StabilityPool check the active status of a trove by comparing TroveManager's getTroveStatus with 1: BorrowerOperations: https://github.com/code-423n4/2021-12-yetifinance/blob/main/packages/contracts/contracts/BorrowerOperations.sol#L902 https://github.com/code-423n4/2021-12-yetifinance/blob/main/packages/contracts/contracts/BorrowerOperations.sol#L907 StabilityPool: https://github.com/code-423n4/2021-12-yetifinance/blob/main/packages/contracts/contracts/StabilityPool.sol#L1104

TroveManagers inherit Status enumeration from TroveManagerBase: https://github.com/code-423n4/2021-12-yetifinance/blob/main/packages/contracts/contracts/Dependencies/TroveManagerBase.sol#L72

With further system development it will be harder to track fixes needed on enumeration change. Consider implementing TroveManager.isTroveActive(borrower) where trove.status is checked against Status.active and the corresponding boolean is returned.

#0 - kingyetifinance

2022-01-05T06:35:30Z

@LilYeti: Internal note: Make public function for this

Findings Information

🌟 Selected for report: hyh

Also found by: WatchPug, p4st13r4

Labels

bug
disagree with severity
G (Gas Optimization)
sponsor confirmed

Awards

26.3494 USDC - $26.35

External Links

#0 - kingyetifinance

2022-01-06T09:20:23Z

A gas improvement actually.

#1 - 0xtruco

2022-01-11T10:30:34Z

Already Fixed #159 #256

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