Yeti Finance contest - kenzo'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: 1/25

Findings: 8

Award: $14,768.76

🌟 Selected for report: 7

πŸš€ Solo Findings: 3

Findings Information

🌟 Selected for report: jayjonah8

Also found by: dalgarim, kenzo

Labels

bug
duplicate
3 (High Risk)
disagree with severity

Awards

1434.4958 USDC - $1,434.50

External Links

Handle

kenzo

Vulnerability details

StabilityPool's receiveCollateral should only be called by ActivePool, but that check is missing. Anybody can call it and update StabilityPool's total collateral variable.

Impact

Wrong amounts of total collateral in StabilityPool (totalColl). As far as I can see, this value is only used in view functions.

Proof of Concept

receiveCollateral: (notice comment and lack of caller check) (Code ref)

// Should be called by ActivePool // __after__ collateral is transferred to this contract from Active Pool function receiveCollateral(address[] memory _tokens, uint256[] memory _amounts) external override { totalColl.amounts = _leftSumColls(totalColl, _tokens, _amounts); emit StabilityPoolBalancesUpdated(_tokens, _amounts); }

The rest of the pools do check the caller on this function.

Add _requireCallerIsActivePool() to the function.

#0 - kingyetifinance

2022-01-06T06:55:57Z

@LilYeti: Duplicate with #74 and should be severity level 3

Findings Information

🌟 Selected for report: kenzo

Labels

bug
3 (High Risk)
disagree with severity
sponsor confirmed

Awards

5312.9473 USDC - $5,312.95

External Links

Handle

kenzo

Vulnerability details

WJLP doesn't update the inner accounting (for JOE rewards) when unwrapping user's tokens. The user will continue to receive rewards, on the expanse of users who haven't claimed their rewards yet.

Impact

Loss of yield for users.

Proof of Concept

The unwrap function just withdraws JLP from MasterChefJoe, burns the user's WJLP, and sends the JLP back to the user. It does not update the inner accounting (userInfo). (Code ref)

function unwrapFor(address _to, uint _amount) external override { _requireCallerIsAPorSP(); _MasterChefJoe.withdraw(_poolPid, _amount); // msg.sender is either Active Pool or Stability Pool // each one has the ability to unwrap and burn WAssets they own and // send them to someone else _burn(msg.sender, _amount); JLP.transfer(_to, _amount); }

Need to keep userInfo updated. Have to take into consideration the fact that user can choose to set the reward claiming address to be a different account than the one that holds the WJLP.

#0 - kingyetifinance

2022-01-07T08:00:02Z

@LilYeti : Due to the specific issue mentioned here, it is actually different than #141 . If indeed the token is transferred before sending it to the protocol, it will be accepted as collateral but will never be able to leave the system, via liquidation, redemption, and should not have been able to leave except the flawed implementation of unwrap for in borrower operations allows this to be withdrawn. Large error nonetheless, recommend upgrading to severity 3.

Findings Information

🌟 Selected for report: kenzo

Also found by: UncleGrandpa925

Labels

bug
3 (High Risk)
sponsor confirmed

Awards

2390.8263 USDC - $2,390.83

External Links

Handle

kenzo

Vulnerability details

After updating user's rewards in _userUpdate, if the user has not claimed them, and _userUpdate is called again (eg. on another wrap), the user's unclaimed rewards will lose the previous unclaimed due to wrong calculation.

Impact

Loss of yield for user.

Proof of Concept

When updating the user's unclaimedJoeReward, the function doesn't save it's previous value. (Code ref)

if (user.amount > 0) { user.unclaimedJOEReward = user.amount.mul(accJoePerShare).div(1e12).sub(user.rewardDebt); } if (_isDeposit) { user.amount = user.amount.add(_amount); } else { user.amount = user.amount.sub(_amount); } // update for JOE rewards that are already accounted for in user.unclaimedJOEReward user.rewardDebt = user.amount.mul(accJoePerShare).div(1e12);

So for example, rewards can be lost in the following scenario. We'll mark "acc1" for the value of "accJoePerShare" at step 1.

  1. User Zebulun wraps 100 tokens. After _userUpdate is called: unclaimedJOEReward = 0, rewardDebt = 100*acc1.
  2. Zebulun wraps 50 tokens: unclaimedJOEReward = 100acc2 - 100acc1, rewardDebt = 150 * acc2.
  3. Zebulun wraps 1 token: unclaimedJOEReward = 150acc3 - 150acc2, rewardDebt = 151*acc3 So in the last step, Zebulun's rewards only take into account the change in accJoePerShare in steps 2-3, and lost the unclaimed rewards from steps 1-2.

Change the unclaimed rewards calculation to:

user.unclaimedJOEReward = user.unclaimedJOEReward.add(user.amount.mul(accJoePerShare).div(1e12).sub(user.rewardDebt));

#0 - kingyetifinance

2022-01-07T07:53:54Z

@LilYeti : Probably severity 3 due to loss of allocation of funds for other users wrapping LP tokens.

#1 - 0xtruco

2022-01-16T10:45:18Z

Actually not a problem, the accrued reward is updated as time goes on and should be overridden. Follows same logic from Master chef v2 here: https://snowtrace.io/address/0xd6a4F121CA35509aF06A0Be99093d08462f53052#code

#2 - alcueca

2022-01-17T10:00:19Z

@0xtruco, are you sure of what you are saying? To me the issue seems to still exist. Could you elaborate?

#3 - 0xtruco

2022-01-19T02:13:09Z

@alcueca Yes you are correct this actually is an issue. I initially thought that we were harvesting rewards just like TJ when users wrapped tokens but turns out that line is not there. This is in the new version of our code but was not in the version submitted at the time of the contest. Thanks for looking into it more! Back to high risk.

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

kenzo

Vulnerability details

WJLP's wrap takes _from and _to as parameter, and doesn't check that msg.sender=_from. This means that anybody can claim to himself tokens that a user approved for WJLP.

Impact

Loss of user funds.

Proof of Concept

The problem is in the wrap function. (Code ref)

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

No check is being done on from\to parameters. After a user has .approved WJLP, a malicious user can call wrap(_amount, fromLegitimateUser, toMaliciousUser, toMaliciousUser) and gain the legitimate user's tokens.

Probably remove _from from the parameters, and wrap only msg.sender's tokens.

#0 - kingyetifinance

2022-01-07T06:34:12Z

@LilYeti : Duplicate #58

#1 - alcueca

2022-01-15T06:44:38Z

Duplicate #208

Findings Information

🌟 Selected for report: kenzo

Labels

bug
2 (Med Risk)
sponsor confirmed

Awards

1593.8842 USDC - $1,593.88

External Links

Handle

kenzo

Vulnerability details

WJLP can only be unwrapped from the Active Pool or Stability Pool. A user who decided to wrap his JLP, but not use all of them in a trove, Wouldn't be able to just unwrap them.

Impact

Impaired functionality for users. Would have to incur fees for simple unwrapping.

Proof of Concept

The unwrap functionality is only available from unwrapFor function, and that function is only callable from AP or SP. (Code ref)

function unwrapFor(address _to, uint _amount) external override { _requireCallerIsAPorSP();

Allow anybody to call the function. As it will burn the holder's WJLP, a user could only unwrap tokens that are not in use.

#0 - kingyetifinance

2022-01-05T06:59:22Z

Added unwrap function

Findings Information

🌟 Selected for report: kenzo

Labels

bug
2 (Med Risk)
sponsor confirmed

Awards

1593.8842 USDC - $1,593.88

External Links

Handle

kenzo

Vulnerability details

When ActivePool sends collateral which is a wrapped asset, it first unwraps the asset, and only after that updates the rewards. This should be done in opposite order. As a comment in WJLP's unwrapFor rightfully mentions - "Prior to this being called, the user whose assets we are burning should have their rewards updated".

Impact

Lost yield for user.

Proof of Concept

In ActivePool's sendCollateralsUnwrap (which is used throughout the protocol), it firsts unwraps the asset, and only afterwards calls claimRewardFor which will update the rewards: (Code ref)

IWAsset(_tokens[i]).unwrapFor(_to, _amounts[i]); if (_collectRewards) { IWAsset(_tokens[i]).claimRewardFor(_to); }

claimRewardFor will end up calling _userUpdate: (Code ref)

function _userUpdate(address _user, uint256 _amount, bool _isDeposit) private returns (uint pendingJoeSent) { uint256 accJoePerShare = _MasterChefJoe.poolInfo(_poolPid).accJoePerShare; UserInfo storage user = userInfo[_user]; if (user.amount > 0) { user.unclaimedJOEReward = user.amount.mul(accJoePerShare).div(1e12).sub(user.rewardDebt); } if (_isDeposit) { user.amount = user.amount.add(_amount); } else { user.amount = user.amount.sub(_amount); } user.rewardDebt = user.amount.mul(accJoePerShare).div(1e12); }

Now, as ActivePool has already called unwrapFor and has burnt the user's tokens, and let's assume they all were used as collateral, it means user.amount=0*, and the user's unclaimedJOEReward won't get updated to reflect the rewards from the last user update. This is why, indeed as the comment in unwrapFor says, user's reward should be updated prior to that.

*Note: at the moment unwrapFor doesn't updates the user's user.amount, but as I detailed in another issue, that's a bug, as that means the user will continue accruing rewards even after his JLP were removed from the protocol.

Change the order of operations in sendCollateralsUnwrap to first send the updated rewards and then unwrap the asset. You can also consider adding to the beginning of unwrapFor a call to _userUpdate(_to, 0, true) to make sure the rewards are updated before unwrapping. Note: as user can choose to have JOE rewards accrue to a different address than the address that uses WJLP as collateral, you'll have to make sure you update the current accounts. I'll detail this in another issue.

#0 - kingyetifinance

2022-01-05T07:54:24Z

@LilYeti: Thanks for the thorough explanation.

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

kenzo

Vulnerability details

Some ERC20 tokens only return false and do not revert on failed transfer or transferForm. Yeti mostly handles this, but not in all places.

Impact

Wouldn't be able to easily tell if transfer succeeded.

Proof of Concept

When YetiFinanceTreasury sendsToken to the team wallet, it doesn't check whether the transfer succeeded. (Code ref)

function sendToken(IERC20 _token, address _to, uint _amount) external onlyTeam { _token.transfer(_to, _amount); }

Add a return value to the function and make sure the transfer succeeded.

#0 - kingyetifinance

2022-01-05T10:10:51Z

Duplicate #116

#1 - alcueca

2022-01-15T06:03:02Z

For now, a duplicate of #1

#2 - alcueca

2022-01-15T07:30:33Z

Duplicate of #94

Findings Information

🌟 Selected for report: kenzo

Also found by: shenwilly

Labels

bug
1 (Low Risk)
sponsor confirmed

Awards

239.0826 USDC - $239.08

External Links

Handle

kenzo

Vulnerability details

When changing collateral's ratio, it is rightly checked to be smaller than 110%. However when adding new collateral, the ratio check is not there, so it can be added with ratio that is larger than 110%.

Impact

Accidentally adding an asset with larger ratio would result in users being able to withdraw more YUSD than supplied VC.

Proof of Concept

When an asset is being added, there is no sanity check that the ratio is within the correct range. (Code ref)

This is unlike changeRatio, which validates that the new ratio is in correct range. (Code ref)

require(_ratio < 1100000000000000000, "ratio must be less than 1.10 => greater than 1.1 would mean taking out more YUSD than collateral VC");

Add the same ratio check to addCollateral.

#0 - 0xtruco

2022-01-11T11:39:05Z

Findings Information

🌟 Selected for report: kenzo

Labels

bug
1 (Low Risk)
disagree with severity
sponsor confirmed

Awards

531.2947 USDC - $531.29

External Links

Handle

kenzo

Vulnerability details

LockupContract, LockupContractFactory amd ShortLockupContract all have comments that say:

Within the first year from deployment, the deployer of the YETIToken (Liquity AG's address) may transfer YETI only to valid LockupContracts, and no other addresses (this is enforced in YETIToken.sol's transfer() function). The above two restrictions ensure that until one year after system deployment, YETI tokens originating from Liquity AG cannot enter circulating supply and cannot be staked to earn system revenue.

This comment is outdated (verified with sponsor). There is no such lockup on YETI tokens issued to team/treasury. (There might be other type of vesting which is probably implemented using TeamLockup.)

Impact

Confusion, wrong description of team's capability to use yeti tokens issued.

Proof of Concept

Code ref.

Remove outdated comments.

#0 - kingyetifinance

2022-01-05T17:52:51Z

@LilYeti: This is a comment error, is realistically risk 0.

#1 - 0xtruco

2022-01-12T01:16:17Z

#2 - alcueca

2022-01-15T16:35:26Z

Comment errors are low severity

Findings Information

🌟 Selected for report: WatchPug

Also found by: kenzo, pauliax

Labels

bug
duplicate
1 (Low Risk)
sponsor confirmed

Awards

143.4496 USDC - $143.45

External Links

Handle

kenzo

Vulnerability details

ERC20_8 which is inherited by WJLP doesn't update totalSupply upon mint/burn.

Impact

totalSupply() will always return 0.

Proof of Concept

The mint and burn functions do not increase total supply. (Code ref) The only other place in the contract which uses totalSupply is the getter.

Keep totalSupply up to date within mint and burn functions.

#0 - alcueca

2022-01-14T21:48:38Z

Duplicate of #259

Findings Information

🌟 Selected for report: kenzo

Labels

bug
1 (Low Risk)
disagree with severity
sponsor confirmed

Awards

531.2947 USDC - $531.29

External Links

Handle

kenzo

Vulnerability details

TeamLockup mentions on "vestingLength" that it is the "number of YETI that are claimable every day after vesting starts". However, the vesting calculation treats it as if was the number of YETI that are claimable every second, not every day.

Impact

Tokens would be released faster than planned. Or, if the tokens are planned to be released every second and not every day (I'm guessing it's less likely), then this is a wrong comment.

Proof of Concept

The description of vestingLength: (Code ref)

uint immutable vestingLength; // number of YETI that are claimable every day after vesting starts

The calculation to decide how many tokens can be released: (Code ref)

uint timePastVesting = block.timestamp.sub(vestingStart); uint available = _min(totalVest,(totalVest.mul(timePastVesting)).div(vestingLength));

The problem is that timePastVesting is in seconds, and vestingLength is in days.

Divide the calculation by 1 day to align the units.

#0 - kingyetifinance

2022-01-06T06:14:47Z

@LilYeti: The comment was incorrect, but this could have produced problems. Recommend security level 1 though.

#1 - alcueca

2022-01-15T16:34:51Z

Issues with comments are low severity, which is usually excessive. However in this case, low severity fits perfectly.

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