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: 4/110
Findings: 6
Award: $4,255.29
🌟 Selected for report: 4
🚀 Solo Findings: 2
🌟 Selected for report: carrotsmuggler
Also found by: 0x52, 0xDecorativePineapple, 0xPanas, Bahurum, Jeiwan, Lambda, PwnPatrol, R2, Respx, auditor0517, durianSausage, hyh, ladboy233, pauliax, scaraven, teawaterwire, zzzitron
36.6124 USDC - $36.61
PegOracle.sol always returns 0 for assets with 18 decimals due to precision loss, which is a majority of all chainlink oracles
nowPrice = (price2 * 10000) / price1;
nowPrice is set to the ratio between prices scaled to 4 decimals. If the oracle return 18 decimals (as a vast majority of chainlink oracles do) then the price will not be scaled at all. When the ratio is returned, it is divided by 1,000,000. Since the ratio is only scaled to 4 decimals and is being divided 7 decimals the division will always return 0 due to precision loss. Additionally in the contest description it states that it should return as 18 decimals but in no scenario does it return the ratio to 18 decimals.
PegOracle.sol#latestRoundData price calculations should be rewritten as follows:
//price is multiplied by 10**decimals to preserve the underlying decimals in the ratio if (price1 > price2) { nowPrice = (price2 * (10**decimals)) / price1; } else { nowPrice = (price1 * (10**decimals)) / price2; } //ratio is scaled to 18 decimals if not already int256 decimals10 = int256(10**(18 - decimals)); nowPrice = nowPrice * decimals10; return ( roundID1, nowPrice, startedAt1, timeStamp1, answeredInRound1 );
#0 - MiguelBits
2022-10-03T20:01:03Z
#1 - HickupHH3
2022-10-17T10:17:56Z
dup #195
125.2594 USDC - $125.26
Permanent loss of access to funds
At the end of the epoch or during a depeg event, funds are assigned from one vault to the other. In the event that this occurs in a situation where the receiving vault is empty, then funds could be sent to a vault in which there is no one to claim it. This would result in the funds being completely irretrievable. When creating vaults the admin is not required to add funds to both vaults meaning that a situation like this could arise.
Vault pairs should have a override functionality that marks both vaults as ended if either of the two vaults is empty
#0 - HickupHH3
2022-10-18T07:18:29Z
dup of #312
505.5715 USDC - $505.57
Insurance is to protect the user in case the pegged asset drops significantly below the underlying but risk users are required to payout if the pegged asset is worth more than the underlying
if (price1 > price2) { nowPrice = (price2 * 10000) / price1; } else { nowPrice = (price1 * 10000) / price2; }
The above lines calculates the ratio using the lower of the two prices, which means that in the scenario that the pegged asset is worth more than the underlying, a depeg event will be triggered. This is problematic for two reasons. The first is that many pegged assets are designed to maintain at least the value of the underlying. They put very strong incentives to keep the asset from going below the peg but usually use much looser policies to bring the asset down to the peg, since an upward break from the peg is usually considered benign. The second is that when a pegged asset move above the underlying, the users who are holding the asset are benefiting from the appreciation of the asset; therefor the insurance is not needed.
Because of these two reasons, it is my opinion that sellers would demand a higher premium from buyers as a result of the extra risk introduced by the possibility of having to pay out during an upward depeg. It is also my opinion that these higher premiums would push users seeking insurance to other cheaper products that don't include this risk
The ratio returned should always the ratio of the pegged asset to the underlying (i.e. pegged/underlying)
#0 - MiguelBits
2022-10-03T20:00:30Z
#1 - HickupHH3
2022-10-17T13:38:33Z
not a duplicate.
Pegged tokens go both ways: either valued more or less than the asset it's pegging to (underlying token).
The warden is arguing that when the pegged token is worth more than the underlying (eg. worth > $1 for a stablecoin), the users are still eligible for a payout, which he argues shouldnt be the case.
I agree with the warden; from experience, most projects see it as a positive if their algo stablecoin is worth more than the underlying, and so, wouldn't do nothing about it. In fact, they'd probably use it as a shilling point to attract more users to mint more of these tokens to help bring the price down. This scenario should not be covered by the insurers.
#2 - HickupHH3
2022-10-17T13:55:13Z
Agree with rationale of high severity provided by #419:
Setting the severity to be high as that's the violation of core logic with substantial fund loss for all hedge users.
Between this and #419, it's hard to decide which one to be selected for the report. I like how #419 provides numbers to guide the reader. However, this issue gives good reasoning as to why only the "negative" de-peg should be covered.
Ideally, I would select both, but the stronger reasoning edges out.
505.5715 USDC - $505.57
https://github.com/code-423n4/2022-09-y2k-finance/blob/ac3e86f07bc2f1f51148d2265cc897e8b494adf7/src/Vault.sol#L244-L252 https://github.com/code-423n4/2022-09-y2k-finance/blob/ac3e86f07bc2f1f51148d2265cc897e8b494adf7/src/SemiFungibleVault.sol#L205-L213 https://github.com/code-423n4/2022-09-y2k-finance/blob/ac3e86f07bc2f1f51148d2265cc897e8b494adf7/src/SemiFungibleVault.sol#L237-L239 https://github.com/code-423n4/2022-09-y2k-finance/blob/ac3e86f07bc2f1f51148d2265cc897e8b494adf7/src/SemiFungibleVault.sol#L244-L246 https://github.com/code-423n4/2022-09-y2k-finance/blob/ac3e86f07bc2f1f51148d2265cc897e8b494adf7/src/SemiFungibleVault.sol#L251-L258 https://github.com/code-423n4/2022-09-y2k-finance/blob/ac3e86f07bc2f1f51148d2265cc897e8b494adf7/src/SemiFungibleVault.sol#L263-L270
Other protocols that integrate with Y2K may wrongly assume that the functions are EIP-4626 compliant. Thus, it might cause integration problems in the future that can lead to wide range of issues for both parties.
All official EIP-4626 requirements can be found on it's official page. Non-compliant functions are listed below along with the reason they are not compliant:
The following functions are missing but should be present:
The following functions are non-compliant because they don't account for withdraw and deposit locking:
All of the above functions should return 0 when their respective functions are disabled (i.e. maxDeposit should return 0 when deposits are disabled)
previewDeposit is not compliant because it must account for fees which it does not
totalAssets is not compliant because it does not always return the underlying managed by the vault because it fails to include the assets paid out during a depeg or the end of the epoch.
All functions listed above should be modified to meet the specifications of EIP-4626
#0 - HickupHH3
2022-10-17T04:10:19Z
The premise is valid because it's stated in the README:
Y2K leverages ERC4626 Vault standard for this protocol, this contract is a fork of that standard, although we replaced all uses of ERC20 to ERC1155.
As per the ruling in a previous contest regarding EIP4626.
Judging this and all duplicate regarding EIP4626 implementation as High Risk. EIP4626 is aimed to create a consistent and robust implementation patterns for Tokenized Vaults. A slight deviation from 4626 would broke composability and potentially lead to loss of funds. It is counterproductive to implement EIP4626 but does not conform to it fully.
The missing functions are the most problematic; one expects the mint()
and redeem()
to be present, but they're absent instead.
Disagree on the max*()
functions issues; SemiFungibleVault
is not pausable, functions can't be disabled / paused. Perhaps the inheriting contracts should override these functions, but the way I see it, they can be arbitrarily set in the template.
Agree on subsequent points mentioned.
🌟 Selected for report: 0x52
1541.1415 USDC - $1,541.14
Staking is unable to be paused as intended
StakingRewards.sol inherits pausable and implements the whenNotPaused modifier on stake, but doesn't implement any method to actually pause or unpause the contract. Pausable.sol only implements internal functions, which requires external or public functions to be implemented to wrap them. Since nothing like this has been implemented, the entire pausing system is rendered useless and staking cannot be paused as is intended.
Create simple external pause and unpause functions that can be called by owner
#0 - HickupHH3
2022-10-17T07:15:26Z
Great catch!
While the contract is taken from Synthetix's StakingRewards; note that they use a different version of Pausable that comes with a setPaused()
function. This is notably absent from OZ's implementation; one has to have the pause and unpause function explicitly created.
🌟 Selected for report: 0x52
1541.1415 USDC - $1,541.14
Fees are taken on funds deposited as collateral
uint256 feeValue = calculateWithdrawalFeeValue(entitledShares, id);
In L226 of Vault.sol#withdraw the fee is taken on the entire collateral deposited by the risk users. This is problematic for two reasons. The first is that the collateral provided by the risk users will likely be many many times higher than the premium being paid by the hedge users. This will create a strong disincentive to use the protocol because it is likely a large portion of the profits will be taking by fees and the risk user may unexpectedly lose funds overall if premiums are too low.
The second issue is that this method of fees directly contradicts project documents which clearly indicate that fees are only taken on the premium and insurance payouts, not when risk users are receiving their collateral back.
Fee calculations should be restructured to only take fees on premiums and insurance payouts
#0 - HickupHH3
2022-10-18T09:19:01Z
Agree with the warden. If the withdrawal fee exceeds the premium paid, risk users are disincentivised to provide collateral.
The second issue is that this method of fees directly contradicts project documents which clearly indicate that fees are only taken on the premium and insurance payouts, not when risk users are receiving their collateral back.
Not sure if the warden is referring to the fee as the trading fee c
in the article, but I would agree if it's the case. Implementation isn't according to spec. The actual fees charged might be more than expected.