Platform: Code4rena
Start Date: 21/06/2022
Pot Size: $50,000 USDC
Total HM: 31
Participants: 99
Period: 5 days
Judges: moose-code, JasoonS, denhampreen
Total Solo HM: 17
Id: 139
League: ETH
Rank: 4/99
Findings: 5
Award: $2,900.65
🌟 Selected for report: 4
🚀 Solo Findings: 2
🌟 Selected for report: 0x52
1211.7009 USDC - $1,211.70
User is able to withdraw unstaked asset sooner than they should be
Unstake() allows the user to bypass the rebase() call by setting _trigger to false. Since rebase() is bypassed, epoch.number could potentially be stale i.e. doesn't match the Tokemak epoch. A user could potentially call unstake() with _trigger = false immediately after an epoch has ended but expiry would be set using the stale epoch.number because it wouldn't be updated by rebase(). This would allow the user to withdraw early before their funds were actually available in the contract because their withdrawal would be considered to be in the epoch before they actually initiated the withdrawal.
Rebase() cannot be optional when calling unstake
#0 - toshiSat
2022-06-28T16:49:30Z
We use a coolDownAmount of 2 to get around this
🌟 Selected for report: 0x52
545.2654 USDC - $545.27
Some user withdrawals won't be available for withdrawal even though it should be
sendWithdrawalRequest can only happen once per cycle. canBatchTransactions (L386) must return true for the actual withdrawal request to happen. It checks in L362 that currentCycleIndex > lastTokeCycleIndex and in L397 of sendWithdrawlRequest, lastTokeCycleIndex = currentCycleIndex. This means that for the rest of the cycle, canBatchTransactions will return false. This means that if a user requests a withdrawal towards the end of epoch after the withdrawal has been submitted but before the end of the epoch, their withdrawal will be set to the current epoch but the actual token amount of their withdrawal won't be processed. This will lead to a discrepancy between the number of tokens withdrawn and the number of tokens allowed to be withdrawn by users, which means that not all users who are "eligible" for withdrawal will actually be able to withdraw because there won't be enough tokens for everyone.
The requirement in L362 should be removed. As noted in the contract, "TOKE's requestWithdrawal overwriting the amount if you call it more than once per cycle". Overwriting the withdrawal is perfectly okay though because it already uses requestWithdrawalAmount which is a cumulative measure of the tokens that need to be withdrawn because it is only decreased when the asset it actually received from a withdrawal
#0 - toshiSat
2022-06-28T16:48:22Z
We get around this by having a coolDownPeriod of 2. It saves on gas and we only have to call sendWithdrawalRequest
once per cycle
🌟 Selected for report: 0x52
Also found by: elprofesor
Rebases can be frontrun with very little token downtime even when warmUpPeriod > 0
A user can call stake the block before epoch.endTime <= block.timestamp, allowing the user to bypass the forced rebase called in L416 of the the stake function. If warmUpPeriod > 0 then the user will receive a "warmUpInfo" with the value of their deposit. The very next block, the user can then call instantUnstakeCurve.
This will call rebase again in L633 and this time epoch.endTime <= block.timestamp will be true and it will trigger an actual rebase, distributing the pending rewards. _retrieveBalanceFromUser (L617) will then allows the user to unstake all the funds locked in warm up. The issue is that when unstaking it uses userWarmInfo.credits meaning that any rebalance rewards are kept. This allows the user to get in, collect the rebase, then immediately get out.
Being able to unstake tokens even when in the warm up period is a useful feature but tokens unstaked during that period should not be allowed to accumulate any rebases or it can lead to situations like this. L537-L539 should be changed to: warmUpBalance = userWarmInfo.amount
#0 - toshiSat
2022-06-28T16:44:47Z
duplicate #226
🌟 Selected for report: 0x52
Also found by: berndartmueller
545.2654 USDC - $545.27
Users of moveFundsToUpgradedContract() in migration.sol may forfeit rebase rewards
L54 calls instantUnstake(false) meaning that it skips the optional rebase. If there is a pending rebase then the user calling the function will not get this rebase and miss out on potential rewards.
Add an input bool _trigger then add the following code to the start of the function:
if (_trigger) { rebase(); }
This allows users to optionally call rebase if they are concerned about losing pending rebase rewards
#0 - toshiSat
2022-06-28T16:34:37Z
duplicate #276
🌟 Selected for report: IllIllI
Also found by: 0x1337, 0x1f8b, 0x29A, 0x52, 0xDjango, 0xNazgul, 0xNineDec, 0xc0ffEE, 0xf15ers, 0xmint, Bnke0x0, BowTiedWardens, Chom, ElKu, FudgyDRS, Funen, GalloDaSballo, GimelSec, JC, Kaiziron, Lambda, Limbooo, Metatron, MiloTruck, Noah3o6, Picodes, PumpkingWok, PwnedNoMore, Sm4rty, StErMi, TomJ, TrungOre, UnusualTurtle, Waze, _Adam, aga7hokakological, ak1, antonttc, berndartmueller, cccz, cryptphi, csanuragjain, defsec, delfin454000, dipp, elprofesor, exd0tpy, fatherOfBlocks, hake, hansfriese, hubble, joestakey, kenta, ladboy233, mics, oyc_109, pashov, pedr02b2, reassor, robee, samruna, scaraven, shung, sikorico, simon135, sseefried, tchkvsky, unforgiven, zzzitron
53.1412 USDC - $53.14
moveFundsToUpgradedContract() will fail because of fee
In L54 the function uses instantUnstake to unstake but it doesn't account for the fees taken by it. Because of this fee, it won't receive the full amount and then staking in L56 will fail because it tries to call the balance before fees
Take the balance of the token before and after instantUnstake to figure out how much is actually unstaked then call L56 with that update balance
#0 - toshiSat
2022-06-28T16:35:22Z
duplicate #64