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: 9/99
Findings: 4
Award: $1,960.72
π Selected for report: 1
π Solo Findings: 1
π Selected for report: 0x1f8b
Also found by: BowTiedWardens, Lambda, StErMi, berndartmueller, csanuragjain, neumo, rfa
119.2495 USDC - $119.25
https://github.com/code-423n4/2022-06-yieldy/blob/524f3b83522125fb7d4677fa7a7e5ba5a2c0fe67/src/contracts/BatchRequests.sol#L50-L59 https://github.com/code-423n4/2022-06-yieldy/blob/524f3b83522125fb7d4677fa7a7e5ba5a2c0fe67/src/contracts/BatchRequests.sol#L33-L44
Removing Yieldy contract addresses from the contracts
array with BatchRequests.removeAddress
replaces the contract address with a zero-address (due to how delete
works).
Each function that loops over the contracts
array or accesses an array item by index, should zero-address check the value before calling any external contract functions. If this zero-address check is missing, an external call to this zero-address will revert.
BatchRequests.canBatchContractByIndex
function canBatchContractByIndex(uint256 _index) external view returns (address, bool) { return ( contracts[_index], IStaking(contracts[_index]).canBatchTransactions() // @audit-info `contracts` with zero-address elements (due to deletion) will revert - add zero-address check and return false ); }
BatchRequests.canBatchContracts
function canBatchContracts() external view returns (Batch[] memory) { uint256 contractsLength = contracts.length; Batch[] memory batch = new Batch[](contractsLength); for (uint256 i; i < contractsLength; ) { bool canBatch = IStaking(contracts[i]).canBatchTransactions(); // @audit-info `contracts` with zero-address elements (due to deletion) will revert - add zero-address check batch[i] = Batch(contracts[i], canBatch); unchecked { ++i; } } return batch; }
Manual review
Add zero-address checks to both mentioned functions.
#0 - toshiSat
2022-06-27T18:35:46Z
sponsor confirmed
π Selected for report: 0x52
Also found by: berndartmueller
Rebasing allows the protocol to "distribute" profit/rewards to Yieldy (and Foxy) token holders by increasing the supply of tokens and increasing the balance of each token holder relative to the token balance (creditBalances
).
The order of rebasing and unstaking matters, as the token balance of an account is calculated based on the current balance (technically it's not the token balance, it's credit balances creditBalances
).
Unstaking (reducing the account balance) before a rebase will make a user loose out on the distributed (rebased) rewards.
Hence, it's always advised to rebase
before any unstaking event or whenever the creditBalances
of an account are reduced.
Impact: Migrating funds from the old Foxy contract to the new Yieldy contracts while having an outstanding rebase (profit was not distributed yet) will lead to a loss of potential rewards from the old staking contract.
IStaking(OLD_CONTRACT).instantUnstake(false); // @audit-info the boolean function parameter determines if a rebase should occur. In this case, no rebase will take place
Manual review
Use true
as a function parameter for instantUnstake
to always rebase and distribute potential rewards.
#0 - toshiSat
2022-06-27T19:30:52Z
dispute severity: This seems low/medium, but you are correct and we will change this.
π Selected for report: berndartmueller
1211.7009 USDC - $1,211.70
The function BatchRequests.sendWithdrawalRequests
allows calling the sendWithdrawalRequests
function on all of the Yieldy contracts at once. However, due to the unbounded for
loop, if many Yieldy contracts are added to contracts
, this function can potentially DoS due to reaching the block gas limit.
BatchRequests.sendWithdrawalRequests
function sendWithdrawalRequests() external { uint256 contractsLength = contracts.length; for (uint256 i; i < contractsLength; ) { if ( contracts[i] != address(0) && IStaking(contracts[i]).canBatchTransactions() ) { IStaking(contracts[i]).sendWithdrawalRequests(); } unchecked { ++i; } } }
Manual review
Add offset
and limit
function parameters to implement a "paginated" for loop.
#0 - toshiSat
2022-06-27T19:23:08Z
sponsor acknowledged: Only the owner of the contract can add addresses to the contract
#1 - JasoonS
2022-07-28T12:30:11Z
Hmm, would consider making this Low. But keeping it medium highlights the importance to be aware of this.
π 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
84.4957 USDC - $84.50
Instant unstaking usually requires paying fees to the LiquidityReserve
liquidity providers. Hence, a user migrating from an old Yieldy contract to a new contract (via Migration.moveFundsToUpgradedContract
) receives fewer staking tokens after paying fees and therefore userWalletBalance
is more than the available staking tokens in the migration contract.
Usually, fees will be set to 0
for the migration period. But if fees are kept, migrating will revert due to insufficient staking tokens received from the old contract.
As this issue is very easy to fix and it also allows to keep migrators to pay fees, this finding is submitted as medium severity.
Migration.moveFundsToUpgradedContract
function moveFundsToUpgradedContract() external { uint256 userWalletBalance = IYieldy(OLD_YIELDY_TOKEN).balanceOf( msg.sender ); IYieldy(OLD_YIELDY_TOKEN).transferFrom( msg.sender, address(this), userWalletBalance ); IStaking(OLD_CONTRACT).instantUnstake(false); // @audit-info receives less staking tokens than `userWalletBalance` due to fees being payed IStaking(NEW_CONTRACT).stake(userWalletBalance, msg.sender); // @audit-info reverts due to insufficient staking tokens }
Manual review
Use the actual staking token balance IERC20Upgradeable(stakingToken).balanceOf(address(this))
right after unstaking from the old contract:
function moveFundsToUpgradedContract() external { uint256 userWalletBalance = IYieldy(OLD_YIELDY_TOKEN).balanceOf( msg.sender ); IYieldy(OLD_YIELDY_TOKEN).transferFrom( msg.sender, address(this), userWalletBalance ); IStaking(OLD_CONTRACT).instantUnstake(false); IStaking(NEW_CONTRACT).stake(IERC20Upgradeable(stakingToken).balanceOf(address(this)), msg.sender); }
#0 - toshiSat
2022-06-27T17:43:55Z
disagree with severity. We are not going to set fees, but this would allow for fees to still be used, would still be a nice change
#1 - JasoonS
2022-07-28T12:42:54Z
Informational issue