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: 1/198
Findings: 3
Award: $3,561.20
🌟 Selected for report: 1
🚀 Solo Findings: 1
🌟 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
The constructor of VariableSupplyERC20Token
takes in a maxSupply_
argument, described in the NatSpec as
@param maxSupply_ - What's the maximum supply. The contract won't allow minting over this amount. Set to 0 for no limit.
Also in the mint()
method we have the comment
// If we're using maxSupply, we need to make sure we respect it
The issue is that this maxSupply_
can be bypassed easily.
Steps:
maxSupply_
argument value in the constructormintableSupply
holds this valuemintableSupply
is more than zero, and if it is this means we have a “maxSupply” setmintableSupply
by passing the same value as amount
in mint()
so aftermintableSupply -= amount;
now mintableSupply
is zero
mint()
the check from step 3. is bypassed because mintableSupply
is zero and the admin can mint tokens endlesslyIf the token admin is compromised or malicious he can mint endless amounts of tokens and dump them on an exchange, basically making the other tokens worthless. This will result in big value losses in all token holders and vesting participants.
Save maxSupply
in a storage variable and check totalSupply
against it when calling mint()
#0 - 0xean
2022-09-23T23:58:06Z
dupe of #3
#1 - pashov
2022-09-27T19:17:57Z
@0xean This is more like a duplicate of #3, not #103
#2 - 0xean
2022-09-27T19:20:51Z
yup my bad, it was correct in the judging sheet but wrong here. Updated my comment above.
🌟 Selected for report: pashov
3011.5992 USDC - $3,011.60
In the _baseVestedAmount
of VTVLVesting.sol
we see the following code
uint40 finalVestingDurationSecs = _claim.endTimestamp - _claim.startTimestamp; // length of the interval uint112 linearVestAmount = _claim.linearVestAmount * truncatedCurrentVestingDurationSecs / finalVestingDurationSecs;
Let’s look at truncatedCurrentVestingDurationSecs
as just the duration passed from the start of the vesting period for the PoC (this doesn’t omit important data in this context).
Now think of the following scenario:
We have a token $TKN that has 6 decimals (those are the decimals of both USDT & USDC). We want to distribute 10,000 of those tokens to a user vested over a 10 year period.
10 years in seconds is 315360000 ****(this is finalVestingDurationSecs
)
This means that we will distribute 10,000 * 10^6 = 10 000 000 000 fractions of a token for 315360000 seconds, meaning we will distribute 310 fractions of a token each second - this is linearVestAmount
Now, since finalVestingDurationSecs
is so big (315360000) it will almost always round linearVestAmount
to zero when dividing by it, up until
_claim.linearVestAmount * truncatedCurrentVestingDurationSecs
becomes a bigger number than 315360000, but since _claim.linearVestAmount
is 310 we will need the current vesting duration to be at least 1 017 290 seconds which is 12 days postponed vesting. 12 days in a moving market can make a big difference if the user was expecting the tokens to start vesting from the first day.
Unexpected postponing of vesting can result in waiting times for users to receive their must-be-vested tokens. This period can be used by other token holders to dump the token and decrease the price of it, resulting in a loss of capital for the vesting receiver.
Enforce the contract to work with only 18 decimal tokens with a require
statement in the constructor.
#0 - 0xean
2022-09-24T19:35:00Z
Downgrading to M, there is a lot of external factors presented here by the warden to line up to a loss of funds.
#1 - lawrencehui
2022-10-07T01:02:00Z
I acknowledge the warden's concern of the rounding but I think the result of loss of funds is one of the extreme edge case. I would suggest instead of restricting only to 18 decimal tokens (which is impractical as we would also want to include USDC and USDT for vesting too!), I would implement the rounding checking in the frontend UI and prompt user of potential delay caused by rounding / truncation as described in this issue.
#2 - 0xean
2022-10-07T22:58:44Z
Given that smart contracts can be interacted with in any number of ways (etherscan, programmatically, etc), I don't think the mitigation negates the risk entirely and am going to stick with the M severity here. The wardens demonstrates clearly the way in which this can happen. While it may be a bit outside of the normal vesting schedule expected I do think its valuable to understand the bounds of the math you have employed here.
🌟 Selected for report: TomJ
Also found by: ayeslick, csanuragjain, pashov
The function _createClaimUnchecked()
has input data validation, but it is missing one for endTimestamp
apart from checking that it is later than startTimestamp
. In the code we see the following comment
// Do we need to check whether _startTimestamp is greater than the current block.timestamp? // Or do we allow schedules that started in the past? // -> Conclusion: we want to allow this, for founders that might have forgotten to add some users, or to avoid issues with transactions not going through because of discoordination between block.timestamp and sender's local time
This is fine, but we should check that endTimestamp
is after block.timestamp
- if not, the funds will be immediately vested, since the time has passed. This can be the case if the contract owner puts wrong startTimestamp
and endTimestamp
for example due to a front-end bug or just wrong input.
This issue can result in funds immediately vested to user(employee), which is not a desired outcome of this contract. If the user gets the tokens immediately he can just go and dump them on the market, resulting in price crash which is loss of funds for every holder.
Add the following check
require(_endTimestamp > block.timestamp, "INVALID_END_TIMESTAMP");
#0 - 0xean
2022-09-25T19:22:53Z
dupe of #292