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: 39/115
Findings: 2
Award: $129.33
🌟 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
https://github.com/code-423n4/2023-09-venus/blob/main/contracts/Tokens/Prime/Prime.sol#L339-L342
If a user who would be issued an irrevocable token has staked more than 1,000 XVS tokens for at least 90 days. After their irrevocable token gets burnt, the vulnerability allows the user to immediately become eligible for claiming a revocable token as long as they have more than 1,000 staked XVS, without having to wait for the minimum required waiting period.
Here is the scenario
Suppose a user, who meets the criteria to receive an irrevocable token, has staked more than 1,000 XVS tokens for at least 90 days - they are eligible to claim revocable token, but have yet to claim one. At this point in time, their staked time has been recorded in the Prime.sol contract (block.timestamp - stakedAt[user] >= STAKING_PERIOD)
Scenario steps:
Here is the coded scenario to illustrate the vulnerability:
describe("mint and burn", () => { let prime: PrimeScenario; let xvsVault: XVSVault; let xvs: XVS; beforeEach(async () => { ({ prime, xvsVault, xvs } = await loadFixture(deployProtocol)); }); it.only("POC - user immediately gets eligible for claiming revocable token without having to wait for the minimum period", async () => { // user stakes 10000 XVS for the last 90 days await xvs.connect(user1).approve(xvsVault.address, bigNumber18.mul(10000)); await xvsVault.connect(user1).deposit(xvs.address, 0, bigNumber18.mul(10000)); await mine(90 * 24 * 60 * 60); // user gets issued irrevocable token await prime.issue(true, [user1.getAddress()]); // user unstakes XVS await xvsVault.connect(user1).requestWithdrawal(xvs.address, 0, bigNumber18.mul(10000)); // user’s irrevocable token gets burned by the governance for some reason (can be because they no longer stake any XVS tokens) await prime.burn(user1.getAddress()); let irrevocableToken = await prime.tokens(user1.getAddress()); expect(irrevocableToken.isIrrevocable).to.be.equal(false); expect(irrevocableToken.exists).to.be.equal(false); // user restakes >1k XVS to the platform await xvs.connect(user1).approve(xvsVault.address, bigNumber18.mul(10000)); await xvsVault.connect(user1).deposit(xvs.address, 0, bigNumber18.mul(10000)); // they’ll immediately get eligible for claiming revocable token without having to wait for the minimum period. await expect(prime.connect(user1).claim()).to.be.not.reverted; let revocableToken = await prime.tokens(user1.getAddress()); expect(revocableToken.isIrrevocable).to.be.equal(false); expect(revocableToken.exists).to.be.equal(true); }); });
VsCode
Reset the staked time to 0 after issuing irrevocable token for users
/** * @notice Directly issue prime tokens to users * @param isIrrevocable are the tokens being issued * @param users list of address to issue tokens to */ function issue(bool isIrrevocable, address[] calldata users) external { _checkAccessAllowed("issue(bool,address[])"); if (isIrrevocable) { for (uint256 i = 0; i < users.length; ) { Token storage userToken = tokens[users[i]]; if (userToken.exists && !userToken.isIrrevocable) { _upgrade(users[i]); } else { _mint(true, users[i]); _initializeMarkets(users[i]); stakedAt[users[i]] = 0; } unchecked { i++; } } } else { for (uint256 i = 0; i < users.length; ) { _mint(false, users[i]); _initializeMarkets(users[i]); delete stakedAt[users[i]]; unchecked { i++; } } } }
Other
#0 - c4-pre-sort
2023-10-04T21:39:46Z
0xRobocop marked the issue as duplicate of #633
#1 - c4-judge
2023-11-01T19:55:35Z
fatherGoose1 marked the issue as satisfactory
#2 - c4-judge
2023-11-05T00:50:32Z
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
https://github.com/code-423n4/2023-09-venus/blob/main/contracts/Tokens/Prime/Prime.sol#L350-L352
Stated in the discord channel:
“What is the primary use of the issue function?
Issue the initial set of Prime tokens, to the users that were staking more than 1,000 XVS tokens for the last 90 days. Moreover, it will be used to issue the irrevocable tokens (the business criteria for issuing these irrevocable tokens are not defined yet)”.
A user can bypass these criteria by first becoming eligible to be issued a revocable token, then frontrunning the issue
function and unstaking their XVS tokens (resulting in their staked XVS balance dropping below 1,000 XVS).
Since the issue
function lacks checks when minting a revocable token, the user would still receive a revocable token when the issue
function is executed, even though they no longer meet the qualification criteria (they no longer stake >= 1000 XVS tokens).
Here is the coded scenario to illustrate the vulnerability
describe("mint and burn", () => { let prime: PrimeScenario; let xvsVault: XVSVault; let xvs: XVS; beforeEach(async () => { ({ prime, xvsVault, xvs } = await loadFixture(deployProtocol)); }); it.only("POC - user still receives a revocable token when the `issue` function is executed, even though they’re no longer qualified.", async () => { // user stakes 10000 XVS for the last 90 days - make them eligible for being issued a revocable token by the protocol await xvs.connect(user1).approve(xvsVault.address, bigNumber18.mul(10000)); await xvsVault.connect(user1).deposit(xvs.address, 0, bigNumber18.mul(10000)); await mine(90 * 24 * 60 * 60); // user frontruns `issue` function to unstake XVS tokens await xvsVault.connect(user1).requestWithdrawal(xvs.address, 0, bigNumber18.mul(9700)); // user gets issued revocable token, despite not staking more than 1000 XVS tokens await expect(prime.issue(false, [user1.getAddress()])).to.be.not.reverted; let token = await prime.tokens(user1.getAddress()); expect(token.isIrrevocable).to.be.equal(false); expect(token.exists).to.be.equal(true); }); });
VsCode
In issue
function, add a requirement to check if a user has staked a sufficient amount of XVS tokens before minting to them a revocable token
/** * @notice Directly issue prime tokens to users * @param isIrrevocable are the tokens being issued * @param users list of address to issue tokens to */ function issue(bool isIrrevocable, address[] calldata users) external { _checkAccessAllowed("issue(bool,address[])"); if (isIrrevocable) { for (uint256 i = 0; i < users.length; ) { Token storage userToken = tokens[users[i]]; if (userToken.exists && !userToken.isIrrevocable) { _upgrade(users[i]); } else { _mint(true, users[i]); _initializeMarkets(users[i]); } unchecked { i++; } } } else { for (uint256 i = 0; i < users.length; ) { uint256 totalStaked = _xvsBalanceOfUser(users[i]); bool isAccountEligible = isEligible(totalStaked); if (!isAccountEligible) { revert IneligibleToClaim(); } _mint(false, users[i]); _initializeMarkets(users[i]); delete stakedAt[users[i]]; unchecked { i++; } } } }
MEV
#0 - c4-pre-sort
2023-10-04T23:45:22Z
0xRobocop marked the issue as low quality report
#1 - 0xRobocop
2023-10-04T23:46:25Z
If issue
should validate if a user has enough staking or not seems like a design decision.
Leaving for sponsor validation.
#2 - c4-pre-sort
2023-10-04T23:46:30Z
0xRobocop marked the issue as primary issue
#3 - c4-pre-sort
2023-10-04T23:46:34Z
0xRobocop marked the issue as high quality report
#4 - c4-sponsor
2023-10-24T16:14:37Z
chechu marked the issue as disagree with severity
#5 - chechu
2023-10-24T16:14:53Z
Consider QA
If the user frontrun the issue function:
The issue function is only executed by Governance, and we tried to reduce the gas consumed to emit as many Prime tokens as possible in one transaction
#6 - c4-sponsor
2023-10-24T18:48:05Z
chechu (sponsor) acknowledged
#7 - c4-judge
2023-10-31T17:56:56Z
fatherGoose1 changed the severity to QA (Quality Assurance)
#8 - fatherGoose1
2023-10-31T17:58:00Z
Agree with QA given the lack of incentives. The prime token will lack utility with lower XVS staked as the user won't accrue much rewards.
#9 - c4-judge
2023-11-03T02:43:06Z
fatherGoose1 marked the issue as grade-b