Yield-Convex contest - leastwood's results

Fixed-rate borrowing and lending on Ethereum

General Information

Platform: Code4rena

Start Date: 28/01/2022

Pot Size: $30,000 USDC

Total HM: 4

Participants: 22

Period: 3 days

Judge: GalloDaSballo

Total Solo HM: 2

Id: 80

League: ETH

Yield

Findings Distribution

Researcher Performance

Rank: 1/22

Findings: 3

Award: $12,844.92

🌟 Selected for report: 2

🚀 Solo Findings: 1

Findings Information

🌟 Selected for report: leastwood

Also found by: kenzo

Labels

bug
3 (High Risk)
resolved
sponsor confirmed

Awards

3964.904 USDC - $3,964.90

External Links

Handle

leastwood

Vulnerability details

Impact

ConvexYieldWrapper.sol is a wrapper contract for staking convex tokens on the user's behalf, allowing them to earn rewards on their deposit. Users will interact with the Ladle.sol contract's batch() function which:

  • Approves Ladle to move the tokens.
  • Transfers the tokens to ConvexYieldWrapper.sol.
  • Wraps/stakes these tokens.
  • Updates accounting and produces debt tokens within Ladle.sol.

During wrap() and unwrap() actions, _checkpoint() is used to update the rewards for the from_ and to_ accounts. However, the reference contract implements a _beforeTokenTransfer() function which has been removed from Yield Protocol's custom implementation.

As a result, it is possible to transfer wCVX tokens to another account after an initial checkpoint has been made. By manually calling user_checkpoint() on the new account, this user is able to update its deposited balance of the new account while the sender's balance is not updated. This can be repeated to effectively replicate a user's deposited balance over any number of accounts. To claim yield generated by the protocol, the user must only make sure that the account calling getReward() holds the tokens for the duration of the call.

Proof of Concept

The exploit can be outlined through the following steps:

  • Alice receives 100 wCVX tokens from the protocol after wrapping their convex tokens.
  • At that point in time, _getDepositedBalance() returns 100 as its result. A checkpoint has also been made on this balance, giving Alice claim to her fair share of the rewards.
  • Alice transfers her tokens to her friend Bob who then manually calls user_checkpoint() to update his balance.
  • Now from the perspective of the protocol, both Alice and Bob have 100 wCVX tokens as calculated by the _getDepositedBalance() function.
  • If either Alice or Bob wants to claim rewards, all they need to do is make sure the 100 wCVX tokens are in their account upon calling getReward(). Afterwards, the tokens can be transferred out.

Tools Used

Manual code review. Discussion/confirmation with the Yield Protocol team.

Consider implementing the _beforeTokenTransfer() function as shown in the reference contract. However, it is important to ensure the wrapper contract and collateral vaults are excluded from the checkpointing so they are not considered in the rewards calculations.

#0 - alcueca

2022-02-02T12:11:25Z

Confirmed. The fact that rewards can be drained also means that users lose on their expected rewards, so I think that Sev 3 is right.

#1 - GalloDaSballo

2022-02-14T23:06:29Z

In systems that track growing rewards, anytime a user balances changes, it's important to recalculate their balances as to properly distribute pending rewards and to influence the future-rate at which rewards will be distributed (process generally called accruing)

In the case of the ConvexYieldWrapper, the warden has shown that because the wCVX token doesn't perform a _checkpoint on each transfer, a malicious attacker could repeatedly transfer their tokens in order to reuse the same balance in multiple accounts, effectively sybil attacking the protocol.

The fix seems to be straightforward, however the impact of the finding breaks the accounting of the protocol, as such I believe High Severity to be appropraite

#2 - GalloDaSballo

2022-02-14T23:07:47Z

The sponsor has mitigated in a subsequent PR by overriding the _transfer function

Findings Information

🌟 Selected for report: leastwood

Labels

bug
3 (High Risk)
sponsor confirmed

Awards

8810.8979 USDC - $8,810.90

External Links

Handle

leastwood

Vulnerability details

Impact

ConvexYieldWrapper.sol is a wrapper contract for staking convex tokens on the user's behalf, allowing them to earn rewards on their deposit. Users will interact with the Ladle.sol contract's batch() function which:

  • Approves Ladle to move the tokens.
  • Transfers the tokens to ConvexYieldWrapper.sol.
  • Wraps/stakes these tokens.
  • Updates accounting and produces debt tokens within Ladle.sol.

_getDepositedBalance() takes into consideration the user's total collateral stored in all of their owned vaults. However, as a vault owner, you are allowed to give the vault to another user, move collateral between vaults and add/remove collateral. Therefore, it is possible to manipulate the result of this function by checkpointing one user's balance at a given time, transferring ownership to another user and then create a new checkpoint with this user.

As a result, a user is able to generate protocol yield multiple times over on a single collateral amount. This can be abused to effectively extract all protocol yield.

Proof of Concept

Consider the following exploit scenario:

  • Alice owns a vault which has 100 tokens worth of collateral.
  • At that point in time, _getDepositedBalance() returns 100 as its result. A checkpoint has also been made on this balance, giving Alice claim to her fair share of the rewards.
  • Alice then calls Ladle.give(), transferring the ownership of the vault to Bob and calls ConvexYieldWrapper.addVault().
  • Bob is able to call user_checkpoint() and effectively update their checkpointed balance.
  • At this point in time, both Alice and Bob have claim to any yield generated by the protocol, however, there is only one vault instance that holds the underlying collateral.

https://github.com/code-423n4/2022-01-yield/blob/main/contracts/ConvexYieldWrapper.sol#L100-L120

function _getDepositedBalance(address account_) internal view override returns (uint256) { if (account_ == address(0) || account_ == collateralVault) { return 0; } bytes12[] memory userVault = vaults[account_]; //add up all balances of all vaults registered in the wrapper and owned by the account uint256 collateral; DataTypes.Balances memory balance; uint256 userVaultLength = userVault.length; for (uint256 i = 0; i < userVaultLength; i++) { if (cauldron.vaults(userVault[i]).owner == account_) { balance = cauldron.balances(userVault[i]); collateral = collateral + balance.ink; } } //add to balance of this token return _balanceOf[account_] + collateral; }

Tools Used

Manual code review. Discussion/confirmation with the Yield Protocol team.

Ensure that any change to a vault will correctly checkpoint the previous and new vault owner. The affected actions include but are not limited to; transferring ownership of a vault to a new account, transferring collateral to another vault and adding/removing collateral to/from a vault.

#0 - GalloDaSballo

2022-02-18T14:57:45Z

The warden identified a way to sidestep the accounting in the ConvexYieldWrapper.

Because ConvexYieldWrapper takes lazy accounting, transferring vaults at the Ladle level allows to effectively register the same vault under multiple accounts, which ultimately allow to steal more yield than expected.

While the loss of yield can be classified as a medium severity, the fact that the warden was able to break the accounting invariants of the ConvexYieldWrapper leads me to raise the severity to high

#1 - GalloDaSballo

2022-02-18T14:59:44Z

Ultimately mitigation will require to _checkpoint also when vault operations happen (especially transfer), this may require a rethinking at the Ladle level as the reason why the warden was able to sidestep the checkpoint is because the Ladle doesn't notify the Wrapper of any vault transfers

#2 - alcueca

2022-02-21T09:19:20Z

Yes, that's right. To fix this issue we will deploy a separate Ladle to deal specifically with convex tokens. The fix will probably involve removing stir and give instead of notifying the wrapper, but we'll see.

Findings Information

🌟 Selected for report: throttle

Also found by: 0x1f8b, TomFrenchBlockchain, WatchPug, cccz, defsec, hack3r-0m, hyh, kenzo, leastwood, sirhashalot, ye0lde

Labels

bug
duplicate
2 (Med Risk)

Awards

69.1238 USDC - $69.12

External Links

Handle

leastwood

Vulnerability details

Impact

latestRoundData is missing additional validation to ensure that the round is complete and has returned a valid/expected price. This is documented here.

Proof of Concept

https://github.com/code-423n4/2022-01-yield/blob/main/contracts/Cvx3CrvOracle.sol#L120-L127

(, int256 daiPrice, , , ) = DAI.latestRoundData(); (, int256 usdcPrice, , , ) = USDC.latestRoundData(); (, int256 usdtPrice, , , ) = USDT.latestRoundData(); require( daiPrice > 0 && usdcPrice > 0 && usdtPrice > 0, "Chainlink pricefeed reporting 0" );

Tools Used

Manual code review. Chainlink best practices.

Consider validating the output of latestRoundData to match the following code snippet:

( uint80 roundID, int256 daiPrice, , uint256 updateTime, uint80 answeredInRound ) = DAI.latestRoundData(); require( answeredInRound >= roundID, "Yield: Chainlink Price Stale" ); require(daiPrice > 0, "Yield: Chainlink Malfunction"); require(updateTime != 0, "Yield: Incomplete round");

The same recommendation can be applied to the USDC.latestRoundData() and USDT.latestRoundData() calls.

#0 - iamsahu

2022-02-01T19:29:09Z

Duplicate of #136

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