Platform: Code4rena
Start Date: 21/06/2022
Pot Size: $30,000 USDC
Total HM: 12
Participants: 96
Period: 3 days
Judge: HardlyDifficult
Total Solo HM: 5
Id: 140
League: ETH
Rank: 6/96
Findings: 3
Award: $1,914.55
π Selected for report: 1
π Solo Findings: 0
π Selected for report: WatchPug
Also found by: hansfriese
631.623 USDC - $631.62
function _getTwav() internal view returns(uint256 _twav){ if (twavObservations[TWAV_BLOCK_NUMBERS - 1].timestamp != 0) { uint8 _index = ((twavObservationsIndex + TWAV_BLOCK_NUMBERS) - 1) % TWAV_BLOCK_NUMBERS; TwavObservation memory _twavObservationCurrent = twavObservations[(_index)]; TwavObservation memory _twavObservationPrev = twavObservations[(_index + 1) % TWAV_BLOCK_NUMBERS]; _twav = (_twavObservationCurrent.cumulativeValuation - _twavObservationPrev.cumulativeValuation) / (_twavObservationCurrent.timestamp - _twavObservationPrev.timestamp); } }
Since _blockTimestamp
is uint32
, subtraction underflow is desired at _twavObservationCurrent.timestamp - _twavObservationPrev.timestamp
.
See: https://github.com/Uniswap/v2-periphery/blob/master/contracts/examples/ExampleOracleSimple.sol#L43
function update() external { (uint price0Cumulative, uint price1Cumulative, uint32 blockTimestamp) = UniswapV2OracleLibrary.currentCumulativePrices(address(pair)); uint32 timeElapsed = blockTimestamp - blockTimestampLast; // overflow is desired
Because the solidity version used by the current implementation is 0.8.10
, and there are some breaking changes in Solidity v0.8.0:
Arithmetic operations revert on underflow and overflow.
Ref: https://docs.soliditylang.org/en/v0.8.13/080-breaking-changes.html#silent-changes-of-the-semantics
The timestamp subtraction may revert due to underflow.
Since _getTwav()
is used in NibblVault.sol#_rejectBuyout()
, if it reverts and there is a buyout
, an essential feature of the NibblVault
contract will be unavailable, causing users' funds to be frozen in the contract.
Change to:
function _updateTWAV(uint256 _valuation, uint32 _blockTimestamp) internal { uint32 _timeElapsed; unchecked { _timeElapsed = _blockTimestamp - lastBlockTimeStamp; } uint256 _prevCumulativeValuation = twavObservations[((twavObservationsIndex + TWAV_BLOCK_NUMBERS) - 1) % TWAV_BLOCK_NUMBERS].cumulativeValuation; unchecked { twavObservations[twavObservationsIndex] = TwavObservation(_blockTimestamp, _prevCumulativeValuation + (_valuation * _timeElapsed)); //add the previous observation to make it cumulative } twavObservationsIndex = (twavObservationsIndex + 1) % TWAV_BLOCK_NUMBERS; lastBlockTimeStamp = _blockTimestamp; }
function _getTwav() internal view returns(uint256 _twav){ if (twavObservations[TWAV_BLOCK_NUMBERS - 1].timestamp != 0) { uint8 _index = ((twavObservationsIndex + TWAV_BLOCK_NUMBERS) - 1) % TWAV_BLOCK_NUMBERS; TwavObservation memory _twavObservationCurrent = twavObservations[(_index)]; TwavObservation memory _twavObservationPrev = twavObservations[(_index + 1) % TWAV_BLOCK_NUMBERS]; unchecked { _twav = (_twavObservationCurrent.cumulativeValuation - _twavObservationPrev.cumulativeValuation) / (_twavObservationCurrent.timestamp - _twavObservationPrev.timestamp); } } }
#0 - KenzoAgada
2022-06-25T01:23:49Z
If I'm not mistaken, timestamp 4294967296 is 2106, I wouldn't call the contract breaking in 84 years a high severity issue. Plus the contract is truncating the timestamp on purpose. Seems to me more like a design choice and less of a bug.
#1 - mingwatch
2022-06-27T03:34:31Z
If I'm not mistaken, timestamp 4294967296 is 2106, I wouldn't call the contract breaking in 84 years a high severity issue. Plus the contract is truncating the timestamp on purpose. Seems to me more like a design choice and less of a bug.
https://github.com/code-423n4/2022-06-nibbl/blob/main/contracts/Twav/Twav.sol#L23-L25
function _updateTWAV(uint256 _valuation, uint32 _blockTimestamp) internal { uint32 _timeElapsed; unchecked { _timeElapsed = _blockTimestamp - lastBlockTimeStamp; }
According to the above code, is obviously not a design choice.
#2 - KenzoAgada
2022-06-27T03:52:16Z
According to the above code, is obviously not a design choice.
Ah, I think I understand what you mean, it is not handled consistently.
#3 - HardlyDifficult
2022-06-30T21:43:50Z
it is not handled consistently
Thanks for the clarifications!
_getTwav
would overflow once timestamps overflow uint32, but only when the current observation has overflowed while the previous observation did not.
The window for this vulnerability is very small, just at the time timestamp starts to overflow in 2106 - vaults active before or after that time should work as expected.
This appears to be a Medium risk finding. There's potentially a case to be made for high here but it's hard to make that call without a more complete POC included. The vault is an upgradeable contract so they have 84 years to sort this out -- but it does seem like an issue that should be fixed.
function buy(uint256 _minAmtOut, address _to) external override payable notBoughtOut lock whenNotPaused returns(uint256 _purchaseReturn) { //Make update on the first tx of the block if (status == Status.buyout) { uint32 _blockTimestamp = uint32(block.timestamp % 2**32); if (_blockTimestamp != lastBlockTimeStamp) { _updateTWAV(getCurrentValuation(), _blockTimestamp); _rejectBuyout(); } } uint256 _initialTokenSupply = initialTokenSupply; uint256 _totalSupply = totalSupply(); if (_totalSupply >= _initialTokenSupply) { _purchaseReturn = _buyPrimaryCurve(msg.value, _totalSupply); } else { uint256 _lowerCurveDiff = getMaxSecondaryCurveBalance() - secondaryReserveBalance; if (_lowerCurveDiff >= msg.value) { _purchaseReturn = _buySecondaryCurve(msg.value, _totalSupply); } else { //Gas Optimization _purchaseReturn = _initialTokenSupply - _totalSupply; secondaryReserveBalance += _lowerCurveDiff; // _purchaseReturn = _buySecondaryCurve(_to, _lowerCurveDiff); _purchaseReturn += _buyPrimaryCurve(msg.value - _lowerCurveDiff, _totalSupply + _purchaseReturn); } } require(_minAmtOut <= _purchaseReturn, "NibblVault: Return too low"); _mint(_to, _purchaseReturn); emit Buy(msg.sender, _purchaseReturn, msg.value); }
When the price is in the SecondaryCurve
, and the user can buy with msg.value == getMaxSecondaryCurveBalance() - secondaryReserveBalance + 1
to escape the CuratorFee
and AdminFee
for the part in the SecondaryCurve
of the purchase. See L320, _initialTokenSupply - _totalSupply
is the part that escaped the fees.
It appears that this is caused by an incorrect code update intended for Gas Optimization. A very similar code in the sell()
function was updated correctly: https://github.com/code-423n4/2022-06-nibbl/blob/8c3dbd6adf350f35c58b31723d42117765644110/contracts/NibblVault.sol#L373-L386
The issue will cause the protocol and the curators to lose part of their fees.
Change to:
if (_totalSupply >= _initialTokenSupply) { _purchaseReturn = _buyPrimaryCurve(msg.value, _totalSupply); } else { uint256 _lowerCurveDiff = getMaxSecondaryCurveBalance() - secondaryReserveBalance; if (_lowerCurveDiff >= msg.value) { _purchaseReturn = _buySecondaryCurve(msg.value, _totalSupply); } else { //Gas Optimization _purchaseReturn = _initialTokenSupply - _totalSupply; secondaryReserveBalance += _lowerCurveDiff; _purchaseReturn = _chargeFee(_purchaseReturn); // _purchaseReturn = _buySecondaryCurve(_lowerCurveDiff, _totalSupply); _purchaseReturn += _buyPrimaryCurve(msg.value - _lowerCurveDiff, _totalSupply + _purchaseReturn); } }
#0 - mundhrakeshav
2022-06-26T10:16:04Z
#173
π Selected for report: peritoflores
Also found by: WatchPug
1052.705 USDC - $1,052.70
function _updateTWAV(uint256 _valuation, uint32 _blockTimestamp) internal { uint32 _timeElapsed; unchecked { _timeElapsed = _blockTimestamp - lastBlockTimeStamp; } uint256 _prevCumulativeValuation = twavObservations[((twavObservationsIndex + TWAV_BLOCK_NUMBERS) - 1) % TWAV_BLOCK_NUMBERS].cumulativeValuation; twavObservations[twavObservationsIndex] = TwavObservation(_blockTimestamp, _prevCumulativeValuation + (_valuation * _timeElapsed)); //add the previous observation to make it cumulative twavObservationsIndex = (twavObservationsIndex + 1) % TWAV_BLOCK_NUMBERS; lastBlockTimeStamp = _blockTimestamp; }
Because the solidity version used by the current implementation is 0.8.10
, and there are some breaking changes in Solidity v0.8.0:
Arithmetic operations revert on underflow and overflow.
Ref: https://docs.soliditylang.org/en/v0.8.13/080-breaking-changes.html#silent-changes-of-the-semantics
When _prevCumulativeValuation
is big enough, _updateTWAV()
will revert due to overflow.
Since _updateTWAV()
is used in NibblVault.sol#buy()
and NibblVault.sol#sell()
, when it reverts due to overflow, the NibblVault
contract as a whole will malfunction, and users' funds may be frozen in the contract.
Change to:
function _updateTWAV(uint256 _valuation, uint32 _blockTimestamp) internal { uint32 _timeElapsed; unchecked { _timeElapsed = _blockTimestamp - lastBlockTimeStamp; } uint256 _prevCumulativeValuation = twavObservations[((twavObservationsIndex + TWAV_BLOCK_NUMBERS) - 1) % TWAV_BLOCK_NUMBERS].cumulativeValuation; unchecked { twavObservations[twavObservationsIndex] = TwavObservation(_blockTimestamp, _prevCumulativeValuation + (_valuation * _timeElapsed)); //add the previous observation to make it cumulative } twavObservationsIndex = (twavObservationsIndex + 1) % TWAV_BLOCK_NUMBERS; lastBlockTimeStamp = _blockTimestamp; }
#0 - HardlyDifficult
2022-07-03T12:29:51Z