Platform: Code4rena
Start Date: 04/11/2022
Pot Size: $42,500 USDC
Total HM: 9
Participants: 88
Period: 4 days
Judge: 0xean
Total Solo HM: 2
Id: 180
League: ETH
Rank: 5/88
Findings: 4
Award: $1,076.82
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: neko_nyaa
Also found by: 0x52, 0xSmartContract, 0xc0ffEE, Josiah, KingNFT, Lambda, R2, RaymondFam, Ruhum, TomJ, Trust, TwelveSec, __141345__, c7e7eff, cccz, cryptostellar5, fs0c, hansfriese, horsefacts, ladboy233, minhtrng, pashov, rvierdiiev, sashik_eth, tonisives, wagmi
8.5414 USDC - $8.54
https://github.com/code-423n4/2022-11-size/blob/main/src/SizeSealed.sol#L163
The protocol did an excellent job by having the baseToken
checked for its code existence whilst filtering off fee-on-transfer token with only one conditional check on lines 103 - 105, considering safeTransfer
and safeTransferFrom
from the Solmate libraries have been known for not checking the existence of code at the token address. Although the seller would unlikely be choosing a quoteToken
that is devoid of a contract in it, e.g. not yet deployed or has been destructed, a deflationary quoteToken
might have been chosen unknowingly. This could lead to accounting errors resulting in refund()
, withdrwa()
and/or cancelBid()
to revert for the last batch of bidders due to insufficient contract balance.
The instances entailed are listed as follows if the seller chooses to finalize when calling reveal()
:
Line 163 SafeTransferLib.safeTransferFrom(ERC20(a.params.quoteToken), msg.sender, address(this), quoteAmount); Line 351 SafeTransferLib.safeTransfer(ERC20(a.params.quoteToken), msg.sender, b.quoteAmount); Line 381 SafeTransferLib.safeTransfer(ERC20(a.params.quoteToken), msg.sender, refundedQuote);
The following instance will also affect the last batch of unlucky bidder if the seller chooses to prematurely cancel the auction or not finalizing when calling reveal()
:
Line 439 SafeTransferLib.safeTransfer(ERC20(a.params.quoteToken), msg.sender, b.quoteAmount);
Manual inspection.
As such, a similar check should also be applied on the quoteToken
by refactoring line 163 to:
uint256 balanceBeforeTransfer = ERC20(a.params.quoteToken).balanceOf(address(this)); SafeTransferLib.safeTransferFrom(ERC20(a.params.quoteToken), msg.sender, address(this), quoteAmount); uint256 balanceAfterTransfer = ERC20(a.params.quoteToken).balanceOf(address(this)); if (balanceAfterTransfer - balanceBeforeTransfer != quoteAmount) { revert UnexpectedBalanceChange(); }
#0 - c4-judge
2022-11-09T17:49:44Z
0xean marked the issue as duplicate
#1 - c4-judge
2022-12-06T00:25:51Z
0xean marked the issue as satisfactory
🌟 Selected for report: Trust
Also found by: 0x1f8b, 0xdapper, HE1M, KIntern_NA, Lambda, Picodes, RaymondFam, RedOneN, TomJ, V_B, __141345__, c7e7eff, chaduke, codexploder, corerouter, cryptonue, fs0c, gz627, hihen, joestakey, ktg, ladboy233, minhtrng, rvierdiiev, simon135, skyle, slowmoses, wagmi, yixxas
5.604 USDC - $5.60
https://github.com/code-423n4/2022-11-size/blob/main/src/SizeSealed.sol#L157-L159
A bidder could repeatedly call bid()
and then cancelBid()
to increasingly add elements to a.bid
. Before too long, the conditional check on lines 157 - 159 is going to start reverting whenever the next bidder is trying to make a new bid.
Line 157 if (bidIndex >= 1000) { Line 158 revert InvalidState(); Line 159 }
Manual inspection
Consider replacing the cancelled a.bids[bidIndex]
with the last element of a.bids
, and then pop the last element. That way, a.bids.length
would entail only valid and uncancelled bids, making rooms for new bidders to participate. Here is a suggested code block to be included in cancelBid
after transferring b.quoteAmount
to the bidder:
a.bids[bidIndex] = a.bids[bids.length - 1]; a.bids.pop();
#0 - c4-judge
2022-11-09T15:41:00Z
0xean marked the issue as duplicate
#1 - c4-judge
2022-12-06T00:25:38Z
0xean marked the issue as satisfactory
🌟 Selected for report: 0x1f8b
Also found by: 0xSmartContract, 0xc0ffEE, Aymen0909, B2, Deivitto, Josiah, KingNFT, Rahoz, RaymondFam, RedOneN, ReyAdmirado, Trust, ajtra, aviggiano, brgltd, c7e7eff, cryptonue, ctf_sec, delfin454000, djxploit, lukris02, peanuts, rvierdiiev, shark, simon135, slowmoses, tnevler, trustindistrust
711.6787 USDC - $711.68
delete
to Clear Variablesdelete a
assigns the initial value for the type to a
. i.e. for integers it is equivalent to a = 0
, but it can also be used on arrays, where it assigns a dynamic array of length zero or a static array of the same length with all elements reset. For structs, it assigns a struct with all members reset. Similarly, it can also be used to set an address to zero address. It has no effect on whole mappings though (as the keys of mappings may be arbitrary and are generally unknown). However, individual keys and what they map to can be deleted: If a
is a mapping, then delete a[x]
will delete the value stored at x.
The delete key better conveys the intention and is also more idiomatic. Consider replacing assignments of zero with delete statements. Here are some the three instances entailed:
https://github.com/code-423n4/2022-11-size/blob/main/src/SizeSealed.sol#L432-L435 https://github.com/code-423n4/2022-11-size/blob/main/src/SizeSealed.sol#L379 https://github.com/code-423n4/2022-11-size/blob/main/src/SizeSealed.sol#L404
block.timestamp
UnreliableThe use of block.timestamp
as part of the time checks can be slightly altered by miners/validators to favor them in contracts that have logic strongly dependent on them.
Consider taking into account this issue and warning the users that such a scenario could happen. If the alteration of timestamps cannot affect the protocol in any way, consider documenting the reasoning and writing tests enforcing that these guarantees will be preserved even if the code changes in the future.
Here are some of the instances entailed:
https://github.com/code-423n4/2022-11-size/blob/main/src/SizeSealed.sol#L29-L37 https://github.com/code-423n4/2022-11-size/blob/main/src/SizeSealed.sol#L60 https://github.com/code-423n4/2022-11-size/blob/main/src/SizeSealed.sol#L425-L426 https://github.com/code-423n4/2022-11-size/blob/main/src/SizeSealed.sol#L460
The following instance of for loop is going to have the last element of the array omitted.
https://github.com/code-423n4/2022-11-size/blob/main/src/SizeSealed.sol#L302
Consider refactoring the code line as follows:
for (uint256 i; i < seenBidMap.length; i++) {
The following refactoring of code is another alternative albeit less gas efficient though because of the adoption of <=
instead of <
:
for (uint256 i; i <= seenBidMap.length - 1; i++) {
Consider making the naming of local variables more verbose and descriptive so all other peer developers would better be able to comprehend the intended statement logic, significantly enhancing the code readability. Here are some of the instances entailed:
https://github.com/code-423n4/2022-11-size/blob/main/src/SizeSealed.sol#L28 https://github.com/code-423n4/2022-11-size/blob/main/src/SizeSealed.sol#L86 https://github.com/code-423n4/2022-11-size/blob/main/src/SizeSealed.sol#L131 https://github.com/code-423n4/2022-11-size/blob/main/src/SizeSealed.sol#L181 https://github.com/code-423n4/2022-11-size/blob/main/src/SizeSealed.sol#L221 https://github.com/code-423n4/2022-11-size/blob/main/src/SizeSealed.sol#L337-L338 https://github.com/code-423n4/2022-11-size/blob/main/src/SizeSealed.sol#L359-L360 https://github.com/code-423n4/2022-11-size/blob/main/src/SizeSealed.sol#L392 https://github.com/code-423n4/2022-11-size/blob/main/src/SizeSealed.sol#L418-L419 https://github.com/code-423n4/2022-11-size/blob/main/src/SizeSealed.sol#L456
Consider indexing parameters for events, serving as logs filter when looking for specifically wanted data. Up to three parameters in an event function can receive the attribute indexed
which will cause the respective arguments to be treated as log topics instead of data. There are the instances entailed:
https://github.com/code-423n4/2022-11-size/blob/main/src/interfaces/ISizeSealed.sol#L97-L122
Solidity contracts can use a special form of comments, i.e., the Ethereum Natural Language Specification Format (NatSpec) to provide rich documentation for functions, return variables and more. Please visit the following link for further details:
https://docs.soliditylang.org/en/v0.8.16/natspec-format.html
Here are some of the instances entailed that are devoid of useful comments:
https://github.com/code-423n4/2022-11-size/blob/main/src/SizeSealed.sol#L28-L43 https://github.com/code-423n4/2022-11-size/blob/main/src/SizeSealed.sol#L466-L480 https://github.com/code-423n4/2022-11-size/blob/main/src/interfaces/ISizeSealed.sol#L28-L34 https://github.com/code-423n4/2022-11-size/blob/main/src/interfaces/ISizeSealed.sol#L40-L48 https://github.com/code-423n4/2022-11-size/blob/main/src/interfaces/ISizeSealed.sol#L63-L68 https://github.com/code-423n4/2022-11-size/blob/main/src/interfaces/ISizeSealed.sol#L86-L91 https://github.com/code-423n4/2022-11-size/blob/main/src/interfaces/ISizeSealed.sol#L97-L122
A system’s design specification and supporting documentation should be almost as important as the system’s implementation itself. Users rely on high-level documentation to understand the big picture of how a system works. Without spending time and effort to create palatable documentation, a user’s only resource is the code itself, something the vast majority of users cannot understand. Security assessments depend on a complete technical specification to understand the specifics of how a system works. When a behavior is not specified (or is specified incorrectly), security assessments must base their knowledge in assumptions, leading to less effective review. Maintaining and updating code relies on supporting documentation to know why the system is implemented in a specific way. If code maintainers cannot reference documentation, they must rely on memory or assistance to make high-quality changes. Currently, the only documentation for Growth DeFi is a single README file, as well as code comments.
Conditional checks should appear as early as possible in a function logic. In the event of a revert, it would avoid incurring gas on code executions coming after the checks. The following code block should execute before updating ebid
:
https://github.com/code-423n4/2022-11-size/blob/main/src/SizeSealed.sol#L155-L159
Or, better yet, right after Auction a
has been cached
cliffPercent
is meant to be divided by 1e18 to furnish a normalized value between 0 and 1. As such, it should be replaced with cliffAmount
on line 31 of CommonTokenMath.sol
to better portray the base tokens unlocked at vesting start.
https://github.com/code-423n4/2022-11-size/blob/main/src/util/CommonTokenMath.sol#L31
Additionally, the function logic did not implement any step wise release of vested token periodically. Hence, the graphical staircase should be replaced with a linear straight line to better portray the implication of a continuous function represented in lines 64 to 66.
https://github.com/code-423n4/2022-11-size/blob/main/src/util/CommonTokenMath.sol#L64-L66
Likewise, totalBaseAmount
on line 18 shadowing the maximum amount of baseToken
to be auctioned in struct AuctionParameters
should be replaced with baseAmount
to be more inline with the input parameter of tokensAvailableAtTime
.
https://github.com/code-423n4/2022-11-size/blob/main/src/util/CommonTokenMath.sol#L18
The following code block could take in more structured checks by implementing some threshold limits:
https://github.com/code-423n4/2022-11-size/blob/main/src/SizeSealed.sol#L60-L74
(timings.endTimestamp <= block.timestamp)
would easily be bypassed if timings.endTimestamp == block.timestamp + 1
. Consider adding a reasonable amount of threshold to block.timestamp
.
In a reverse manner, (timings.startTimestamp >= timings.endTimestamp)
could easily be skipped if timings.endTimestamp == timings.startTimestamp + 1
Consider also adding a reasonable amount of threshold to timings.startTimestamp
.
Like wise, (timings.endTimestamp > timings.vestingStartTimestamp)
and (timings.vestingStartTimestamp > timings.vestingEndTimestamp)
could return false by correspondingly having (timings.endTimestamp == timings.vestingStartTimestamp)
and (timings.vestingStartTimestamp == timings.vestingEndTimestamp)
. A threshold should also be added to timings.endTimestamp
and timings.vestingStartTimestamp
respectively.
As for line 72, consider refactoring it to:
if (timings.cliffPercent > 1e18 || timings.cliffPercent == 0) {
When ensuring the minimum bid is not more than the total reserve of the auction as commented in line 75, the conditional check would allow seller to set auctionParams.minimumBidQuote
to a value as much as auctionParams.reserveQuotePerBase * auctionParams.totalBaseAmount
. This could very much limit the number of bidders having the needed resources to trade.
https://github.com/code-423n4/2022-11-size/blob/main/src/SizeSealed.sol#L76-L82
Consider adding an appropriate divider to auctionParams.reserveQuotePerBase
.
#0 - c4-judge
2022-11-10T02:55:08Z
0xean marked the issue as grade-a
🌟 Selected for report: 0x1f8b
Also found by: 0xSmartContract, 0xdeadbeef, Aymen0909, B2, Bnke0x0, Deivitto, Diana, Dinesh11G, JC, RaymondFam, ReyAdmirado, Rolezn, Sathish9098, TomJ, ajtra, aviggiano, chaduke, cryptostellar5, djxploit, gianganhnguyen, gogo, halden, karanctf, leosathya, lukris02, mcwildy, oyc_109, ret2basic, skyle, slowmoses
351.0013 USDC - $351.00
In EVM, there is no opcode for non-strict inequalities (>=, <=) and two operations are performed (> + = or < + =). As an example, consider replacing >= with the strict counterpart > in the following line of code:
https://github.com/code-423n4/2022-11-size/blob/main/src/SizeSealed.sol#L63
if (timings.startTimestamp > timings.endTimestamp - 1) {
Similarly, as an example, consider replacing <= with the strict counterpart < in the following line of code:
https://github.com/code-423n4/2022-11-size/blob/main/src/SizeSealed.sol#L60
if (timings.endTimestamp < block.timestamp + 1) {
||
Costs Less Gas Than Its Equivalent &&
Rule of thumb: (x && y)
is (!(!x || !y))
Even with the 10k Optimizer enabled: ||
, OR conditions cost less than their equivalent &&
, AND conditions.
As an example, the following code line may be rewritten as:
https://github.com/code-423n4/2022-11-size/blob/main/src/SizeSealed.sol#L258
if (!(sharedPoint.x != 1 || sharedPoint.y != 1)) continue;
When running a function we could pass the function parameters as calldata or memory for variables such as strings, bytes, structs, arrays etc. If we are not modifying the passed parameter, we should pass it as calldata because calldata is more gas efficient than memory.
Calldata is a non-modifiable, non-persistent area where function arguments are stored, and behaves mostly like memory. However, it alleviates the compiler from the abi.decode()
step that copies each index of the calldata to the memory index, bypassing the cost of 60 gas on each iteration. Here are some of the instances entailed:
https://github.com/code-423n4/2022-11-size/blob/main/src/SizeSealed.sol#L217
A storage pointer is cheaper since copying a state struct in memory would incur as many SLOADs and MSTOREs as there are slots. In another words, this causes all fields of the struct/array to be read from storage, incurring a Gcoldsload (2100 gas) for each field of the struct/array, and then further incurring an additional MLOAD rather than a cheap stack read. As such, declaring the variable with the storage keyword and caching any fields that need to be re-read in stack variables will be much cheaper, involving only Gcoldsload for all associated field reads. Read the whole struct/array into a memory variable only when it is being returned by the function, passed into a function that requires memory, or if the array/struct is being read from another memory array/struct. Here are some of the instances entailed:
https://github.com/code-423n4/2022-11-size/blob/main/src/SizeSealed.sol#L148 https://github.com/code-423n4/2022-11-size/blob/main/src/SizeSealed.sol#L186 https://github.com/code-423n4/2022-11-size/blob/main/src/SizeSealed.sol#L231 https://github.com/code-423n4/2022-11-size/blob/main/src/SizeSealed.sol#L256
if ... else
Using ternary operator instead of the if else statement saves gas. For instance the following code block may be rewritten as:
https://github.com/code-423n4/2022-11-size/blob/main/src/SizeSealed.sol#L272-L276
quotePerBase == data.previousQuotePerBase ? { if (data.previousIndex > bidIndex) revert InvalidSorting(); } : { revert InvalidSorting(); }
Consider having the logic of a modifier embedded through an internal or private (if no contracts inheriting) function to reduce contract size if need be. For instance, the following instance of modifier may be rewritten as follows:
function _atState(Auction storage a, States _state) private view { if (block.timestamp < a.timings.startTimestamp) { if (_state != States.Created) revert InvalidState(); } else if (block.timestamp < a.timings.endTimestamp) { if (_state != States.AcceptingBids) revert InvalidState(); } else if (a.data.lowestQuote != type(uint128).max) { if (_state != States.Finalized) revert InvalidState(); } else if (block.timestamp <= a.timings.endTimestamp + 24 hours) { if (_state != States.RevealPeriod) revert InvalidState(); } else if (block.timestamp > a.timings.endTimestamp + 24 hours) { if (_state != States.Voided) revert InvalidState(); } else { revert(); } } modifier atState(Auction storage a, States _state) { _atState(a, _state); _; }
The order of function will also have an impact on gas consumption. Because in smart contracts, there is a difference in the order of the functions. Each position will have an extra 22 gas. The order is dependent on method ID. So, if you rename the frequently accessed function to more early method ID, you can save gas cost. Please visit the following site for further information:
Before deploying your contract, activate the optimizer when compiling using “solc --optimize --bin sourceFile.sol”. By default, the optimizer will optimize the contract assuming it is called 200 times across its lifetime. If you want the initial contract deployment to be cheaper and the later function executions to be more expensive, set it to “ --optimize-runs=1”. Conversely, if you expect many transactions and do not care for higher deployment cost and output size, set “--optimize-runs” to a high number.
module.exports = { solidity: { version: "0.8.17", settings: { optimizer: { enabled: true, runs: 1000, }, }, }, };
Please visit the following site for further information:
https://docs.soliditylang.org/en/v0.5.4/using-the-compiler.html#using-the-commandline-compiler
Here's one example of instance on opcode comparison that delineates the gas saving mechanism:
for !=0 before optimization PUSH1 0x00 DUP2 EQ ISZERO PUSH1 [cont offset] JUMPI
after optimization DUP1 PUSH1 [revert offset] JUMPI
Disclaimer: There have been several bugs with security implications related to optimizations. For this reason, Solidity compiler optimizations are disabled by default, and it is unclear how many contracts in the wild actually use them. Therefore, it is unclear how well they are being tested and exercised. High-severity security issues due to optimization bugs have occurred in the past . A high-severity bug in the emscripten -generated solc-js compiler used by Truffle and Remix persisted until late 2018. The fix for this bug was not reported in the Solidity CHANGELOG. Another high-severity optimization bug resulting in incorrect bit shift results was patched in Solidity 0.5.6. Please measure the gas savings from optimizations, and carefully weigh them against the possibility of an optimization-related bug. Also, monitor the development and adoption of Solidity compiler optimizations to assess their maturity.
uint256
Can be Cheaper Than uint8
and Other Unsigned Integer Type of Smaller Bit SizeWhen dealing with function arguments or memory values, there is no inherent benefit because the compiler does not pack these values. Your contract’s gas usage may be higher because the EVM operates on 32 bytes at a time. Therefore, if the element is smaller than that, the EVM must use more operations in order to reduce the size of the element from 32 bytes to the desired size. The EVM needs to properly enforce the limits of this smaller type.
It is only more efficient when you can pack variables of uint8, uint16, uint32, uint64, ... into the same storage slot with other neighboring variables smaller than 32 bytes. Here are some of the instances entailed:
https://github.com/code-423n4/2022-11-size/blob/main/src/util/CommonTokenMath.sol#L48-L50
"Checked" math, which is default in ^0.8.0 is not free. The compiler will add some overflow checks, somehow similar to those implemented by SafeMath
. While it is reasonable to expect these checks to be less expensive than the current SafeMath
, one should keep in mind that these checks will increase the cost of "basic math operation" that were not previously covered. This particularly concerns variable increments in for loops. When no arithmetic overflow/underflow is going to happen, unchecked { ++i ;}
to use the previous wrapping behavior further saves gas just as in the for loop below as an example:
https://github.com/code-423n4/2022-11-size/blob/main/src/SizeSealed.sol#L302-L305
for (uint256 i; i < seenBidMap.length - 1; ) { if (seenBidMap[i] != type(uint256).max) { revert InvalidState(); } unchecked { ++i; } }
abi.encode()
Costs More Gas Than abi.encodePacked()
Changing abi.encode()
to abi.encodePacked()
can save gas considering the former pads extra null bytes at the end of the call data, which is unnecessary. Please visit the following the link delineating how abi.encodePacked()
is more gas efficient in general:
https://github.com/ConnorBlockchain/Solidity-Encode-Gas-Comparison
Here is one of the instances entailed:
https://github.com/code-423n4/2022-11-size/blob/main/src/SizeSealed.sol#L467
You can have further advantages in term of gas cost by simply using named return values as temporary local variable. As an example, the following instance of code block can refactored as follows:
https://github.com/code-423n4/2022-11-size/blob/main/src/SizeSealed.sol#L466-L468
function computeCommitment(bytes32 message) public pure returns (bytes32 commitmentHash) { commitmentHash = keccak256(abi.encode(message)); }
+=
generally costs 22 more gas than writing out the assigned equation explicitly. The amount of gas wasted can be quite sizable when repeatedly operated in a loop. As an example, the following line of code could be rewritten as:
https://github.com/code-423n4/2022-11-size/blob/main/src/SizeSealed.sol#L294
data.filledBase = data.filledBase + baseAmount;
From inside a smart contract, calling its internal functions is cheaper than calling its public functions, because when you call a public function, all the parameters are again copied into memory and passed to that function. By contrast, when you call an internal function, references of those parameters are passed and they are not copied into memory again. This saves a bit of gas, especially when the parameters are big.
As such, finalize()
should have its visibility changed to internal
since calling it internally is the only viable way to have a.data.privKey
updated.
#0 - c4-judge
2022-11-10T02:29:48Z
0xean marked the issue as grade-a