Olympus DAO contest - Jeiwan's results

Version 3 of Olympus protocol, a decentralized floating currency.

General Information

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

Olympus DAO

Findings Distribution

Researcher Performance

Rank: 21/147

Findings: 4

Award: $1,111.24

🌟 Selected for report: 1

🚀 Solo Findings: 0

Findings Information

🌟 Selected for report: rvierdiiev

Also found by: Jeiwan, Lambda, Trust, datapunk, devtooligan, itsmeSTYJ, zzzitron

Labels

bug
duplicate
2 (Med Risk)
disagree with severity
edited-by-warden

Awards

113.9192 DAI - $113.92

External Links

Lines of code

https://github.com/code-423n4/2022-08-olympus/blob/main/src/policies/Heart.sol#L92-L109

Vulnerability details

Impact

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.

Proof of Concept

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.

Tools Used

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.

Findings Information

🌟 Selected for report: Jeiwan

Also found by: datapunk

Labels

bug
2 (Med Risk)
sponsor confirmed

Awards

857.4359 DAI - $857.44

External Links

Lines of code

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

Vulnerability details

Impact

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).

Proof of Concept

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.

Tools Used

Allow EOA addresses as instruction targets or disallow non-contract admin addresses.

#0 - fullyallocated

2022-09-01T21:27:11Z

Nice find + writeup

#0 - ind-igo

2022-09-09T04:15:10Z

good one

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax © 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter