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: 35/110
Findings: 4
Award: $167.09
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 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
combines two different Chainlink oracles, thereby allowing the protocol to directly compare prices of two different tokens. Chainlink has two main types of oracles, X/USD pairs and X/ETH pairs. The former uses 8 decimals, while the latter uses 18 decimals. PegOracle
correctly handles X/USD pairs however incorrectly handles X/ETH pairs and therefore would incorrectly report any price which is derived from ETH pairs.
This will cause ETH pairs to produce prices which are too low and therefore vaults will automatically be registered as depegged even if the underlying assets have not depegged allowing hedge users to unfairly profit.
PegOracle
based on ETH-pairs where decimals is initialised to be 18decimals = priceFeed1.decimals();
latestRoundData()
is called, nowPrice
is calculated and has 4 decimals due to price2
and price1
cancelling each other's decimals//@audit 18 + 4 - 18 nowPrice = (price2 * 10000) / price1;
priceFeed1.decimals()
has 18 decimals, decimals10 becomes 1int256 decimals10 = int256(10**(18 - priceFeed1.decimals()));
nowPrice
stays at 4 decimals and when it is returned it now has -2 decimals even though PegOracle.decimals() = 18
//4 - 6 = -2 decimals nowPrice / 1000000
price
rounds down to 0 which is incorrectThe current formula for the number of decimals of the returned price
is 16 - x
where x = priceFeed1.decimals()
. Therefore, for 8
decimals (USD pairs) PegOracle
is correct.
VS Code
Rewrite latestRoundData()
to:
function latestRoundData() public view returns ( uint80 roundID, int256 nowPrice, uint256 startedAt, uint256 timeStamp, uint80 answeredInRound ) { ( uint80 roundID1, int256 price1, uint256 startedAt1, uint256 timeStamp1, uint80 answeredInRound1 ) = priceFeed1.latestRoundData(); int256 price2 = getOracle2_Price(); if (price1 > price2) { nowPrice = (price2 * decimals) / price1; } else { nowPrice = (price1 * decimals) / price2; } return ( roundID1, nowPrice, startedAt1, timeStamp1, answeredInRound1 ); }
#0 - scaraven
2022-09-20T09:18:59Z
I was in a bit of a rush when submitting this and made a mistake, the fix should be nowPrice = (price2 * 10**(decimals)) / price1;
#1 - HickupHH3
2022-10-18T03:38:47Z
dup of #195
🌟 Selected for report: hyh
Also found by: 0x4non, 0xNazgul, Haruxe, KIntern_NA, PwnPatrol, Respx, Tointer, joestakey, pauliax, peritoflores, rotcivegaf, scaraven
85.8509 USDC - $85.85
https://github.com/code-423n4/2022-09-y2k-finance/blob/2175c044af98509261e4147edeb48e1036773771/src/Vault.sol#L217 https://github.com/code-423n4/2022-09-y2k-finance/blob/2175c044af98509261e4147edeb48e1036773771/src/rewards/StakingRewards.sol#L99-L105 https://github.com/OpenZeppelin/openzeppelin-contracts/blob/7a14f6c5953a1f2228280e6eb1dfee8e5c28d79a/contracts/token/ERC1155/ERC1155.sol#L125
In Vault.sol
, after an epoch has ended (or a depeg event has occurred), users can withdraw()
on behalf of other users as long as the receiver of asset
has been approved by the owner. Such receiver
's can also include StakingRewards.sol
if a user has staked their assets because users must approve the StakingRewards
contract so that they can stake()
. Therefore, a malicious user can burn ERC1155 shares of users who have staked and send the resulting funds to the StakingRewards
contract.
In order for this to work, users must have staked and withdrawn from StakingRewards
without withdrawing from Vault.sol
(though it is not impossible that an attacker frontruns the second action)
Funds are not permanently lost as the asset funds can be recovered through recoverERC20()
but this is dependent on the owner to properly distribute funds back to affected users. Alternatively, the owners can use this to steal funds for themselves as an indirect rug pull vector.
100 WETH
into a vault and receives 100 ERC1155
tokensStakingRewards
and stakes her tokens into StakingRewards
through stake()
StakingRewards
withdraw()
on behalf of Alice where recipient
is StakingRewards
StakingRewards
where they are temporarily lockedVS Code
Consider disallowing users who have no approval to withdraw on behalf of other users. Instead, only allow the owners or approved operators e.g.
if( msg.sender != owner && isApprovedForAll(owner, msg.sender) == false) revert OwnerDidNotAuthorize(msg.sender, owner);
#0 - MiguelBits
2022-10-03T20:13:55Z
#1 - HickupHH3
2022-10-17T03:36:31Z
Valid attack vector and impact well explained
8.0071 USDC - $8.01
https://github.com/code-423n4/2022-09-y2k-finance/blob/2175c044af98509261e4147edeb48e1036773771/src/oracles/PegOracle.sol#L58-L63 https://github.com/code-423n4/2022-09-y2k-finance/blob/2175c044af98509261e4147edeb48e1036773771/src/Controller.sol#L308-L309 https://github.com/code-423n4/2022-09-y2k-finance/blob/2175c044af98509261e4147edeb48e1036773771/src/oracles/PegOracle.sol#L103
In PegOracle.latestRoundData()
, when priceFeed1.latestRoundData()
is called there are no checks whether the data is stale or not. This could lead to the oracle reporting incorrect prices which can negatively affect hedge and risk users, e.g. the oracle reports a price of 1.0 even if a depeg has occurred.
Additionally, even though all other sections of code which use oracles do check that answeredInRound >= round
, it may be the case that the chain link nodes have stopped working and therefore not updated to a new round in a very long time causing the data to be stale.
VS Code
Add checks for staleness of data including checking the timestamp of the data e.g.
uint80 roundID1, int256 price1, , uint256 timeStamp1, uint80 answeredInRound1 ) = priceFeed1.latestRoundData(); require(price1 > 0, "Chainlink price <= 0"); require( answeredInRound1 >= roundID1, "RoundID from Oracle is outdated!" ); //@audit make sure that the timestamp is not from too long ago require(timeStamp1 > block.timestamp - OracleWindow, "Timestamp is too low");
#0 - HickupHH3
2022-10-15T07:25:40Z
dup of #61
🌟 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
Throughout most contracts, a single user is given control over changing important parameters e.g. VaultFactory.setController()
. If this user is at any point malicious, then it is very easy for the user to manipulate parameters in a way to steal funds from normal users e.g. setting a malicious oracle which incorrectly reports a depeg
It is highly recommended to either implement delays when changing parameters so that users have time to react to changes which may seem malicious and/or making sure that a multi-sig wallet or DAO control the contracts
#0 - MiguelBits
2022-09-29T23:53:15Z
this user admin is intended to be a multi-sig
#1 - HickupHH3
2022-10-29T14:02:51Z
dup #194.
warden's primary QA.
#2 - HickupHH3
2022-11-07T00:01:04Z
satisfactory because the warden gave a specific example of centralisation risk of how users can be rugged:
then it is very easy for the user to manipulate parameters in a way to steal funds from normal users e.g. setting a malicious oracle which incorrectly reports a depeg