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: 15/99
Findings: 6
Award: $1,214.50
π Selected for report: 0
π Solo Findings: 0
π Selected for report: WatchPug
Also found by: BowTiedWardens, cccz, minhquanym, parashar, pashov, shung, zzzitron
In Staking
contract, users are allowed to stake for another _recipient
. And it also updates warmUpInfo.expiry
for _recipient
address so _recipient
can only claim after this new expiry
is passed.
Attackers can abuse this to constantly stake
1 wei for another users, keep increasing expiry
value and cause users unable to call claim()
Scenario
STAKING_TOKEN
into Staking
stake(1, address(Alice))
right before expiry is passed.claim()
because this check always failedManual Review
Consider remove this feature or add an option for users to allow others stake for themselves.
#0 - toshiSat
2022-06-27T23:40:08Z
duplicate #109
π Selected for report: 0x29A
Also found by: minhquanym
In BatchRequests.removeAddress()
function, it uses delete
keyword to remove contracts[i]
from contracts
array. This actually only set contracts[i] = address(0)
and not do anything with the length of contracts
array. The result is elements of contracts
array is removed but the length of array is still the same (should decrease instead).
It can lead to the case when contracts
array is too large but almost full of address(0)
. And because sendWithdrawalRequests()
is processed the whole array each time, it can lead to out of gas.
Please refer to Solidity docs https://docs.soliditylang.org/en/develop/types.html#delete
Manual Review
We can delete an element of array and update the length of array by swapping the element to the end of array and pop it.
#0 - toshiSat
2022-06-27T23:39:05Z
duplicate #115
π Selected for report: WatchPug
Also found by: minhquanym
In LiquidityReserve, instantUnstake()
will try to claimWithdraw()
to get more funds before transfering to recipient. But in Staking contract, instantUnstakeReserve()
only checks if current balance of reserve is enough (not accounted for unclaimed amount).
This can lead to the case when users should be able to instantUnstake()
after claimWithdraw()
and balance of reserve is increased. But because it fails this check then the TX is failed.
Scenario
instantUnstakeReserve()
with amount = 1100. It should not fail because totalValue
of reserve is now 1500.1000 < 1100
, the TX is failed.Manual Review
Consider remove this check because it will fail if reserve donβt have enough funds anyway or call claimWithdraw()
before check reserve balance.
#0 - toshiSat
2022-06-27T23:35:53Z
duplicate #190
π Selected for report: BowTiedWardens
Also found by: PwnedNoMore, TrungOre, hansfriese, hubble, minhquanym, shung
In Yieldy
contract, data about rebase is stored in rebases
list everytime rebase()
is called and then emits event. It does that by calling _storeRebase()
and pass in _previousCirculating
and push in rebases
data like this
totalStakedBefore: _previousCirculating, totalStakedAfter: _totalSupply,
But because it uses updatedTotalSupply
as _previousCirculating
and _totalSupply
is updated in this line, totalStakedBefore
and totalStakedAfter
is basically the same value.
This bug will have impact on client side (Frontend, Backend) because data emitted is incorrect.
rebase()
function:
_totalSupply = updatedTotalSupply; _storeRebase(updatedTotalSupply, _profit, _epoch);
and in _storeRebase()
function
totalStakedBefore: _previousCirculating, totalStakedAfter: _totalSupply,
Manual Review
Use correct variable to pass in _storeRebase()
function
#0 - toshiSat
2022-06-27T23:31:54Z
duplicate #221
π Selected for report: Chom
Also found by: hansfriese, minhquanym
In Yieldy._mint()
function, total supply of Yieldy
is capped at MAX_SUPPLY
. But the logic is wrong, making the total supply of Yieldy
cannot reach MAX_SUPPLY
(only maximum MAX_SUPPLY - 1
)
require(_totalSupply < MAX_SUPPLY, "Max supply");
This will prevent Staking contract from minting Yieldy token exactly. And also it is inconsistent with rebase()
function which allow to update total supply to maximum MAX_SUPPLY
.
if (updatedTotalSupply > MAX_SUPPLY) { updatedTotalSupply = MAX_SUPPLY; }
MAX_SUPPLY = 10M
, current _totalSupply = 7M
.mint()
to mint _amount = 3M
more Yieldy. But mint()
will revert because total supply after add new amount not smaller than MAX_SUPPLY
._totalSupply + _amount = 10M = maxSupply
Manual Review
Update <
to <=
require(_totalSupply <= MAX_SUPPLY, "Max supply");
#0 - toshiSat
2022-07-06T14:34:30Z
duplicate #200
π Selected for report: BowTiedWardens
Also found by: 0v3rf10w, 0x1f8b, 0x29A, 0xKitsune, 0xNazgul, 0xf15ers, 0xkatana, 0xmint, 8olidity, ACai, Bnke0x0, Chom, ElKu, Fabble, Fitraldys, FudgyDRS, Funen, GalloDaSballo, GimelSec, IllIllI, JC, Kaiziron, Lambda, Limbooo, MiloTruck, Noah3o6, Nyamcil, Picodes, PwnedNoMore, Randyyy, RedOneN, Sm4rty, StErMi, TomJ, Tomio, TrungOre, UnusualTurtle, Waze, _Adam, aga7hokakological, ajtra, antonttc, asutorufos, bardamu, c3phas, defsec, delfin454000, exd0tpy, fatherOfBlocks, hansfriese, ignacio, joestakey, kenta, ladboy233, m_Rassska, mics, minhquanym, oyc_109, pashov, reassor, robee, s3cunda, sach1r0, saian, sashik_eth, scaraven, sikorico, simon135, slywaters
28.1525 USDC - $28.15
In LiquidityReserve.enableLiquidityReserve()
, storage variable stakingContract
is assigned using _stakingContract
so basically they have same value. We should use _stakingContract
to save gas because itβs memory variable.
warmUpInfo
to memory only after making sure we will use it.In Staking.claim()
function, Claim
data is loaded to memory in the beginning of function even when if condition can be failed.
Should put it inside if
block to save gas like this
if (_isClaimAvailable(_recipient)) { Claim memory info = warmUpInfo[_recipient]; // do something }