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: 21/147
Findings: 4
Award: $1,111.24
🌟 Selected for report: 1
🚀 Solo Findings: 0
🌟 Selected for report: rvierdiiev
Also found by: Jeiwan, Lambda, Trust, datapunk, devtooligan, itsmeSTYJ, zzzitron
113.9192 DAI - $113.92
https://github.com/code-423n4/2022-08-olympus/blob/main/src/policies/Heart.sol#L92-L109
The beat function can be called multiple times within the same block or in next blocks without triggering the Heart_OutOfCycle error. The function is expected to be called not often than once in frequency times (a related test case). However, if a beat gets delayed for a longer period or observation frequency was reduced, further beats will be able to circumvent the restriction. As a result, the caller of beat
(which can be any address) will get extra amount of reward tokens for calling beat
multiple times in a block.
Here's a Forge test case that reproduces the issue (part of HeartTest):
function testCorrectness_beatOutOfCycle() public { uint256 frequency = heart.frequency(); vm.warp(block.timestamp + frequency); uint256 startBalance = rewardToken.balanceOf(address(this)); heart.beat(); vm.warp(block.timestamp + frequency); heart.beat(); vm.warp(block.timestamp + frequency * 5); heart.beat(); heart.beat(); vm.warp(block.timestamp + 1); heart.beat(); heart.beat(); heart.beat(); uint256 endBalance = rewardToken.balanceOf(address(this)); assertEq(endBalance, startBalance + heart.reward() * 7); }
First two beats work as expected. The third beat is delayed, which sets lastBeat
to an older timestamp. This makes it possible to call beat
multiple beats in the same block or call beat
again in the next block. In the end, the caller was rewarded 7 times for calling beat
, including the out-of-cycle calls. Since calling beat
multiple times in a block triggers changes in the Price policy and the Operator contract only once (during the first call in a block), the rest calls get rewarded for doing no useful work.
Another situation than can cause the issue is when price observation frequency was reduced:
function testCorrectness_beatOutOfCycle2() public { // frequency was set to 8 hours initially uint256 frequency = heart.frequency(); vm.warp(block.timestamp + frequency); uint256 startBalance = rewardToken.balanceOf(address(this)); heart.beat(); price.changeObservationFrequency(2 hours); vm.warp(block.timestamp + frequency); heart.beat(); heart.beat(); heart.beat(); heart.beat(); uint256 endBalance = rewardToken.balanceOf(address(this)); assertEq(endBalance, startBalance + heart.reward() * 5); }
In this case, the delay is shorter and equals to the old frequency value (it can be even shorter to allow only one extra beat
). Such situation can happen when observation frequency was reduced but those who trigger beat
were not aware or notified about that (or they delayed the next call intentionally). As a result, they'll be able to get extra reward by calling beat
multiple times in a block.
In the beat
function, set lastBeat
to block.timestamp
.
#0 - Oighty
2022-09-02T17:14:14Z
Since the function has a keeper reward, we expect some competition to call this and it to be called almost immediately when available; therefore, stale beats should not accumulate. The two reasons this may not happen are 1: if the Ethereum network goes down (which we assume it won't) and 2: if the reward value is too low to compensate for the gas required. We intend to adequately incentive the function to avoid 2.
The reason to increase lastBeat
by the frequency instead of the current timestamp is to prevent the cycle times from drifting. If block.timestamp
is used, then the beats will slowly drift each time it is called a few seconds after the cycle is available.
#1 - Oighty
2022-09-07T21:22:23Z
See comment on #79
#2 - 0xean
2022-09-19T13:30:14Z
closing as dupe of #79 - downgrading severity to match.
857.4359 DAI - $857.44
https://github.com/code-423n4/2022-08-olympus/blob/main/src/Kernel.sol#L252-L253 https://github.com/code-423n4/2022-08-olympus/blob/main/src/modules/INSTR.sol#L52
After contracts are deployed and initialized, the admin address in Kernel
contract can only be set to a contract. Granting and revoking roles will be possible to do only via a contract, which looks like an unintended behavior since these operations cannot be performed via governance (the governance contract is designed to be the only executor).
Admin address can be changed to any address (EOA or contract) in the executeAction
function in Kernel
:
https://github.com/code-423n4/2022-08-olympus/blob/main/src/Kernel.sol#L252-L253
This piece explicitly allows EOA addresses since the other actions in the function (besides ChangeExecutor
) are checked to have only a contract as the target (see ensureContract
function calls in the other actions). This, and the fact that roles cannot be managed via governance, leads to the conclusion that an admin is designed to be an EOA.
However, in the store
function in INSTR
, action target can only be a contract:
https://github.com/code-423n4/2022-08-olympus/blob/main/src/modules/INSTR.sol#L52
After the contracts are deployed, INSTR
will be the only contract that's allowed to call Kernel.executeAction
:
https://github.com/code-423n4/2022-08-olympus/blob/main/src/scripts/Deploy.sol#L220
Thus, there will be no way to change admin to an EOA. If admin needs to be an EOA, the INSTR
contract needs to be patched and re-deployed to allow non-contract targets.
Allow EOA addresses as instruction targets or disallow non-contract admin addresses.
#0 - fullyallocated
2022-09-01T21:27:11Z
Nice find + writeup
🌟 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
107.303 DAI - $107.30
Finding: Constructor parameter are not validated Severity: QA Description: Constructor parameters are not validated at:
tokens_
and rangeParams_
parameters
(the former is used to set immutable variables, which cannot be modified after being set incorrectly in constructor).Finding: Misconfiguration risk due to usage of arrays to pass function arguments Severity: QA Description: The usage of arrays when passing multiple arguments to functions can cause misconfiguration issues because deployer/ user has to ensure the order of array elements is correct when calling a function. It's recommended to use parameters structures in these cases:
tokens_
and rangeParams_
. Passing tokens in a wrong order might result in an unusable contract.tokens_
and configParams
. Passing tokens in a wrong order might result in an unusable contract. configParams
contains many values that are hard to read/track in the code since they're referenced by their index in the array, not their name.Poor readability of array arguments forced developers to add the comments with argument names and their order.
An example of a parameters structure:
Finding: Poor validation of "debt buffer" arguments Severity: QA Description: These function arguments are not validated according to the documentation:
cushionDebtBuffer
at https://github.com/code-423n4/2022-08-olympus/blob/main/src/policies/Operator.sol#L97debtBuffer_
at https://github.com/code-423n4/2022-08-olympus/blob/main/src/policies/Operator.sol#L527In both of these cases, the passed value is checked to be lower than 10000
, however, in MarketParams
documentation in IBondAuctioneer:
Minimum is the greater of 10% or initial max payout as a percentage of capacity. The value must be > 10% but can exceed 100% if desired.
Also, the documentation says:
If the value is too small, the market will not be able function normally and close prematurely.
However, there's no minimal value check in the above functions.
Finding: Unused function
Severity: QA
Description:
fromRole
in KernelUtils.sol
is not used:
#0 - ind-igo
2022-09-09T04:15:10Z
good one
🌟 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.5835 DAI - $32.58
calldata
storage type instead of memory
in these cases: