Platform: Code4rena
Start Date: 06/09/2022
Pot Size: $90,000 USDC
Total HM: 33
Participants: 168
Period: 9 days
Judge: GalloDaSballo
Total Solo HM: 10
Id: 157
League: ETH
Rank: 101/168
Findings: 3
Award: $100.11
π Selected for report: 0
π Solo Findings: 0
π Selected for report: MEP
Also found by: 0xSky, CertoraInc, MiloTruck, PwnPatrol, R2, Tointer, V_B, __141345__, antonttc, azephiar, cccz, d3e4, datapunk, davidbrai, easy_peasy, hansfriese, minhtrng, neumo, pcarranzav, peritoflores, scaraven, teawaterwire, tonisives, unforgiven, wagmi, zkhorse, zzzitron
5.6134 USDC - $5.61
Founders received less token supply than their ownership percentage
In Token._isForFounder()
function, it checked if this token id is belonged to some founder by taking the remainder when dividing to 100 and checked with tokenRecipient[]
mapping.
uint256 baseTokenId = _tokenId % 100;
That means if a founder received a scheduled with baseTokenId >= 100
, he will never receive that share during vesting.
It is possible due to function _getNextTokenId()
. This function is used to finds the next available base token id for a founder. It keeps increasing value by 1 unit until it found the available base token id but forget to take the modulo when the value bigger than 100.
In Token._addFounders()
, loop in line 108 - 119 assigned tokenRecipient[]
for each founder.
for (uint256 j; j < founderPct; ++j) { // Get the available token id baseTokenId = _getNextTokenId(baseTokenId); // Store the founder as the recipient tokenRecipient[baseTokenId] = newFounder; emit MintScheduled(baseTokenId, founderId, newFounder); // Update the base token id (baseTokenId += schedule) % 100; }
Consider the scenario
baseTokenId = 99
and it is assigned to another founder._getNextTokenId()
to get next baseTokenId
. And it returned 100 (since 99 is not available)_isForFounder()
only take into accounted base token id < 100, so founder will never receive his share.Manual Review
Consider take the modulo when baseTokenId >= 100
in _getNextTokenId()
function.
#0 - GalloDaSballo
2022-09-20T12:59:33Z
π Selected for report: CertoraInc
Also found by: 0xSky, PwnPatrol, Tomo, V_B, antonttc, bin2chen, pcarranzav, peritoflores, rbserver, scaraven, wagmi, zzzitron
49.075 USDC - $49.08
Founder can own 100% of token supply and settings.totalOwnership
value can be manipulated.
In Token._addFounders()
function, line 88 made sure that total ownership of founders will never be bigger than 100%.
if ((totalOwnership += uint8(founderPct)) > 100) revert INVALID_FOUNDER_OWNERSHIP();
But because of unsafe cast uint8(founderPct)
, there is a case that founderPct > 100
but the check is still passed.
In that case founderPct > 100
, that founder may even own more than 100% and all token supply will be minted to him during vesting because [line 108] use founderPct
in a loop to update the mint schedule
for (uint256 j; j < founderPct; ++j) {
Consider the scenario
uint256 founderPct = 0x01201 = 4609; uint8(founderPct) = 0x01 = 1;
Line 102 calculate the schedule
uint256 schedule = 100 / founderPct = 100 / 4609 = 0;
And loop to update the mint schedule still use the original founderPct
value (not casted to uint8 yet)
for (uint256 j; j < founderPct; ++j) { // Get the available token id baseTokenId = _getNextTokenId(baseTokenId); // Store the founder as the recipient tokenRecipient[baseTokenId] = newFounder; emit MintScheduled(baseTokenId, founderId, newFounder); // Update the base token id (baseTokenId += schedule) % 100; }
Since schedule = 0
, value of baseTokenId
will keep increasing by 1 unit each time and all id from 0 to 99 (which mean 100% token supply) will be owned by founder.
Manual Review
Consider use casted value (uint8) to do other calculation after checking total ownership or use SafeCast library
#0 - tbtstl
2022-09-26T18:34:39Z
#1 - GalloDaSballo
2022-09-26T19:22:19Z
Dup of #303
π Selected for report: pfapostol
Also found by: 0x1f8b, 0x4non, 0x5rings, 0xA5DF, 0xSmartContract, 0xc0ffEE, 0xkatana, Aymen0909, Bnke0x0, CertoraInc, Chandr, CodingNameKiki, Cr4ckM3, Deivitto, DimSon, Franfran, JAGADESH, JC, Jeiwan, Lambda, LeoS, Matin, Metatron, Migue, MiloTruck, PPrieditis, PaludoX0, R2, RaymondFam, Respx, ReyAdmirado, Rolezn, Saintcode_, Samatak, SnowMan, StevenL, Tointer, TomJ, Tomo, WatchDogs, Waze, _Adam, __141345__, ajtra, asutorufos, ballx, brgltd, bulej93, c3phas, ch0bu, dharma09, djxploit, durianSausage, easy_peasy, fatherOfBlocks, gianganhnguyen, gogo, imare, leosathya, lucacez, martin, oyc_109, pauliax, peiw, prasantgupta52, ret2basic, rfa, robee, sikorico, simon135, tofunmi, volky, wagmi, zishansami
45.4217 USDC - $45.42
Id | Title |
---|---|
1 | Should use already cached variable to save gas instead of calling function |
2 | No need condition in the last else block |
In propose()
function, the value of currentProposalThreshold
is already get from proposalThreshold()
and cached but when checking votes it calls proposalThreshold()
which is unnessary and cost more gas
uint256 currentProposalThreshold = proposalThreshold(); // Cannot realistically underflow and `getVotes` would revert unchecked { // Ensure the caller's voting weight is greater than or equal to the threshold if (getVotes(msg.sender, block.timestamp - 1) < proposalThreshold()) revert BELOW_PROPOSAL_THRESHOLD(); }
Should use already cached one currentProposalThreshold
to do the check
In castVote()
function, since _support > 2
will be reverted already, we do not need to check _support == 2
in the last else block. Because the value of _support
is always equaled to 2 after previous checks.
#0 - GalloDaSballo
2022-09-26T20:43:38Z
220