Gro Protocol contest - cmichel's results

The first protocol to balance your exposure, tranche risk and boost yields all at once.

General Information

Platform: Code4rena

Start Date: 01/07/2021

Pot Size: $100,000 USDC

Total HM: 10

Participants: 7

Period: 7 days

Judge: ghoulsol

Total Solo HM: 4

Id: 17

League: ETH

Gro Protocol

Findings Distribution

Researcher Performance

Rank: 1/7

Findings: 7

Award: $26,071.30

🌟 Selected for report: 9

🚀 Solo Findings: 2

Findings Information

🌟 Selected for report: gpersoon

Also found by: cmichel

Labels

bug
duplicate
3 (High Risk)
disagree with severity

Awards

5885.9975 USDC - $5,886.00

External Links

Handle

cmichel

Vulnerability details

Vulnerability Details

The safetyCheck function performs an unsafe subtraction on two uint256 before casting them to int256. The subtraction can underflow and the cast to int256 can either fail and revert the transaction (if greater than type(int256).max), or, fit into an int256 and corrupt the safetyCheck making it always return false.

// _ratio - lastRatio[i] are uint256s and underflows
_ratio = abs(int256(_ratio - lastRatio[i]));

If the lastRatio[i] is even just 1 "wei" less than _ratio, the result will be type(uint256).max and the cast to int256 will fail due to the size limit of signed integers. All functions implementing the safetyCheck will revert and the protocol can become stuck and unusable.

As only the absolute value is relevant the following code should work without having to cast to int256:

uint256 ratioDiff = _ratio > lastRatio[i] ? _ratio - lastRatio[i] : lastRatio[i] - _ratio;

#0 - kitty-the-kat

2021-07-14T15:32:47Z

low severity Its not a problem in 0.6.12. No plan to change to 0.8.x

#1 - flabble-gro

2021-07-14T20:25:53Z

Duplicate of #6

#2 - ghoul-sol

2021-07-25T21:33:34Z

Duplicate of #6 ergo it's a high risk.

Findings Information

🌟 Selected for report: cmichel

Also found by: shw

Labels

bug
3 (High Risk)
sponsor confirmed
disagree with severity

Awards

5885.9975 USDC - $5,886.00

External Links

Handle

cmichel

Vulnerability details

Vulnerability Details

The safetyCheck function has several issues that impact how precise the checks are:

  1. only checks if the a/b and a/c ratios are within BASIS_POINTS. By transitivity b/c is only within 2 * BASIS_POINTS if a/b and a/c are in range. For a more precise check whether both USDC and USDT are within range, b/c must be checked as well.

  2. If a/b is within range, this does not imply that b/a is within range.

"inverted ratios, a/b bs b/a, while producing different results should both reflect the same change in any one of the two underlying assets, but in opposite directions"

Example: lastRatio = 1.0 ratio: a = 1.0, b = 0.8 => a/b = 1.25, b/a = 0.8 If a/b was used with a 20% range, it'd be out of range, but b/a is in range.

  1. The natspec for the function states that it checks Curve and an external oracle, but no external oracle calls are checked, both _ratio and lastRatio are only from Curve. Only _updateRatios checks the oracle.

In addition, check if b/c is within BASIS_POINTS.

#0 - kitty-the-kat

2021-07-14T20:37:22Z

Makes strong assumption about the range of possible values - small differences between a and b will result in small differences between a/b and b/a - Extreme cases are handled by emergency. Agree on b/c check

#1 - kitty-the-kat

2021-07-14T20:38:39Z

medium severity - will only cause stop of deposits/withdrawals against curve, work around to put in emergency mode

#2 - ghoul-sol

2021-07-26T03:35:15Z

A possibility of stopping deposits or withdrawals deserves high risk.

Findings Information

🌟 Selected for report: cmichel

Also found by: 0xRajeev, a_delamo

Labels

bug
2 (Med Risk)
sponsor confirmed

Awards

1059.4796 USDC - $1,059.48

External Links

Handle

cmichel

Vulnerability details

Vulnerability Details

The Chainlink API (latestAnswer) used in the Buoy3Pool oracle wrappers is deprecated:

This API is deprecated. Please see API Reference for the latest Price Feed API. Chainlink Docs

Impact

It seems like the old API can return stale data. Checks similar to that of the new API using latestTimestamp and latestRoundare are needed. This could lead to stale prices according to the Chainlink documentation:

Add the recommended checks:

(
    uint80 roundID,
    int256 price,
    ,
    uint256 timeStamp,
    uint80 answeredInRound
) = chainlink.latestRoundData();
require(
    timeStamp != 0,
    “ChainlinkOracle::getLatestAnswer: round is not complete”
);
require(
    answeredInRound >= roundID,
    “ChainlinkOracle::getLatestAnswer: stale data”
);
require(price != 0, "Chainlink Malfunction”);

Findings Information

🌟 Selected for report: cmichel

Labels

bug
2 (Med Risk)
sponsor confirmed

Awards

3923.9983 USDC - $3,924.00

External Links

Handle

cmichel

Vulnerability details

Vulnerability Details

The two BaseVaultAdaptor.calculateShare functions computes share = amount.mul(uint256(10)**decimals).div(sharePrice)

uint256 sharePrice = _getVaultSharePrice();
// amount is in "token" decimals, share should be in "vault" decimals
share = amount.mul(uint256(10)**decimals).div(sharePrice);

This assumes that the sharePrice is always in token decimals and that token decimals is the same as vault decimals.

This both happens to be the case for Yearn vaults, but will not necessarily be the case for other protocols. As this functionality is in the BaseVaultAdaptor and not in the specific VaultAdaptorYearnV2_032, consider generalizing the conversion.

Impact

Integrating a token where the token or price is reported in a different precision will lead to potential losses as more shares are computed.

The conversion seems highly protocol specific, calculateShare should be an abstract function like _getVaultSharePrice, that is implemented in the specific adaptors.

Findings Information

🌟 Selected for report: cmichel

Labels

bug
2 (Med Risk)
disagree with severity
sponsor acknowledged

Awards

3923.9983 USDC - $3,924.00

External Links

Handle

cmichel

Vulnerability details

Vulnerability Details

The protocol computes a factor when minting (and burning) tokens which is the exchange rate of rebase to base tokens (base supply / total assets value), see GToken.factor(). The first user can manipulate this factor such that it always returns 0.

Example:

  • Attacker deposits 100.0 DAI and mints 100 * 1e18 PWRD: DepositHandler.depositGToken with dollarAmount = 100.0 = 100 * 1e18, then ctrl.mintGToken(pwrd, msg.sender, 1e18) calls gt.mint(account, gt.factor(), amount=1e18) where gt.factor() returns getInitialBase() = 1e18 because the person is the first minter and it mints amount * factor / _BASE = 1e18
  • The ctrl.mintGToken call also increases total assets: pnl.increaseGTokenLastAmount(...)
  • The attacker now burns (withdraws) all minted tokens again except a single wei using one of the withdrawal functions in WithdrawHandler. Because of the withdrawal fee the total assets are only decreased by the post-fee amount (IPnL(pnl).decreaseGTokenLastAmount(pwrd, amount=userBalance - 1, bonus=fee);), i.e., with a 2% withdrawal fee the total assets stay at 2% of 100$ = 2 * 1e18.
  • The result is that GToken.factor() always returns totalSupplyBase().mul(BASE).div(totalAssets) = 1 * 1e18 / (2 * 1e18) = 0

Impact

The resulting factor is 0 and thus any user deposits by depositGToken will mint 0 base tokens to the depositor. This means all deposits and future value accrues to the attacker who holds the only base tokens.

An attacker could even frontrun the first minter to steal their deposit this way.

Uniswap solves a similar problem by sending the first 1000 tokens to the zero address which makes the attack 1000x more expensive. The same should work here, i.e., on first mint (total base supply == 0), lock some of the first minter's tokens by minting ~1% of the initial amount to the zero address instead of to the first minter.

#0 - kitty-the-kat

2021-07-14T15:29:37Z

Known issue which will be handled by ops - low risk as gro protocol is the first depositor

#1 - ghoul-sol

2021-07-26T16:02:32Z

Even though it's a known issue its consequences are significant. Only because it can be mitigated by ops quite easily, I'll degrade it to medium level.

Findings Information

🌟 Selected for report: cmichel

Labels

bug
duplicate
1 (Low Risk)

Awards

1307.9994 USDC - $1,308.00

External Links

Handle

cmichel

Vulnerability details

Vulnerability Details

The function performs type conversions and subtraction without over-/underflow checks:

uint256 check = abs(int256(_ratio) - int256(chainRatios[i].div(CHAIN_FACTOR)));

We recommend checking if the values fit within the type range first, otherwise revert with a meaningful error message, as well as checking for underflows.

#0 - kitty-the-kat

2021-07-14T20:53:34Z

#6

#1 - ghoul-sol

2021-07-25T21:35:26Z

This is partially a duplicate of #6 but it focuses on low risk issue so I'll record is as a separate (low risk) issue.

Findings Information

🌟 Selected for report: gpersoon

Also found by: cmichel

Labels

bug
1 (Low Risk)
sponsor disputed

Awards

588.5998 USDC - $588.60

External Links

Handle

cmichel

Vulnerability details

Vulnerability Details

The Insurance.setUnderlyingTokenPercent function sets the target allocations per vault which is used throughout the code ("delta"). It does not validate that the sum of the underlyingTokensPercents is 100%, it could be higher or lower.

If the sum is less than 100% it leads to severe issues in parts of the code, for example, it's used in getVaultDeltaForDeposit to get the sorted vaultIndexes through this call exposure.sortVaultsByDelta.

However, exposure.sortVaultsByDelta has a bug with tracking min and max indexes if they don't add up to 100%, because the else branch of minDelta must never be taken.

Example bug: Assume: unifiedTotalAssets = 100k, unifiedAssets = [40k, 30k, 30k], targetPercents = [30%, 30%, 30%].

Then for i = 0: delta = 40k - 100k * 30% = 10k > 0 => sets maxDelta = 10k, maxIndex = 0 Then for i = 1: delta = 30k - 100k * 30% = 0 > 0 is false but else branch with 0 < 0 is false as well => nothing is set Then for i = 2: delta = 30k - 100k * 30% = 0 > 0 is false but else branch with 0 < 0 is false as well => nothing is set

Resulting in both maxIndex = 0, minIndex = 0 and thus vaultIndexes = [0, 3, 0] depositing into the first vault twice and completely skipping the others.

Validate that it adds up to 100%. Alternatively, fix the minDelta and maxDelta starting values (0 is bad) and computation.

#0 - kitty-the-kat

2021-07-14T19:54:51Z

low risk: Insurance.setUnderlyingTokenPercent is set by governance and will be behind a timelock similar issue in #11, which is set to low risk, which I agree with

#1 - ghoul-sol

2021-07-25T21:43:25Z

This requires human error to be an issue. While I agree that best practices recommend data validation, I'd imagine that governance TX would be checked 10 times before sending. I'm agreeing with sponsor, this is low risk.

#2 - ghoul-sol

2021-07-25T21:43:58Z

Duplicate of #11

Findings Information

🌟 Selected for report: 0xRajeev

Also found by: cmichel

Labels

bug
duplicate
1 (Low Risk)

Awards

588.5998 USDC - $588.60

External Links

Handle

cmichel

Vulnerability details

Vulnerability Details

The LogAdaptorToken and LogAdaptorVault events of BaseVaultAdaptor are not used anywhere, also not in the derived classes.

Impact

Unused code can hint at programming or architectural errors.

Use it or remove it.

#0 - kitty-the-kat

2021-07-14T20:49:19Z

#47

#1 - ghoul-sol

2021-07-25T21:58:27Z

Duplicate of #47

Findings Information

🌟 Selected for report: cmichel

Also found by: 0xRajeev

Labels

bug
1 (Low Risk)
sponsor acknowledged

Awards

588.5998 USDC - $588.60

External Links

Handle

cmichel

Vulnerability details

Vulnerability Details

Some parameters of functions are not checked for invalid values:

  • BaseVaultAdaptor.constructor: The addresses should be checked for non-zero values
  • LifeGuard3Pool.constructor: The addresses should be checked for non-zero values
  • Buoy3Pool.constructor: The addresses should be checked for non-zero values
  • PnL.constructor: The addresses should be checked for non-zero values
  • Controllable.setController: Does not check that newController != controller

Impact

A wrong user input or wallets defaulting to the zero addresses for a missing input can lead to the contract needing to redeploy or wasted gas.

Validate the parameters.

#0 - kitty-the-kat

2021-07-15T11:43:21Z

Low risk/Non critical - Deployment script handles these cases, but good practice to have 0x checks to stop wasting gas and having to redeploy.

Findings Information

🌟 Selected for report: 0xRajeev

Also found by: cmichel

Labels

bug
duplicate
1 (Low Risk)

Awards

588.5998 USDC - $588.60

External Links

Handle

cmichel

Vulnerability details

Vulnerability Details

On single deposits, the getVaultDeltaForDeposit function is supposed to return 100% allocation for the desired vault to invest in. However, it always sets 100% allocation to the first vault (index 0) because of using the wrong variable:

uint256[N_COINS] memory vaultIndexes;
// ...
// uses uninitialized vaultIndexes!
// same as investDelta[0] = 10000;
investDelta[vaultIndexes[0]] = 10000;

Impact

The investDelta also indicates that one would invest 100% in the first vault even if vaultIndexes suggests a different priority.

Use _vaultIndexes (return value of sortVaultsByDelta) instead of the uninitialized vaultIndexes.

#0 - kitty-the-kat

2021-07-14T15:48:42Z

investDelta isnt used, variable has been removed to not cause confusion

#1 - flabble-gro

2021-07-14T20:25:34Z

Duplicate of #65

#2 - ghoul-sol

2021-07-26T03:03:25Z

Indeed, investDelta is returned but not used. Duplicate of #65 so low risk.

Findings Information

🌟 Selected for report: cmichel

Labels

bug
1 (Low Risk)
disagree with severity
sponsor disputed

Awards

1307.9994 USDC - $1,308.00

External Links

Handle

cmichel

Vulnerability details

Vulnerability Details

When depositing, a referral can be chosen and the only check is:

account != address(0) && referral != address(0) && referrals[account] == address(0)

One can refer themselves

Impact

(From the contracts that are part of this repo, it's not immediately clear what the referrals are used for.) If they are used for anything, rational actors will always refer themselves to maximize profits making the referral system useless.

Whitelist big influencers that are allowed to be used as referrals to avoid everyone referring themselves or another account they control.

#0 - kitty-the-kat

2021-07-14T15:27:19Z

not an issue/non-critical Makes no difference, referrals are calculated offchain and not used for anything on chain

#1 - ghoul-sol

2021-07-26T15:50:39Z

Even if this is calculated off-chain, technically being able to refer ourselves is an issue. Even offchain this needs to be filtered out which is extra work. I'm keeping this as low risk.

Findings Information

🌟 Selected for report: cmichel

Labels

bug
G (Gas Optimization)
sponsor acknowledged

Awards

210.7126 USDC - $210.71

External Links

Handle

cmichel

Vulnerability details

Vulnerability Details

The RebasingGToken.transfer function logs LogTransfer(msg.sender, recipient, amount) but the previous super._transfer call already logs the exact same arguments Transfer(sender, recipient, amount).

The LogTransfer event is not needed for the rebasing token as it is the same as the Transfer event of the base class. Removing and not emitting LogTransfer saves gas on every transfer.

Findings Information

🌟 Selected for report: cmichel

Labels

bug
G (Gas Optimization)
sponsor confirmed

Awards

210.7126 USDC - $210.71

External Links

Handle

cmichel

Vulnerability details

Vulnerability Details

Allocaiton.calcProtocolExposureDelta should break out of the loop to save gas after protocolExposedDeltaUsd is set.

if (protocolExposedDeltaUsd == 0 && protocolExposure[i] > sysState.rebalanceThreshold) {
    // ...Calculate the delta between exposure and target
    uint256 target = sysState.rebalanceThreshold.sub(sysState.targetBuffer);
    protocolExposedDeltaUsd = protocolExposure[i].sub(target).mul(sysState.totalCurrentAssetsUsd).div(
        PERCENTAGE_DECIMAL_FACTOR
    );
    protocolExposedIndex = i;
    // @audit break here
}
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