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: 101/154
Findings: 1
Award: $61.26
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 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
There is a loss of precision in calculating rewardPerSecond
. This is due to rewardPerSecond
being assigned the result of the lossy division of distributionPeriod
from amount
(line 112). This loss in precision results in a growing deviation between expected and actual issuance of $Oath tokens to the stability pool.
The test case on line 170 passes when it shouldn’t due to improper usage of arithmetic operators and type conversion on BigNumber variables, resulting in the test case comparing the integers 4 and 3 (line 181) rather than their intended values. When corrected, this test case does not pass with the current implementation of fund()
in CommunityIssuance.sol due to loss of precision with calculating rewardPerSecond
. The corrected test case fails with AssertionError: expected 2255118 [Wei] to be at most 1000 [Wei]
.
The corrected test case:
it("aggregates multiple funding rounds within a distribution period", async () => { await setupFunder(accounts[0], oathToken, communityIssuanceTester, million); await communityIssuanceTester.fund(thousand); await th.fastForwardTime(604800, web3.currentProvider); // 7 days pass await communityIssuanceTester.unprotectedIssueLQTY(); // Issue Oath await communityIssuanceTester.fund(thousand); await th.fastForwardTime(604800, web3.currentProvider); await communityIssuanceTester.unprotectedIssueLQTY(); await communityIssuanceTester.fund(thousand); await th.fastForwardTime(1209600, web3.currentProvider); // 14 days pass await communityIssuanceTester.unprotectedIssueLQTY(); const final = await communityIssuanceTester.totalOATHIssued(); // Get total issued Oath th.assertIsApproximatelyEqual(final, thousand.mul(th.toBN(3)), 1000); })
DECIMAL_PRECISION
(inherited from BaseMath.sol) when assigning rewardPerSecond
to minimize loss of precisionLine 44 (optional, but recommended): uint internal rewardPerSecond;
Line 112: rewardPerSecond = amount.mul(DECIMAL_PRECISION).div(distributionPeriod);
getRewardPerSecond
instead of accessing the rewardPerSecond
property directlyfunction getRewardPerSecond() public view returns (uint) { return rewardPerSecond.div(DECIMAL_PRECISION); }
getRewardAmount
to accurately calculate rewards generated over a given time periodfunction getRewardAmount(uint seconds_) public view returns (uint) { return rewardPerSecond.mul(seconds_).div(DECIMAL_PRECISION); }
getRewardPerSecond
and getRewardAmount
Line 89: issuance = getRewardAmount(timePassed);
Line 108: uint notIssued = getRewardAmount(timeLeft);
Line 116: emit LogRewardPerSecond(getRewardPerSecond());
it("Issues a set amount of rewards per second", async () => { await setupFunder(accounts[0], oathToken, communityIssuanceTester, million); await communityIssuanceTester.fund(thousand); const blockTimestampOne = await th.getLatestBlockTimestamp(web3); await th.fastForwardTime(100, web3.currentProvider); await communityIssuanceTester.unprotectedIssueLQTY(); const issuance = await communityIssuanceTester.totalOATHIssued(); const blockTimestampTwo = await th.getLatestBlockTimestamp(web3); const difference = blockTimestampTwo - blockTimestampOne; const rewards = await communityIssuanceTester.getRewardAmount(difference); assert.isTrue(issuance.eq(rewards)); })
it("aggregates multiple funding rounds within a distribution period", async () => { await setupFunder(accounts[0], oathToken, communityIssuanceTester, million); await communityIssuanceTester.fund(thousand); await communityIssuanceTester.fund(thousand); await communityIssuanceTester.fund(thousand); const final = await communityIssuanceTester.getRewardAmount(1209600); // 14 days - default distribution period const compare = thousand.mul(th.toBN(3)); th.assertIsApproximatelyEqual(final, compare, 1000); })
getRewardPerSecond
in other test casesLine 107: const rps = await communityIssuanceTester.getRewardPerSecond();
Line 189: const rps = await communityIssuanceTester.getRewardPerSecond();
getRewardAmount
Line 12: issuance = getRewardAmount(timePassed);
#0 - c4-judge
2023-03-09T12:33:50Z
trust1995 marked the issue as grade-b
#1 - 0xBebis
2023-03-20T17:22:45Z
this was a known issue but i think this is a decent lightweight fix
#2 - c4-sponsor
2023-03-20T17:22:52Z
0xBebis marked the issue as sponsor acknowledged