Yield-Convex contest - WatchPug'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: 3/22

Findings: 4

Award: $3,010.91

🌟 Selected for report: 6

🚀 Solo Findings: 1

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)
sponsor disputed

Awards

69.1238 USDC - $69.12

External Links

Handle

WatchPug

Vulnerability details

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

    function _peek(
        bytes6 base,
        bytes6 quote,
        uint256 baseAmount
    ) private view returns (uint256 quoteAmount, uint256 updateTime) {
        require(
            (base == ethId && quote == cvx3CrvId) ||
                (base == cvx3CrvId && quote == ethId),
            "Invalid quote or base"
        );
        (, int256 daiPrice, , , ) = DAI.latestRoundData();
        (, int256 usdcPrice, , , ) = USDC.latestRoundData();
        (, int256 usdtPrice, , , ) = USDT.latestRoundData();

        require(
            daiPrice > 0 && usdcPrice > 0 && usdtPrice > 0,
            "Chainlink pricefeed reporting 0"
        );

On Cvx3CrvOracle.sol, we are using Chainlink's latestRoundData API, but there is no check if the return value indicates stale data. This could lead to stale prices according to the Chainlink documentation:

See also: https://github.com/yieldprotocol/vault-v2/blob/master/contracts/oracles/chainlink/ChainlinkMultiOracle.sol#L88-L90

Recommendation

Consider adding missing checks for stale data.

For example:

(uint80 daiRoundID, int256 daiPrice, , uint256 daiTimestamp, uint80 daiAnsweredInRound) = DAI.latestRoundData();
require(daiPrice > 0, "DAI: Chainlink price <= 0"); 
require(daiAnsweredInRound >= daiRoundID, "DAI: Stale price");
require(daiTimestamp != 0, "DAI: Round not complete");

(uint80 usdcRoundID, int256 usdcPrice, , uint256 usdcTimestamp, uint80 usdcAnsweredInRound) = USDC.latestRoundData();
require(usdcPrice > 0, "USDC: Chainlink price <= 0"); 
require(usdcAnsweredInRound >= usdcRoundID, "USDC: Stale price");
require(usdcTimestamp != 0, "USDC: Round not complete");

(uint80 usdtRoundID, int256 usdtPrice, , uint256 usdtTimestamp, uint80 usdtAnsweredInRound) = USDT.latestRoundData();
require(usdtPrice > 0, "USDT: Chainlink price <= 0"); 
require(usdtAnsweredInRound >= usdtRoundID, "USDT: Stale price");
require(usdtTimestamp != 0, "USDT:Round not complete");

#0 - iamsahu

2022-02-01T19:27:05Z

#136 And the suggested solution would lead to a stack too deep error.

Findings Information

🌟 Selected for report: WatchPug

Labels

bug
2 (Med Risk)
disagree with severity
resolved
sponsor confirmed

Awards

2643.2694 USDC - $2,643.27

External Links

Handle

WatchPug

Vulnerability details

https://github.com/code-423n4/2022-01-yield/blob/e946f40239b33812e54fafc700eb2298df1a2579/contracts/ConvexStakingWrapper.sol#L206-L224

function _calcRewardIntegral(
    uint256 _index,
    address[2] memory _accounts,
    uint256[2] memory _balances,
    uint256 _supply,
    bool _isClaim
) internal {
    RewardType storage reward = rewards[_index];

    uint256 rewardIntegral = reward.reward_integral;
    uint256 rewardRemaining = reward.reward_remaining;

    //get difference in balance and remaining rewards
    //getReward is unguarded so we use reward_remaining to keep track of how much was actually claimed
    uint256 bal = IERC20(reward.reward_token).balanceOf(address(this));
    if (_supply > 0 && (bal - rewardRemaining) > 0) {
        rewardIntegral = uint128(rewardIntegral) + uint128(((bal - rewardRemaining) * 1e20) / _supply);
        reward.reward_integral = uint128(rewardIntegral);
    }

reward.reward_integral is uint128, if a early user mint (wrap) just 1 Wei of convexToken, and make _supply == 1, and then tranferring 5e18 of reward_token to the contract.

As a result, reward.reward_integral can exceed type(uint128).max and overflow, causing the rewards distribution to be disrupted.

Recommendation

Consider wrap a certain amount of initial totalSupply, e.g. 1e8, and never burn it. And consider using uint256 instead of uint128 for reward.reward_integral. Also, consdier lower 1e20 down to 1e12.

#0 - alcueca

2022-02-02T15:33:25Z

Assets are not a direct risk if it is the first user disrupting the contract. We would need to redeploy better code, but that's it. I suggest this is reported as medium, as merits for a DoS attack.

As suggested elsewhere, the right solution if uint128 is to be used in storage, is to cast up to uint256 for calculations, and then back to uint128 to store again.

#1 - GalloDaSballo

2022-02-18T00:55:26Z

The warden identified a way an early depositor can grief the system, I believe the finding to be valid, and because:

  • It would conditionally disrupt the system
  • It would "brick the contract" at the loss of the griefer
  • No additional funds would be at risk

I believe medium severity to be appropriate

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