Platform: Code4rena
Start Date: 20/09/2022
Pot Size: $30,000 USDC
Total HM: 12
Participants: 198
Period: 3 days
Judge: 0xean
Total Solo HM: 2
Id: 164
League: ETH
Rank: 31/198
Findings: 3
Award: $246.04
🌟 Selected for report: 0
🚀 Solo Findings: 0
From the contest description it seems that the sponsor doesn't intend to allow admin to revoke a claim after it end:
our vesting contract is deliberately designed to allow admin revocation in the circumstances of early employment termination (before the end of vesting period specified).
However, it's still possible for an admin to revoke it as long as the recipient hasn't withdrawn it.
A recipient will loose the amount of the claim he hasn't withdrawn.
You might assume users will withdraw their funds as soon as they're available and therefore the likelihood of a significant amount of the claim remaining there is low, but I think in reality people often get caught up with other stuff and postpone dealing with their financial stuff (esp. in the web3 world, which might be complicated and time consuming for new users).
The following test checks that scenario (passing = bug exists): (The test is put in place of the test at VTVLVesting.ts#L503-L511 )
it('allows admin to revoke a valid claim', async () => { const {vestingContract} = await createPrefundedVestingContract({tokenName, tokenSymbol, initialSupplyTokens}); let tx = await vestingContract.createClaim(recipientAddress, startTimestamp, endTimestamp, cliffReleaseTimestamp, releaseIntervalSecs, linearVestAmount, cliffAmount); let reciept = await tx.wait(); let block = await ethers.provider.getBlock(reciept.blockNumber); await ethers.provider.send("evm_increaseTime", [endTimestamp.toNumber() - block.timestamp + 3600]); tx = await vestingContract.revokeClaim(recipientAddress); reciept = await (tx).wait(); block = await ethers.provider.getBlock(reciept.blockNumber); expect(block.timestamp).is.greaterThan(endTimestamp.toNumber()); expect(await (await vestingContract.getClaim(recipientAddress)).isActive).to.be.equal(false); });
Require that the claim hasn't ended before revoking it.
#0 - indijanc
2022-09-24T17:18:48Z
Duplicate of #475
#1 - 0xean
2022-09-24T18:28:00Z
closing, dupe.
🌟 Selected for report: AkshaySrivastav
Also found by: 0v3rf10w, 0x040, 0x1f8b, 0x4non, 0x5rings, 0x85102, 0xA5DF, 0xDecorativePineapple, 0xNazgul, 0xSky, 0xSmartContract, 0xbepresent, 0xf15ers, 0xmatt, 2997ms, Aeros, Aymen0909, B2, Bahurum, Bnke0x0, CertoraInc, Chom, ChristianKuri, CodingNameKiki, Deivitto, Diana, Diraco, Dravee, ElKu, Funen, IllIllI, JC, JLevick, JohnSmith, JohnnyTime, KIntern_NA, Lambda, Margaret, MasterCookie, OptimismSec, RaymondFam, Respx, ReyAdmirado, RockingMiles, Rohan16, Rolezn, Ruhum, RustyRabbit, Sm4rty, SooYa, StevenL, TomJ, Tomo, V_B, Waze, Yiko, __141345__, a12jmx, ajtra, ak1, async, ayeslick, aysha, berndartmueller, bin2chen, bobirichman, brgltd, bulej93, c3phas, carrotsmuggler, cccz, ch13fd357r0y3r, chatch, cryptostellar5, cryptphi, csanuragjain, d3e4, datapunk, delfin454000, dic0de, djxploit, durianSausage, eighty, erictee, exd0tpy, fatherOfBlocks, gogo, got_targ, hansfriese, ignacio, ikbkln, indijanc, innertia, joestakey, karanctf, ladboy233, leosathya, lukris02, martin, medikko, millersplanet, nalus, natzuu, neko_nyaa, neumo, obront, oyc_109, pcarranzav, peanuts, pedr02b2, pedroais, peiw, peritoflores, prasantgupta52, rajatbeladiya, rbserver, reassor, ret2basic, rokinot, romand, rotcivegaf, rvierdiiev, sach1r0, seyni, sikorico, slowmoses, sorrynotsorry, supernova, tibthecat, tnevler, ubermensch, yongskiws, zzykxx, zzzitron
18.8577 USDC - $18.86
There's no check that the admin isn't _msgSender()
, in a scenario where the sender is only admin that can lead to loss of governance over the protocol.
Losing governance over the protocol will lead to loss of the ability to:
withdrawAdmin()
)Meaning, any remaining funds that aren't reserved for existing claims will be frozen.
Require that the _msgSender() != admin
There's already a comment in the code addressing this, but I'd just like to point out that there's no reason to require initial supply to be greater than zero only when the max supply is unlimited. Not allowing initial supply to be zero when more tokens can be minted afterwards (unlimited > limited) doesn't make much sense.
Remove the requirement at line 27 (require(initialSupply_ > 0 || maxSupply_ > 0, "INVALID_AMOUNT");
).
In anyways the actual max supply is greater than zero.
🌟 Selected for report: IllIllI
Also found by: 0v3rf10w, 0x040, 0x1f8b, 0x4non, 0x85102, 0xA5DF, 0xDanielC, 0xNazgul, 0xSmartContract, 0xbepresent, 0xc0ffEE, 0xsam, 2997ms, AkshaySrivastav, Amithuddar, Atarpara, Aymen0909, B2, Bnke0x0, CertoraInc, Chom, ChristianKuri, CodingNameKiki, Deivitto, Diana, DimitarDimitrov, Diraco, Funen, JC, JLevick, JohnSmith, Junnon, KIntern_NA, Lambda, MasterCookie, Matin, Noah3o6, Ocean_Sky, OptimismSec, RaymondFam, Respx, ReyAdmirado, RockingMiles, Rohan16, Rolezn, Ruhum, Saintcode_, Satyam_Sharma, Sm4rty, SnowMan, SooYa, Sta1400, StevenL, Tadashi, Tagir2003, TomJ, Tomio, Tomo, V_B, Waze, WilliamAmbrozic, Yiko, __141345__, a12jmx, adriro, ajtra, ak1, async, aysha, beardofginger, bobirichman, brgltd, bulej93, c3phas, carrotsmuggler, caventa, ch0bu, cryptostellar5, cryptphi, csanuragjain, d3e4, delfin454000, dharma09, djxploit, durianSausage, eighty, emrekocak, erictee, exd0tpy, fatherOfBlocks, francoHacker, gianganhnguyen, gogo, got_targ, hxzy, ignacio, ikbkln, imare, indijanc, jag, jpserrat, karanctf, ladboy233, leosathya, lucacez, lukris02, m9800, malinariy, martin, medikko, mics, millersplanet, mrpathfindr, nalus, natzuu, neko_nyaa, oyc_109, pauliax, peanuts, pedroais, peiw, pfapostol, prasantgupta52, rbserver, ret2basic, rokinot, rotcivegaf, rvierdiiev, sach1r0, samruna, seyni, slowmoses, subtle77, supernova, tgolding55, tibthecat, tnevler, w0Lfrum, yaemsobak, zishansami
9.086 USDC - $9.09
Caching can be done at 2 places:
_createClaimUnchecked()
for numTokensReservedForVesting
that is being read twice (the 2nd is implicit at a +=
assignment)
withdraw()
- the usrClaim
variable is used a few times
Code diff:
diff --git a/contracts/VTVLVesting.sol b/contracts/VTVLVesting.sol index c564c6f..70f51d7 100644 --- a/contracts/VTVLVesting.sol +++ b/contracts/VTVLVesting.sol @@ -291,14 +291,15 @@ contract VTVLVesting is Context, AccessProtected { // Not necessary to use the more complex logic from _baseVestedAmount uint112 allocatedAmount = _cliffAmount + _linearVestAmount; + uint112 _numTokensReservedForVesting = numTokensReservedForVesting; // Still no effects up to this point (and tokenAddress is selected by contract deployer and is immutable), so no reentrancy risk - require(tokenAddress.balanceOf(address(this)) >= numTokensReservedForVesting + allocatedAmount, "INSUFFICIENT_BALANCE"); + require(tokenAddress.balanceOf(address(this)) >= _numTokensReservedForVesting + allocatedAmount, "INSUFFICIENT_BALANCE"); // Done with checks // Effects limited to lines below claims[_recipient] = _claim; // store the claim - numTokensReservedForVesting += allocatedAmount; // track the allocated amount + numTokensReservedForVesting = _numTokensReservedForVesting + allocatedAmount; // track the allocated amount vestingRecipients.push(_recipient); // add the vesting recipient to the list emit ClaimCreated(_recipient, _claim); // let everyone know } @@ -365,20 +366,21 @@ contract VTVLVesting is Context, AccessProtected { // Get the message sender claim - if any Claim storage usrClaim = claims[_msgSender()]; + Claim memory _usrClaimCached = usrClaim; // we can use block.timestamp directly here as reference TS, as the function itself will make sure to cap it to endTimestamp // Conversion of timestamp to uint40 should be safe since 48 bit allows for a lot of years. - uint112 allowance = vestedAmount(_msgSender(), uint40(block.timestamp)); + uint112 allowance = _baseVestedAmount(_usrClaimCached, uint40(block.timestamp)); // Make sure we didn't already withdraw more that we're allowed. - require(allowance > usrClaim.amountWithdrawn, "NOTHING_TO_WITHDRAW"); + require(allowance > _usrClaimCached.amountWithdrawn, "NOTHING_TO_WITHDRAW"); // Calculate how much can we withdraw (equivalent to the above inequality) - uint112 amountRemaining = allowance - usrClaim.amountWithdrawn; + uint112 amountRemaining = allowance - _usrClaimCached.amountWithdrawn; // "Double-entry bookkeeping" // Carry out the withdrawal by noting the withdrawn amount, and by transferring the tokens. - usrClaim.amountWithdrawn += amountRemaining; + usrClaim.amountWithdrawn = _usrClaimCached.amountWithdrawn + amountRemaining; // Reduce the allocated amount since the following transaction pays out so the "debt" gets reduced numTokensReservedForVesting -= amountRemaining;
Gas report diff (-
is before, +
is after):
···················|······················|··············|·············|·············|···············|·············· | TestERC20Token · transfer · 47587 · 52339 · 47809 · 45 · - │ ···················|······················|··············|·············|·············|···············|·············· -| VTVLVesting · createClaim · 139603 · 173947 · 167756 · 28 · - │ +| VTVLVesting · createClaim · 139472 · 173816 · 167627 · 28 · - │ ···················|······················|··············|·············|·············|···············|·············· -| VTVLVesting · createClaimsBatch · 181558 · 387422 · 284486 · 3 · - │ +| VTVLVesting · createClaimsBatch · 181427 · 387029 · 284224 · 3 · - │ ···················|······················|··············|·············|·············|···············|·············· | VTVLVesting · revokeClaim · - · - · 40827 · 6 · - │ ···················|······················|··············|·············|·············|···············|·············· | VTVLVesting · setAdmin · 26511 · 48423 · 37467 · 4 · - │ ···················|······················|··············|·············|·············|···············|·············· -| VTVLVesting · withdraw · 61171 · 78271 · 72938 · 10 · - │ +| VTVLVesting · withdraw · 60598 · 77698 · 72365 · 10 · - │ ···················|······················|··············|·············|·············|···············|·············· | VTVLVesting · withdrawAdmin · 43156 · 64960 · 54058 · 4 · - │ ···················|······················|··············|·············|·············|···············|·············· @@ -116,7 +118,7 @@ ··········································|··············|·············|·············|···············|·············· | TestERC20Token · - · - · 1193264 · 4 % · - │ ··········································|··············|·············|·············|···············|·············· -| VTVLVesting · 3740716 · 3740728 · 3740727 · 12.5 % · - │ +| VTVLVesting · 3806813 · 3806825 · 3806824 · 12.7 % · - │ ·-----------------------------------------|--------------|-------------|-------------|---------------|-------------·
Using custom errors can save a significant amount of gas when the function reverts + reduces code size and saves deployment gas.