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: 34/198
Findings: 2
Award: $236.95
🌟 Selected for report: 0
🚀 Solo Findings: 0
218.0935 USDC - $218.09
Admin can indirectly withdraw an existing claim by revoking the claim and then using the withdrawAdmin
function.
As per the VTVL contest page:
No one other than the designated vesting recipient (not even the admin) can withdraw on an existing claim - but the admin can revoke the claim.
The above statement has contradicting elements in it. The admin has the power to revoke a claim by using the revokeClaim function. This is okay, but the revokeClaim
function ignores any unclaimed amount of the recipient address
.
uint112 amountRemaining = finalVestAmt - _claim.amountWithdrawn; // and this is subtracted from the state variable as follows: numTokensReservedForVesting -= amountRemaining;
Whatever is not accounted in the numTokensReservedForVesting
variable can be withdrawn by the admin.
As you see in the withdrawAdmin
function:
uint256 amountRemaining = tokenAddress.balanceOf(address(this)) - numTokensReservedForVesting; require(amountRemaining >= _amountRequested, "INSUFFICIENT_BALANCE");
Which means that the unclaimed tokens which should have been belonged to the recipient address, is now added to the free balance
of the contract which the admin can transfer back to himself.
This indirectly is a violation of the statement that, not even the admin can withdraw on an existing claim
.
I added the following POC, in the VTVLVesting.ts
file:
it("Test for loss of unclaimed tokens on claim revoke", async() => { const {tokenContract, vestingContract} = await createPrefundedVestingContract({tokenName, tokenSymbol, initialSupplyTokens}); const startTimestamp = dateToTs('2023-02-01'); const month1 = 30 * 24* 60 * 60; const releaseIntervalSecs = BigNumber.from(month1); // 1 month const endTimestamp = startTimestamp.add(releaseIntervalSecs.mul(48)); // 4 years const cliffReleaseTimestamp = 0; // dateToTs(new Date('2023-02-01')); const linearVestAmount = ethers.utils.parseUnits('100', 18); const cliffAmount = 0; //ethers.utils.parseUnits('10', 18); const [recipientAddress] = await ethers.getSigners(); let tx = await vestingContract.createClaim(recipientAddress.address, startTimestamp, endTimestamp, cliffReleaseTimestamp, releaseIntervalSecs, linearVestAmount, cliffAmount); let currentBalance = await tokenContract.balanceOf(recipientAddress.address); console.log("initialBalance of claim holder= ", ethers.utils.formatEther(currentBalance)); let returnedClaimInfo = await vestingContract.getClaim(recipientAddress.address); expect(tx).to.emit(vestingContract, "ClaimCreated").withArgs(recipientAddress.address, returnedClaimInfo); await ethers.provider.send("evm_mine", [Number(startTimestamp)+month1*6+10]); //6 months fast forwarded let claimableAmount = await vestingContract.claimableAmount(recipientAddress.address); console.log("claimableAmount after 6 months= ", ethers.utils.formatEther(claimableAmount)); console.log("Admin revokes the account..."); tx = await vestingContract.revokeClaim(recipientAddress.address); claimableAmount = await vestingContract.claimableAmount(recipientAddress.address); console.log("claimableAmount after account revoked= ", ethers.utils.formatEther(claimableAmount)); currentBalance = await tokenContract.balanceOf(recipientAddress.address); console.log("Balance of claim holder after claim revoke=", ethers.utils.formatEther(currentBalance)); });
The following console.log prints were the result:
initialBalance of claim holder= 0.0 claimableAmount after 6 months= 12.5 Admin revokes the account... claimableAmount after account revoked= 0.0 Balance of claim holder after claim revoke= 0.0
You can see that after the revoking of the claim, both the claim recipient balance and his claimableAmount are zero.
Manual analysis, Hardhat, VSCode
In the revokeClaim function, send the unclaimed tokens to the claim owner before deactivating the claim. The code lines from the withdraw() function can be borrowed for this.
This will make sure that the vested address doesn't have to live in fear and withdraw the tokens every time they get released at the end of releaseIntervalsSecs
.
#0 - indijanc
2022-09-24T17:42:05Z
Duplicate of #475
#1 - 0xean
2022-09-24T18:29:20Z
closing as duplicate.
🌟 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.8577 USDC - $18.86
createClaimsBatch
function could be modified to avoid unexpected revertsRight now, the createClaimsBatch function repeatedly calls the _createClaimUnchecked internal function. Which in turn contains many require statements before a claim
is created. If any one of these claim creation's revert, then the whole createClaimsBatch
function would revert wasting a significant amount of gas.
I recommend that a try and catch
block approach should be used instead. The try
block could try creating the claim with the _createClaimUnchecked
function and if it reverts, we can add the particular recipient to an address array in the catch
block. This list of failed recipient addresses could be returned to the user, which he can check and make necessary corrections so that he can call the batch function again.
withdrawOtherToken
functionwithdrawOtherToken function should have an emit statement to let the user base know that the admin has withdrawn the other tokens
which were wrongly sent to the contract.
admin
called the functionThe following functions doesn't emit information on which admin called the method. I suggest this information to be emitted as well.