Platform: Code4rena
Start Date: 12/12/2022
Pot Size: $36,500 USDC
Total HM: 8
Participants: 103
Period: 7 days
Judge: berndartmueller
Id: 193
League: ETH
Rank: 47/103
Findings: 2
Award: $64.99
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: 0xSmartContract
Also found by: 0xGusMcCrae, 8olidity, Bnke0x0, IllIllI, JC, RaymondFam, Rolezn, SleepingBugs, UNCHAIN, ahayashi, aviggiano, caventa, cozzetti, h0wl, helios, immeas, ladboy233, minhquanym, obront, rjs, rvierdiiev, shung, unforgiven, yixxas
50.16 USDC - $50.16
There are ERC20 tokens with transfer at fees. For checking if the transferred amount is the same as expected, code already compares balanceOf before and balanceOf after transfer. People can get confused in cases where real value doesn't match, also applications like subgraphs that uses this value won't work as expected.
https://github.com/code-423n4/2022-12-caviar/blob/039095ee6b73709289d88cb99397e8b9028224c7/src/Pair.sol#L93-L99 https://github.com/code-423n4/2022-12-caviar/blob/039095ee6b73709289d88cb99397e8b9028224c7/src/Pair.sol#L136-L141 https://github.com/code-423n4/2022-12-caviar/blob/039095ee6b73709289d88cb99397e8b9028224c7/src/Pair.sol#L171-L175 https://github.com/code-423n4/2022-12-caviar/blob/039095ee6b73709289d88cb99397e8b9028224c7/src/Pair.sol#L202-L206
Consider implementing a system like:
uint256 balanceBefore = _token.balanceOf(address(this)); _token.safeTransferFrom(_from, address(this), _amount); uint256 balanceAfter = _token.balanceOf(address(this)); // check / control flow when (balanceAfter - balanceBefore != _amount);
Consider comparing before and after balance to get the actual transferred amount.
mint
can lead to burn tokensWhen minting LP tokens, 0 address is not being checked and therefore can be burned the amount
Check zero address before assigning or using it
block.timestamp
used as time proxyRisk of using block.timestamp
for time should be considered.
block.timestamp
is not an ideal proxy for time because of issues with synchronization, miner manipulation and changing block times.
This kind of issue may affect the code allowing the code before the expected deadline, modifying the normal functioning or reverting sometimes.
SWC ID: 116
https://github.com/code-423n4/2022-12-caviar/blob/039095ee6b73709289d88cb99397e8b9028224c7/src/Pair.sol#L346 closeTimestamp = block.timestamp + CLOSE_GRACE_PERIOD;
https://github.com/code-423n4/2022-12-caviar/blob/039095ee6b73709289d88cb99397e8b9028224c7/src/Pair.sol#L367 require(block.timestamp >= closeTimestamp, "Not withdrawable yet");
block.timestamp
as time proxy and evaluate if block numbers can be used as an approximation for the application logic. Both have risks that need to be factored in.Magic numbers are hardcoded numbers used in the code which are ambiguous to their intended purpose. These should be replaced with constants to make code more readable and maintainable.
Values are hardcoded and would be more readable and maintainable if declared as a constant
https://github.com/code-423n4/2022-12-caviar/blob/039095ee6b73709289d88cb99397e8b9028224c7/src/Pair.sol#L399 return (outputAmount * 1000 * baseTokenReserves()) / ((fractionalTokenReserves() - outputAmount) * 997);
https://github.com/code-423n4/2022-12-caviar/blob/039095ee6b73709289d88cb99397e8b9028224c7/src/Pair.sol#L408 return (inputAmountWithFee * baseTokenReserves()) / ((fractionalTokenReserves() * 1000) + inputAmountWithFee);
Replace magic hardcoded numbers with declared constants.
Some of the contracts include an unlocked pragma, e.g., pragma solidity >=0.8.17.
Locking the pragma helps ensure that contracts are not accidentally deployed using an old compiler version with unfixed bugs.
https://github.com/code-423n4/2022-12-caviar/blob/039095ee6b73709289d88cb99397e8b9028224c7/src/Caviar.sol#L2 pragma solidity ^0.8.17;
https://github.com/code-423n4/2022-12-caviar/blob/039095ee6b73709289d88cb99397e8b9028224c7/src/Pair.sol#L2 pragma solidity ^0.8.17;
https://github.com/code-423n4/2022-12-caviar/blob/039095ee6b73709289d88cb99397e8b9028224c7/src/LpToken.sol#L2 pragma solidity ^0.8.17;
https://github.com/code-423n4/2022-12-caviar/blob/039095ee6b73709289d88cb99397e8b9028224c7/src/lib/SafeERC20Namer.sol#L2 pragma solidity ^0.8.17;
Lock pragmas to a specific Solidity version. Consider converting ^0.8.17 into 0.8.17
Missing Natspec and regular comments affect readability and maintainability of a codebase.
Contracts has partial or full lack of comments
@param
or @return
#0 - c4-judge
2023-01-16T11:27:52Z
berndartmueller marked the issue as grade-b
🌟 Selected for report: Rolezn
Also found by: 0x1f8b, 0xAgro, 0xSmartContract, 0xab00, 0xhacksmithh, Aymen0909, Bnke0x0, Breeje, Diana, HardlyCodeMan, IllIllI, JC, JrNet, Madalad, NoamYakov, RaymondFam, ReyAdmirado, SleepingBugs, UdarTeam, c3phas, carlitox477, cryptonue, gz627, lukris02, millersplanet, oyc_109, pavankv, ret2basic, saneryee, tnevler
14.833 USDC - $14.83
unchecked
in loopsUnchecked operations as the ++i
on for loops are cheaper than checked one.
In Solidity 0.8+, there’s a default overflow check on unsigned integers. It’s possible to uncheck this in for-loops and save some gas at each iteration, but at the cost of some code readability, as this uncheck cannot be made inline..
The code would go from:
for (uint256 i; i < numIterations; i++) { // ... }
to
for (uint256 i; i < numIterations;) { // ... unchecked { ++i; } } The risk of overflow is inexistent for a uint256 here.
https://github.com/code-423n4/2022-12-caviar/blob/039095ee6b73709289d88cb99397e8b9028224c7/SafeERC20Namer.sol#L13 https://github.com/code-423n4/2022-12-caviar/blob/039095ee6b73709289d88cb99397e8b9028224c7/SafeERC20Namer.sol#L17 https://github.com/code-423n4/2022-12-caviar/blob/039095ee6b73709289d88cb99397e8b9028224c7/SafeERC20Namer.sol#L22 https://github.com/code-423n4/2022-12-caviar/blob/039095ee6b73709289d88cb99397e8b9028224c7/SafeERC20Namer.sol#L33 https://github.com/code-423n4/2022-12-caviar/blob/039095ee6b73709289d88cb99397e8b9028224c7/SafeERC20Namer.sol#L39
Add unchecked ++i
at the end of all the for loop where it's not expected to overflow and remove them from the for header
payable
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
https://github.com/code-423n4/2022-12-caviar/blob/039095ee6b73709289d88cb99397e8b9028224c7/src/LpToken.sol#L19 function mint(address to, uint256 amount) public onlyOwner {
https://github.com/code-423n4/2022-12-caviar/blob/039095ee6b73709289d88cb99397e8b9028224c7/src/LpToken.sol#L26 function burn(address from, uint256 amount) public onlyOwner {
Consider adding payable to save gas
>=
cheaper than >
Strict inequalities ( >
) are more expensive than non-strict ones ( >=
). This is due to some supplementary checks (ISZERO
, 3 gas)
It's important to consider whether incrementing/decrementing one of the operators could result in an over/underflow.
This optimization is applicable when the optimizer is turned off.
https://github.com/code-423n4/2022-12-caviar/blob/039095ee6b73709289d88cb99397e8b9028224c7/src/Pair.sol#L80 https://github.com/code-423n4/2022-12-caviar/blob/039095ee6b73709289d88cb99397e8b9028224c7/src/Pair.sol#L117 https://github.com/code-423n4/2022-12-caviar/blob/039095ee6b73709289d88cb99397e8b9028224c7/src/Pair.sol#L120 https://github.com/code-423n4/2022-12-caviar/blob/039095ee6b73709289d88cb99397e8b9028224c7/src/Pair.sol#L157 https://github.com/code-423n4/2022-12-caviar/blob/039095ee6b73709289d88cb99397e8b9028224c7/src/Pair.sol#L189 https://github.com/code-423n4/2022-12-caviar/blob/039095ee6b73709289d88cb99397e8b9028224c7/src/Pair.sol#L367
Consider using >= 1
instead of > 0
to avoid some opcodes
Same solution with other values from x >= y
to x > y+1
or x-1 > y
<X> += <Y>
costs more gas than <X> = <X> + <Y>
for state variablesx+=y
costs more gas than x=x+y for state variables
https://github.com/code-423n4/2022-12-caviar/blob/039095ee6b73709289d88cb99397e8b9028224c7/src/Pair.sol#L453 balanceOf[to] += amount; https://github.com/code-423n4/2022-12-caviar/blob/039095ee6b73709289d88cb99397e8b9028224c7/src/Pair.sol#L448 balanceOf[from] -= amount;
Don't use +=
for state variables as it cost more gas.
A value which value is already known can be used directly rather than reading it from the storage
function close() public { // check that the sender is the caviar owner require(caviar.owner() == msg.sender, "Close: not owner"); // set the close timestamp with a grace period closeTimestamp = block.timestamp + CLOSE_GRACE_PERIOD; // remove the pair from the Caviar contract caviar.destroy(nft, baseToken, merkleRoot); emit Close(closeTimestamp); }
Recommendation Change to:
function close() public { // check that the sender is the caviar owner require(caviar.owner() == msg.sender, "Close: not owner"); // set the close timestamp with a grace period uint _closeTimestamp = block.timestamp + CLOSE_GRACE_PERIOD; // @audit closeTimestamp = _closeTimestamp; // @audit // remove the pair from the Caviar contract caviar.destroy(nft, baseToken, merkleRoot); emit Close(_closeTimestamp); // @audit }
Set directly the value to avoid unnecessary storage read to save some gas
require()
check should be refactoredduplicated require()
/ revert()
checks should be
refactored to a modifier or function to save gas
Event appears twice and can be reduced
https://github.com/code-423n4/2022-12-caviar/blob/039095ee6b73709289d88cb99397e8b9028224c7/src/Pair.sol#L74 require(baseToken == address(0) ? msg.value == baseTokenAmount : msg.value == 0, "Invalid ether input"); https://github.com/code-423n4/2022-12-caviar/blob/039095ee6b73709289d88cb99397e8b9028224c7/src/Pair.sol#L151 require(baseToken == address(0) ? msg.value == maxInputAmount : msg.value == 0, "Invalid ether input");
refactor this checks to different functions to save gas
require()
statements that use &&
saves gasInstead of using the && operator in a single require statement to check multiple conditions, consider using multiple require statements with 1 condition per require statement (saving 3 gas per & ):
Split require statements
Not inlining costs 20
to 40
gas because of two extra JUMP
instructions and additional stack operations needed for function calls.
https://github.com/code-423n4/2022-12-caviar/blob/039095ee6b73709289d88cb99397e8b9028224c7/src/Pair.sol#L463 function _validateTokenIds(uint256[] calldata tokenIds, bytes32[][] calldata proofs) internal view {
Consider changing internal function only called once to inline code for gas savings
#0 - c4-judge
2023-01-14T17:12:28Z
berndartmueller marked the issue as grade-b