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: 155/198
Findings: 2
Award: $9.83
🌟 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
In VariableSupplyERC20Token contract, if maxSupply_ is greater than 0 then contract won't allow minting over this amount. If maxSupply_0 is 0 then unlimited minting is allowed. However due to the bug (explained below), unlimited minting is allowed even if maxSupply_ is limited. The state variable mintableSupply eventually be equal to 0 after minting amount equals to maxSupply_, as per https://github.com/code-423n4/2022-09-vtvl/blob/main/contracts/token/VariableSupplyERC20Token.sol#L43 When mintableSupply is equal to 0 then unlimited minting is allowed. The condition at https://github.com/code-423n4/2022-09-vtvl/blob/main/contracts/token/VariableSupplyERC20Token.sol#L40 fails, leading to minting at https://github.com/code-423n4/2022-09-vtvl/blob/main/contracts/token/VariableSupplyERC20Token.sol#L45
The following test case exposes the bug
import { ethers } from "hardhat"; const { expect } = require("chai"); describe("VariableSupplyERC20Token", function () { it("should not exceed mintableSupply", async function () { const [owner] = await ethers.getSigners(); const Token = await ethers.getContractFactory("VariableSupplyERC20Token"); const VariableSupplyERC20Token = await Token.deploy("token", "tok", 0, 50); await VariableSupplyERC20Token.mint(owner.address, 50); expect(await VariableSupplyERC20Token.balanceOf(owner.address)).to.equal(50); await VariableSupplyERC20Token.mint(owner.address, 50); //Should not mint further, balanceOf(owner.address) should be 50 and not 100 expect(await VariableSupplyERC20Token.balanceOf(owner.address)).to.equal(50); }); });
Provide maxSupply_=50 in constructor. This sets the state variable mintableSupply to 50 and limits token minting to amount of 50. The first mint() is called with amount of 50, increases total supply by 50 and decreases mintableSupply to 0. If mint() is called again, then minting is not allowed since mintableSupply is equal to 0. However the test case fails, with second mint successful with total supply greater than maxSupply_.
1) VariableSupplyERC20Token should not exceed mintableSupply: AssertionError: Expected "100" to be equal 50
Visual Code, Hardhat test
Adding another state variable to track unlimited minting solves the bug. The following patch resolves the issue.
diff --git a/contracts/token/VariableSupplyERC20Token.sol b/contracts/token/VariableSupplyERC20Token.sol index 1297a14..76981cf 100644 --- a/contracts/token/VariableSupplyERC20Token.sol +++ b/contracts/token/VariableSupplyERC20Token.sol @@ -9,6 +9,7 @@ import "../AccessProtected.sol"; */ contract VariableSupplyERC20Token is ERC20, AccessProtected { uint256 public mintableSupply; + bool private unlimtedMint; /** @notice A ERC20 token contract that allows minting at will, with limited or unlimited supply. No burning possible @@ -25,6 +26,7 @@ contract VariableSupplyERC20Token is ERC20, AccessProtected { // However, if both are 0 we might have a valid scenario as well - user just wants to create a token but doesn't want to mint anything // Should we allow this? require(initialSupply_ > 0 || maxSupply_ > 0, "INVALID_AMOUNT"); + if(maxSupply_ == 0) unlimtedMint = true; mintableSupply = maxSupply_; // Note: the check whether initial supply is less than or equal than mintableSupply will happen in mint fn. @@ -37,12 +39,18 @@ contract VariableSupplyERC20Token is ERC20, AccessProtected { 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 (unlimtedMint == true) { + _mint(account, amount); + return; + } 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; + unchecked { + mintableSupply -= amount; + } + _mint(account, amount); } - _mint(account, amount); } // We can't really have burn, because that could make our vesting contract not work.
#0 - 0xean
2022-09-24T00:25:29Z
dupe of #3
🌟 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
https://github.com/code-423n4/2022-09-vtvl/blob/main/contracts/VTVLVesting.sol [G-01] No need to initialize variables (either storage, memory, local) to 0. [G-02] Use pre increment in "for loop", ++i instead of i++ [G-03] Use Unchecked for pre increment in "for loop" [G-04] Cache the storage variable
Create a local variable to cache numTokensReservedForVesting and update it later. Saves one SLOAD operation. https://github.com/code-423n4/2022-09-vtvl/blob/main/contracts/VTVLVesting.sol#L295
[G-05] Use unchecked wherever arithmetic operation does not under/over flow
diff --git a/contracts/VTVLVesting.sol b/contracts/VTVLVesting.sol index c564c6f..3297c32 100644 --- a/contracts/VTVLVesting.sol +++ b/contracts/VTVLVesting.sol @@ -24,7 +24,7 @@ contract VTVLVesting is Context, AccessProtected { * In other words, this represents the amount the contract is scheduled to pay out at some point if the * owner were to never interact with the contract. */ - uint112 public numTokensReservedForVesting = 0; + uint112 public numTokensReservedForVesting; /** @notice A structure representing a single claim - supporting linear and cliff vesting. @@ -36,7 +36,7 @@ contract VTVLVesting is Context, AccessProtected { uint40 endTimestamp; // When does the vesting end - the vesting goes linearly between the start and end timestamps uint40 cliffReleaseTimestamp; // At which timestamp is the cliffAmount released. This must be <= startTimestamp uint40 releaseIntervalSecs; // Every how many seconds does the vested amount increase. - + // uint112 range: range 0 – 5,192,296,858,534,827,628,530,496,329,220,095. // uint112 range: range 0 – 5,192,296,858,534,827. uint112 linearVestAmount; // total entitlement @@ -145,7 +145,7 @@ contract VTVLVesting is Context, AccessProtected { @param _referenceTs Timestamp for which we're calculating */ function _baseVestedAmount(Claim memory _claim, uint40 _referenceTs) internal pure returns (uint112) { - uint112 vestAmt = 0; + uint112 vestAmt; // the condition to have anything vested is to be active if(_claim.isActive) { @@ -158,7 +158,9 @@ contract VTVLVesting is Context, AccessProtected { // If we're past the cliffReleaseTimestamp, we release the cliffAmount // We don't check here that cliffReleaseTimestamp is after the startTimestamp if(_referenceTs >= _claim.cliffReleaseTimestamp) { - vestAmt += _claim.cliffAmount; + unchecked { + vestAmt += _claim.cliffAmount; + } } // Calculate the linearly vested amount - this is relevant only if we're past the schedule start @@ -176,7 +178,9 @@ contract VTVLVesting is Context, AccessProtected { uint112 linearVestAmount = _claim.linearVestAmount * truncatedCurrentVestingDurationSecs / finalVestingDurationSecs; // Having calculated the linearVestAmount, simply add it to the vested amount - vestAmt += linearVestAmount; + unchecked { + vestAmt += linearVestAmount; + } } } @@ -289,16 +293,23 @@ contract VTVLVesting is Context, AccessProtected { }); // Our total allocation is simply the full sum of the two amounts, _cliffAmount + _linearVestAmount // Not necessary to use the more complex logic from _baseVestedAmount - uint112 allocatedAmount = _cliffAmount + _linearVestAmount; + uint112 allocatedAmount; + unchecked { + allocatedAmount = _cliffAmount + _linearVestAmount; + } + //Cache the storage variable + uint112 _numTokensReservedForVesting = numTokensReservedForVesting; // Still no effects up to this point (and tokenAddress is selected by contract deployer and is immutable), so no reentrancy risk - require(tokenAddress.balanceOf(address(this)) >= numTokensReservedForVesting + allocatedAmount, "INSUFFICIENT_BALANCE"); + require(tokenAddress.balanceOf(address(this)) >= _numTokensReservedForVesting + allocatedAmount, "INSUFFICIENT_BALANCE"); // Done with checks // Effects limited to lines below claims[_recipient] = _claim; // store the claim - numTokensReservedForVesting += allocatedAmount; // track the allocated amount + unchecked { + numTokensReservedForVesting = _numTokensReservedForVesting + allocatedAmount; // track the allocated amount + } vestingRecipients.push(_recipient); // add the vesting recipient to the list emit ClaimCreated(_recipient, _claim); // let everyone know } @@ -350,8 +361,11 @@ contract VTVLVesting is Context, AccessProtected { "ARRAY_LENGTH_MISMATCH" ); - for (uint256 i = 0; i < length; i++) { + for (uint256 i; i < length;) { _createClaimUnchecked(_recipients[i], _startTimestamps[i], _endTimestamps[i], _cliffReleaseTimestamps[i], _releaseIntervalsSecs[i], _linearVestAmounts[i], _cliffAmounts[i]); + unchecked { + ++i; + } } // No need for separate emit, since createClaim will emit for each claim (and this function is merely a convenience/gas-saver for multiple claims creation)
https://github.com/code-423n4/2022-09-vtvl/blob/main/contracts/token/VariableSupplyERC20Token.sol
[G-04] Cache the storage variable [G-05] Use unchecked wherever arithmetic operation does not under/over flow
diff --git a/contracts/token/VariableSupplyERC20Token.sol b/contracts/token/VariableSupplyERC20Token.sol index 1297a14..c535e47 100644 --- a/contracts/token/VariableSupplyERC20Token.sol +++ b/contracts/token/VariableSupplyERC20Token.sol @@ -37,10 +37,13 @@ contract VariableSupplyERC20Token is ERC20, AccessProtected { 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"); + uint256 _mintableSupply = mintableSupply; + 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; + unchecked { + mintableSupply = _mintableSupply - amount; + } } _mint(account, amount); }