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: 17/115
Findings: 3
Award: $235.12
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 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#L207
If a user no longer exists before the call to updateScores()
has been made, the whole transaction will revert and will not allow for the other user's scores to be updated, incurring in extra gas costs by having to call this function again with a new input. This is especially relevant because, according to the "Update cap multipliers and alpha" section of the README, the call to updateScores()
is done by a script developed by Venus. They intend to,for example, after adding a new market, update all the scores using updateScores()
, but users could leave the market on purpose, possibly making big batches of user score updates revert, leading to DoS and massive gas costs.
The highlighted line below shows how the transaction reverts if a user has exited:
function updateScores(address[] memory users) external { ... if (!tokens[user].exists) revert UserHasNoPrimeToken(); ... }
Insert the following test in Prime.ts:describe("update score",...)
:
it.only("POC update scores fails due to user exiting a market", async () => { await xvs.connect(user3).approve(xvsVault.address, bigNumber18.mul(2000)); await xvsVault.connect(user3).deposit(xvs.address, 0, bigNumber18.mul(2000)); await prime.issue(false, [user3.getAddress()]); await prime.addMarket(vbnb.address, bigNumber18.mul(1), bigNumber18.mul(1)); let interest = await prime.interests(vbnb.address, user3.getAddress()); expect(interest.accrued).to.be.equal(0); expect(interest.score).to.be.equal(0); expect(interest.rewardIndex).to.be.equal(0); let market = await prime.markets(vbnb.address); expect(market.supplyMultiplier).to.be.equal(bigNumber18.mul(1)); expect(market.borrowMultiplier).to.be.equal(bigNumber18.mul(1)); expect(market.rewardIndex).to.be.equal(0); expect(market.sumOfMembersScore).to.be.equal(0); let nextScoreUpdateRoundId = await prime.nextScoreUpdateRoundId(); let totalScoreUpdatesRequired = await prime.totalScoreUpdatesRequired(); let pendingScoreUpdates = await prime.pendingScoreUpdates(); let isScoreUpdated = await prime.isScoreUpdated(nextScoreUpdateRoundId, user3.getAddress()); expect(nextScoreUpdateRoundId).to.be.equal(3); expect(totalScoreUpdatesRequired).to.be.equal(2); expect(pendingScoreUpdates).to.be.equal(2); expect(isScoreUpdated).to.be.equal(false); await xvsVault.connect(user3).requestWithdrawal(xvs.address, 0, bigNumber18.mul(2000)); await expect(prime.updateScores([user1.getAddress(), user3.getAddress()])).to.be.revertedWithCustomError(prime, "UserHasNoPrimeToken"); });
Vscode, Hardhat
Skip the user that does not exist instead of reverting.
Loop
#0 - c4-pre-sort
2023-10-07T00:08:12Z
0xRobocop marked the issue as duplicate of #555
#1 - c4-judge
2023-11-01T20:26:21Z
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: 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#L331
According to the documentation, one of the goals of updateScores()
is, after a market is added, update the score of all users.
In this case, as the function is expected to be called with a large array, it would incur in DoS and significant gas costs if the transaction were to revert.
This is the case if new tokens are issued and updateScores()
is called afterwards, as neither pendingScoreUpdates
or nextScoreUpdateRoundId
are increased.
Function updateScores
in Prime
decrements pendingScoreUpdates
each time a score is updated. Thus, if a new token is issued, but this variable is not increased, it won't be possible to update all users, if one of the new users calls updateScores()
with its address. The vulnerable line is highlighted below, which will underflow:
function updateScores(address[] memory users) external { ... pendingScoreUpdates--; ... }
Add the following test to Prime.ts:describe("update scores", ...)
:
it.only("POC update scores fails due to token being issued", async () => { await xvs.connect(user3).approve(xvsVault.address, bigNumber18.mul(2000)); await xvsVault.connect(user3).deposit(xvs.address, 0, bigNumber18.mul(2000)); await prime.issue(false, [user3.getAddress()]); await prime.addMarket(vbnb.address, bigNumber18.mul(1), bigNumber18.mul(1)); // token issued after adding market await prime.issue(false, [user2.getAddress()]); let interest = await prime.interests(vbnb.address, user3.getAddress()); expect(interest.accrued).to.be.equal(0); expect(interest.score).to.be.equal(0); expect(interest.rewardIndex).to.be.equal(0); let market = await prime.markets(vbnb.address); expect(market.supplyMultiplier).to.be.equal(bigNumber18.mul(1)); expect(market.borrowMultiplier).to.be.equal(bigNumber18.mul(1)); expect(market.rewardIndex).to.be.equal(0); let nextScoreUpdateRoundId = await prime.nextScoreUpdateRoundId(); let totalScoreUpdatesRequired = await prime.totalScoreUpdatesRequired(); let pendingScoreUpdates = await prime.pendingScoreUpdates(); let isScoreUpdated = await prime.isScoreUpdated(nextScoreUpdateRoundId, user3.getAddress()); expect(nextScoreUpdateRoundId).to.be.equal(3); expect(totalScoreUpdatesRequired).to.be.equal(2); expect(pendingScoreUpdates).to.be.equal(2); expect(isScoreUpdated).to.be.equal(false); // update user2 so user1 and user3 won't have enough pending score updates await prime.updateScores([user2.getAddress()]); await expect(prime.updateScores([user1.getAddress(), user3.getAddress()])).to.be.revertedWithPanic("0x11"); });
Vscode, Hardhat
Call the internal function _startScoreUpdateRound()
at the end of function issue()
.
Other
#0 - c4-pre-sort
2023-10-04T23:49:28Z
0xRobocop marked the issue as duplicate of #555
#1 - c4-judge
2023-11-01T20:28:18Z
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: 0xDetermination
Also found by: 0xblackskull, 0xweb3boy, ADM, Breeje, Pessimistic, PwnStars, SBSecurity, Satyam_Sharma, ThreeSigma, al88nsk, blutorque, debo, dethera, maanas, neumo, oakcobalt, pina, said, sces60107, tapir, tsvetanovv, xAriextz
32.2731 USDC - $32.27
https://github.com/code-423n4/2023-09-venus/blob/main/contracts/Tokens/Prime/Prime.sol#L208
User that calls updateScores() where an account's score was already updated will enter an endless loop, reverting the transaction and spending all the gas.
The error occurs due to doing continue
, but the i
variable only being incremented at the end of the loop, in a unchecked
statement:
function updateScores(address[] memory users) external { ... for (uint256 i = 0; i < users.length; ) { ... if (isScoreUpdated[nextScoreUpdateRoundId][user]) continue; // here, it never increments the variable i ... unchecked { i++; } ... } }
A POC was carried out to confirm the behaviour. Add the following test to Prime.ts:describe("update score", ...):
it.only("POC update scores infinite loop", async () => { await xvs.connect(user3).approve(xvsVault.address, bigNumber18.mul(2000)); await xvsVault.connect(user3).deposit(xvs.address, 0, bigNumber18.mul(2000)); await prime.issue(false, [user3.getAddress()]); await prime.addMarket(vbnb.address, bigNumber18.mul(1), bigNumber18.mul(1)); let interest = await prime.interests(vbnb.address, user3.getAddress()); expect(interest.accrued).to.be.equal(0); expect(interest.score).to.be.equal(0); expect(interest.rewardIndex).to.be.equal(0); let market = await prime.markets(vbnb.address); expect(market.supplyMultiplier).to.be.equal(bigNumber18.mul(1)); expect(market.borrowMultiplier).to.be.equal(bigNumber18.mul(1)); expect(market.rewardIndex).to.be.equal(0); expect(market.sumOfMembersScore).to.be.equal(0); let nextScoreUpdateRoundId = await prime.nextScoreUpdateRoundId(); let totalScoreUpdatesRequired = await prime.totalScoreUpdatesRequired(); let pendingScoreUpdates = await prime.pendingScoreUpdates(); let isScoreUpdated = await prime.isScoreUpdated(nextScoreUpdateRoundId, user3.getAddress()); expect(nextScoreUpdateRoundId).to.be.equal(3); expect(totalScoreUpdatesRequired).to.be.equal(2); expect(pendingScoreUpdates).to.be.equal(2); expect(isScoreUpdated).to.be.equal(false); await prime.updateScores([user1.getAddress()]); await prime.updateScores([user1.getAddress(), user3.getAddress()]); });
Vscode, Hardhat
Either increment if the condition to skip the current iteration is verified or increment the i variable normally in the loop statement.
Loop
#0 - c4-pre-sort
2023-10-04T22:45:01Z
0xRobocop marked the issue as duplicate of #556
#1 - c4-judge
2023-11-01T20:22:45Z
fatherGoose1 marked the issue as satisfactory
🌟 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
[L - 1] Prime
- Round id is only considered on updateScores()
, not in other functions.
The function updateScores()
checks the variables pendingScoreUpdates
and nextScoreUpdateRoundId
before allowing for updates to happen, and after updates pendingScoreUpdates
and isScoreUpdated
. These conditions can be easily circumvented by calling the function accrueInterestAndUpdateScore()
that allows for updates to happen without going through any of the previously mentioned conditions.
[L - 2] There is no input validation for the updateMultiplier
function
In this function, neither the variables supplyMultiplier
nor borrowMultiplier
are checked to be between reasonable values
[N - 1] PrimeLiquidityProvider
Change function name _ensureZeroAddress
-> _ensureValidAddress
The function _ensureZeroAddress
does not ensure that the address given is a zero address, like the name suggests, it ensures that it is a valid address (i.e. not a zero address), and thus it should be named _ensureValidAddress
[N - 2] PrimeLiquidityProvider
, lastAccruedBlock
Incorrect comment
The comment "/// @notice The rate at which token is distributed to the Prime contract" does not accurately describe the variable. A more accurate comment could be "The block number of the latest accrual for that token"
[N - 3] Prime
- There is no guarantee that when a new round starts, all users have updated their scores.
There are several functions (updateAlpha
, updateMultipliers
and addMarket
) that call the function _startScoreUpdateRound
, this function updates the values nextScoreUpdateRoundId
, totalScoreUpdatesRequired
, and pendingScoreUpdates
. However, there is no check to ensure that the previous round of updates has ended and therefore there are still pendingScoreUpdates
for that specific round.
#0 - c4-pre-sort
2023-10-07T02:03:13Z
0xRobocop marked the issue as low quality report
#1 - c4-judge
2023-11-03T02:44:46Z
fatherGoose1 marked the issue as grade-b