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: 3/198
Findings: 4
Award: $1,611.82
🌟 Selected for report: 1
🚀 Solo Findings: 0
1355.2196 USDC - $1,355.22
https://github.com/code-423n4/2022-09-vtvl/blob/f68b7f3e61dad0d873b5b5a1e8126b839afeab5f/contracts/VTVLVesting.sol#L183-L187 https://github.com/code-423n4/2022-09-vtvl/blob/f68b7f3e61dad0d873b5b5a1e8126b839afeab5f/contracts/VTVLVesting.sol#L198
As the comments in _baseVestedAmount()
explain, once there is any _claim.amountWithdrawn
, it will be returned if it is greater than the calculated value vestAmt
. However, vestAmt
takes account of time, _referenceTs
, whereas _claim.amountWithdrawn
is always the amount withdrawn to date. Therefore, for all historical values below _claim.amountWithdrawn
, including timestamps before _claim.startTimestamp
and before _claim.cliffReleaseTimestamp
, _claim.amountWithdrawn
will be returned.
Given that VTVL is intended to be an accessible platform for use by a wide variety of users, this behaviour does create a security risk. Consider these scenarios:
vestedAmount()
. Airdrops are especially likely to be interested in historical values. These values would be distorted by how much users have claimed and so would result in an undesirable distribution of resources.VTVLVesting
and attempts to use _baseVestedAmount()
(which is internal
and so can be used by inheriting contracts). The inheriting contract might apportion rewards based on historical usage.VTVLVesting
in future.diff --git a/test/VTVLVesting.ts b/test/VTVLVestingPOC.ts index bb609fb..073e53f 100644 --- a/test/VTVLVesting.ts +++ b/test/VTVLVestingPOC.ts @@ -500,14 +500,37 @@ describe('Revoke Claim', async () => { const recipientAddress = await randomAddress(); const [owner, owner2] = await ethers.getSigners(); - it('allows admin to revoke a valid claim', async () => { + it('POC: WITHDRAWN DATA IS UNRELIABLE', async () => { const {vestingContract} = await createPrefundedVestingContract({tokenName, tokenSymbol, initialSupplyTokens}); - await vestingContract.createClaim(recipientAddress, startTimestamp, endTimestamp, cliffReleaseTimestamp, releaseIntervalSecs, linearVestAmount, cliffAmount); + const startTimestamp2 = startTimestamp.add(releaseIntervalSecs.mul(100)); + const endTimestamp2 = endTimestamp.add(releaseIntervalSecs.mul(100)); + const cliffReleaseTimestamp2 = cliffReleaseTimestamp.add(releaseIntervalSecs.mul(100)); + await vestingContract.createClaim(owner2.address, startTimestamp2, endTimestamp2, cliffReleaseTimestamp2, releaseIntervalSecs, linearVestAmount, cliffAmount); + + // Fast forward to middle of claim + const halfWay = startTimestamp2.toNumber() + (endTimestamp2.toNumber()-startTimestamp2.toNumber())/2; + await ethers.provider.send("evm_mine", [halfWay]); + + let vestAmt = await vestingContract.vestedAmount(owner2.address, startTimestamp); + console.log("NO WITHDRAWAL, BEFORE VEST START: ",vestAmt.toString()); + vestAmt = await vestingContract.vestedAmount(owner2.address, startTimestamp2); + console.log("NO WITHDRAWAL, AT VEST START: ",vestAmt.toString()); + vestAmt = await vestingContract.vestedAmount(owner2.address, halfWay); + console.log("NO WITHDRAWAL, HALF WAY THROUGH VEST: ",vestAmt.toString()); + vestAmt = await vestingContract.vestedAmount(owner2.address, endTimestamp2); + console.log("NO WITHDRAWAL, AT VEST END: ",vestAmt.toString()); + + await (await vestingContract.connect(owner2).withdraw()).wait(); - (await vestingContract.revokeClaim(recipientAddress)).wait(); + vestAmt = await vestingContract.vestedAmount(owner2.address, startTimestamp); + console.log("WITHDRAWAL, BEFORE VEST START: ",vestAmt.toString()); + vestAmt = await vestingContract.vestedAmount(owner2.address, startTimestamp2); + console.log("WITHDRAWAL, AT VEST START: ",vestAmt.toString()); + vestAmt = await vestingContract.vestedAmount(owner2.address, halfWay); + console.log("WITHDRAWAL, HALF WAY THROUGH VEST: ",vestAmt.toString()); + vestAmt = await vestingContract.vestedAmount(owner2.address, endTimestamp2); + console.log("WITHDRAWAL, AT VEST END: ",vestAmt.toString()); - // Make sure it gets reverted - expect(await (await vestingContract.getClaim(recipientAddress)).isActive).to.be.equal(false); }); it('prohibits a random user from revoking a valid claim', async () => {
Manual Inspection
For active claims, there is no reason to consider _claim.amountWithdrawn
, as it will always have been below or equal to vestAmt
at any point in time. So only consider vestAmt
for inactive claims. For them, return the lowest of vestAmt
and _claim.amountWithdrawn
. This will keep the values monotonic with time without distorting the historical values. It will act as though _claim.amountWithdrawn
was withdrawn and the claim was revoked in the block when vestAmt
reached _claim.amountWithdrawn
. That is a distortion, but it is required to provide monotonicity.
#0 - 0xean
2022-09-25T18:52:21Z
Good find.
🌟 Selected for report: Czar102
Also found by: 0xSmartContract, Lambda, Respx, csanuragjain, hansfriese, sashik_eth
This exploit allows an admin to empty out all reward tokens without triggering any revoke events or updating any claims to revoked.
For this scenario to be dangerous, the admin would need to set up arrangements whereby payments were made by other contracts based on the existence of claims in the VTVLVesting
contract, and on those claims not being revoked.
It is conceivable, however, that if VTVL became established wth a decent reputation, than a malicious actor might well be able to convince others to trust the VTVL vesting contract as a trigger condition for payments.
So long as those other contracts do not check _claim.amountWithdrawn
, but rely on the timestamps, values and isActive
flag, they would be tricked by this reentrancy exploit.
This would cause significant damage to the tricked users and also to the VTVL project.
Reentrancy is possible within withdrawAdmin()
by an admin user in very specific circumstances. It requires the token tokenAddress
to respond to transfers by passing control before the transfer. If such a hook exists, the reentrancy would work like this:
numTokensReservedForVesting
.withdrawAdmin()
with _amountRequested
set to numTokensReservedForVesting
.amountRemaining
will be set to (2xnumTokensReservedForVesting
)-numTokensReservedForVesting
= numTokensReservedForVesting
.withdrawAdmin()
with _amountRequested
set to numTokensReservedForVesting
.amountRemaining
will still be set to numTokensReservedForVesting
as the balance of the contract has not yet changed.numTokensReservedForVesting
tokens.numTokensReservedForVesting
completes, emptying the VTVLVesting
contract of tokenAddress
tokens without revoking any claims.Manual Inspection
Use a reentrancy guard on withdrawAdmin()
or, if that is strongly not desired, implement a deposit function for tokenAddress
and have the contract track its own balance internally so that this value can be updated before any transfers.
#0 - 0xean
2022-09-24T21:32:06Z
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
Generally speaking, there are a lot of comments, which is good, but the quality of comments needs to be improved. I have tried to highlight as many technical comment errors as I could, but there are some other general issues.
https://github.com/code-423n4/2022-09-vtvl/blob/f68b7f3e61dad0d873b5b5a1e8126b839afeab5f/contracts/token/VariableSupplyERC20Token.sol#L22 Use the actual variable name in this comment, as on line 23.
https://github.com/code-423n4/2022-09-vtvl/blob/f68b7f3e61dad0d873b5b5a1e8126b839afeab5f/contracts/token/VariableSupplyERC20Token.sol#L30 Write "function", not "fn" for clarity.
https://github.com/code-423n4/2022-09-vtvl/blob/f68b7f3e61dad0d873b5b5a1e8126b839afeab5f/contracts/AccessProtected.sol#L16 The constructor is missing its comment header.
https://github.com/code-423n4/2022-09-vtvl/blob/f68b7f3e61dad0d873b5b5a1e8126b839afeab5f/contracts/AccessProtected.sol#L29
isAdmin()
is missing its comment header.
https://github.com/code-423n4/2022-09-vtvl/blob/f68b7f3e61dad0d873b5b5a1e8126b839afeab5f/contracts/VTVLVesting.sol#L17
https://github.com/code-423n4/2022-09-vtvl/blob/f68b7f3e61dad0d873b5b5a1e8126b839afeab5f/contracts/VTVLVesting.sol#L83
Note that the variable tokenAddress
is actually the token, not its address. The address is address(tokenAddress). Consider renaming this to token
.
https://github.com/code-423n4/2022-09-vtvl/blob/f68b7f3e61dad0d873b5b5a1e8126b839afeab5f/contracts/VTVLVesting.sol#L121
Whilst logically this is correct, it could be better explained. The modifier only require
s one thing, so this could be better expressed.
https://github.com/code-423n4/2022-09-vtvl/blob/f68b7f3e61dad0d873b5b5a1e8126b839afeab5f/contracts/VTVLVesting.sol#L159
Remove this comment, or add significant explanation as to why you are mentioning this here (probably better to remove). Also, cliffReleaseTimestamp
is not allowed to be after startTimestamp
, so it is doubly confusing.
https://github.com/code-423n4/2022-09-vtvl/blob/f68b7f3e61dad0d873b5b5a1e8126b839afeab5f/contracts/VTVLVesting.sol#L258-L260 For clarity and confidence, rewrite these comments as a statement or two, not as a discussion with yourself.
https://github.com/code-423n4/2022-09-vtvl/blob/f68b7f3e61dad0d873b5b5a1e8126b839afeab5f/contracts/VTVLVesting.sol#L245
As _createClaimUnchecked()
consists mostly of checks, consider some alternative names to reflect the fact that it is an internal function without access modifiers: _permissionlessCreateClaim
, _privateCreateClaim
or just _createClaim
https://github.com/code-423n4/2022-09-vtvl/blob/f68b7f3e61dad0d873b5b5a1e8126b839afeab5f/contracts/VTVLVesting.sol#L365
Remove " - if any" from this comment as hasActiveClaim(_msgSender())
on line 364 guarantees there will be a claim.
https://github.com/code-423n4/2022-09-vtvl/blob/f68b7f3e61dad0d873b5b5a1e8126b839afeab5f/contracts/VTVLVesting.sol#L398
For clarity, consider renaming withdrawAdmin
to adminWithdraw()
. Generally, if the noun is first it is the thing acting. If it is second, it is the thing being acted upon.
https://github.com/code-423n4/2022-09-vtvl/blob/f68b7f3e61dad0d873b5b5a1e8126b839afeab5f/contracts/VTVLVesting.sol#L203 As in the summary, this is not the sort of comment that should reach production. It will hurt confidence in the code. If the function should be removed, it should already be gone. If not, this comment certainly should be.
https://github.com/code-423n4/2022-09-vtvl/blob/f68b7f3e61dad0d873b5b5a1e8126b839afeab5f/contracts/VTVLVesting.sol#L266
If you are asking for an opinion, I think it would be good to enforce _cliffReleaseTimestamp == _startTimestamp == _endTimestamp
in that scenario. Regardless, this issue should be decided and this comment should be removed for confidence and clarity.
Technically incorrect comments have been included in this section. Unclear comments are above.
https://github.com/code-423n4/2022-09-vtvl/blob/f68b7f3e61dad0d873b5b5a1e8126b839afeab5f/contracts/AccessProtected.sol#L22 The comment refers to an "owner" but there is no such thing in this contract.
https://github.com/code-423n4/2022-09-vtvl/blob/f68b7f3e61dad0d873b5b5a1e8126b839afeab5f/contracts/token/VariableSupplyERC20Token.sol#L25-L27
To answer the question in the comment: yes, I think you should allow both to be zero. Your reasons for allowing initialSupply_ == 0
are not affected by your reasons for allowing maxSupply_ == 0
, or vice versa. I suggest you remove lines 25-27.
https://github.com/code-423n4/2022-09-vtvl/blob/f68b7f3e61dad0d873b5b5a1e8126b839afeab5f/contracts/AccessProtected.sol#L39-L43
Note that it is possible with setAdmin()
to remove all admins and leave the contract without admins permanently. This might be desirable for some contracts, but consider whether it is wanted for this project. If not, one possible protection is to keep a counter of the number of admins and refuse to remove an admin if it would cause the counter to go below 1. The counter's limit could also be an immutable variable set in the constructor to allow extra flexibility.
https://github.com/code-423n4/2022-09-vtvl/blob/f68b7f3e61dad0d873b5b5a1e8126b839afeab5f/contracts/VTVLVesting.sol#L126
This comment incorrectly states that an existing claim can have its start timestamp modified in createClaim()
. Remove the part in brackets to correct it.
https://github.com/code-423n4/2022-09-vtvl/blob/f68b7f3e61dad0d873b5b5a1e8126b839afeab5f/contracts/VTVLVesting.sol#L41 This comment is incorrect and contradicts the previous line. It should be removed.
https://github.com/code-423n4/2022-09-vtvl/blob/f68b7f3e61dad0d873b5b5a1e8126b839afeab5f/contracts/VTVLVesting.sol#L44 " - released at the cliffReleaseTimestamp" at the end of this comment is incorrect and appears to be accidentally copied from the previous line.
https://github.com/code-423n4/2022-09-vtvl/blob/f68b7f3e61dad0d873b5b5a1e8126b839afeab5f/contracts/VTVLVesting.sol#L120
This comment is technically incorrect. They are not opposites as both modifiers will revert if _claim.startTimestamp == 0
and _claim.isActive == false
.
https://github.com/code-423n4/2022-09-vtvl/blob/f68b7f3e61dad0d873b5b5a1e8126b839afeab5f/contracts/VTVLVesting.sol#L370 Change the comment to "40 bit".
https://github.com/code-423n4/2022-09-vtvl/blob/f68b7f3e61dad0d873b5b5a1e8126b839afeab5f/contracts/VTVLVesting.sol#L245-L304
There is no check that _endTimestamp
is not in the past, even _linearVestAmount > 0
.
https://github.com/code-423n4/2022-09-vtvl/blob/f68b7f3e61dad0d873b5b5a1e8126b839afeab5f/contracts/VTVLVesting.sol#L53
The value of the state variable vestingRecipients
seems a little dubious. It will contain all recipients ever, even those with revoked claims or claims that reached their end time long ago. Consider implementing an enumerable mapping library to have a list which contains only the addresses with active claims.
🌟 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/f68b7f3e61dad0d873b5b5a1e8126b839afeab5f/contracts/VTVLVesting.sol#L256
https://github.com/code-423n4/2022-09-vtvl/blob/f68b7f3e61dad0d873b5b5a1e8126b839afeab5f/contracts/VTVLVesting.sol#L292
allocatedAmount
can be caulculated earlier in the function and then used in the test on line 256.
https://github.com/code-423n4/2022-09-vtvl/blob/f68b7f3e61dad0d873b5b5a1e8126b839afeab5f/contracts/VTVLVesting.sol#L374-L377
usrClaim.amountWithdrawn
is accessed twice from storage. It can be copied into a memory variable on the first access and then that memory variable used on line 377 to save gas.