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: 16/110
Findings: 4
Award: $745.55
🌟 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/main/src/oracles/PegOracle.sol#L39-L83
There are at least a few problems with the PegOracle
. I am grouping them into one submission because some of them are not that significant but the last one I believe deserves a higher severity.
latestRoundData
queries getOracle2_Price
but getOracle1_Price
is not invoked and the price is fetched directly instead:( uint80 roundID1, int256 price1, uint256 startedAt1, uint256 timeStamp1, uint80 answeredInRound1 ) = priceFeed1.latestRoundData();
leaving it unvalidated against the stale data and passing this burden up to the caller.
int256 decimals10 = int256(10**(18 - priceFeed1.decimals()));
Also, this will revert if the price feed has >18 decimals, it should verify that in the constructor.
I wrote a simple POC that you can try: https://gist.github.com/pauliax/770034a5b744dd2fbba6fa01d9e20eb6
When priceFeed1Decimals
is set to 18, then no matter what values are price1
and price2
, it will always return 0. Even though this contract fetches the decimals, the code still assumes a default value of 8 which can cause misbehaving.
Consider fixing the aforementioned issues of the oracle.
#0 - HickupHH3
2022-10-17T10:46:45Z
uggghhhh really should have split into 2 issues....
(1) is a dup of #61 (3) is a dup of #195.
Am grouping as a dup of #195. Really unforunate; should have submitted as separate issues.
🌟 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
Judge has assessed an item in Issue #486 as High risk. The relevant finding follows:
#0 - HickupHH3
2022-11-07T00:33:03Z
In SemiFungibleVault / Vault when isApprovedForAll(owner, receiver) is true then anyone can withdraw from the vault on behalf of the owner:
dup #434, but lacks sufficient description for full credit.
🌟 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
570.2631 USDC - $570.26
StakingRewards
does not need SafeMath
when using the Solidity version starting from 0.8:pragma solidity 0.8.15; using SafeMath for uint256; _balances[msg.sender] = _balances[msg.sender].add(amount);
deposit
which respectively calls transferFrom
:function depositETH(uint256 id, address receiver) ... IWETH(address(asset)).deposit{value: msg.value}(); assert(IWETH(address(asset)).transfer(msg.sender, msg.value)); return deposit(id, msg.value, receiver); } function deposit( uint256 id, uint256 assets, address receiver ) ... asset.transferFrom(msg.sender, address(this), shares);
It would be more convenient to adapt these functions so that users won't need extra approval txs.
SemiFungibleVault
/ Vault
when isApprovedForAll(owner, receiver)
is true then anyone can withdraw from the vault on behalf of the owner:function withdraw( uint256 id, uint256 assets, address receiver, address owner ) external virtual returns (uint256 shares) { require( msg.sender == owner || isApprovedForAll(owner, receiver), "Only owner can withdraw, or owner has approved receiver for all" ); ...
function withdraw( uint256 id, uint256 assets, address receiver, address owner ) external override epochHasEnded(id) marketExists(id) returns (uint256 shares) { if( msg.sender != owner && isApprovedForAll(owner, receiver) == false) revert OwnerDidNotAuthorize(msg.sender, owner);
I believe that assets should only be withdrawn by the intention of either the owner or the receiver, not by anyone.
/** @notice You can only call functions that use this modifier after the current epoch has started */ modifier epochHasEnded(uint256 id)
🌟 Selected for report: pfapostol
Also found by: 0x040, 0x1f8b, 0x4non, 0xNazgul, 0xSmartContract, 0xc0ffEE, 0xkatana, Aymen0909, Bnke0x0, Deivitto, Diana, JAGADESH, KIntern_NA, Lambda, MiloTruck, R2, RaymondFam, Respx, ReyAdmirado, Rohan16, RoiEvenHaim, Rolezn, Ruhum, Saintcode_, Samatak, Sm4rty, SnowMan, Tomio, Tomo, WilliamAmbrozic, _Adam, __141345__, ajtra, ak1, async, c3phas, ch0bu, cryptostellar5, d3e4, delfin454000, dharma09, djxploit, durianSausage, eierina, erictee, fatherOfBlocks, gianganhnguyen, gogo, ignacio, imare, jag, jonatascm, leosathya, lukris02, malinariy, oyc_109, pashov, pauliax, peanuts, peiw, prasantgupta52, robee, rokinot, rotcivegaf, rvierdiiev, seyni, simon135, slowmoses, sryysryy, tnevler, zishansami
52.8286 USDC - $52.83
getOracle1_Price
and getOracle2_Price
are too similar. They should be re-used with different parameters, e.g.:function getOracle_Price(AggregatorV3Interface priceFeed) public view returns (int256 price) { ( uint80 roundID, int256 price, , uint256 timeStamp, uint80 answeredInRound ) = priceFeed.latestRoundData(); require(price > 0, "Chainlink price <= 0"); require( answeredInRound >= roundID, "RoundID from Oracle is outdated!" ); require(timeStamp != 0, "Timestamp == 0 !"); return price; }
PegOracle
, decimals are cached in the constructor:uint8 public decimals; decimals = priceFeed1.decimals();
However, when fetching the price, the decimals are queried again:
int256 decimals10 = int256(10**(18 - priceFeed1.decimals()));
keccak256(abi.encodePacked("rY2K"))
never changes here:if ( keccak256(abi.encodePacked(symbol)) == keccak256(abi.encodePacked("rY2K")) )
Admin state variable and modifier onlyAdmin
are never used in the Controller. Consider removing them to save some gas.
Results of repeated calls should be cached and re-used, e.g. here it externally calls vaultFactory.getVaults(marketIndex)
twice:
if( vaultFactory.getVaults(marketIndex).length != VAULTS_LENGTH) revert MarketDoesNotExist(marketIndex); ... address[] memory vaultsAddress = vaultFactory.getVaults(marketIndex);