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: 77/198
Findings: 3
Award: $28.77
🌟 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
According to docs for maxSupply_ :
The contract won't allow minting over this amount. Set to 0 for no limit. This means that if maxSupply_ is set, then no minting beyond maxSupply_ is allowed. max supply == 0 means mint at will.
However, according to the logic placed in VariableSupplyERC20Token.sol it is still possible to mint as the state will switch to mint at will when mintableSupply reaches 0
It is possible to continue minting past the maxSupply that was defined by maxSupply_
Assume: maxSupply_ = 100; mintableSupply = 100; Admin calls mint(0x1, 100);
This will mint 100 tokens to 0x1 mintableSupply -= 100 -> mintableSupply = 100 - 100 = 0
Since mintableSupply is now equal to 0, admin can mint at will since it won't enter the condition of:
If(mintableSupply > 0)
And will skip to calling _mint(account, amount); https://github.com/code-423n4/2022-09-vtvl/tree/main/contracts/token/VariableSupplyERC20Token.sol#L45
Another option is, assume: initialSupply_ = maxSupply_ = 100
The constructor calls mint(msgSender(), inmitialSupply); This will mint 100 tokes to _msgSender() and supposedly reach maxSupply of 100 and mintableSupply -= 100 -> mintableSupply = 100 - 100 = 0
However, it is still possible to mint at will since mintableSupply is now equal to 0, admin can mint at will since it won't enter the condition of:
If(mintableSupply > 0)
Quick test done through brownie (since I don't know how to handle hardhat):
from brownie import accounts, config, VariableSupplyERC20Token def main(): deployer = accounts[0] someUser = accounts[1] tokenContract = VariableSupplyERC20Token.deploy("testToken", "T", 0, 100, {'from': deployer}) print(f'mintableSupply before mint: {tokenContract.mintableSupply()}') # @audit will show mintableSupply = 100 tokenContract.mint(someUser, 100) print(f'mintableSupply after mint: {tokenContract.mintableSupply()}') # @audit will show mintableSupply = 0 tokenContract.mint(someUser, 100) # @audit still possible to mint because mintableSupply = 0 means mintAtwill even though it's not, since initial mintableSupply = 100
Add a mintAtWill bool to check whether the state is defined as mint at will.
contract VariableSupplyERC20Token is ERC20, AccessProtected { uint256 public mintableSupply; bool public mintAtWill; // @audit added mintAtWill bool constructor(string memory name_, string memory symbol_, uint256 initialSupply_, uint256 maxSupply_) ERC20(name_, symbol_) { require(initialSupply_ > 0 || maxSupply_ > 0, "INVALID_AMOUNT"); mintableSupply = maxSupply_; if(initialSupply_ > 0) { if(maxSupply_ == 0) { // @audit max supply == 0 means mint at will. mintAtWill = true; //@audit change mintAtWill to true } mint(_msgSender(), initialSupply_); } } function mint(address account, uint256 amount) public onlyAdmin { require(account != address(0), "INVALID_ADDRESS"); if(mintableSupply > 0) { require(amount <= mintableSupply, "INVALID_AMOUNT"); mintableSupply -= amount; } else { require(mintAtWill, "Mint at will is disabled"); // @audit add require check for mintAtWill } _mint(account, amount); } }
#0 - 0xean
2022-09-24T00:15:18Z
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
18.9219 USDC - $18.92
Total Low Severity: 3 Total Non-Critical Severity: 7
Severity: Low
According to package.json, the OZ packages are currently set to ^4.5.0
"@openzeppelin/contracts": "^4.5.0",
Package.json#L7
Update the versions of @openzeppelin/contracts and @openzeppelin/contracts-upgradeable to be the latest in package.json. I also recommend double checking the versions of other dependencies as a precaution, as they may include important bug fixes.
Severity: Low
Missing zero address check for accidently calling revokeClaim on address(0).
function revokeClaim(address _recipient) external onlyAdmin hasActiveClaim(_recipient) {
https://github.com/code-423n4/2022-09-vtvl/tree/main/contracts/VTVLVesting.sol#L418
Consider adding zero-address checks in the mentioned codebase.
Severity: Low
The non-upgradeable standard version of OpenZeppelin’s library, such as ERC20, Ownable, Context and SafeERC20 are inherited / used by the contracts.
It would be safer to use the upgradeable versions of the library contracts to avoid unexpected behaviour.
Where applicable, use the contracts from @openzeppelin/contracts-upgradeable instead of @openzeppelin/contracts.
Severity: Non-Critical
Since isActive is bool, there is no need to check for "true" value, can simply call: require(_claim.isActive, "NO_ACTIVE_CLAIM");
require(_claim.isActive == true, "NO_ACTIVE_CLAIM");
https://github.com/code-423n4/2022-09-vtvl/tree/main/contracts/VTVLVesting.sol#L111
require(_claim.isActive, "NO_ACTIVE_CLAIM");
Severity: Non-Critical
Found usage of floating pragmas ^0.8.14 of Solidity
https://github.com/code-423n4/2022-09-vtvl/tree/main/contracts/token/VariableSupplyERC20Token.sol#L2
OpenZeppelin recommends that the initializer modifier be applied to constructors. Per OZs Post implementation contract should be initialized to avoid potential griefs or exploits. https://forum.openzeppelin.com/t/uupsupgradeable-vulnerability-post-mortem/15680/5
Severity: Non-Critical
contract VTVLVesting is Context, AccessProtected
https://github.com/code-423n4/2022-09-vtvl/tree/main/contracts/VTVLVesting.sol#L11
contract FullPremintERC20Token is ERC20
https://github.com/code-423n4/2022-09-vtvl/tree/main/contracts/token/FullPremintERC20Token.sol#L8
contract VariableSupplyERC20Token is ERC20, AccessProtected
Severity: Non-Critical
Contracts are allowed to override their parents’ functions and change the visibility from external to public.
function setAdmin(
https://github.com/code-423n4/2022-09-vtvl/tree/main/contracts/AccessProtected.sol#L39
function withdrawAdmin(
https://github.com/code-423n4/2022-09-vtvl/tree/main/contracts/VTVLVesting.sol#L398
function mint(
Severity: Non-Critical
// require(_claim.linearVestAmount + _claim.cliffAmount > 0, "INVALID_VESTED_AMOUNT"); // require(_claim.endTimestamp > 0, "NO_END_TIMESTAMP");
https://github.com/code-423n4/2022-09-vtvl/tree/main/contracts/VTVLVesting.sol#L114-L115
// require(_claim.isActive == false, "CLAIM_ALREADY_EXISTS"); // Further checks aren't necessary (to save gas), as they're done at creation time (createClaim) // require(_claim.endTimestamp == 0, "CLAIM_ALREADY_EXISTS"); // require(_claim.linearVestAmount + _claim.cliffAmount == 0, "CLAIM_ALREADY_EXISTS"); // require(_claim.amountWithdrawn == 0, "CLAIM_ALREADY_EXISTS");
https://github.com/code-423n4/2022-09-vtvl/tree/main/contracts/VTVLVesting.sol#L133-L138
// uint constant _initialSupply = 100 * (10**18);
https://github.com/code-423n4/2022-09-vtvl/tree/main/contracts/token/FullPremintERC20Token.sol#L9
Severity: Non-Critical
// Potential TODO: sanity check, if _linearVestAmount == 0, should we perhaps force that start and end ts are the same?
https://github.com/code-423n4/2022-09-vtvl/tree/main/contracts/VTVLVesting.sol#L266
Severity: Non-Critical
The current form of relative path import is not recommended for use because it can unpredictably pollute the namespace. Instead, the Solidity docs recommend specifying imported symbols explicitly. https://docs.soliditylang.org/en/v0.8.15/layout-of-source-files.html#importing-other-source-files
import "./AccessProtected.sol";
https://github.com/code-423n4/2022-09-vtvl/tree/main/contracts/VTVLVesting.sol#L9
import "../AccessProtected.sol";
https://github.com/code-423n4/2022-09-vtvl/tree/main/contracts/token/VariableSupplyERC20Token.sol#L5
Use specific imports syntax per solidity docs recommendation.
🌟 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.1106 USDC - $9.11
Total Gas Optimizations: 10
Severity: Gas Optimizations
When a function with a memory array is called externally, the abi.decode() step has to use a for-loop to copy each index of the calldata to the memory index. Each iteration of this for-loop costs at least 60 gas (i.e. 60 * <mem_array>.length). Using calldata directly, obliviates the need for such a loop in the contract code and runtime execution. Structs have the same overhead as an array of length one
function allVestingRecipients() external view returns (address[] memory) {
https://github.com/code-423n4/2022-09-vtvl/tree/main/contracts/VTVLVesting.sol#L223
function createClaimsBatch( address[] memory _recipients, uint40[] memory _startTimestamps, uint40[] memory _endTimestamps, uint40[] memory _cliffReleaseTimestamps, uint40[] memory _releaseIntervalsSecs, uint112[] memory _linearVestAmounts, uint112[] memory _cliffAmounts)
https://github.com/code-423n4/2022-09-vtvl/tree/main/contracts/VTVLVesting.sol#L333-L340
Severity: Gas Optimizations
Saves 6 gas per loop
for (uint256 i = 0; i < length; i++) {
https://github.com/code-423n4/2022-09-vtvl/tree/main/contracts/VTVLVesting.sol#L353
For example, use ++i instead of i++
Severity: Gas Optimizations
for (uint256 i = 0; i < length; i++) {
https://github.com/code-423n4/2022-09-vtvl/tree/main/contracts/VTVLVesting.sol#L353
Severity: Gas Optimizations
This change saves 6 gas per instance
require(_linearVestAmount + _cliffAmount > 0, "INVALID_VESTED_AMOUNT");
https://github.com/code-423n4/2022-09-vtvl/tree/main/contracts/VTVLVesting.sol#L256
require(_startTimestamp > 0, "INVALID_START_TIMESTAMP");
https://github.com/code-423n4/2022-09-vtvl/tree/main/contracts/VTVLVesting.sol#L257
require(_releaseIntervalSecs > 0, "INVALID_RELEASE_INTERVAL");
https://github.com/code-423n4/2022-09-vtvl/tree/main/contracts/VTVLVesting.sol#L263
require(bal > 0, "INSUFFICIENT_BALANCE");
https://github.com/code-423n4/2022-09-vtvl/tree/main/contracts/VTVLVesting.sol#L449
Severity: Gas Optimizations
vestAmt += _claim.cliffAmount;
https://github.com/code-423n4/2022-09-vtvl/tree/main/contracts/VTVLVesting.sol#L161
vestAmt += linearVestAmount;
https://github.com/code-423n4/2022-09-vtvl/tree/main/contracts/VTVLVesting.sol#L179
numTokensReservedForVesting += allocatedAmount;
https://github.com/code-423n4/2022-09-vtvl/tree/main/contracts/VTVLVesting.sol#L301
usrClaim.amountWithdrawn += amountRemaining;
https://github.com/code-423n4/2022-09-vtvl/tree/main/contracts/VTVLVesting.sol#L381
numTokensReservedForVesting -= amountRemaining;
https://github.com/code-423n4/2022-09-vtvl/tree/main/contracts/VTVLVesting.sol#L383
numTokensReservedForVesting -= amountRemaining; // Reduces the allocation
https://github.com/code-423n4/2022-09-vtvl/tree/main/contracts/VTVLVesting.sol#L433
mintableSupply -= amount;
Severity: Gas Optimizations
The following functions could be set external to save gas and improve code quality. External call cost is less expensive than of public functions.
function setAdmin(address admin, bool isEnabled) public onlyAdmin {
https://github.com/code-423n4/2022-09-vtvl/tree/main/contracts/AccessProtected.sol#L39
function withdrawAdmin(uint112 _amountRequested) public onlyAdmin {
https://github.com/code-423n4/2022-09-vtvl/tree/main/contracts/VTVLVesting.sol#L398
function mint(address account, uint256 amount) public onlyAdmin {
Severity: Gas Optimizations
Lower than uint256 size storage variables are less gas efficient. Using uint64 does not give any efficiency, actually, it is the opposite as EVM operates on default of 256-bit values so uint64 is more expensive in this case as it needs a conversion. It only gives improvements in cases where you can pack variables together, e.g. structs.
uint112 public numTokensReservedForVesting = 0;
https://github.com/code-423n4/2022-09-vtvl/tree/main/contracts/VTVLVesting.sol#L27
uint40 startTimestamp;
https://github.com/code-423n4/2022-09-vtvl/tree/main/contracts/VTVLVesting.sol#L35
uint40 endTimestamp;
https://github.com/code-423n4/2022-09-vtvl/tree/main/contracts/VTVLVesting.sol#L36
uint40 cliffReleaseTimestamp;
https://github.com/code-423n4/2022-09-vtvl/tree/main/contracts/VTVLVesting.sol#L37
uint40 releaseIntervalSecs;
https://github.com/code-423n4/2022-09-vtvl/tree/main/contracts/VTVLVesting.sol#L38
uint112 linearVestAmount;
https://github.com/code-423n4/2022-09-vtvl/tree/main/contracts/VTVLVesting.sol#L42
uint112 cliffAmount;
https://github.com/code-423n4/2022-09-vtvl/tree/main/contracts/VTVLVesting.sol#L43
uint112 amountWithdrawn;
https://github.com/code-423n4/2022-09-vtvl/tree/main/contracts/VTVLVesting.sol#L44
uint112 vestAmt = 0;
https://github.com/code-423n4/2022-09-vtvl/tree/main/contracts/VTVLVesting.sol#L148
uint40 currentVestingDurationSecs = _referenceTs - _claim.startTimestamp;
https://github.com/code-423n4/2022-09-vtvl/tree/main/contracts/VTVLVesting.sol#L167
uint40 truncatedCurrentVestingDurationSecs = (currentVestingDurationSecs / _claim.releaseIntervalSecs) * _claim.releaseIntervalSecs;
https://github.com/code-423n4/2022-09-vtvl/tree/main/contracts/VTVLVesting.sol#L169
uint40 finalVestingDurationSecs = _claim.endTimestamp - _claim.startTimestamp;
https://github.com/code-423n4/2022-09-vtvl/tree/main/contracts/VTVLVesting.sol#L170
uint112 linearVestAmount = _claim.linearVestAmount * truncatedCurrentVestingDurationSecs / finalVestingDurationSecs;
https://github.com/code-423n4/2022-09-vtvl/tree/main/contracts/VTVLVesting.sol#L176
uint112 allocatedAmount = _cliffAmount + _linearVestAmount;
https://github.com/code-423n4/2022-09-vtvl/tree/main/contracts/VTVLVesting.sol#L292
uint112 allowance = vestedAmount(_msgSender(), uint40(block.timestamp));
https://github.com/code-423n4/2022-09-vtvl/tree/main/contracts/VTVLVesting.sol#L371
uint112 amountRemaining = allowance - usrClaim.amountWithdrawn;
https://github.com/code-423n4/2022-09-vtvl/tree/main/contracts/VTVLVesting.sol#L377
uint112 finalVestAmt = finalVestedAmount(_recipient);
https://github.com/code-423n4/2022-09-vtvl/tree/main/contracts/VTVLVesting.sol#L422
uint112 amountRemaining = finalVestAmt - _claim.amountWithdrawn;
https://github.com/code-423n4/2022-09-vtvl/tree/main/contracts/VTVLVesting.sol#L429
Severity: Gas Optimizations
The unchecked keyword is new in solidity version 0.8.0, so this only applies to that version or higher, which these instances are. This saves 30-40 gas PER LOOP
for (uint256 i = 0; i < length; i++) {
https://github.com/code-423n4/2022-09-vtvl/tree/main/contracts/VTVLVesting.sol#L353
Severity: Gas Optimizations
Booleans are more expensive than uint256 or any type that takes up a full word because each write operation emits an extra SLOAD to first read the slot's contents, replace the bits taken up by the boolean, and then write back. This is the compiler's defense against contract upgrades and pointer aliasing, and it cannot be disabled.
mapping(address => bool) private _admins;
https://github.com/code-423n4/2022-09-vtvl/tree/main/contracts/AccessProtected.sol#L12
Severity: Gas Optimizations
Since isActive is bool, there is no need to check for "true" value, can simply call: require(_claim.isActive, "NO_ACTIVE_CLAIM");
require(_claim.isActive == true, "NO_ACTIVE_CLAIM");
https://github.com/code-423n4/2022-09-vtvl/tree/main/contracts/VTVLVesting.sol#L111
require(_claim.isActive, "NO_ACTIVE_CLAIM");