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: 12/198
Findings: 4
Award: $552.99
🌟 Selected for report: 0
🚀 Solo Findings: 0
The system has measures in place to prevent the admin from withdrawing an amount that would result in a balance which is lower then the remaining tokens reserved for vesting (which should never happen according to the protocol documents: "They can also withdraw the remaining amount not allocated on claims back to their wallet."). However, the whole balance can be withdrawn using withdrawOtherTokens
for some specific tokens. Namely, there are tokens that have multiple entrypoints, but the check in withdrawOtherToken
only checks if the addresses are different. Note that these tokens are not some obscure tokens that nobody uses, Compound was almost hit by a very similar problem, because TrueUSD had two entrypoints.
Let's say that some token with two addresses, address(A)
and address(B)
is used as the underlying token. tokenAddress
is set to address(A)
. The admin can now call withdrawOtherToken(address(B))
and drain the whole contract balance. This does not even emit some special events, so it will be very hard for the end user to detect the reason for it.
Check that the withdrawal does not decrease the balance of the underlying token.
#0 - 0xean
2022-09-24T21:24:36Z
dupe of #429 - worth noting TrueUSD disabled this functionality, so this is rare.
🌟 Selected for report: Czar102
Also found by: 0xSmartContract, Lambda, Respx, csanuragjain, hansfriese, sashik_eth
https://github.com/code-423n4/2022-09-vtvl/blob/26dda235d38d0f870c1741c9f7eef03229172bbe/contracts/VTVLVesting.sol#L407 https://github.com/code-423n4/2022-09-vtvl/blob/26dda235d38d0f870c1741c9f7eef03229172bbe/contracts/VTVLVesting.sol#L388
The system has measures in place to prevent the admin from withdrawing an amount that would result in a balance which is lower then the remaining tokens reserved for vesting (which should never happen according to the protocol documents: "They can also withdraw the remaining amount not allocated on claims back to their wallet."). However, when an ERC777 token is used as the underlying token (which works without problems and probably will be done by users, as ERC777 is backwards compatible with ERC20), this check can be circumvented (see Proof Of Concept). This allows administrators to drain the whole contract and leads to a situation where tokenAddress.balanceOf(address(this)) < numTokensReservedForVesting
.
withdrawAdmin(50_000)
.tokensToSend
hook on the smart contract. Now, the contract can call withdrawAdmin(50_000)
within the hook again. Note that tokenAddress.balanceOf(address(this))
is still 1,000,000 (because the state was not yet updated), so the amountRemaining
check succeeds.Note that this is not the only way to exploit ERC777 reentrancy. It could also be exploited by the admin in withdraw
(when there is a claim for the admin) and instead of calling withdrawAdmin
repeatedly, the admin could also call createClaim
in the hook to create a claim that pushes numTokensReservedForVesting
over the balance. This could be even harder for users to detect, because there would be no events for repeated withdrawals (only a claim creation, which is generally a normal operation, with the only difference that this claim should not have been allowed).
Add a reentrancy guard to withdraw
, withdrawAdmin
, and createClaim
.
#0 - 0xean
2022-09-24T21:16:20Z
dupe of #6
🌟 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.8655 USDC - $18.87
uint112
for amounts (especially for numTokensReservedForVesting
, which is the global sum) can be insufficient for certain tokens. Consider a token with 24 decimals and where one token has a value of 0.0000001 USD. The maximum amount for which the protocol can be used is 2^112 / 10^24 * 0.0000001 USD = 519.23 USD, which is extremely low.🌟 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
require(amount <= mintableSupply, "INVALID_AMOUNT");
in VariableSupplyERC20Token.mint
is unnecessary, because the subtraction on the next line will underflow anyways if this is not true.mintableSupply
in VariableSupplyERC20Token.mint
is not necessary. The remaining mintable supply could always be calculated based on the initialSupply_
(if it would be stored), the maximum supply and the already minted tokens (standard ERC20 variable _totalSupply
). The remaining supply is then maxSupply_ - _totalSupply + initialSupply_
.VTVLVesting.sol:353
, the loop iteration can be marked as unchecked
because an overflow is not possible (as the iterator is bounded) and i++
can be replaced with ++i
.