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: 2/99
Findings: 3
Award: $4,126.36
🌟 Selected for report: 1
🚀 Solo Findings: 1
🌟 Selected for report: Picodes
4039.0029 USDC - $4,039.00
https://github.com/code-423n4/2022-06-yieldy/blob/524f3b83522125fb7d4677fa7a7e5ba5a2c0fe67/src/contracts/LiquidityReserve.sol#L126 https://github.com/code-423n4/2022-06-yieldy/blob/524f3b83522125fb7d4677fa7a7e5ba5a2c0fe67/src/contracts/LiquidityReserve.sol#L176 https://github.com/code-423n4/2022-06-yieldy/blob/524f3b83522125fb7d4677fa7a7e5ba5a2c0fe67/src/contracts/LiquidityReserve.sol#L206
Using sandwich attacks and JIT (Just-in-time liquidity), the yield of LiquidityReserve
could be extracted for liquidity providers.
The yield of LiquidityReserve
is distributed when a user calls instantUnstakeReserve()
in Staking
. Then, in instantUnstake
, totalLockedValue
increases with the fee paid by the user withdrawing. The fee is shared between all liquidity providers as they all see the value of their shares increase.
Therefore, an attacker could do the following sandwich attack when spotting a call to instantUnstakeReserve()
.
stakingToken
and addLiquidity
leading to a fee of say
x`removeLiquidity
and repay the loan, taking a large proportion of the user feeThe problem here is that you can instantly add and remove liquidity without penalty, and that the yield is instantly distributed.
To mitigate this, you can
#0 - 0xean
2022-06-27T22:14:25Z
This is not unique to the protocol and is a vulnerability in almost all of the LP designs that are prevalent today. There is no loss of user funds here either.
Would downgrade too Low or QA.
#1 - Picodes
2022-06-28T22:47:31Z
In standard cases of JIT, for example in a DEX, the attacker takes a risk as the liquidity he adds is used during the swap, and this liquidity is useful for the protocol as leads to a better price for the user, which is not the case here
#2 - 0xean
2022-06-28T23:03:17Z
@Picodes - that is fair but the liquidity is still useful and I still don't see how this qualifies as high severity. Eventually it would mean that the liquidity reserve would need less liquidity parked in it if JITers always where hitting it.
#3 - Picodes
2022-06-29T05:34:14Z
To me it's high because: (correct me if I am missing things)
instantUnstake
100k, it’ll lock all the LP liquidity. But if someone JITs this, the fees will go to the attacker and not the LP which provided the service by accepting to have its liquidity locked.#4 - moose-code
2022-08-01T14:12:46Z
Agree going to leave this as high. Any whale that does a large unstake will be susceptible to having more of the fee's eroded to a predatory sandwich attack which provides no value to the system.
#5 - Picodes
2022-08-18T10:33:27Z
@JeeberC4 what is the duplicate issue ?
#6 - Picodes
2022-08-26T07:21:44Z
@moose-code, @JasoonS, @DenhamPreen this is flagged as a duplicate of H-06, is it a mistake ?
#7 - JasoonS
2022-08-29T17:58:45Z
Thanks, seems it was made a duplicate by mistake.
🌟 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
60.7879 USDC - $60.79
3 Non Critical - 4 Low
Overall the code and documentations are of high standard.
Here and here, the new value is called _vestingPeriod
in both functions, which is confusing and looks like a typo.
_getTokemakBalance
when possible for readabilityHere, you re do _getTokemakBalance
, so it’d be cleaner to use the internal function.
Furthermore line 372 you do a useless casting ITokePool(tokePoolContract)
as tokePoolContract
is already of type ITokePool
.
Here, the comment states: “the only way balance < _amount is when using unstakeAllFromTokemak”, but this in false:
In unstakeAllFromTokemak
, due to this line, you’d have balance = amount
.
For upgradeable contracts, there must be a storage gap to "allow developers to freely add new state variables in the future without compromising the storage compatibility with existing deployments" (quote OpenZeppelin). Otherwise, it may be very difficult to write new implementation code. Without storage gaps, the variables in child contracts might be overwritten by the upgraded base contracts if new variables are added to the base contract.
This is a development best practice, and not a large issue in the case of this audit as the contracts do not inherit one another.
(Credits to code-423n4/2022-05-alchemix-findings#44)
Mitigation Steps:
Recommend adding appropriate storage gaps at the end of the storage upgradeable contracts. Please reference OpenZeppelin upgradeable contract templates.
uint256[50] private __gap;
Code: https://github.com/code-423n4/2022-06-yieldy/blob/524f3b83522125fb7d4677fa7a7e5ba5a2c0fe67/src/contracts/StakingStorage.sol#L7 https://github.com/code-423n4/2022-06-yieldy/blob/524f3b83522125fb7d4677fa7a7e5ba5a2c0fe67/src/contracts/LiquidityReserveStorage.sol#L4 https://github.com/code-423n4/2022-06-yieldy/blob/524f3b83522125fb7d4677fa7a7e5ba5a2c0fe67/src/contracts/YieldyStorage.sol#L6
When using an upgradeable proxy over an implementation it is important to ensure the underlying implementation is initialised in addition to the proxied contract.
Consider the following example where we have a TransparentUpgradeableProxy as contractA and a Implementation as contractB . As a parameter to the constructor of contractA the data is provided to make a delegate call to contractB.initialize() which updates the storage as required in contractA .
Thus, contractB has not been initialized, only contractA has had its state updated. As a result any user is able to call initialize() on contractB , giving themselves full permissions over the contract. There are two reasons why it is not desirable for malicious users to have full control over a contract:
Ensure that the initialize() function is called on all underlying implementations during deployment. This could easily be done by adding in the implementation:
// @custom:oz-upgrades-unsafe-allow constructor constructor() initializer {}
LiquidityReserve
Reference: https://eips.ethereum.org/EIPS/eip-4626
LiquidityReserve
could theoretically be an ERC4626. To facilitate eventual listings, integrations and increase composability, consider respecting the interface as it isn’t far from what you already have. I feel this could even be a medium issue as EIP are what makes DeFi composable, so should be followed when possible.
This also holds for Yieldy
tokens, although it would require more work. The best solution in this case could be to have an ERC4626 wrapper.
But again, I expect this standard to lead to the creation of aggregators or standardize UX. Also it’ll be way more easier for all smart contract based funds managers (ex Yearn) to use your contracts and benefit from your strategy if you respect the standard.
The safety of deployment could be increased: for example it could be checked in Staking
or LiquidityReserves
that the stakingToken
are the same to avoid unusable deployments.
🌟 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
26.5713 USDC - $26.57
[Gas - 01] - Duplicate function
In BatchRequests
, contracts
being public, there is no need for getAddressByIndex
.
Code: https://github.com/code-423n4/2022-06-yieldy/blob/524f3b83522125fb7d4677fa7a7e5ba5a2c0fe67/src/contracts/BatchRequests.sol#L65 https://github.com/code-423n4/2022-06-yieldy/blob/524f3b83522125fb7d4677fa7a7e5ba5a2c0fe67/src/contracts/BatchRequests.sol#L9
[Gas - 02] - When removing an entry from an array, place it at the end of the array before deleting
In BatchRequests
, considering the frequencies of iterations of the contracts
array, you’ll save gas if before removing an entry in contracts
, you place it at the end of the array.
This way you can reduce the length of contracts
by 1, and you save gas and SLOAD on each iteration.
Instead of delete contracts[i];
you can do:
contracts[i] = contracts[contracts.length - 1]; contracts. pop();