Multiple address/ID mappings can be combined into a single mapping of an address/ID to a struct, where appropriate
1
3
Using calldata instead of memory for read-only arguments in external functions saves gas
7
4
State variables should be cached in stack variables rather than re-reading them from storage
1
5
Multiple accesses of a mapping/array should use a local variable cache
6
6
<array>.length should not be looked up in every loop of a for-loop
10
7
++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
10
8
Optimize names to save gas
1
9
Using bools for storage incurs overhead
2
10
Using > 0 costs more gas than != 0 when used on a uint in a require() statement
1
11
It costs more gas to initialize non-constant/non-immutable variables to zero than to let the default of zero be applied
11
12
++i costs less gas than i++, especially when it's used in for-loops (--i/i-- too)
10
13
Using private rather than public for constants, saves gas
3
14
Inverting the condition of an if-else-statement wastes gas
1
15
Use custom errors rather than revert()/require() strings to save gas
33
Total: 98 instances over 15 issues
Gas Optimizations
1. Using EIP-712 hashes for positionIds wastes gas
EIP-712 introduces a lot of metadata into the data being hashed, in order for it to be able to be read before the data is signed. When the signature isn't being checked, it's inefficient to use these hashes over normal keccak256() hashes. This gist shows that the most simple order (all zero-byte values, no internal arrays) has an overhead of 658 gas as compared to normal hashing. More complicated orders, especially with internal arrays, will save even more. Message signing should be separate from positionId calculation, which will save gas. These hashes are used in all of the major functions of the project, so savings over time will be significant
2. Multiple address/ID mappings can be combined into a single mapping of an address/ID to a struct, where appropriate
Saves a storage slot for the mapping. Depending on the circumstances and sizes of types, can avoid a Gsset (20000 gas) per mapping combined. Reads and subsequent writes can also be cheaper when a function requires both values and they both fit in the same storage slot. Finally, if both fields are accessed in the same function, can save ~42 gas per access due to not having to recalculate the key's keccak256 hash (Gkeccak256 - 30 gas) and that calculation's associated stack operations.
There is 1 instance of this issue:
File: contracts/src/PuttyV2.sol #1
149 mapping(uint256 => uint256) public positionExpirations;
150
151 /**
152 @notice Whether or not a position has been exercised. Maps
153 from positionId to isExercised.
154 */
155 mapping(uint256 => bool) public exercisedPositions;
156
157 /**
158 @notice The floor asset token ids for a position. Maps from
159 positionId to floor asset token ids. This should only
160 be set for a long call position in `fillOrder`, or for
161 a short put position in `exercise`.
162 */
163: mapping(uint256 => uint256[]) public positionFloorAssetTokenIds;
3. Using calldata instead of memory for read-only arguments in external functions saves gas
When a function with a memory array is called externally, the abi.decode() step has to use a for-loop to copy each index of the calldata to the memory index. Each iteration of this for-loop costs at least 60 gas (i.e. 60 * <mem_array>.length). Using calldata directly, obliviates the need for such a loop in the contract code and runtime execution.
If the array is passed to an internal function which passes the array to another internal function where the array is modified and therefore memory is used in the external call, it's still more gass-efficient to use calldata when the external function uses modifiers, since the modifiers may prevent the internal functions from being called. Structs have the same overhead as an array of length one
Note that I've also flagged instances where the function is public but can be marked as external since it's not called by the contract, and cases where a constructor is involved
There are 7 instances of this issue:
File: contracts/src/PuttyV2.sol
/// @audit _baseURI
209 constructor(
210 string memory _baseURI,
211 uint256 _fee,
212: address _weth
/// @audit order
389: function exercise(Order memory order, uint256[] calldata floorAssetTokenIds) public payable {
/// @audit order
466: function withdraw(Order memory order) public {
/// @audit orders
546 function batchFillOrder(
547 Order[] memory orders,
548 bytes[] calldata signatures,
549 uint256[][] memory floorAssetTokenIds
550: ) public returns (uint256[] memory positionIds) {
/// @audit floorAssetTokenIds
546 function batchFillOrder(
547 Order[] memory orders,
548 bytes[] calldata signatures,
549 uint256[][] memory floorAssetTokenIds
550: ) public returns (uint256[] memory positionIds) {
/// @audit order
573 function acceptCounterOffer(
574 Order memory order,
575 bytes calldata signature,
576 Order memory originalOrder
577: ) public payable returns (uint256 positionId) {
/// @audit originalOrder
573 function acceptCounterOffer(
574 Order memory order,
575 bytes calldata signature,
576 Order memory originalOrder
577: ) public payable returns (uint256 positionId) {
4. State variables should be cached in stack variables rather than re-reading them from storage
The instances below point to the second+ access of a state variable within a function. Caching of a state variable replace each Gwarmaccess (100 gas) with a much cheaper stack read. Other less obvious fixes/optimizations include having local memory caches of state variable structs, or having local caches of state variable contracts/addresses.
There is 1 instance of this issue:
File: contracts/src/PuttyV2.sol #1
/// @audit fee on line 498
499: feeAmount = (order.strike * fee) / 1000;
5. Multiple accesses of a mapping/array should use a local variable cache
The instances below point to the second+ access of a value inside a mapping/array, within a function. Caching a mapping's value in a local storage or calldata variable when the value is accessed multiple times, saves ~42 gas per access due to not having to recalculate the key's keccak256 hash (Gkeccak256 - 30 gas) and that calculation's associated stack operations. Caching an array's struct avoids recalculating the array offsets into memory/calldata
There are 6 instances of this issue:
File: contracts/src/PuttyV2.sol
/// @audit assets[i] on line 595
596: uint256 tokenAmount = assets[i].tokenAmount;
/// @audit assets[i] on line 612
612: ERC721(assets[i].token).safeTransferFrom(from, address(this), assets[i].tokenId);
/// @audit assets[i] on line 638
638: ERC20(assets[i].token).safeTransfer(msg.sender, assets[i].tokenAmount);
/// @audit assets[i] on line 648
648: ERC721(assets[i].token).safeTransferFrom(address(this), msg.sender, assets[i].tokenId);
/// @audit arr[i] on line 731
731: keccak256(abi.encode(ERC20ASSET_TYPE_HASH, arr[i].token, arr[i].tokenAmount))
/// @audit arr[i] on line 745
745: keccak256(abi.encode(ERC721ASSET_TYPE_HASH, arr[i].token, arr[i].tokenId))
6. <array>.length should not be looked up in every loop of a for-loop
The overheads outlined below are PER LOOP, excluding the first loop
storage arrays incur a Gwarmaccess (100 gas)
memory arrays use MLOAD (3 gas)
calldata arrays use CALLDATALOAD (3 gas)
Caching the length changes each of these to a DUP<N> (3 gas), and gets rid of the extra DUP<N> needed to store the stack offset
There are 10 instances of this issue:
File: contracts/src/PuttyV2.sol
556: for (uint256 i = 0; i < orders.length; i++) {
594: for (uint256 i = 0; i < assets.length; i++) {
611: for (uint256 i = 0; i < assets.length; i++) {
627: for (uint256 i = 0; i < floorTokens.length; i++) {
637: for (uint256 i = 0; i < assets.length; i++) {
647: for (uint256 i = 0; i < assets.length; i++) {
658: for (uint256 i = 0; i < floorTokens.length; i++) {
670: for (uint256 i = 0; i < whitelist.length; i++) {
728: for (uint256 i = 0; i < arr.length; i++) {
742: for (uint256 i = 0; i < arr.length; i++) {
7. ++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
There are 10 instances of this issue:
File: contracts/src/PuttyV2.sol
556: for (uint256 i = 0; i < orders.length; i++) {
594: for (uint256 i = 0; i < assets.length; i++) {
611: for (uint256 i = 0; i < assets.length; i++) {
627: for (uint256 i = 0; i < floorTokens.length; i++) {
637: for (uint256 i = 0; i < assets.length; i++) {
647: for (uint256 i = 0; i < assets.length; i++) {
658: for (uint256 i = 0; i < floorTokens.length; i++) {
670: for (uint256 i = 0; i < whitelist.length; i++) {
728: for (uint256 i = 0; i < arr.length; i++) {
742: for (uint256 i = 0; i < arr.length; i++) {
public/external function names and public member variable names can be optimized to save gas. See this link for an example of how it works. Below are the interfaces/abstract contracts that can be optimized so that the most frequently-called functions use the least amount of gas possible during method lookup. Method IDs that have two leading zero bytes can save 128 gas each during deployment, and renaming functions to have lower method IDs will save 22 gas per call, per sorted position shifted
// 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.
11. It costs more gas to initialize non-constant/non-immutable variables to zero than to let the default of zero be applied
Not overwriting the default for stack variables saves 8 gas. Storage and memory variables have larger savings
There are 11 instances of this issue:
File: contracts/src/PuttyV2.sol
497: uint256 feeAmount = 0;
556: for (uint256 i = 0; i < orders.length; i++) {
594: for (uint256 i = 0; i < assets.length; i++) {
611: for (uint256 i = 0; i < assets.length; i++) {
627: for (uint256 i = 0; i < floorTokens.length; i++) {
637: for (uint256 i = 0; i < assets.length; i++) {
647: for (uint256 i = 0; i < assets.length; i++) {
658: for (uint256 i = 0; i < floorTokens.length; i++) {
670: for (uint256 i = 0; i < whitelist.length; i++) {
728: for (uint256 i = 0; i < arr.length; i++) {
742: for (uint256 i = 0; i < arr.length; i++) {
12. ++i costs less gas than i++, especially when it's used in for-loops (--i/i-- too)
Saves 5 gas per loop
There are 10 instances of this issue:
File: contracts/src/PuttyV2.sol
556: for (uint256 i = 0; i < orders.length; i++) {
594: for (uint256 i = 0; i < assets.length; i++) {
611: for (uint256 i = 0; i < assets.length; i++) {
627: for (uint256 i = 0; i < floorTokens.length; i++) {
637: for (uint256 i = 0; i < assets.length; i++) {
647: for (uint256 i = 0; i < assets.length; i++) {
658: for (uint256 i = 0; i < floorTokens.length; i++) {
670: for (uint256 i = 0; i < whitelist.length; i++) {
728: for (uint256 i = 0; i < arr.length; i++) {
742: for (uint256 i = 0; i < arr.length; i++) {
13. Using private rather than public for constants, saves gas
If needed, the values can be read from the verified contract source code, or if there are multiple values there can be a single getter function that returns a tuple of the values of all currently-public constants. Saves 3406-3606 gas in deployment gas due to the compiler not having to create non-payable getter functions for deployment calldata, not having to store the bytes of the value outside of where it's used, and not adding another entry to the method ID table
Summary
Gas Optimizations
positionId
s wastes gasaddress
/ID mappings can be combined into a singlemapping
of anaddress
/ID to astruct
, where appropriatecalldata
instead ofmemory
for read-only arguments inexternal
functions saves gas<array>.length
should not be looked up in every loop of afor
-loop++i
/i++
should beunchecked{++i}
/unchecked{i++}
when it is not possible for them to overflow, as is the case when used infor
- andwhile
-loopsbool
s for storage incurs overhead> 0
costs more gas than!= 0
when used on auint
in arequire()
statementconstant
/non-immutable
variables to zero than to let the default of zero be applied++i
costs less gas thani++
, especially when it's used infor
-loops (--i
/i--
too)private
rather thanpublic
for constants, saves gasrevert()
/require()
strings to save gasTotal: 98 instances over 15 issues
Gas Optimizations
1. Using EIP-712 hashes for
positionId
s wastes gasEIP-712 introduces a lot of metadata into the data being hashed, in order for it to be able to be read before the data is signed. When the signature isn't being checked, it's inefficient to use these hashes over normal
keccak256()
hashes. This gist shows that the most simple order (all zero-byte values, no internal arrays) has an overhead of 658 gas as compared to normal hashing. More complicated orders, especially with internal arrays, will save even more. Message signing should be separate frompositionId
calculation, which will save gas. These hashes are used in all of the major functions of the project, so savings over time will be significantThere is 1 instance of this issue:
https://github.com/code-423n4/2022-06-putty/blob/main/contracts/src/PuttyV2.sol#L305-L308
2. Multiple
address
/ID mappings can be combined into a singlemapping
of anaddress
/ID to astruct
, where appropriateSaves a storage slot for the mapping. Depending on the circumstances and sizes of types, can avoid a Gsset (20000 gas) per mapping combined. Reads and subsequent writes can also be cheaper when a function requires both values and they both fit in the same storage slot. Finally, if both fields are accessed in the same function, can save ~42 gas per access due to not having to recalculate the key's keccak256 hash (Gkeccak256 - 30 gas) and that calculation's associated stack operations.
There is 1 instance of this issue:
https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L149-L163
3. Using
calldata
instead ofmemory
for read-only arguments inexternal
functions saves gasWhen a function with a
memory
array is called externally, theabi.decode()
step has to use a for-loop to copy each index of thecalldata
to thememory
index. Each iteration of this for-loop costs at least 60 gas (i.e.60 * <mem_array>.length
). Usingcalldata
directly, obliviates the need for such a loop in the contract code and runtime execution.If the array is passed to an
internal
function which passes the array to another internal function where the array is modified and thereforememory
is used in theexternal
call, it's still more gass-efficient to usecalldata
when theexternal
function uses modifiers, since the modifiers may prevent the internal functions from being called. Structs have the same overhead as an array of length oneNote that I've also flagged instances where the function is
public
but can be marked asexternal
since it's not called by the contract, and cases where a constructor is involvedThere are 7 instances of this issue:
https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L209-L212
4. State variables should be cached in stack variables rather than re-reading them from storage
The instances below point to the second+ access of a state variable within a function. Caching of a state variable replace each Gwarmaccess (100 gas) with a much cheaper stack read. Other less obvious fixes/optimizations include having local memory caches of state variable structs, or having local caches of state variable contracts/addresses.
There is 1 instance of this issue:
https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L499
5. Multiple accesses of a mapping/array should use a local variable cache
The instances below point to the second+ access of a value inside a mapping/array, within a function. Caching a mapping's value in a local
storage
orcalldata
variable when the value is accessed multiple times, saves ~42 gas per access due to not having to recalculate the key's keccak256 hash (Gkeccak256 - 30 gas) and that calculation's associated stack operations. Caching an array's struct avoids recalculating the array offsets into memory/calldataThere are 6 instances of this issue:
https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L596
6.
<array>.length
should not be looked up in every loop of afor
-loopThe overheads outlined below are PER LOOP, excluding the first loop
MLOAD
(3 gas)CALLDATALOAD
(3 gas)Caching the length changes each of these to a
DUP<N>
(3 gas), and gets rid of the extraDUP<N>
needed to store the stack offsetThere are 10 instances of this issue:
https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L556
7.
++i
/i++
should beunchecked{++i}
/unchecked{i++}
when it is not possible for them to overflow, as is the case when used infor
- andwhile
-loopsThe
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 loopThere are 10 instances of this issue:
https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L556
8. Optimize names to save gas
public
/external
function names andpublic
member variable names can be optimized to save gas. See this link for an example of how it works. Below are the interfaces/abstract contracts that can be optimized so that the most frequently-called functions use the least amount of gas possible during method lookup. Method IDs that have two leading zero bytes can save 128 gas each during deployment, and renaming functions to have lower method IDs will save 22 gas per call, per sorted position shiftedThere is 1 instance of this issue:
https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L53
9. Using
bool
s for storage incurs overheadhttps://github.com/OpenZeppelin/openzeppelin-contracts/blob/58f635312aa21f947cae5f8578638a85aa2519f5/contracts/security/ReentrancyGuard.sol#L23-L27 Use
uint256(1)
anduint256(2)
for true/false to avoid a Gwarmaccess (100 gas) for the extra SLOAD, and to avoid Gsset (20000 gas) when changing fromfalse
totrue
, after having beentrue
in the pastThere are 2 instances of this issue:
https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L143
https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L155
10. Using
> 0
costs more gas than!= 0
when used on auint
in arequire()
statementThis change saves 6 gas per instance. The optimization works until solidity version 0.8.13 where there is a regression in gas costs.
There is 1 instance of this issue:
https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L599
11. It costs more gas to initialize non-
constant
/non-immutable
variables to zero than to let the default of zero be appliedNot overwriting the default for stack variables saves 8 gas. Storage and memory variables have larger savings
There are 11 instances of this issue:
https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L497
12.
++i
costs less gas thani++
, especially when it's used infor
-loops (--i
/i--
too)Saves 5 gas per loop
There are 10 instances of this issue:
https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L556
13. Using
private
rather thanpublic
for constants, saves gasIf needed, the values can be read from the verified contract source code, or if there are multiple values there can be a single getter function that returns a tuple of the values of all currently-public constants. Saves 3406-3606 gas in deployment gas due to the compiler not having to create non-payable getter functions for deployment calldata, not having to store the bytes of the value outside of where it's used, and not adding another entry to the method ID table
There are 3 instances of this issue:
https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L89-L90
https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L95-L96
https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L101-L122
14. Inverting the condition of an
if
-else
-statement wastes gasFlipping the
true
andfalse
blocks instead saves 3 gasThere is 1 instance of this issue:
https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L404-L406
15. Use custom errors rather than
revert()
/require()
strings to save gasCustom errors are available from solidity version 0.8.4. Custom errors save ~50 gas each time they're hit by avoiding having to allocate and store the revert string. Not defining the strings also save deployment gas
There are 33 instances of this issue:
https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2Nft.sol#L12
https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L214