Platform: Code4rena
Start Date: 25/08/2022
Pot Size: $75,000 USDC
Total HM: 35
Participants: 147
Period: 7 days
Judge: 0xean
Total Solo HM: 15
Id: 156
League: ETH
Rank: 40/147
Findings: 3
Award: $434.16
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: hyh
Also found by: CertoraInc, d3e4, rbserver
347.2615 DAI - $347.26
Price data can get lost due to small changes in the price in PRICE.updateMovingAverage()
.
The average os updated by adding or subtracting the delta divided by the number of observations. However, if the delta is not divisible by numObs
, the calculation here won't be accurate and some price data will be lost due to rounding errors.
if (currentPrice > earliestPrice) { _movingAverage += (currentPrice - earliestPrice) / numObs; } else { _movingAverage -= (earliestPrice - currentPrice) / numObs; }
Let's assume the moving average is a 120 day moving average sampled three times a day, which means numObs = 360
.
Also assume the current moving average is 2e18 (2 reserve tokens per ohm), and the price changes to 2.001e18
which means the delta here is 0.001e18 = 1e15
.
Now, the PRICE.updateMovingAverage()
function will calculate the average change by:
(currentPrice - earliestPrice) / numObs = (2.001e18 - 2e18) / 360 = 1e15 / 360 = 2777777777777.777...
. However, due to the operations being applied to uint
s, the actual delta is 2777777777777
and this value will be added to the moving average instead of the actual value.
Manual audit
Consider maintain the sum of the observation prices instead of the actual average, and each time the average is accessed divide it by the number of observations. That way, The delta itself will be added/subtracted from the maintained variable instead of the delta divided by the number of observations which caused to data loss.
Another possible solution is to update the average as so:
if (currentPrice > earliestPrice) { _movingAverage = (_movingAverage * numObs + (currentPrice - earliestPrice)) / numObs; } else { _movingAverage = (_movingAverage * numObs - (currentPrice - earliestPrice)) / numObs; }
#0 - Oighty
2022-09-08T18:15:57Z
See comment on #483
#1 - 0xean
2022-09-19T16:04:23Z
closing as dupe of #483
🌟 Selected for report: zzzitron
Also found by: 0x040, 0x1f8b, 0x52, 0x85102, 0xDjango, 0xNazgul, 0xNineDec, 0xSky, 0xSmartContract, 0xkatana, 8olidity, Aymen0909, Bahurum, BipinSah, Bnke0x0, CRYP70, CertoraInc, Ch_301, Chandr, Chom, CodingNameKiki, Deivitto, DimSon, Diraco, ElKu, EthLedger, Funen, GalloDaSballo, Guardian, IllIllI, JansenC, Jeiwan, Lambda, LeoS, Margaret, MasterCookie, PPrieditis, PaludoX0, Picodes, PwnPatrol, RaymondFam, ReyAdmirado, Rohan16, Rolezn, Ruhum, Sm4rty, StevenL, The_GUILD, TomJ, Tomo, Trust, Waze, __141345__, ajtra, ak1, apostle0x01, aviggiano, bin2chen, bobirichman, brgltd, c3phas, cRat1st0s, carlitox477, cccz, ch13fd357r0y3r, cloudjunky, cryptphi, csanuragjain, d3e4, datapunk, delfin454000, devtooligan, dipp, djxploit, durianSausage, eierina, enckrish, erictee, fatherOfBlocks, gogo, grGred, hansfriese, hyh, ignacio, indijanc, itsmeSTYJ, ladboy233, lukris02, martin, medikko, mics, natzuu, ne0n, nxrblsrpr, okkothejawa, oyc_109, p_crypt0, pfapostol, prasantgupta52, rajatbeladiya, rbserver, reassor, ret2basic, robee, rokinot, rvierdiiev, shenwilly, sikorico, sorrynotsorry, tnevler, tonisives, w0Lfrum, yixxas
54.3128 DAI - $54.31
The correct one:/// @dev Policy -> Keycode -> Function Selector -> bool for permission mapping(Keycode => mapping(Policy => mapping(bytes4 => bool))) public modulePermissions;
/// @dev Keycode -> Policy -> Function Selector -> bool for permission mapping(Keycode => mapping(Policy => mapping(bytes4 => bool))) public modulePermissions;
Kernel
contract, which might be a thing you'd like to do.🌟 Selected for report: pfapostol
Also found by: 0x040, 0x1f8b, 0x85102, 0xDjango, 0xNazgul, 0xNineDec, 0xSmartContract, 0xkatana, Amithuddar, Aymen0909, Bnke0x0, CertoraInc, Chandr, CodingNameKiki, Deivitto, Dionysus, Diraco, ElKu, Fitraldys, Funen, GalloDaSballo, Guardian, IllIllI, JC, JansenC, Jeiwan, LeoS, Metatron, Noah3o6, RaymondFam, ReyAdmirado, Rohan16, RoiEvenHaim, Rolezn, Ruhum, Saintcode_, Shishigami, Sm4rty, SooYa, StevenL, Tagir2003, The_GUILD, TomJ, Tomo, Waze, __141345__, ajtra, apostle0x01, aviggiano, bobirichman, brgltd, c3phas, cRat1st0s, carlitox477, cccz, ch0bu, chrisdior4, d3e4, delfin454000, djxploit, durianSausage, erictee, exolorkistis, fatherOfBlocks, gogo, grGred, hyh, ignacio, jag, karanctf, kris, ladboy233, lukris02, m_Rassska, martin, medikko, natzuu, ne0n, newfork01, oyc_109, peiw, rbserver, ret2basic, robee, rokinot, rvierdiiev, sikorico, simon135, tnevler, zishansami
32.589 DAI - $32.59
calldata
modifier instead of memory
for array, struct and dynamic arguments in order to save gas.
PRICE
- line 205RANGE
- lines 79-80BondCallback
- line 152Governance
- line 162Operator
- line 96-97PriceConfig
- line 45TreasuryCustodian
- line 53KernelUtils.ensureValidKeycode()
to save gas spent on loop index and condition operationsKernel
contract (if (!isRole[role_]) revert Kernel_RoleDoesNotExist(role_);
) is redundant because !isRole[role_] => !hasRole[addr_][role_]
Kernel._activatePolicy()
function, in order to avoid a redundant calculation, use:
instead ofactivePolicies.push(policy_); getPolicyIndex[policy_] = activePolicies.length - 1;
(This optimization can be done in more places)getPolicyIndex[policy_] = activePolicies.length; activePolicies.push(policy_);
activeProposal.proposalId
in the Governance.vote()
functionupdateMovingAverage
in the PRICE.updateMovingAverage()
!= 0
instead of > 0
when comparing a uint
with 0:
OlympusERC20 - line 611
FullMath - lines 35, 122
Governance - line 247