Platform: Code4rena
Start Date: 14/09/2022
Pot Size: $50,000 USDC
Total HM: 25
Participants: 110
Period: 5 days
Judge: hickuphh3
Total Solo HM: 9
Id: 162
League: ETH
Rank: 6/110
Findings: 7
Award: $2,541.42
🌟 Selected for report: 2
🚀 Solo Findings: 1
🌟 Selected for report: csanuragjain
Also found by: Lambda, R2, bin2chen, datapunk, rbserver, unforgiven
390.0123 USDC - $390.01
https://github.com/code-423n4/2022-09-y2k-finance/blob/main/src/Controller.sol#L96
Depeg event can still happen when the price of a pegged asset is equal to the strike price of a Vault which is incorrect.
This docs clearly mentions:
"When the price of a pegged asset is below the strike price of a Vault, a Keeper(could be anyone) will trigger the depeg event and both Vaults(hedge and risk) will swap their total assets with the other party." - https://code4rena.com/contests/2022-09-y2k-finance-contest
Assume strike price of vault is 1 and current price of pegged asset is also 1
User calls triggerDepeg function which calls isDisaster modifier to check the depeg eligibility
Now lets see isDisaster modifier
modifier isDisaster(uint256 marketIndex, uint256 epochEnd) { address[] memory vaultsAddress = vaultFactory.getVaults(marketIndex); if( vaultsAddress.length != VAULTS_LENGTH ) revert MarketDoesNotExist(marketIndex); address vaultAddress = vaultsAddress[0]; Vault vault = Vault(vaultAddress); if(vault.idExists(epochEnd) == false) revert EpochNotExist(); if( vault.strikePrice() < getLatestPrice(vault.tokenInsured()) ) revert PriceNotAtStrikePrice(getLatestPrice(vault.tokenInsured())); if( vault.idEpochBegin(epochEnd) > block.timestamp) revert EpochNotStarted(); if( block.timestamp > epochEnd ) revert EpochExpired(); _; }
if( vault.strikePrice() < getLatestPrice(vault.tokenInsured()) ) revert PriceNotAtStrikePrice(getLatestPrice(vault.tokenInsured()));
Since in our case price of vault=price of pegged asset so if condition does not execute and finally isDisaster completes without any revert meaning go ahead of depeg
But this is incorrect since price is still not below strike price and is just equal
Change the isDisaster modifier to revert when price of a pegged asset is equal to the strike price of a Vault
if( vault.strikePrice() <= getLatestPrice(vault.tokenInsured()) ) revert PriceNotAtStrikePrice(getLatestPrice(vault.tokenInsured()));
#0 - MiguelBits
2022-09-23T14:51:18Z
After discussion , the docs clearly state only below the strike Price
This docs clearly mentions: "When the price of a pegged asset is below the strike price of a Vault, a Keeper(could be anyone) will trigger the depeg event and both Vaults(hedge and risk) will swap their total assets with the other party." - https://code4rena.com/contests/2022-09-y2k-finance-contest
#1 - csanuragjain
2022-09-25T16:04:06Z
@MiguelBits Exactly when it is below the strike price but in this case depeg is happening when price is equal and not below Can you please suggest
#2 - MiguelBits
2022-09-25T16:19:26Z
Oh I see what you mean, need to correct it!
#3 - HickupHH3
2022-10-17T14:01:15Z
Ah, a matter of when the equality sign matters a lot. Critically, in this case. Agree with warden that it should be <=
and not <
only.
🌟 Selected for report: Lambda
Also found by: Deivitto, R2, Rolezn, csanuragjain
155.5605 USDC - $155.56
https://github.com/code-423n4/2022-09-y2k-finance/blob/main/src/SemiFungibleVault.sol#L96
Fee on transfer not considered on asset token at deposit function. This means users will mint more tokens than the funds which were actually deposited by user.
function deposit( uint256 id, uint256 assets, address receiver ) public virtual returns (uint256 shares) { // Check for rounding error since we round down in previewDeposit. require((shares = previewDeposit(id, assets)) != 0, "ZERO_SHARES"); // Need to transfer before minting or ERC777s could reenter. asset.safeTransferFrom(msg.sender, address(this), assets); _mint(receiver, id, shares, EMPTY); emit Deposit(msg.sender, receiver, id, assets, shares); afterDeposit(id, assets, shares); }
deposit is called with amount 100
Below line transfer funds to contract. Since 10% is fees so contract receive only 100-10% = 90
asset.safeTransferFrom(msg.sender, address(this), assets);
require((shares = previewDeposit(id, assets)) != 0, "ZERO_SHARES"); _mint(receiver, id, shares, EMPTY);
Compute the balance before and post transfer and subtract them to get actual received amount
uint256 before = asset.balanceOf(address(this)); asset.safeTransferFrom(msg.sender, address(this), assets); uint256 after = asset.balanceOf(address(this)); assets=after-before; require((shares = previewDeposit(id, assets)) != 0, "ZERO_SHARES");
#0 - HickupHH3
2022-10-31T14:14:04Z
Valid because SemiFungibleVault
may not be applied to WETH only (unlike Vault
), but generic tokens. Furthermore, if there is an intention to make semi fungible vaults an EIP standard, then one may have to consider catering to FoT tokens as well, unless explicitly stated otherwise.
#1 - HickupHH3
2022-10-31T14:15:20Z
dup of #221
🌟 Selected for report: cccz
Also found by: Respx, Saintcode_, csanuragjain, fatherOfBlocks, pashov, unforgiven
90.0028 USDC - $90.00
https://github.com/code-423n4/2022-09-y2k-finance/blob/main/src/rewards/StakingRewards.sol#L213
The recoverERC20 function only checks tokenAddress != address(stakingToken) which means Admin at any time is allowed to extract the Reward token.
function recoverERC20(address tokenAddress, uint256 tokenAmount) external onlyOwner { require( tokenAddress != address(stakingToken), "Cannot withdraw the staking token" ); ERC20(tokenAddress).safeTransfer(owner, tokenAmount); emit Recovered(tokenAddress, tokenAmount); }
Revise the function as below:
function recoverERC20(address tokenAddress, uint256 tokenAmount) external onlyOwner { require( tokenAddress != address(stakingToken) && tokenAddress != address(rewardsToken), "Cannot withdraw the staking token" ); }
#0 - HickupHH3
2022-10-29T09:44:49Z
dup #49
🌟 Selected for report: cccz
Also found by: csanuragjain, pashov
320.0832 USDC - $320.08
https://github.com/code-423n4/2022-09-y2k-finance/blob/main/src/rewards/StakingRewards.sol#L183
The contract only checks if balanceOf rewardsToken is greater than or equal to the future rewards. The current checks are not sufficient to make sure the contract has enough amount of rewardsToken.
Consider changing the function notifyRewardAmount to addRward and use transferFrom to transfer rewardsToken into the contract in the same function
https://github.com/code-423n4/2022-02-concur-findings/issues/209
#0 - HickupHH3
2022-10-31T14:24:30Z
dup #50
8.0071 USDC - $8.01
https://github.com/code-423n4/2022-09-y2k-finance/blob/main/src/oracles/PegOracle.sol#L63
In latestRoundData function, Validations are not performed over priceFeed1.latestRoundData()
require(price1 > 0, "Chainlink price <= 0"); require( answeredInRound1 >= roundID1, "RoundID from Oracle is outdated!" ); require(timeStamp1 != 0, "Timestamp == 0 !");
A function getOracle1_Price function is already implemented having required checks. Seems like this was meant to call getOracle1_Price function only which was mistakenly missed
int256 price1 = getOracle1_Price();
#0 - HickupHH3
2022-11-01T10:13:44Z
dup #61
🌟 Selected for report: csanuragjain
1541.1415 USDC - $1,541.14
https://github.com/code-423n4/2022-09-y2k-finance/blob/main/src/rewards/StakingRewards.sol#L183
If there is no deposit for sometime in start then reward for those period is never used
rewardRate = reward.div(rewardsDuration);
On very first deposit better to have (block.timestamp-startTime) * rewardRate amount of reward being marked unused which can be used in next notifyrewardamount
#0 - HickupHH3
2022-11-01T10:18:35Z
I see this as protocol leaked value since the rewards would be "lost" and isn't attributed to anyone.
Currently, the sweeper function allows the reward token to be withdrawn, thus providing a form of recovery. However, #49 and its dups points out that this is a vuln, and if fixed, will remove this recovery.
🌟 Selected for report: Respx
Also found by: 0x1f8b, 0xDecorativePineapple, 0xNazgul, 0xPanas, 0xSmartContract, 0xc0ffEE, 0xmuxyz, Aymen0909, Bahurum, Bnke0x0, CodingNameKiki, Deivitto, Jeiwan, Lambda, Picodes, PwnPatrol, R2, RaymondFam, Rolezn, Ruhum, Saintcode_, SooYa, Tointer, V_B, ajtra, ak1, async, auditor0517, brgltd, c3phas, carrotsmuggler, cccz, csanuragjain, datapunk, djxploit, durianSausage, eierina, erictee, gogo, imare, joestakey, jonatascm, kv, ladboy233, leosathya, lukris02, oyc_109, pashov, pauliax, rbserver, robee, rokinot, rvierdiiev, scaraven, simon135, unforgiven, wagmi, zzzitron
36.6223 USDC - $36.62
Contract: https://github.com/code-423n4/2022-09-y2k-finance/blob/main/src/rewards/StakingRewards.sol#L225
Issue: The contract currently allows Owner to set duration as 0 which means notifyRewardAmount will always fail (division by zero). Also there seems to be no min/max cap on reward duration which highly impacts how much rewards are distributed
rewardRate = reward.div(rewardsDuration);
Recommendation: Do not allow 0 reward duration and also set a min/max cap for reward duration
Contract: https://github.com/code-423n4/2022-09-y2k-finance/blob/main/src/rewards/StakingRewards.sol#L82
Issue: The constructor has currently no check to see if stakingToken==rewardsToken. This could cause blunder where user staked token will be sent out as rewards
Recommendation: Add below check in constructor
require(_stakingToken!=_rewardsToken);
Contract: https://github.com/code-423n4/2022-09-y2k-finance/blob/main/src/VaultFactory.sol#L195
Issue: In createNewMarket function, The first market index starts with 1 as seen at VaultFactory.sol#L195. However checks in function like deployMoreAssets fails to consider this and only checks index > marketIndex which means 0 will be considered as valid market index when it is not
Recommendation: Consider adding one more check index!=0 to verify for correct market index