Platform: Code4rena
Start Date: 16/02/2023
Pot Size: $144,750 USDC
Total HM: 17
Participants: 154
Period: 19 days
Judge: Trust
Total Solo HM: 5
Id: 216
League: ETH
Rank: 26/154
Findings: 2
Award: $504.79
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: peakbolt
Also found by: 0xTheC0der, 0xbepresent, 0xsomeone, codeislight, trustindistrust
443.5294 USDC - $443.53
https://github.com/code-423n4/2023-02-ethos/blob/73687f32b934c9d697b97745356cdf8a1f264955/Ethos-Vault/contracts/abstract/ReaperBaseStrategyv4.sol#L156-L160 https://github.com/code-423n4/2023-02-ethos/blob/73687f32b934c9d697b97745356cdf8a1f264955/Ethos-Vault/contracts/ReaperVaultV2.sol#L205-L217 https://github.com/code-423n4/2023-02-ethos/blob/73687f32b934c9d697b97745356cdf8a1f264955/Ethos-Vault/contracts/ReaperVaultV2.sol#L191-L199 https://github.com/code-423n4/2023-02-ethos/blob/73687f32b934c9d697b97745356cdf8a1f264955/Ethos-Vault/contracts/ReaperVaultV2.sol#L227-L231 https://github.com/code-423n4/2023-02-ethos/blob/73687f32b934c9d697b97745356cdf8a1f264955/Ethos-Vault/test/starter-test.js#L1066-L1082
An emergency exit of a strategy can only be triggered with guardian or higher privileges. As a result all funds of the strategy are deposited into the vault on the next harvest.
However, strategist privileges (lower than guardian) are sufficient to reduce the deposit amout such that up to 1/9 of the strategy funds are still left in the strategy contract after harvest.
Strategy.setEmergencyExit() (called by guardian) calls Vault.revokeStrategy() which sets the allocBPS
of the strategy to zero. This is necessary to make Vault.availableCapital() return a value <= 0 for the strategy such that all strategy funds are deposited into the vault when calling Strategy.harvest().
However, Vault.updateStrategyAllocBPS() (called by strategist) allows to restore a non-zero value of allocBPS
. As a result, stratMaxAllocation
can become > stratCurrentAllocation
in Vault.availableCapital() which therefore returns a value > 0. Consequently, a following call to Strategy.harvest() will not deposit all funds into the vault. Depending on allocBPS
and totalAllocBPS
(depends on number of strategies) up to 1/9 of funds are left in the strategy contract after emergency exit.
It should not be possible in any case that the emergency exit amout is reduced using lower privileges (strategist) than initially required (guardian) to trigger the emergency exit.
Assets are not at direct risk since they are still within the protocol, but the function of the protocol or its availability is impacted because the vault contract does not receive the expected funds in case of emergency.
According to Severity Categorization: 2 — Med
Side quest:
Fix test cases "Emergency scenarios" by replacing L1063 and L1081 with expect(stratBalance).to.be.eq(toWantUnit('0'));
such that an empty strategy balance is required after emergency shutdown/exit. However, this is just for "beauty" since in case of an incomplete emergency harvest, the test cases already fail a few lines above when checking the vault balance, see "Main quest".
Main quest:
Fix test case "Strategy should handle emergency exit" by replacing L1076 with await strategy.setEmergencyExit();
. Because at the moment this test case is completely the same as "Vault should handle emergency shutdown".
Provoke the bug by adding await vault.connect(strategist).updateStrategyAllocBPS(strategy.address, 10000);
after L1076.
Run this test case and see that the vault does not receive the expected funds: AssertionError: expected 900000026 to be at least 1000000000.
Optional: Print the remaining balance of the strategy contract to see that it's > 0, i.e. emergency exit is incomplete.
Test diff:
diff --git a/starter-test.js.orig b/starter-test.js index 4069176..5a8539f 100644 --- a/starter-test.js.orig +++ b/starter-test.js @@ -1060,10 +1060,10 @@ describe('Vaults', function () { vaultBalance = await want.balanceOf(vault.address); expect(vaultBalance).to.be.gte(toWantUnit('10')); stratBalance = await strategy.balanceOf(); - expect(stratBalance).to.be.gte(toWantUnit('0')); + expect(stratBalance).to.be.eq(toWantUnit('0')); }); - it('Strategy should handle emergency exit', async function () { + it('Strategy should handle emergency exit (strategist should not be able to interfere)', async function () { const {vault, strategy, want, wantHolder} = await loadFixture(deployVaultAndStrategyAndGetSigners); await vault.connect(wantHolder)['deposit(uint256)'](toWantUnit('10')); await strategy.harvest(); @@ -1073,12 +1073,13 @@ describe('Vaults', function () { let stratBalance = await strategy.balanceOf(); expect(stratBalance).to.be.gte(toWantUnit('9')); - await vault.setEmergencyShutdown(true); + await strategy.setEmergencyExit(); + await vault.connect(strategist).updateStrategyAllocBPS(strategy.address, 10000);
VS Code, Hardhat
Flag a strategy as revoked
in Vault.revokeStrategy() and therefore do not allow subsequent changes of allocBPS
in Vault.updateStrategyAllocBPS() by checking for this flag. A guardian (or higher) should have the privilege to clear the revoked
flag, or clear it automatically during emergency harvest.
#0 - c4-judge
2023-03-10T14:34:49Z
trust1995 marked the issue as duplicate of #332
#1 - c4-judge
2023-03-10T14:34:54Z
trust1995 marked the issue as satisfactory
🌟 Selected for report: GalloDaSballo
Also found by: 0x3b, 0xAgro, 0xSmartContract, 0xTheC0der, 0xackermann, 0xnev, 0xsomeone, ABA, BRONZEDISC, Bjorn_bug, Bnke0x0, Breeje, Co0nan, CodeFoxInc, CodingNameKiki, DadeKuma, DeFiHackLabs, IceBear, Josiah, Kaysoft, Lavishq, MohammedRizwan, PaludoX0, PawelK, Phantasmagoria, Raiders, RaymondFam, Rickard, Rolezn, Sathish9098, SleepingBugs, SuperRayss, UdarTeam, Udsen, Viktor_Cortess, arialblack14, ast3ros, bin2chen, brgltd, btk, catellatech, ch0bu, chaduke, chrisdior4, codeislight, cryptonue, delfin454000, descharre, dontonka, emmac002, fs0c, hacker-dom, hansfriese, imare, lukris02, luxartvinsec, martin, matrix_0wl, peanuts, rbserver, shark, tnevler, trustindistrust, tsvetanovv, vagrant, yongskiws, zzzitron
61.2601 USDC - $61.26
Require that the addresses in _strategists[]
and _multisigRoles[]
in ReaperBaseStrategyv4 .__ReaperBaseStrategy_init as well as ReaperVaultV2.constructor are all different from each other.
According to the NatSpect comment of ReaperVaultV2.getPricePerFullShare, the functions returns an uint256 with 18 decimals. However, the code shows that the underlying token's decimals are mirrored which might be != 18. This function is not called by any other contract under scope, but an external contract might be at risk when relying on the wrong comment.
ReaperStrategyGranarySupplyOnly
contractThe upgradeable contract ReaperStrategyGranarySupplyOnly
inherits from the non-stateless contract ReaperBaseStrategyv4
which does not contain a storage gap (e.g. uint256[50] private __gap;
) after its storage variables. Therfore, adding another storage variable to ReaperBaseStrategyv4
will shift down the storage in the inheritance chain. This will break the ReaperStrategyGranarySupplyOnly
contract in case of an upgrade. Consider adding a gap to ReaperBaseStrategyv4
in order to reserve space for future storage variables.
The vault DEPOSITOR role is not granted to anyone within the whole codebase. I suppose the role is granted on demand by the default admin after deployment. However, you might also want to handle this within ReaperVaultV2.constructor like you do with the other roles.
StabilityPool
contractThere are two ToDo's (#1 and #2) about unused local variables in the StabilityPool
contract.
#0 - c4-judge
2023-03-09T15:29:27Z
trust1995 marked the issue as grade-b
#1 - c4-sponsor
2023-03-28T20:11:10Z
0xBebis marked the issue as sponsor acknowledged