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: 16/198
Findings: 3
Award: $416.87
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: Trust
Also found by: 0xSky, CertoraInc, KIntern_NA, bin2chen, hansfriese, neko_nyaa, neumo, rokinot, wastewa
388.9184 USDC - $388.92
https://github.com/code-423n4/2022-09-vtvl/blob/main/contracts/VTVLVesting.sol#L176 https://github.com/code-423n4/2022-09-vtvl/blob/main/contracts/VTVLVesting.sol#L41
Line 41 of VTVLVesting.sol
implies the assumption that a vesting can reach up to 5,192,296,858,534,827
tokens of 18 decimal. However, line 176 can potentially overflow, effective locking funds in the contract.
https://github.com/code-423n4/2022-09-vtvl/blob/main/contracts/VTVLVesting.sol#L176
Specifically, _claim.linearVestAmount
is of uint112
data type, and truncatedCurrentVestingDurationSecs
is of uint40
, and thus the data type when multiplied together will be uint112
.
For a realistic demonstration, let us assume a vesting schedule of 2 years, that is $86400 * 365 * 2 = 63072000$ seconds. This means that any vesting of more than $82323326$ tokens (assuming 18 decimals), after enough time has passed, will result in _claim.linearVestAmount * truncatedCurrentVestingDurationSecs
value exceeding uint112
data type, causing the transaction to revert.
As a result, because the only possible paths to withdraw funds in an active claim are either through withdraw()
or through admin revoking a claim, then withdrawing the amount, both of which must go through _baseVestedAmount()
, the funds are permanently locked in the contract.
This is identified as a vulnerability because the comment on line 41 implies the assumption of a much larger vesting capability by the contract. In practice, there are numerous 18-decimal tokens with a total supply of 10 or 11 figures, with large vestings amount on a year-long schedule.
Reverse engineering the affected code shows that test()
indeed reverts. Screenshot.:
pragma solidity ^0.8.14; contract Test{ uint40 constant ONE_YEAR = 31536000; uint112 constant LINEAR_VEST_AMOUNT = 85_000_000*1e18; constructor() {} function test() public view returns (uint112) { uint40 currentVestingDurationSecs = ONE_YEAR*2; uint40 truncatedCurrentVestingDurationSecs = (currentVestingDurationSecs / 1) * 1; uint40 finalVestingDurationSecs = ONE_YEAR*2; uint112 linearVestAmount = LINEAR_VEST_AMOUNT * truncatedCurrentVestingDurationSecs / finalVestingDurationSecs; return linearVestAmount; } }
Remix IDE
Appropriate typecasting before multiplication will prevent the issue:
uint112 linearVestAmount = uint112(uint256(_claim.linearVestAmount) * truncatedCurrentVestingDurationSecs / finalVestingDurationSecs);
#0 - 0xean
2022-09-24T19:21:12Z
dupe of #95
🌟 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
hasActiveClaim
modifier has an unnecessary checkThe unnecessary check is here:
require(_claim.startTimestamp > 0, "NO_ACTIVE_CLAIM");
https://github.com/code-423n4/2022-09-vtvl/blob/main/contracts/VTVLVesting.sol#L107
The condition _claim.isActive == true
is enough, as a storage slot will be all set to 0 if the claim was never created (i.e. a claim that was not created cannot be active).
withdrawAdmin()
should be made external
rather than public
For it is not called anywhere within the contract. Also on the docs it is external
https://github.com/code-423n4/2022-09-vtvl/blob/main/contracts/VTVLVesting.sol#L398 https://github.com/code-423n4/2022-09-vtvl/blob/main/docs/VTVLVesting.md#withdrawadmin
ClaimRevoked
's revocationTimestamp
is redundantThe revocationTimestamp
emitted is nothing more than block.timestamp
of the operation, which information is readily available when querying or subscribing to events.
https://github.com/code-423n4/2022-09-vtvl/blob/main/contracts/VTVLVesting.sol#L436
🌟 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
mintableSupply
can be made immutable by looking at the token's total supply.Variable mintableSupply
from VariableSupplyERC20Token.sol
can be made immutable instead of storage.
The mint
function from said contract can be modified to only mint if the condition totalSupply() + amount <= mintableSupply
is satisfied.
function mint(address account, uint256 amount) public onlyAdmin { require(account != address(0), "INVALID_ADDRESS"); if (mintableSupply > 0) { require(totalSupply() + amount <= mintableSupply, "INVALID_AMOUNT"); } _mint(account, amount); }
The gas cost of the addition operation can be further eliminated by placing the check after the mint function:
function mint(address account, uint256 amount) public onlyAdmin { require(account != address(0), "INVALID_ADDRESS"); _mint(account, amount); require(totalSupply() <= mintableSupply, "INVALID_AMOUNT"); }
Saves one Gsset (20000 gas) on construction, as well as an SLOAD (2100 slot) read into the original mintableSupply
slot each time the mint
function is called.
!= 0
for non-zero uint comparison takes less gas than >0
https://github.com/code-423n4/2022-09-vtvl/blob/main/contracts/token/VariableSupplyERC20Token.sol#L31 https://github.com/code-423n4/2022-09-vtvl/blob/main/contracts/token/VariableSupplyERC20Token.sol#L40
https://github.com/code-423n4/2022-09-vtvl/blob/main/contracts/VTVLVesting.sol#L256 https://github.com/code-423n4/2022-09-vtvl/blob/main/contracts/VTVLVesting.sol#L257 https://github.com/code-423n4/2022-09-vtvl/blob/main/contracts/VTVLVesting.sol#L263 https://github.com/code-423n4/2022-09-vtvl/blob/main/contracts/VTVLVesting.sol#L449
_admins
: Using bool
data type for storage incurs extra gas overheadmapping(address => bool) private _admins;
Use uint256
for less gas cost
https://github.com/code-423n4/2022-09-vtvl/blob/main/contracts/AccessProtected.sol#L12
numTokensReservedForVesting
: Uses smaller-than-32-bytes data type, as well as initializing default valueBoth of which costs extra gas
uint112 public numTokensReservedForVesting = 0;
Can be changed to the following
uint256 public numTokensReservedForVesting;
https://github.com/code-423n4/2022-09-vtvl/blob/main/contracts/VTVLVesting.sol#L27
++i
costs less gas than i++
https://github.com/code-423n4/2022-09-vtvl/blob/main/contracts/VTVLVesting.sol#L353
require
statements with &&
can be split into multiple requires to save gashttps://github.com/code-423n4/2022-09-vtvl/blob/main/contracts/VTVLVesting.sol#L344