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: 83/110
Findings: 2
Award: $52.80
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 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
require()
should be used instead of assert()
Prior to solidity version 0.8.0, hitting an assert consumes the remainder of the transaction’s available gas rather than returning it, as require()
/revert()
do. assert()
should be avoided even past solidity version 0.8.0 as its documentation states that “The assert function creates an error of type Panic(uint256). … Properly functioning code should never create a Panic, not even on invalid external input. If this happens, then there is a bug in your contract which you should fix”.
src/Vault.sol:L190 assert(IWETH(address(asset)).transfer(msg.sender, msg.value));
decimals()
not part of ERC20 standard.decimals()
is not part of the official ERC20 standard and might fall for tokens that do not implement it. While in practice it is very unlikely, as usually most of the tokens implement it, this should still be considered as a potential issue.
src/Controller.sol:L299 uint256 decimals = 10**(18-(priceFeed.decimals())); src/oracles/PegOracle.sol:L29 (priceFeed1.decimals() == priceFeed2.decimals()), src/oracles/PegOracle.sol:L36 decimals = priceFeed1.decimals(); src/oracles/PegOracle.sol:L73 int256 decimals10 = int256(10**(18 - priceFeed1.decimals()));
_safemint()
should be used rather than _mint()
wherever possible_mint()
is discouraged in favor of _safeMint()
which ensures that the recipient is either an EOA or implements IERC721Receiver
. Both OpenZeppelin and solmate have versions of this function
src/SemiFungibleVault.sol:L96 _mint(receiver, id, shares, EMPTY); src/Vault.sol:L169 _mint(receiver, id, shares, EMPTY);
When changing state variables events are not emitted. Emitting events allows monitoring activities with off-chain monitoring tools.
src/Vault.sol:L350 function setClaimTVL(uint256 id, uint256 claimTVL) public onlyController {
Zero-address checks are a best practice for input validation of critical address parameters. Accidental use of zero-addresses may result in exceptions, burn fees/tokens, or force redeployment of contracts.
https://github.com/code-423n4/2022-09-y2k-finance/blob/main/src/rewards/RewardsFactory.sol#L68-L70 https://github.com/code-423n4/2022-09-y2k-finance/blob/main/src/rewards/StakingRewards.sol#L81-L83 https://github.com/code-423n4/2022-09-y2k-finance/blob/main/src/VaultFactory.sol#L312
Code architecture, incentives, and error handling/reporting questions/issues should be resolved before deployment.
src/Vault.sol:L196 @notice Withdraw entitled deposited assets, checking if a depeg event //TODO add GOV token rewards
🌟 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
16.1756 USDC - $16.18
abi.encode()
is less efficient than abi.encodePacked()
Consider using abi.encodePacked()
instead for efficieny.
src/rewards/RewardsFactory.sol:L118 bytes32 hashedIndex = keccak256(abi.encode(_marketIndex, _epochEnd)); src/rewards/RewardsFactory.sol:L150 return keccak256(abi.encode(_index, _epoch));
Save 6 gas when assembly is used to check for zero address. e.g:
assembly { if iszero(_addr) { mstore(0x00, "zero address") revert(0x00, 0x20) }
src/oracles/PegOracle.sol:L23 require(_oracle1 != address(0), "oracle1 cannot be the zero address"); src/oracles/PegOracle.sol:L24 require(_oracle2 != address(0), "oracle2 cannot be the zero address");
// Booleans are more expensive than uint256 or any type that takes up a full // word because each write operation emits an extra SLOAD to first read the // slot's contents, replace the bits taken up by the boolean, and then write // back. This is the compiler's defense against contract upgrades and // pointer aliasing, and it cannot be disabled.
https://github.com/OpenZeppelin/openzeppelin-contracts/blob/58f635312aa21f947cae5f8578638a85aa2519f5/contracts/security/ReentrancyGuard.sol#L23-L27 Use uint256(1)
and uint256(2)
for true/false to avoid a Gwarmaccess (100 gas), and to avoid Gsset (20000 gas) when changing from ‘false’ to ‘true’, after having been ‘true’ in the past
src/Vault.sol:L52 mapping(uint256 => bool) public idDepegged; src/Vault.sol:L54 mapping(uint256 => bool) public idExists;
revert()
/require()
string to save gasCustom errors are available from solidity version 0.8.4, it saves around 50 gas each time they are hit by avoiding having to allocate and store the revert string.
src/SemiFungibleVault.sol:L91 require((shares = previewDeposit(id, assets)) != 0, "ZERO_SHARES"); src/Vault.sol:L165 require((shares = previewDeposit(id, assets)) != 0, "ZeroValue"); src/Vault.sol:L187 require(msg.value > 0, "ZeroValue"); src/rewards/StakingRewards.sol:L96 require(amount != 0, "Cannot stake 0"); src/rewards/StakingRewards.sol:L119 require(amount > 0, "Cannot withdraw 0"); src/oracles/PegOracle.sol:L23 require(_oracle1 != address(0), "oracle1 cannot be the zero address"); src/oracles/PegOracle.sol:L24 require(_oracle2 != address(0), "oracle2 cannot be the zero address"); src/oracles/PegOracle.sol:L25 require(_oracle1 != _oracle2, "Cannot be same Oracle"); src/oracles/PegOracle.sol:L98 require(price1 > 0, "Chainlink price <= 0"); src/oracles/PegOracle.sol:L103 require(timeStamp1 != 0, "Timestamp == 0 !"); src/oracles/PegOracle.sol:L121 require(price2 > 0, "Chainlink price <= 0"); src/oracles/PegOracle.sol:L126 require(timeStamp2 != 0, "Timestamp == 0 !");
The code should be refactored such that they no longer exist, or the block should do something useful, such as emitting an event or reverting.
src/SemiFungibleVault.sol:L280 ) internal virtual {} src/SemiFungibleVault.sol:L286 ) internal virtual {}
public functions that are never called by the contract should be declared external to save gas.
src/Controller.sol:L198 function triggerEndEpoch(uint256 marketIndex, uint256 epochEnd) public { src/SemiFungibleVault.sol:L237 function maxDeposit(address) public view virtual returns (uint256) { src/SemiFungibleVault.sol:L818 function maxMint(address) public view virtual returns (uint256) { src/VaultFactory.sol:L295 function setController(address _controller) public onlyAdmin { src/VaultFactory.sol:L758 function changeOracle(address _token, address _oracle) public onlyAdmin {
> 0
costs more gas than != 0
when used on a uint in a require()
statement.When working with unsigned integer types, comparisons with != 0 are cheaper than with > 0 . This changes saves 6 gas per instance.
src/Vault.sol:L187 require(msg.value > 0, "ZeroValue"); src/rewards/StakingRewards.sol:L119 require(amount > 0, "Cannot withdraw 0"); src/oracles/PegOracle.sol:L98 require(price1 > 0, "Chainlink price <= 0"); src/oracles/PegOracle.sol:L121 require(price2 > 0, "Chainlink price <= 0");
If a function modifier such as onlyOwner is used, the function will revert if a normal user tries to pay the function. Marking the function as payable will lower the gas cost for legitimate callers because the compiler will not include checks for whether a payment was provided. The extra opcodes avoided are CALLVALUE(2),DUP1(3),ISZERO(3),PUSH2(3),JUMPI(10),PUSH1(3),DUP1(3),REVERT(0),JUMPDEST(1),POP(2), which costs an average of about 21 gas per call to the function, in addition to the extra deployment cost.
src/Vault.sol:L277 function changeTreasury(address _treasury) public onlyFactory { src/Vault.sol:L287 function changeTimewindow(uint256 _timewindow) public onlyFactory { src/Vault.sol:L295 function changeController(address _controller) public onlyFactory { src/Vault.sol:L350 function setClaimTVL(uint256 id, uint256 claimTVL) public onlyController { src/VaultFactory.sol:L295 function setController(address _controller) public onlyAdmin { src/VaultFactory.sol:L366 function changeOracle(address _token, address _oracle) public onlyAdmin { src/rewards/StakingRewards.sol:L225 function setRewardsDuration(uint256 _rewardsDuration) external onlyOwner {
X += Y
costs more gas than X = X + Y
for state variables.Consider changing X += Y
to X = X + Y
to save gas.
src/VaultFactory.sol:L195 marketIndex += 1;
Saves 6 gas per loop.
src/Vault.sol:L443 for (uint256 i = 0; i < epochsLength(); i++) {
If needed, the value can be read from the verified contract source code. Savings are due to the compiler not having to create non-payable getter functions for deployment calldata, and not adding another entry to the method ID table.
src/Controller.sol:L16 uint256 public constant VAULTS_LENGTH = 2;
Keep revert message lower than or equal to 32 bytes to save gas.
src/oracles/PegOracle.sol:L23 require(_oracle1 != address(0), "oracle1 cannot be the zero address"); src/oracles/PegOracle.sol:L24 require(_oracle2 != address(0), "oracle2 cannot be the zero address"); src/SemiFungibleVault.sol:L116 require( src/rewards/StakingRewards.sol:L217 require( src/rewards/StakingRewards.sol:L226 require(
Explicit initialization with zero is not required for variable declaration because uints are 0 by default. Removing this will reduce contract size and save a bit of gas.
src/Vault.sol:L443 for (uint256 i = 0; i < epochsLength(); i++) { src/rewards/StakingRewards.sol:L36 uint256 public periodFinish = 0;
++i
/i++
should be unchecked{++i}
/unchecked{i++}
when it is not possible for them to overflow, as is the case when used in for- and while-loops.The unchecked
keyword is new in solidity version 0.8.0, so this only applies to that version or higher, which these instances are. This saves 30-40 gas per loop.
src/Vault.sol:L443 for (uint256 i = 0; i < epochsLength(); i++) {