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
Rank: 3/7
Findings: 5
Award: $21,625.53
🌟 Selected for report: 10
🚀 Solo Findings: 0
5885.9975 USDC - $5,886.00
gpersoon
There are a few underflows that are converted via a typecast afterwards to the expected value. If solidity 0.8.x would be used, then the code would revert. int256(a-b) where a and b are uint, For example if a=1 and b=2 then the intermediate result would be uint(-1) == 2256-1 int256(-x) where x is a uint. For example if x=1 then the intermediate result would be uint(-1) == 2256-1 Its better not to have underflows by using the appropriate typecasts. This is especially relevant when moving to solidity 0.8.x
// https://github.com/code-423n4/2021-06-gro/blob/main/contracts/insurance/Exposure.sol#L178 function sortVaultsByDelta(..) .. for (uint256 i = 0; i < N_COINS; i++) { // Get difference between vault current assets and vault target int256 delta = int256(unifiedAssets[i] - unifiedTotalAssets.mul(targetPercents[i]).div(PERCENTAGE_DECIMAL_FACTOR)); // underflow in intermediate result
//https://github.com/code-423n4/2021-06-gro/blob/main/contracts/pnl/PnL.sol#L112 function decreaseGTokenLastAmount(bool pwrd, uint256 dollarAmount, uint256 bonus)... .. emit LogNewGtokenChange(pwrd, int256(-dollarAmount)); // underflow in intermediate result
// https://github.com/code-423n4/2021-06-gro/blob/main/contracts/pools/oracle/Buoy3Pool.sol#L87 function safetyCheck() external view override returns (bool) { ... _ratio = abs(int256(_ratio - lastRatio[i])); // underflow in intermediate result
replace int256(a-b) with int256(a)-int256(b) replace int256(-x) with -int256(x)
#0 - ghoul-sol
2021-07-25T21:32:16Z
Majority of overflow listed above seems low risk with one exception of safetyCheck
. Underflow is a real risk here.safetyCheck
is run every time a deposit is made. Ratios can change and the change does not need to be substantial for it to overflow. For that reason it's a high risk.
3531.5985 USDC - $3,531.60
gpersoon
The function distributeStrategyGainLoss does the following check to allow access to the function: require(index > 0 || index <= N_COINS + 1, "!VaultAdaptor"); However the expression index > 0 || index <= N_COINS + 1 is always TRUE, because the OR (||) is used (should have been AND &&)
This means the function can be executed from any originator. uint256 index = vaultIndexes[msg.sender]; ==> index will be 0 index is smaller than N_COINS + 1, so the require will not stop access. Also index = index - 1; will not pose an problems, because Safemath isn't used and the solidity version is lower than 8. index will be a very 2**256-1; so the rest of function can be executed without problem, disturbing the profit & loss calculation and thus disturbing the values of the tokens.
// https://github.com/code-423n4/2021-06-gro/blob/main/contracts/Controller.sol#L355 function distributeStrategyGainLoss(uint256 gain, uint256 loss) external override { uint256 index = vaultIndexes[msg.sender]; require(index > 0 || index <= N_COINS + 1, "!VaultAdaptor"); .. index = index - 1; if (index < N_COINS) { ... } else { if (gain > 0) { gainUsd = ibuoy.lpToUsd(gain); } else if (loss > 0) { lossUsd = ibuoy.lpToUsd(loss); } } ipnl.distributeStrategyGainLoss(gainUsd, lossUsd, reward); // Check if curve spot price within tollerance, if so update them if (ibuoy.updateRatios()) { // If the curve ratios were successfully updated, realize system price changes ipnl.distributePriceChange(_totalAssets()); } }
Change || to && Use safemath for index = index - 1;
#0 - flabble-gro
2021-07-14T20:19:05Z
Duplicate of issue #69
#1 - ghoul-sol
2021-07-26T03:47:24Z
Duplicate of #69
5885.9975 USDC - $5,886.00
gpersoon
The function sortVaultsByDelta doesn't always work as expected. Suppose all the delta's are positive, and delta1 >= delta2 >= delta3 > 0 Then maxIndex = 0 And (delta < minDelta (==0) ) is never true, so minIndex = 0
Then (assuming bigFirst==true): vaultIndexes[0] = maxIndex = 0 vaultIndexes[2] = minIndex = 0 vaultIndexes[1] = N_COINS - maxIndex - minIndex = 3-0-0 = 3
This is clearly not what is wanted, all vaultIndexes should be different and should be in the range [0..2] This is due to the fact that maxDelta and minDelta are initialized with the value 0. This all could results in withdrawing from the wrong vaults and reverts (because vaultIndexes[1] is out of range).
// https://github.com/code-423n4/2021-06-gro/blob/main/contracts/insurance/Exposure.sol#L178 function sortVaultsByDelta(bool bigFirst,uint256 unifiedTotalAssets,uint256[N_COINS] calldata unifiedAssets,uint256[N_COINS] calldata targetPercents) external pure override returns (uint256[N_COINS] memory vaultIndexes) { uint256 maxIndex; uint256 minIndex; int256 maxDelta; int256 minDelta; for (uint256 i = 0; i < N_COINS; i++) { // Get difference between vault current assets and vault target int256 delta = int256( unifiedAssets[i] - unifiedTotalAssets.mul(targetPercents[i]).div(PERCENTAGE_DECIMAL_FACTOR) ); // Establish order if (delta > maxDelta) { maxDelta = delta; maxIndex = i; } else if (delta < minDelta) { minDelta = delta; minIndex = i; } } if (bigFirst) { vaultIndexes[0] = maxIndex; vaultIndexes[2] = minIndex; } else { vaultIndexes[0] = minIndex; vaultIndexes[2] = maxIndex; } vaultIndexes[1] = N_COINS - maxIndex - minIndex; }
Initialize maxDelta and minDelta: int256 maxDelta = -2255; // or type(int256).min when using a newer solidity version int256 minDelta = 2255; // or type(int256).max when using a newer solidity version Check maxIndex and minIndex are not the same require (maxIndex != minIndex);
#0 - kitty-the-kat
2021-07-14T14:42:54Z
disagree with severity - (Low Risk) Only a problem when stable coin percent sum is less than 100%, which is set by gov.
#1 - ghoul-sol
2021-07-26T14:37:19Z
I'm not sure how sum of stable coin percentage being set to 100% avoids this issue. Scenario above looks valid to me.
588.5998 USDC - $588.60
gpersoon
The function setWithdrawHandler allows the setting of withdrawHandler and emergencyHandler. However emergencyHandler isn't checked for 0 (like the withdrawHandler ) The value of the emergencyHandler is also not emitted (like the withdrawHandler )
// https://github.com/code-423n4/2021-06-gro/blob/main/contracts/Controller.sol#L105 function setWithdrawHandler(address _withdrawHandler, address _emergencyHandler) external onlyOwner { require(_withdrawHandler != address(0), "setWithdrawHandler: 0x"); withdrawHandler = _withdrawHandler; emergencyHandler = _emergencyHandler; emit LogNewWithdrawHandler(_withdrawHandler); }
Add something like: require(_emergencyHandler!= address(0), "setEmergencyHandler: 0x"); event LogNewEmergencyHandler(address tokens); emit LogNewEmergencyHandler(_emergencyHandler);
🌟 Selected for report: gpersoon
1307.9994 USDC - $1,308.00
gpersoon
The values of lastRatio in the contract Buoy3Pool are not initialized (thus they have a value of 0). If safetyCheck() would be called before the first time _updateRatios is called, then safetyCheck() would give unexpected results.
// https://github.com/code-423n4/2021-06-gro/blob/main/contracts/pools/oracle/Buoy3Pool.sol#L25 contract Buoy3Pool is FixedStablecoins, Controllable, IBuoy, IChainPrice { ... mapping(uint256 => uint256) lastRatio;
function safetyCheck() external view override returns (bool) { for (uint256 i = 1; i < N_COINS; i++) { uint256 _ratio = curvePool.get_dy(int128(0), int128(i), getDecimal(0)); _ratio = abs(int256(_ratio - lastRatio[i])); if (_ratio.mul(PERCENTAGE_DECIMAL_FACTOR).div(CURVE_RATIO_DECIMALS_FACTOR) > BASIS_POINTS) { return false; } } return true; }
function _updateRatios(uint256 tolerance) private returns (bool) { ... for (uint256 i = 1; i < N_COINS; i++) { lastRatio[i] = newRatios[i];
Double check if this situation can occur. Perhaps call _updateRatios as soon as possible. Or check in safetyCheck that the lastRatio values are initialized
gpersoon
The function setUnderlyingTokenPercent doesn't check that the sum of all the percentages is 100%. This way the percentages could be accidentally set up the wrong way, with unpredictable results.
Note: the function can only be called by controller or the owner so the likely hood of mistakes is pretty low.
//https://github.com/code-423n4/2021-06-gro/blob/main/contracts/insurance/Insurance.sol#L100 function setUnderlyingTokenPercent(uint256 coinIndex, uint256 percent) external override onlyValidIndex(coinIndex) { require(msg.sender == controller || msg.sender == owner(), "setUnderlyingTokenPercent: !authorized"); underlyingTokensPercents[coinIndex] = percent; emit LogNewTargetAllocation(coinIndex, percent); }
Change setUnderlyingTokenPercent to set the percentages for all the coins at the same time. And check that the sum of the percentages is 100%
#0 - flabble-gro
2021-07-14T20:19:57Z
Duplicate of #100
🌟 Selected for report: gpersoon
1307.9994 USDC - $1,308.00
gpersoon
Safemath is used on several places but not everywhere. Especially on risky places like PnL and distributeStrategyGainLoss it is hardly worth the gas-savings of not using safemath.
In distributeStrategyGainLoss it does make a difference, also due to another issue.
https://github.com/code-423n4/2021-06-gro/blob/main/contracts/pnl/PnL.sol#L215 function handleLoss( uint256 gvtAssets, uint256 pwrdAssets, uint256 loss) private pure returns (uint256, uint256) { uint256 maxGvtLoss = gvtAssets.sub(DEFAULT_DECIMALS_FACTOR); if (loss > maxGvtLoss) { ... } else { gvtAssets = gvtAssets - loss; // won't underflow but safemath won't hurt }
function forceDistribute() private { uint256 total = _controller().totalAssets(); if (total > lastPwrdAssets.add(DEFAULT_DECIMALS_FACTOR)) { lastGvtAssets = total - lastPwrdAssets; // won't underflow but safemath won't hurt } else {
...
//https://github.com/code-423n4/2021-06-gro/blob/main/contracts/Controller.sol#L355 function distributeStrategyGainLoss(uint256 gain, uint256 loss) external override { uint256 index = vaultIndexes[msg.sender]; require(index > 0 || index <= N_COINS + 1, "!VaultAdaptor"); // always true, see separate issue .. index = index - 1; // can underflow
Apply safemath or move to Solidity 0.8.x
gpersoon
The contract BaseVaultAdaptor contains the function migrate which can be used to move all the ERC20 tokens to another contract. This can be seen as a rug pull Even if this is well intended the project could still be called out, see for example: https://twitter.com/RugDocIO/status/1408097542202531840
// https://github.com/code-423n4/2021-06-gro/blob/main/contracts/vaults/BaseVaultAdaptor.sol#L296 function migrate(address child) external onlyOwner { require(child != address(0), "migrate: child == 0x"); IERC20 _token = IERC20(token); uint256 balance = _token.balanceOf(address(this)); _token.safeTransfer(child, balance); emit LogMigrate(address(this), child, balance); }
Remove the code or apply extra safeguards
#0 - flabble-gro
2021-07-14T20:20:51Z
Duplicate of #59
#1 - ghoul-sol
2021-07-26T14:42:13Z
Duplicate of #59 so low risk.
🌟 Selected for report: gpersoon
210.7126 USDC - $210.71
gpersoon
In several functions of BaseVaultAdaptor a value is stored in the variable amount at the end of the function. However this variable is never used afterwards so the storage is unnecessary and just uses gas.
// https://github.com/code-423n4/2021-06-gro/blob/main/contracts/vaults/BaseVaultAdaptor.sol#L165 function withdraw(uint256 amount) external override { .. if (!_withdrawFromAdapter(amount, msg.sender)) { amount = _withdraw(calculateShare(amount), msg.sender);
function withdraw(uint256 amount, address recipient) external override { ... if (!_withdrawFromAdapter(amount, recipient)) { amount = _withdraw(calculateShare(amount), recipient); function withdrawToAdapter(uint256 amount) external onlyOwner { amount = _withdraw(calculateShare(amount), address(this)); } function withdrawByStrategyOrder(
.. if (!_withdrawFromAdapter(amount, recipient)) { amount = _withdrawByStrategyOrder(calculateShare(amount), recipient, reversed);
function withdrawByStrategyIndex(
... if (!_withdrawFromAdapter(amount, recipient)) { amount = _withdrawByStrategyIndex(calculateShare(amount), recipient, strategyIndex);
Replace amount = _withdraw***(...); with _withdraw***(...);
🌟 Selected for report: gpersoon
210.7126 USDC - $210.71
gpersoon
calcProtocolExposureDelta should probably stop executing once it has found the first occurrence where exposure > threshold. (as is also indicated in the comment).
The current code also works (due to the check protocolExposedDeltaUsd == 0), however inserting a break statement at the end of the "if" is more logical and saves a bit of gas.
//https://github.com/code-423n4/2021-06-gro/blob/main/contracts/insurance/Allocation.sol#L286
/// By defenition, only one protocol can exceed exposure in the current setup.
...
function calcProtocolExposureDelta(uint256[] memory protocolExposure, SystemState memory sysState) private pure
returns (uint256 protocolExposedDeltaUsd, uint256 protocolExposedIndex)
{
for (uint256 i = 0; i < protocolExposure.length; i++) {
// If the exposure is greater than the rebalance threshold...
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;
// probably put a break here
}
}
}
Add a break statement at the end of the if
🌟 Selected for report: gpersoon
210.7126 USDC - $210.71
gpersoon
In the function applyFactor of GToken.sol there seems to be an attempt on gas optimization by doing: uint256 _BASE = BASE;
However because BASE is a constant it is cheaper not to do this and just use BASE.
A minor gas optimization is possible by storing a.mul(b) or a.mul(_BASE) in temporary variable because it is evaluated twice.
https://github.com/code-423n4/2021-06-gro/blob/main/contracts/tokens/GToken.sol uint256 public constant BASE = DEFAULT_DECIMALS_FACTOR; function applyFactor(uint256 a,uint256 b, bool base ) internal pure returns (uint256 resultant) { uint256 _BASE = BASE; uint256 diff; if (base) { diff = a.mul(b) % _BASE; resultant = a.mul(b).div(_BASE); } else { diff = a.mul(_BASE) % b; resultant = a.mul(_BASE).div(b); } if (diff >= 5E17) { resultant = resultant.add(1); } }
Use BASE instead of _BASE
Perhaps use the following optimization: if (base) { uint256 tmp=a.mul(b) ; diff = tmp % _BASE; resultant = tmp.div(_BASE); } else { uint256tmp=a.mul(_BASE) ; diff = tmp % b; resultant = tmp.div(b); }
#0 - kitty-the-kat
2021-07-14T17:12:04Z
gtoken contracts already deployed
🌟 Selected for report: gpersoon
1307.9994 USDC - $1,308.00
gpersoon
The parameters maxPercentForWithdraw and maxPercentForDeposit are not directly initialized. If functions, which rely on these parameters, are called, before setWhaleThresholdWithdraw/setWhaleThresholdDeposit, then they will work in a suboptimal way.
// https://github.com/code-423n4/2021-06-gro/blob/main/contracts/insurance/Insurance.sol#L63 uint256 public maxPercentForWithdraw; uint256 public maxPercentForDeposit;
function setWhaleThresholdWithdraw(uint256 _maxPercentForWithdraw) external onlyOwner { maxPercentForWithdraw = _maxPercentForWithdraw; emit LogNewVaultMax(false, _maxPercentForWithdraw); } function setWhaleThresholdDeposit(uint256 _maxPercentForDeposit) external onlyOwner { maxPercentForDeposit = _maxPercentForDeposit; emit LogNewVaultMax(true, _maxPercentForDeposit); }
Assign a default value to maxPercentForWithdraw and maxPercentForDeposit or initialize the values via the constructor.
#0 - kitty-the-kat
2021-07-14T17:05:49Z
This is known and values are updated as part of deployment scripts