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: 21/198
Findings: 1
Award: $388.92
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: Trust
Also found by: 0xSky, CertoraInc, KIntern_NA, bin2chen, hansfriese, neko_nyaa, neumo, rokinot, wastewa
If a Claim
object (for a user) has a large enough vesting time and/or linear vesting amount, then that object can get into a state such that the calculations in _baseVestedAmount
overflow. If the _baseVestedAmount
calculations overflow, then the associated tokens are locked into the contract with no way to retrieve them, because revokeClaim
and withdraw
both rely on this calculation to succeed.
In my PoC below, I show that it is not unreasonable to hit this state. It is very common for employees to vest over 4 years (~1.26*10^(8)
seconds) and for token amounts to go into billions.
On line 167 of VTVLVesting.sol
, the calculation is:
uint112 linearVestAmount = _claim.linearVestAmount * truncatedCurrentVestingDurationSecs / finalVestingDurationSecs;
This calculation overflows if _claim.linearVestAmount * truncatedCurrentVestingDurationSecs
exceeds the max value of uint112
, as solidity does the math operations with the largest type used.
This is very easy to do as I show below (with very reasonable values):
it("create unclaimable vesting schedule", async() => { const [ recip1 ] = await ethers.getSigners() // token params let tokenName = "company Shares" let tokenSymbol = "aapl" let initialSupplyTokens = 6 * (10**9) // 6 billion shares const { tokenContract, vestingContract } = await createPrefundedVestingContract({tokenName, tokenSymbol, initialSupplyTokens}); // claim params const vestDuration = 60 * 60 * 24 * 365 // vesting period is a year const startTimestamp = await getLastBlockTs() - vestDuration; const endTimestamp = startTimestamp + vestDuration; const releaseIntervalSecs = 10; const cliffAmount = BigNumber.from(0); const cliffReleaseTimestamp = 0; const linearVestAmount = ethers.constants.WeiPerEther.mul(initialSupplyTokens); // initial supply * 10 ** 18 (decimal amount) // make claim await vestingContract.createClaim(recip1.address, startTimestamp, endTimestamp, cliffReleaseTimestamp, releaseIntervalSecs, linearVestAmount, cliffAmount); // withdraw and fail due to overflow exception await expect(vestingContract.connect(recip1).withdraw()).to.be.reverted // no function that depends on _baseVestedAmount calculation will work for user await expect(vestingContract.revokeClaim(recip1.address)).to.be.reverted })
Used ts and chai tests already in project (I stuck my PoC code snippet into VTVLVesting.ts
to run it)
A very easy fix is to make a temporary variable that is type uint256
(like making truncatedCurrentVestingDurationSecs uint256
just for the calculation) to use in the multiplication part of the calculation, this makes it possible for multiplication part of the calculation to exceed the max value uint112
(resulting value will definitely fit into a uint112
because the maximum value of this calculation is _claim.linearVestAmount
, which is of type uint112
).
Would not recommend capping values as a fix, as it is would heavily limit use of code.
#0 - 0xean
2022-09-24T19:20:16Z
dupe of #95