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
Rank: 2/22
Findings: 3
Award: $5,796.20
🌟 Selected for report: 2
🚀 Solo Findings: 0
3964.904 USDC - $3,964.90
kenzo
ConvexStakingWrapper saves data for reward calculation in dedicated variables for each user, such as reward.reward_integral_for[account]
.
These variables are not updated when transferring wrapped staked tokens.
(Please note that Convex's original ConvexStakingWrapper does update the rewards state before transfer.)
Wrong accounting for rewards.
Let's say Roy hasn't claimed his rewards, and transfers tokens to Zora.
Next time Roy will call getReward
, it will call the _checkpointAndClaim
method, which will call _getDepositedBalance
to calculate Roy's balance.
Since his balance is now 0, and he didn't have already saved pending rewards (as no checkpoint was created when transferring the tokens), the function will calculate that he has no pending rewards. His rewards have been lost in time, like tears in rain.
Add the following code, like Convex does:
function _beforeTokenTransfer(address _from, address _to, uint256 _amount) internal override { _checkpoint([_from, _to]); }
(Note: you can then remove the _checkpoint
calls from wrap
and unwrap
).
#0 - iamsahu
2022-02-01T20:37:22Z
The solution provided won't work. The _checkpoint
before a token transfer needs to be conditional otherwise it would end up allocating the wrapper contract some of the rewards.
#1 - alcueca
2022-02-02T12:12:07Z
Still, the issue is valid. Duplicate of #86, which we agreed to be Sev 3.
kenzo
When querying Chainlink for stable prices, Cvx3CrvOracle doesn't run sanity checks against stale or incomplete results. This is unlike Yield's ChainlinkMultiOracle, which does execute those checks.
Stale or incorrect results might be returned.
When querying Chainlink, Cvx3CrvOracle only checks that the price is bigger than 0:
(, int256 daiPrice, , , ) = DAI.latestRoundData(); (, int256 usdcPrice, , , ) = USDC.latestRoundData(); (, int256 usdtPrice, , , ) = USDT.latestRoundData(); require(daiPrice > 0 && usdcPrice > 0 && usdtPrice > 0, "Chainlink pricefeed reporting 0");
But further checks are needed to verify the price is not stale, as Yield's ChainlinkMultiOracle does:
(roundId, price,, updateTime, answeredInRound) = AggregatorV3Interface(source.source).latestRoundData(); require(price > 0, "Chainlink price <= 0"); require(updateTime != 0, "Incomplete round"); require(answeredInRound >= roundId, "Stale price");
Add the missing sanity checks.
#0 - iamsahu
2022-02-01T19:28:23Z
#136
🌟 Selected for report: kenzo
881.0898 USDC - $881.09
kenzo
The ConvexStakingWrapper that Yield is based on recently published a fix for earned
function in case the pool is claimed indirectly.
Wrong results might be returned from view function earned
.
This is the fix for earned: fix commit
Apply fix.
#0 - GalloDaSballo
2022-02-14T00:01:36Z
Very much appreciated finding that notifies the sponsor of some recent CVX updates. Because there's no POC, I believe low severity to be appropriate
🌟 Selected for report: kenzo
881.0898 USDC - $881.09
kenzo
Due to the precision of Chainlink oracle and Curve's virtual price, Cvx3CrvOracle will return 0 for very small baseAmount.
As it only happens with very small amounts, the impact is not big as far as I can tell.
However one can imagine for example a scenario where a user will request to buy a small amount of Cvx3Crv, and a contract would use peek
to know how much to charge. Since peek
returns 0, the user wouldn't be charged anything and still be able to receive a small amount of Cvx3Crv.
For example, for real (taken from contracts) values of:
minStable = 388135165257710 virtualPrice = 1020217504409323943 baseAmount = 1000
The oracle will return 0.
Consider reverting the transaction if baseAmount > 0 and quoteAmount == 0.
#0 - alcueca
2022-02-02T12:45:29Z
That's true, and an issue that no one else saw in any of the previous audits, given that this was first done in the ChainlinkMultiOracle.
While I think there might be little risk about it, we'll think more deeply about it, and I personally appreciate you seeing this.
#1 - GalloDaSballo
2022-02-14T22:59:41Z
I believe the finding to be valid, given the limited scope am conflicted on giving medium severity.
In the example given by the warden
uint256 price = (threecrv.get_virtual_price() * minStable) / 1e18;
3.9598229e32 // 32 zeroes / 1e18
Gives us: 3.9598229e14
So for this condition
quoteAmount = (baseAmount * price) / 1e18;
Any baseAmount
with less than 4 units will return a 0.
Hence we quantified the attack.
This is denominated in eth as well.
A gwei is 1e9
Because of EIP1559 there is no way to have a zero gas tx, I believe 1 gwei may be the minimum cost (but may be wrong)
This ultimately means that while the rounding is present there should be no economic incentive in exploiting it, as the MEV extractable has 4 order of magnitude but the minimum cost would have 9 (1 gwei = 10^9 wei)
At this time, given these considerations I believe the finding to be valid and to be of low severity