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: 28/198
Findings: 4
Award: $270.52
🌟 Selected for report: 0
🚀 Solo Findings: 0
218.0935 USDC - $218.09
When a claim is revoked, all pending withdrawals the user has already earned are pulled back.
Since the docs state that the purpose of revoking claims is to allow for early employment termination, the intended behavior should be to allow the employee to withdraw what they have already earned, and revoke all earnings going forward.
There shouldn't be a discrepancy in earnings based on how often a user withdraws their funds, and users should be comfortable leaving their earned funds in the platform without putting them at risk.
In revokeClaim()
, the amountRemaining
is calculated by subtracting the amount withdrawn from the total vesting amount. The claim is then set to inactive, which locks withdrawals going forward.
However, the amount withdrawn does not represent the full amount they have earned, as it does not included the vestedAmount()
for the period since their last withdrawal.
For any users who have earned income in their account (ie if they had called withdraw()
before the claim was revoked, they had income due to withdraw), revoking their claim will effectively steal that income.
Manual Review, Foundry
Implement logic similar to withdraw()
at the beginning of revokeClaim()
: checking the user's vested amount, paying it out, and adjusting their balances.
#0 - indijanc
2022-09-24T17:44:41Z
Duplicate of #475
#1 - 0xean
2022-09-24T18:29:52Z
closing as dupe
🌟 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
When maxSupply
is set in the VariableSupplyERC20Token
, the expectation is that this is the maximum number of tokens that can be minted.
However, since this supply cap is only checked when mintableSupply > 0
, the result is that once the mintableSupply
has been reduced to zero, the supply cap will turn off and unlimited tokens will be available to mint.
In the mint()
function, the following logic checks the supply cap before minting
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; }
As you can see, the logic is only checked if mintableSupply > 0
. This is intended to have 0 used as an "override" value that allows for unlimited minting.
However, mintableSupply
is adjusted as new tokens are minted, and represents the number of tokens remaining in the supply cap. Once maxSupply
tokens have been minted, the mintableSupply
will then be set to 0, and the check will be skipped, allowing unlimited tokens to be minted.
Manual Review, Foundry
Instead of using 0 as the variable that bypasses the check, use type(uint256).max
.
#0 - 0xean
2022-09-24T00:29:07Z
dupe of #3
🌟 Selected for report: rajatbeladiya
Also found by: 0x4non, CertoraInc, Chom, JLevick, JohnSmith, KIntern_NA, Ruhum, RustyRabbit, ak1, berndartmueller, imare, joestakey, obront, rbserver, rotcivegaf, supernova
32.8268 USDC - $32.83
https://github.com/code-423n4/2022-09-vtvl/blob/f68b7f3e61dad0d873b5b5a1e8126b839afeab5f/contracts/VTVLVesting.sol#L253 https://github.com/code-423n4/2022-09-vtvl/blob/f68b7f3e61dad0d873b5b5a1e8126b839afeab5f/contracts/VTVLVesting.sol#L123-L129
There are many situations where an admin may want to start a new claim for a user with a past claim. For example, they may want to offer an employee an additional pool of options to vest, or they may have revoked a claim accidentally, or they may be rehiring an employee who was previously terminated.
In these cases, the protocol does not allow for them to start a new claim for this user.
When _createClaimUnchecked()
is called to create a new claim for a user, it includes the modifier hasNoClaim(_recipient)
.
This check requires that require(_claim.startTimestamp == 0, "CLAIM_ALREADY_EXISTS");
The claim.startTimestamp
is never edited in the protocol. It is set when the claim is created and never changes. Furthermore, claims are stored by address, with only one claim permitted per user.
As a result, admins will not be able to create a new claim for any user who has previously had one.
Manual Review, Foundry
The more complex but thorough solution is to create an architecture that allows for users to have multiple simultaneous claims.
The simpler solution would be to adjust revokeClaim()
so that it resets all information about the claim so that the user has a fresh account for future opportunities. Similarly, withdraw()
could contain logic so that, upon their final withdrawal, a user's account is cleared.
#0 - 0xean
2022-09-24T21:29:20Z
dupe of #140
🌟 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
Admin privileges are stored as a mapping of multiple admins to a boolean flag. Any admin is able to call setAdmin()
, changing the boolean flag for themselves or for any other admin.
This architecture creates the possibility that the final admin could accidentally revoke their own admin capabilities, resulting in no admin able to grant privileges and restore the protocol.
setAdmin()
uses the following logic to set whether a user is an admin or not:
function setAdmin(address admin, bool isEnabled) public onlyAdmin { require(admin != address(0), "INVALID_ADDRESS"); _admins[admin] = isEnabled; emit AdminAccessSet(admin, isEnabled); }
This function checks if the admin being set is address(0)
, but doesn't perform any other check to ensure that the function is safe.
If the final admin were to call setAdmin(myAddress, false)
, the result would be that the protocol would no longer be functional.
Manual Review, Foundry
There are many ways to address this, but the simplest is to not allow an admin to change their own privileges. This ensures that there is always another admin who retains privileges.
Since only admins are able to call this function, the only change that could be made would be to revoke the role, so you can perform a simple check like:
require(admin != _msgSender() "CAN'T REVOKE OWN ADMIN ROLE");
#0 - 0xean
2022-09-23T23:35:56Z
dupe of #469
#1 - 0xean
2022-10-09T23:07:53Z
downgraded to QA