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: 12/96
Findings: 2
Award: $1,092.51
π Selected for report: 0
π Solo Findings: 0
π Selected for report: xiaoming90
Also found by: hyh
1052.705 USDC - $1,052.70
A malicious user can run updateTWAV() on each block, quickly replacing all four values of the twavObservations array with the most recent valuation. I.e. the time weighted averaging essence of the recorded price can be directly reduced to always be just most recent price by any actor who can extract profit from such an update.
I.e. when there are no one around with any interests in price manipulation, TWAP array will hold an average over some random last period as TWAP is updated on user operations. Once there be an actor interested for the price to be manipulated, the price cease to being TWAP as the twavObservations array will be filled with the most recent entry, so the price becomes simply the spot one, which can be changed quickly to extract a profit.
Setting the severity to be high as that's a violation of system logic of price smoothing, which can lead to altering of the system's behavior, say last minute buyout. I.e. a whale can buy some Vault tokens, wait for the very end of buyout price discovery period, quickly sell his tokens and force price update by repeating updateTWAV() call, winning the bid as the reduced price allows the buyout. Token holders will not have the time to react in this case, i.e. no real price discovery will take place.
updateTWAV() can be called by anyone and only requires different blocks to proceed:
/// @notice Updates the TWAV when in buyout /// @dev TWAV can be updated only in buyout state function updateTWAV() external override { require(status == Status.buyout, "NibblVault: Status!=Buyout"); uint32 _blockTimestamp = uint32(block.timestamp % 2**32); if (_blockTimestamp != lastBlockTimeStamp) { _updateTWAV(getCurrentValuation(), _blockTimestamp); _rejectBuyout(); //For the case when TWAV goes up when updated externally } }
It isnβt the main issue as other operations can be used for forced price update if the access to updateTWAV() be restricted. It, however, add some convenience to the attack.
The core issue is that TWAV array update happens unconditionally, always replacing the oldest value, i.e. there are no mechanics to prevent price weighting to be reduced to become non-existent:
/// @notice updates twavObservations array /// @param _blockTimestamp timestamp of the block /// @param _valuation current valuation 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; }
Consider introducing a delay before adding the entry to TWAV array, say require that MIN_TWAV_LENGTH of time have to be in the array.
For example, if last entry removal on price update causes (earliest - oldest) time to drop below MIN_TWAV_LENGTH then refuse the update. This will enforce the averaging at least over MIN_TWAV_LENGTH period, which should be big enough, say 2-3 days, so token holders have the necessary time to act on the valuation change.
#0 - HardlyDifficult
2022-07-02T20:41:19Z
This is a good issue to raise and an interesting recommended solution.
It does break the goal of price smoothing. It's not clear that this report demonstrates this to be a high risk report. It seems to qualify more as Medium: the warden's comment "altering of the system's behavior, say last minute buyout" fits within the definition "Assets not at direct risk, but the function of the protocol or its availability could be impacted."
#1 - HardlyDifficult
2022-07-03T12:53:34Z
#2 - dmitriia
2022-07-04T23:19:27Z
Strongly disagree here, the report shows that the base system functionality can be manipulated. It is not mere function unavailability. This is high risk severity.
#3 - HardlyDifficult
2022-07-04T23:29:44Z
Strongly disagree here, the report shows that the base system functionality can be manipulated. It is not mere function unavailability. This is high risk severity.
This was also shared on the primary report https://github.com/code-423n4/2022-06-nibbl-findings/issues/191 -- let's continue the discussion there.
π Selected for report: IllIllI
Also found by: 0x1f8b, 0x29A, 0x52, 0xNazgul, 0xNineDec, 0xc0ffEE, 0xf15ers, 0xkatana, BowTiedWardens, Chom, ElKu, Funen, GalloDaSballo, JC, JMukesh, JohnSmith, Lambda, Limbooo, MadWookie, MiloTruck, Nethermind, Noah3o6, Nyamcil, Picodes, PwnedNoMore, Randyyy, RoiEvenHaim, SmartSek, StErMi, Tadashi, TerrierLover, TomJ, Tomio, Treasure-Seeker, UnusualTurtle, Varun_Verma, Wayne, Waze, _Adam, apostle0x01, asutorufos, berndartmueller, c3phas, catchup, cccz, cloudjunky, codexploder, cryptphi, defsec, delfin454000, dipp, ellahi, exd0tpy, fatherOfBlocks, hansfriese, hyh, joestakey, kebabsec, kenta, masterchief, minhquanym, naps62, oyc_109, pashov, peritoflores, reassor, rfa, robee, sach1r0, saian, sashik_eth, shenwilly, simon135, slywaters, sorrynotsorry, sseefried, unforgiven, xiaoming90, ych18, zuhaibmohd, zzzitron
39.8094 USDC - $39.81
ecrecover should be checked to be non-zero as zero return value means invalid signature:
address signer = ecrecover(toTypedMessageHash(structHash), v, r, s); require(signer == owner, "NibblVault: invalid signature"); _approve(owner, spender, value); }
Consider adding, for example, require(owner != address(0), "owner cannot be zero")
to NibblVault's permit().
Even if _approve(0x0, ...
doesn't seem to cause much issues, the ability to run it is a malfunction.
As different compiler versions have critical behavior specifics if the contracts get accidentally deployed using another compiler version compared to the one they were tested with, various types of undesired behavior can be introduced
pragma solidity ^0.8.0;
pragma solidity ^0.8.0;
Consider fixing AccessControlMechanism's version to 0.8.10
On calling with arrays of different lengths various malfunctions are possible as the arrays are used as given. System then will fail with low level array access message.
Array lengths aren't controlled:
function withdrawMultipleERC1155(address[] memory _tokens, uint256[] memory _tokenIds, address _to) external override { require(_isApprovedOrOwner(msg.sender, 0), "withdraw:not allowed"); for (uint256 i = 0; i < _tokens.length; i++) {
function withdrawMultipleERC1155(address[] memory _assets, uint256[] memory _assetIDs, address _to) external override boughtOut { require(msg.sender == bidder, "NibblVault: Only winner"); for (uint256 i = 0; i < _assets.length; i++) { uint256 balance = IERC1155(_assets[i]).balanceOf(address(this), _assetIDs[i]);
Consider requiring lengths of _tokens and _tokenIds arrays to match.
#0 - HardlyDifficult
2022-07-02T21:52:18Z
#1 - HardlyDifficult
2022-07-03T22:52:38Z
#2 - HardlyDifficult
2022-07-03T23:24:23Z
#3 - HardlyDifficult
2022-07-04T16:10:40Z
#4 - HardlyDifficult
2022-07-04T16:11:38Z
Good considerations, particularly in the merged reports.