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: 133/198
Findings: 2
Award: $23.67
π 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
The limit on max supply implemented in VariableSupplyERC20Token
does not work, allowing the admin
to mint at will and diluting without limits the value of the tokens paid to users through the VTVLVesting
contract in the past. This is a loss for those users, who accepted to be payed with a token that they believed to have a hard cap and to be non inflationary.
In VariableSupplyERC20Token.sol#L40 the check if(mintableSupply > 0)
is true once all mintableSupply
is minted, then the check require(amount <= mintableSupply
is skipped and tokens can be minted without any limits.
mintableSupply = 100
and initialSupply_ = 0
admin
mints 100 tokenstotalSupply
is now 100.admin
mints x tokens and call succeedstotalSupply
is now 100 + x.Manual review
Use a very high value for mintableSupply
if one wants no supply limits and remove the if condition, like this:
function mint(address account, uint256 amount) public onlyAdmin { require(account != address(0), "INVALID_ADDRESS"); // If we're using maxSupply, we need to make sure we respect it // mintableSupply = 0 means mint at will - if(mintableSupply > 0) { require(amount <= mintableSupply, "INVALID_AMOUNT"); // We need to reduce the amount only if we're using the limit, if not just leave it be mintableSupply -= amount; - } _mint(account, amount); }
#0 - 0xean
2022-09-24T00:28:39Z
dupe of #3
π 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
22.9326 USDC - $22.93
claimableAmount
does not return 0 after a claim has been revokedAfter a claim has been revoked, claimableAmount
(VTVLVesting.sol#L215-218) continues to return the amount that would have been claimable if the claim was still active, while in fact the claimable amount of a revoked claim is 0.
Check for active state of the claim and return 0 if inactive:
function claimableAmount(address _recipient) external view returns (uint112) { Claim storage _claim = claims[_recipient]; - return _baseVestedAmount(_claim, uint40(block.timestamp)) - _claim.amountWithdrawn; + return _claim.isActive ? (_baseVestedAmount(_claim, uint40(block.timestamp)) - _claim.amountWithdrawn) : 0; }
In function setAdmin()
(AccessProtected.sol#L39) there is no check to prevent an admin disabling itself. This can lead to loss of funds in the following scenario:
VTVLVesting
contractVTVLVesting
and wants to delegate the claim creation to another account, so it doesn't create the claims yet.setAdmin(admin, false)
, where admin
is its own addressVTVLVesting
.The website states:
VTVL is a non-custodian service. Any tokens which are minted or vesting will remain in a founderβs wallet and directly transferred to the third party wallet appointed in the smart contract.
This is not true since a 'founder' has to deposit tokens into VTVLVesting
before being able to create a claim. So the contract is cusodian of the founder's funds until they are withdrawn by the designated recipient.
This could cause issues with the risk assumptions made by users and user expectations when they use the product for the first time and in fact they see that the tokens must be sent to the contract.
Update the website accordingly.
Consider putting Natspec comments on all public/external functions. Occurrencies are:
param
and return
): VTVLVesting.sol#L331Ownable.sol
is imported but not used.require(_claim.isActive, "NO_ACTIVE_CLAIM")
.Functions setAdmin()
(AccessProtected.sol#L39) and withdrawAdmin()
VTVLVesting.sol#L398 are public but are never called in their own contract. COnsider making these functions external.
The variable tokenAddress
declared at VTVLVesting.sol#L17 is of type IERC20
and not address
. Consider renaming it to token
. Same is valid for variable _otherTokenAddress
at VTVLVesting.sol#L446, consider renaming it to _otherToken
.
In VariableSupplyERC20Token.sol#L48-L49 there is a comment stating that burnging is not implemented since this would create issues with the vesting contract. As long as a normal burn
function that allows a token owner to burn his own tokens is added, there will be no issues with the vesting contract. The vesting contract holds the tokens and no one else can burn them after they are transfered to it.
uint40
sizeIn VTVLVesting.sol#L370 the comment states that the uint40
type has 48 bits. It has 40 bits.