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: 10/110
Findings: 3
Award: $1,140.17
🌟 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
https://github.com/code-423n4/2022-09-y2k-finance/blob/bca5080635370424a9fe21fe1aded98345d1f723/src/oracles/PegOracle.sol#L28-L36 https://github.com/code-423n4/2022-09-y2k-finance/blob/bca5080635370424a9fe21fe1aded98345d1f723/src/oracles/PegOracle.sol#L67-L78 https://github.com/code-423n4/2022-09-y2k-finance/blob/bca5080635370424a9fe21fe1aded98345d1f723/src/Controller.sol#L291-L300
PegOracle
will return wrong prices when decimals()
are less than 18Code will return more price as less value is returned in decimals()
First of all, decimals()
doesn't need to return exactly 18
decimals, can return more or less than 18
too. However, only is checked that both decimal amounts are the same.
require( (priceFeed1.decimals() == priceFeed2.decimals()), "Decimals must be the same" );//@audit both can be more than 18 decimals oracle1 = _oracle1; oracle2 = _oracle2; decimals = priceFeed1.decimals();
nowPrice
is increased by 10_000
for precision
if (price1 > price2) {//@audited even of the condition 0 can happen on else or double 0 nowPrice = (price2 * 10000) / price1;//@audit here } else { nowPrice = (price1 * 10000) / price2;//@audit here } int256 decimals10 = int256(10**(18 - priceFeed1.decimals())); nowPrice = nowPrice * decimals10;//@audit if decimals10 is more than 100, code will return more value when divided by 1_000_000 return ( roundID1, nowPrice / 1_000_000,//@audit this value is hardcoded and doesn't correspond to the value to normalize again the previous multiplication. startedAt1, timeStamp1, answeredInRound1 );
As decimals()
is less than 18
, it creates a exponentiation than after being used for multiply nowPrice
is tried to be converted back by dividing by a hardcoded value of 1_000_000
.
If decimals()
for example returns a value of 6
, let's assume:
price2
: 3
price1
: 3
priceFeed1.decimals()
: 6
nowPrice
would be: 30_000 / 3 = 10_000
int256 decimals10 = int256(10**(18 - priceFeed1.decimals()));
decimals10 = 10**(12)
nowPrice
= 10_000 * 10**12
= 10_000_000_000_000_000
10_000_000_000_000_000 / 1_000_000
= 10_000_000_000
and finally this price is also used in Controller.sol
( uint80 roundID, int256 price, //@audit returned here previous value , uint256 timeStamp, uint80 answeredInRound ) = priceFeed.latestRoundData(); uint256 decimals = 10**(18-(priceFeed.decimals()));//@audit decimals neither checked price = price * int256(decimals);
And can be seen the same issue with priceFeed.decimals()
priceFeed
and other tokens used to have expected decimals.18
decimals#0 - MiguelBits
2022-09-20T22:08:33Z
Discussion: It is intended to only allow oracles of less than 18 decimals
#1 - HickupHH3
2022-10-17T04:54:44Z
dup of #195
🌟 Selected for report: 0xDecorativePineapple
1066.9441 USDC - $1,066.94
https://github.com/code-423n4/2022-09-y2k-finance/blob/bca5080635370424a9fe21fe1aded98345d1f723/src/oracles/PegOracle.sol#L70-L74 https://github.com/code-423n4/2022-09-y2k-finance/blob/bca5080635370424a9fe21fe1aded98345d1f723/src/rewards/StakingRewards.sol#L175-L178
Solidity integer division might truncate. As a result, performing multiplication before division can sometimes avoid loss of precision.
In general, this is a problem due to precision. In this case, it also affects money/tokens, what makes me suggest medium severity.
There are 2 instances of this:
PegOracle.latestRoundData()
This would affect the price recovered by latestRoundData().if (price1 > price2) { nowPrice = (price2 * 10000) / price1;//@audit divide } else { nowPrice = (price1 * 10000) / price2; //@audit divide } //then multiply nowPrice = nowPrice * decimals10
StakingRewards.earned()
This is a little more tricky to see and affects returned earned amountfunction earned(address account) public view returns (uint256) { return _balances[account] .mul(rewardPerToken().sub(userRewardPerTokenPaid[account])) .div(1e18) .add(rewards[account]);//@audit add should be used before div for max precision }
Let $b_\text{account}$ be _balances[account]
$R_t$ be rewardPerToken()
$uR_\text{account}$ be userRewardPerTokenPaid[account]
$R_\text{account}$ be rewards[account]
then, consider implementing the computation of earned
as follows:
Slither + manual analysis
Reorder the operations. For more info
#0 - MiguelBits
2022-10-03T21:04:37Z
#1 - HickupHH3
2022-10-18T09:44:13Z
dup #323
#2 - HickupHH3
2022-10-18T09:45:00Z
Partial credit as impact isn't as well explained as primary
🌟 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
https://github.com/code-423n4/2022-09-y2k-finance/blob/bca5080635370424a9fe21fe1aded98345d1f723/src/VaultFactory.sol#L157 https://github.com/code-423n4/2022-09-y2k-finance/blob/bca5080635370424a9fe21fe1aded98345d1f723/src/VaultFactory.sol#L163-L374
VaultFactory.sol
The following contracts and functions, allow admins to interact with core functions such as:
VaultFactory.sol
functions for:
Given that admin
is immutable it's very risky because it is irrecoverable from any mistakes
Scenario: If an incorrect address, e.g. for which the private key is not known, is used accidentally then it prevents the use of all the onlyAdmin()
functions forever, which includes the changing of various critical addresses and parameters. This use of incorrect address may not even be immediately apparent given that these functions are probably not used immediately.
When noticed, due to a failing onlyAdmin()
function call, it will force the redeployment of these contracts and require appropriate changes and notifications for switching from the old to new address. This will diminish trust in the protocol and incur a significant reputational damage.
Recommend remove immutable from admin address. Also taking care while deploying the contract / emitting an event when assigning _admin so in case it is wrongly deployed, it can be redeployed earlier. Finally adding a 2 steps transfer from admin for cases when admin needs to be migrate to another address.
#0 - MiguelBits
2022-09-30T00:21:19Z
@3xHarry let me know what you think
#1 - MiguelBits
2022-10-03T12:00:55Z
changing onlyOwner implementation to use Ownable
#2 - HickupHH3
2022-10-18T10:01:00Z
while valid, I think the protocol would realise that they won't be able to create new markets if the owner
was incorrectly set. Can simply redeploy.
Disagree with severity, downgrading to low-risk QA.
User's primary QA.