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: 59/198
Findings: 3
Award: $52.43
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: Czar102
Also found by: 0xDecorativePineapple, 0xNazgul, 0xSky, 0xbepresent, 0xmatt, Atarpara, Bahurum, DimitarDimitrov, Franfran, GimelSec, JGcarv, JLevick, Junnon, OptimismSec, Rolezn, Ruhum, Soosh, Tomo, Trust, __141345__, adriro, ajtra, bin2chen, cRat1st0s, cccz, cryptonue, d3e4, innertia, jag, joestakey, neumo, obront, pashov, pauliax, pcarranzav, peanuts, rajatbeladiya, rbserver, reassor, seyni, wagmi, zzykxx, zzzitron
0.7375 USDC - $0.74
VariableSupplyERC20Token
can be bypassed to mint an infinite amount of tokensVariableSupplyERC20Token
is defined as
A ERC20 token contract that allows minting at will, with limited or unlimited supply. No burning possible
In the case of a limited supply, it can be set during deployment with the constructor parameter maxSupply_
, which is stored in mintableSupply
. It is defined as:
the maximum supply. The contract won't allow minting over this amount.
The issue is that mintableSupply
is decremented upon minting. And because of this check, mintableSupply
can reach 0
. Further mints will bypass the check, meaning it will be possible to mint more tokens that what mintableSupply
intended (unlimited supply)
I consider it Medium because it essentially means a Token
supply can be drastically inflated by the admin (with no actual limit), and such dilution would have a direct impact on the token
value.
The readme does mention the knowledge of admin power
, but this issue falls outside of the acknowledge revoking claim powers, because it impacts the token, not the vesting itself.
VariableSupplyERC20Token
with initialSupply_ == 0
and maxSupply_ = 1e6
.mint(address(Vesting), amount == 1e6)
to mint the total token supply to the vesting contract.
- mintableSupply = 1e6
, so the condition line 40 is true, and the call enters the block in the if
statement.
- the following check passes, as amount == mintableSupply
.
- then, the following computation is performed: mintableSupply -= amount
, meaning mintableSupply
is set to 0
.mint(address(Vesting), amount == 1e8)
.
- mintableSupply = 0
, so the condition line 40 is false. The call bypasses the if
block and calls _mint(Vesting, 1e8)
, minting the tokens.1e8
, while its maximum supply was supposed to be 1e6
Manual Analysis
1 - You can use an additional variable to prevent minting from happening if the maxSupply has been minted
canMint
, initialized to true
mint()
, add the following:36: function mint(address account, uint256 amount) public onlyAdmin { 37: require(account != address(0), "INVALID_ADDRESS"); + require(canMint, "MAX_SUPPLY ALREADY MINTED") 38: // If we're using maxSupply, we need to make sure we respect it 39: // mintableSupply = 0 means mint at will 40: if(mintableSupply > 0) { 41: require(amount <= mintableSupply, "INVALID_AMOUNT"); 42: // We need to reduce the amount only if we're using the limit, if not just leave it be 43: mintableSupply -= amount; + // if all the mintable supply has been minted, set canMint to false + if (mintableSupply == 0) canMint = false; 44: } 45: _mint(account, amount); 46: }
#0 - 0xean
2022-09-24T00:26:45Z
dupe of #3
🌟 Selected for report: rajatbeladiya
Also found by: 0x4non, CertoraInc, Chom, JLevick, JohnSmith, KIntern_NA, Ruhum, RustyRabbit, ak1, berndartmueller, imare, joestakey, obront, rbserver, rotcivegaf, supernova
32.8268 USDC - $32.83
recipient
onceThe function creating claims, _createClaimUnchecked()
, has the hasNoClaim()
modifier, that is defined as opposite hasActiveClaim
, meaning it reverts if there is an active claim for a user.
It reverts if _claim.startTimestamp > 0
.
A claim is inactive if it has been revoked, or if all the claimable amount has been claimed. The issue is that neither revokeClaim()
nor withdraw()
modify _claim.startTimestamp
.
When calling revokeClaim
, _claim.isActive
is the only member modified, the startTimestamp
is not.
This means if a user has had a claim in the past (revoked or not), it is not possible to create another claim for them .
I consider this an issue as it was not mentioned in the docs/Readme, and while it could be a design decision, given the use cases given in the Readme it is questionable. If we take the example of employee token vesting, this would prevent a companys's new employee to have a vesting claim if they were part of that company in the past.
Medium
withdraw()
after _claim.endTimestamp
, redeeming the entire vested amount.createClaim(_recipient == Alice,,,,,)
hasNoClaim(Alice)
reverts because this check does not pass: startTimestamp
still has the value it was set to in her first claim.Manual Analysis
1 - Modify hasNoClaim()
so that it actually becomes the opposite of hasActiveClaim
:
- require(_claim.startTimestamp == 0, "CLAIM_ALREADY_EXISTS"); + require((_claim.startTimestamp == 0) || (!_claim.isActive), "NO_ACTIVE_CLAIM");
2 - Modify withdraw()
so that upon the entire vested amount being withdrawn, _claim.isActive
is set to false:
```finalVestedAmount numTokensReservedForVesting -= amountRemaining; + if (usrClaim.amountWithdrawn == finalVestedAmount) usrClaim.isActive = false; // After the "books" are set, transfer the tokens // Reentrancy note - internal vars have been changed by now // Also following Checks-effects-interactions pattern tokenAddress.safeTransfer(_msgSender(), amountRemaining);
#0 - 0xean
2022-09-25T14:17:01Z
dupe of #140
🌟 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.8574 USDC - $18.86
claims.linearVestAmount
Vesting.tokenAddress
is a fee-on-transfer tokenThe code quality is very high. The main issue is an inconsistency with the meaning of a variable in different comments.
claims.linearVestAmount
In VTVLVesting.sol
, the definition of the struct Claim
states that linearVestAmount
is the total entitlement.
But in createClaim
( and associated internal functions), linearVestAmount
is described as:
The total amount to be linearly vested between _startTimestamp and _endTimestamp
meaning the total entitlement is actually:
allocatedAmount = _cliffAmount + _linearVestAmount
Low
Manual Analysis
Change the comment in the struct definition:
+ uint112 linearVestAmount; // The total amount to be linearly vested between _startTimestamp and _endTimestamp - uint112 linearVestAmount; // total entitlement
Vesting.tokenAddress
is a fee-on-transfer tokenIf the token used in the vesting contract is a fee-on-transfer token, it affects the expected behavior of the withdrawals functions:
withdraw()
will receive less than the amountRemaining
that they were expecting to receive.withdrawAdmin(_amountRequested)
will receive less than _amountRequested
.Low
Manual Analysis
If you decide to accept fee-on-transfer tokens, add comments to explicitly state that in such case the amountRemaining
and _amountRequested
transferred in both withdrawal functions will not match the amount received by the user