Platform: Code4rena
Start Date: 28/09/2023
Pot Size: $36,500 USDC
Total HM: 5
Participants: 115
Period: 6 days
Judge: 0xDjango
Total Solo HM: 1
Id: 290
League: ETH
Rank: 15/115
Findings: 3
Award: $327.81
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: deth
Also found by: 0xDetermination, 0xpiken, 3agle, Brenzee, Flora, HChang26, KrisApostolov, Satyam_Sharma, Testerbot, aycozynfada, berlin-101, gkrastenov, mahdirostami, merlin, rokinot, rvierdiiev, said, santipu_, sl1, tapir, twicek
124.9633 USDC - $124.96
Judge has assessed an item in Issue #593 as 3 risk. The relevant finding follows:
Prime.sol: Users with issued tokens can instantly re-enter the protocol if they were in the 90 days waiting period If an user is issued a prime token while waiting for the claim period, his stakedAt is not zeroed. If the user burns this token in the future by withdrawing from the XVS vault, he can earn it back by calling the claim() function, bypassing the 90 day waiting period.
In order to fix this, consider setting the stakedAt[user] to zero in the token issue, as shown below:
} else { _mint(true, users[i]); _initializeMarkets(users[i]); stakedAt[user] = 0; }
#0 - c4-judge
2023-11-08T17:07:33Z
fatherGoose1 marked the issue as duplicate of #633
#1 - c4-judge
2023-11-08T17:54:55Z
fatherGoose1 marked the issue as satisfactory
🌟 Selected for report: 0xDetermination
Also found by: DeFiHackLabs, Norah, Pessimistic, PwnStars, SpicyMeatball, Testerbot, ThreeSigma, bin2chen, blutorque, deadrxsezzz, dirk_y, ether_sky, hals, neumo, rokinot, said, seerether, turvy_fuzz
198.4834 USDC - $198.48
https://github.com/code-423n4/2023-09-venus/blob/main/contracts/Tokens/Prime/Prime.sol#L828-L831
Previous users who need to have their scores updated after key parameters are updated are unable to do so.
Let's imagine a market with two users with valid scores. If a new market is added, the multipliers are updated, or the alpha is updated, _updateRoundAfterTokenBurned()
is called and sets the pending score updates as 2
. Now a third user joins the market. Since the key parameters to calculate the score are already updated for him, a new update isn't required.
This new user decides to quit the market by requesting a withdrawal from the xvsVault. This call will trigger a xvsUpdate()
call, which will burn the user's tokens and remove his score. The issue lies in the fact that this burn reduces the pendingScoreUpdates
and totalScoreUpdatesRequired
variable, regardless of whether the user needs an update or not, since isScoreUpdated
for this user is false. With the burn, this variable will now be stored as 1
, and only one of the first two users will be able to update their score.
The code below simulates this event, it can be added under the "update score"
description in Prime.ts
it("new users quitting will prevent the score update of old users", async () => { await prime.issue(true, [user2.getAddress()]); await usdt.transfer(user2.address, bigNumber18.mul(10000)); await usdt.connect(user2).approve(vusdt.address, bigNumber18.mul(100)); await vusdt.connect(user2).mint(bigNumber18.mul(100)); //introduce user2 to the market await prime.updateAlpha(4, 5); //0.80 //update the alpha expect(await prime.pendingScoreUpdates()).to.be.equal(2); //now user 1 and 2 needs an update await xvs.connect(user3).approve(xvsVault.address, bigNumber18.mul(100)); await xvsVault.connect(user3).deposit(xvs.address, 0, bigNumber18.mul(100)); await prime.issue(false, [user3.getAddress()]); await usdt.transfer(user3.address, bigNumber18.mul(10000)); await usdt.connect(user3).approve(vusdt.address, bigNumber18.mul(100)); await vusdt.connect(user3).mint(bigNumber18.mul(100)); //user 3 enters the market expect(await prime.pendingScoreUpdates()).to.be.equal(2); //user 1 and 2 still needs an update await xvsVault.connect(user3).requestWithdrawal(xvs.address, 0, bigNumber18.mul(100)); //user 3 exits, triggers his token burn and zeroes his scores //but user 3 burning his tokens decreases the pending score updates as well expect(await prime.pendingScoreUpdates()).to.be.equal(1); //pending score update is now 1, even though 2 users need it await prime.updateScores([user1.getAddress()]); //user 1 sucessfully updates his score await expect(prime.updateScores([user2.getAddress()])).to.be.revertedWithCustomError(prime, "NoScoreUpdatesRequired"); //user 2 is stuck with his old score });
Hardhat, manual code review
Modify the function as the following:
function _updateRoundAfterTokenBurned(address user) internal { if (totalScoreUpdatesRequired > 0 && !isScoreUpdated[nextScoreUpdateRoundId][user]) totalScoreUpdatesRequired--; if (pendingScoreUpdates > 0 && !isScoreUpdated[nextScoreUpdateRoundId][user]) { pendingScoreUpdates--; } }
And also, when creating a new user, set his score update for the current round as true:
function _mint(bool isIrrevocable, address user) internal { if (tokens[user].exists) revert IneligibleToClaim(); tokens[user].exists = true; tokens[user].isIrrevocable = isIrrevocable; isScoreUpdated[nextScoreUpdateRoundId][user] = true; if (isIrrevocable) { totalIrrevocable++; } else { totalRevocable++; } if (totalIrrevocable > irrevocableLimit || totalRevocable > revocableLimit) revert InvalidLimit(); emit Mint(user, isIrrevocable); }
Context
#0 - c4-pre-sort
2023-10-07T00:24:57Z
0xRobocop marked the issue as duplicate of #555
#1 - c4-judge
2023-11-01T20:28:28Z
fatherGoose1 marked the issue as satisfactory
#2 - c4-judge
2023-11-05T00:52:22Z
fatherGoose1 changed the severity to 3 (High Risk)
🌟 Selected for report: Bauchibred
Also found by: 0x3b, 0xDetermination, 0xMosh, 0xScourgedev, 0xTheC0der, 0xTiwa, 0xWaitress, 0xdice91, 0xfusion, 0xpiken, 0xprinc, 0xweb3boy, ArmedGoose, Aymen0909, Breeje, Brenzee, Daniel526, DavidGiladi, DeFiHackLabs, Flora, Fulum, HChang26, Hama, IceBear, J4X, Krace, KrisApostolov, Maroutis, Mirror, MohammedRizwan, Norah, PwnStars, SPYBOY, TangYuanShen, Testerbot, ThreeSigma, Tricko, al88nsk, alexweb3, ast3ros, berlin-101, bin2chen, blutorque, btk, d3e4, deth, e0d1n, ether_sky, ge6a, gkrastenov, glcanvas, hals, imare, inzinko, jkoppel, jnforja, joaovwfreire, josephdara, kutugu, lotux, lsaudit, mahdirostami, merlin, n1punp, nadin, neumo, nisedo, nobody2018, oakcobalt, orion, peanuts, pep7siup, pina, ptsanev, rokinot, rvierdiiev, said, santipu_, sashik_eth, seerether, squeaky_cactus, terrancrypt, tonisives, twicek, vagrant, xAriextz, y4y
4.3669 USDC - $4.37
If an user is issued a prime token while waiting for the claim period, his stakedAt
is not zeroed. If the user burns this token in the future by withdrawing from the XVS vault, he can earn it back by calling the claim()
function, bypassing the 90 day waiting period.
In order to fix this, consider setting the stakedAt[user]
to zero in the token issue, as shown below:
} else { _mint(true, users[i]); _initializeMarkets(users[i]); stakedAt[user] = 0; }
In _claimInterest()
, the PLP unreleased funds are transfered in case the prime contract and the PSR unreleased funds are not enough to cover the interest claim. If there's a prime contract migration, this call will revert as the previous prime contract will not be authorized to claim the PLP tokens. This migration will cause users who have earned interest to only be able to claim what's in the contract and the PSR, if such PSR has not migrated to a new prime token yet. Worst case scenario, the PSR has migrated as well and only the previous prime contract balance can cover the users claims.
Submitting this as low as it's possible for the developers to remigrate to the previous contract in order to fix this. Developers can also claim the interest in the users stead before migrating.
If this happens, getEffectiveDistributionSpeed()
could potentially have a balance lower than accrued value, which leads to this function always reverting whenever it's called.
updateAssetsState()
requires a redudant comptroller argument, given there's only one comptroller and the check serves no purpose in the function.
Tokens, like SHIBA for example, have an extremely high total supply which leads to a very low USD value per token. Since the maximum distribution speed is 1e18 per block, if the protocol includes tokens like these in the future, the reward speed could be too insignificant.
In line 58, when calculating the exponentiation, consider changing e ^ ( ln(ratio) * 𝝰 ) to ratio^alpha instead, since they're mathematically equivalent.
#0 - 0xRobocop
2023-10-07T01:54:12Z
L-01 dup of #633 L-02 dup of #148 NC-02 DUP OF #414
#1 - c4-pre-sort
2023-10-07T01:54:17Z
0xRobocop marked the issue as sufficient quality report
#2 - rokinot
2023-11-06T21:39:15Z
Hi, this report grade was skipped for some reason.
Also, for NC-02, my argument differs from the primary issue of #414, my argument centers around the fact certain tokens with a high notional total supply would have a very low distribution speed in the protocol. I'm not arguing for a medium risk severity however, only that this particular QA issue should be reevaluated.
#3 - fatherGoose1
2023-11-08T17:09:27Z
@rokinot L-01 was upgraded to high as a duplicate of #633. The rest of the issues in this report are either duplicates of QA issues or are not impactful enough to grade the QA report higher than your current highest grade (B): https://github.com/code-423n4/2023-09-venus-findings/issues/39