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: 82/198
Findings: 3
Award: $28.69
🌟 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
An admin can mint as many tokens as they want, even if maxSupply_
is set to a non-zero value in the constructor. The impact is that more tokens can be minted than users thought would get minted. Further impacts are:
totalSupply
We are rating this as a high since unlimited dilution of a token is equivalent to reducing its value to almost zero.
maxSupply_
greater than zero. The value of mintableSupply
is will be set to a value greater than zero on line 28│ File: VariableSupplyERC20Token.sol #28 27 │ require(initialSupply_ > 0 || maxSupply_ > 0, "INVALID_AMOUNT"); 28 │ mintableSupply = maxSupply_; 29 │ 30 │ // Note: the check whether initial supply is less than or equal than mintableSupply will happen in mint fn. 31 │ if(initialSupply_ > 0) { 32 │ mint(_msgSender(), initialSupply_); 33 │ }
The if-statement that controls the supply (line 40-46) of the token can be skipped if mintableSupply == 0
.
An admin only needs to call mint with an amount
equal to the the current mintableSupply
variable.
Now that mintableSupply == 0
any subsequent calls to mint
will skip the if-statement and the control flow always moves to line 45. The admin can mint endlessly, at will.
A hardhat test that demonstrates the bug is:
import { expect } from "chai"; import { ethers } from "hardhat"; describe("VariableERC20TokenTest", async () => { it("Test unlimited VariableERC20 mint", async () => { const [adminOne] = await ethers.getSigners(); const originalMintableSupply = 10_000_000; const initialSupply = 1_000_000; const VariableSupplyERC20Fac = await ethers.getContractFactory( "VariableSupplyERC20Token" ); // const VariableERC20 = await VariableSupplyERC20Fac.connect(adminOne).deploy( "AToken", "ATK", initialSupply, originalMintableSupply ); // exploits // can mint after making the current mintableAmount the amount to be minted(originalMintableSupply - initialSupply) await VariableERC20.connect(adminOne).mint( adminOne.address, originalMintableSupply - initialSupply ); console.log(VariableERC20.mintableSupply()); //mintabeSupply equals to zero and can mint unlimited afterwards expect(await VariableERC20?.mintableSupply()).to.equal(0); await VariableERC20.connect(adminOne).mint( adminOne.address, 10_000_000_000_000 ); }); });
Add an extra state variable that shows and records the number of tokens minted add a require
statement that reverts if the numberMinted + amount > mintableSupply
. Also, make mintableSupply
and immutable variable that represents the maximum supply.
diff --git a/contracts/token/VariableSupplyERC20Token.sol b/contracts/token/VariableSupplyERC20Token.sol index 1297a14..f514394 100644 --- a/contracts/token/VariableSupplyERC20Token.sol +++ b/contracts/token/VariableSupplyERC20Token.sol @@ -8,7 +8,8 @@ import "../AccessProtected.sol"; @notice A ERC20 token contract that allows minting at will, with limited or unlimited supply. */ contract VariableSupplyERC20Token is ERC20, AccessProtected { - uint256 public mintableSupply; + uint256 public immutable mintableSupply; + uint256 public numberMinted; /** @notice A ERC20 token contract that allows minting at will, with limited or unlimited supply. No burning possible @@ -30,6 +31,7 @@ contract VariableSupplyERC20Token is ERC20, AccessProtected { // Note: the check whether initial supply is less than or equal than mintableSupply will happen in mint fn. if(initialSupply_ > 0) { mint(_msgSender(), initialSupply_); + numberMinted += initialSupply_; } } @@ -38,9 +40,8 @@ contract VariableSupplyERC20Token is ERC20, AccessProtected { // 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; + require(numberMinted + amount <= mintableSupply, "INVALID_AMOUNT"); + numberMinted += amount; } _mint(account, amount); }
#0 - 0xean
2022-09-24T00:13:59Z
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.8574 USDC - $18.86
It would probably be a good idea for there to be a contract owner that can never be removed and has more privileges than the admins, so that functionality can be restored if it's deemed necessary.
require(_claim.isActive == true, "NO_ACTIVE_CLAIM");
The == true
is redundant
remove
or unsetAdmin
function for more code clarity (using the same name setAdmin
to remove admins)// Conversion of timestamp to uint40 should be safe since 48 bit allows for a lot of years.
should be changed to since 40 bits allows for
More documentation is required for the "zero admin" case. Discussion with the sponsor confirmed that the case of their being zero admins is supported. However, there should be more documentation provided for this feature. In particular users should be informed that they will not be able to call many of the functions of the contract should all admins be removed.
🌟 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
memory
instead using of storage
for storing the claims struct to some save gasClaim storage _claim = claims[_recipient];
Claim storage _claim = claims[_recipient];
Claim storage _claim = claims[_recipient];
Claim storage _claim = claims[_recipient];
Claim storage _claim = claims[_recipient];
Claim storage usrClaim = claims[_msgSender()];
Claim storage _claim = claims[_recipient];
unchecked
blocks and ++i
to make loops more efficient│ File: contracts/VTVLVesting.sol 353 + │ for (uint256 i = 0; i < length;) { 354 │ _createClaimUnchecked(_recipients[i], _startTimestamps[i], _endTimestamps[i], _cliffRelease │ Timestamps[i], _releaseIntervalsSecs[i], _linearVestAmounts[i], _cliffAmounts[i]); + | unchecked { + | ++i; + | } + | 355 │ } 356 │
withdrawAdmin()
is only declared and not used anywhere else in the contract, make it externalSee L398.
Use unchecked
blocks for them to save gas.
The function is internal and the parameters are not controlled by the user, so it is safe to use unchecked
.
L166-L179 can be changed as follows:
──────┬──────────────────────────────────────────────────────────────────────────────────────────────────────── │ File: contracts/VTVLVesting.sol#L166-L179 166 │ if(_referenceTs > _claim.startTimestamp) { + | unchecked { 167 │ uint40 currentVestingDurationSecs = _referenceTs - _claim.startTimestamp; // How long since the start 168 │ // Next, we need to calculated the duration truncated to nearest releaseIntervalSecs 169 │ uint40 truncatedCurrentVestingDurationSecs = (currentVestingDurationSecs / _claim.releaseIntervalSecs) * _claim.releaseIntervalSecs; 170 │ uint40 finalVestingDurationSecs = _claim.endTimestamp - _claim.startTimestamp; // length of the interval 171 │ 172 │ // Calculate the linear vested amount - fraction_of_interval_completed * linearVestedAmount 173 │ // Since fraction_of_interval_completed is truncatedCurrentVestingDurationSecs / finalV estingDurationSecs, the formula becomes 174 │ // truncatedCurrentVestingDurationSecs / finalVestingDurationSecs * linearVestAmount, so we can rewrite as below to avoid 175 │ // rounding errors 176 │ uint112 linearVestAmount = _claim.linearVestAmount * truncatedCurrentVestingDurationSecs / finalVestingDurationSecs; 177 │ 178 │ // Having calculated the linearVestAmount, simply add it to the vested amount 179 │ vestAmt += linearVestAmount; + | } }