Open code423n4 opened 2 years ago
cool idea with relying on 0xdead to see if an option is exercised or not. unfortunately this doesnt work because a user can intentionally "burn" their NFT to 0xdead which then messes up the logic in withdraw. This can be mitigated, but requires too many changes.
high quality report though.
Executive Summary
The codebase is already pretty well thought out, by extending the idea of using ERC721 IDs as identifiers, we can save massive amount of gas for day to day operations, we can also apply a few basic logic transformations as well as basic gas saving tips to reduce the total gas for the average operation by over 20k gas
The first few refactoring will offer strong gas savings with minimal work (20k+ gas), the remaining findings are minor and I expect most other wardens to also be suggesting them
Massive Gas Savings in
exercise
by removingexercisedPositions
- Around 20k gas on everyexercise
(17k to 23k from foundry tests)It seems like
exercisedPositions
is used exclusively forwithdraw
, however through the cleverly designed "send to 0xdead" system, we can replace theexercisedPositions
function with the followingIn fact, a position was exercised if it was transfered to 0xdead, as such there's no need to store a mapping in storage.
We can delete every mention of
exercisedPositions
as well as the storage mappingThis single change will reduce almost 20k gas from
exercise
To make
withdraw
work we can write the followingAnd we can delete the
exercisedPositions
mapping from the contractYou can use the function above so the test still passes, in case you need a way to verify if a position was exercised, otherwise you can just use the
isExercised
line and delete very other mention ofexercisedPositions
Gas Math
BEFORE exercise ┆ 5759 ┆ 55920 ┆ 68002 ┆ 133534 ┆ 18
withdraw ┆ 3075 ┆ 27840 ┆ 24545 ┆ 71168 ┆ 10
AFTER exercise ┆ 5759 ┆ 42356 ┆ 45806 ┆ 115777 ┆ 18 withdraw ┆ 3075 ┆ 26968 ┆ 23807 ┆ 70836 ┆ 10
Diff
Avoid a SLOAD optimistically - 2.1k gas saved
Because you already computed
isExercised
, you can save an SLOAD (2.1k gas) if you check forisExercised
first.https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L481-L482
You could also refactor to the reverse check (if you expect the majority of positions to expire worthless), either way the short circuit can be used to save a SLOAD, whicever priority you give it
Change to
To save 2.1k gas if the option was exercised
Double SLOAD - 94 gas saved
Everytime a Storage Variable is loaded twice, (SLOAD), it would be cheaper to use a supporting memory variable The cost of a Second SLOAD is 100 gas, the cost of an MSTORE is 3 and and MLOAD another 3 gas.
Using a supporting variable will save 94 gas on the second read, and 97 gas for each subsquent MSTORE
In the case of
fee
this can save 94 gashttps://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L498-L499
Storage Pointer to
floorTokenIds
will save gas (avoid copying whole storage to memory) - Between 400 and 2k gas saved on Withdraw and Exercise - 400 / 2k * 2 gas savedIf you pass a
storage
pointer as argument to amemory
typed function like_transferFloorsOut
You are copying the storage values to memory and then reading them and looping through that copy, this incurs an overhead and is equivalent to looping over storage, copying into memory and then passing the memory variable.
This is one of the few cases where a storage declaration (passing the storage pointer) will save gas
Refactor to
Gas Math
Before
exercise ┆ 5759 ┆ 55920 ┆ 68002 ┆ 133534 ┆ 18 withdraw ┆ 3075 ┆ 27840 ┆ 24545 ┆ 71168 ┆ 10
After
exercise ┆ 5759 ┆ 55176 ┆ 65795 ┆ 133534 ┆ 18
withdraw ┆ 3075 ┆ 27365 ┆ 24545 ┆ 71003 ┆ 10
Save gas by avoiding NOT - Saves 6 gas sometimes
https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L403-L406
Because the logic is fully known, it would be cheaper to swap the if else to:
As that would avoid the extra
NOT
Check msg.value first - 1 gas per instance - 3 gas total
Reading msg.value costs 2 gas, while reading from memory costs 3, this will save 1 gas with no downside
https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L327
Change to
Common Gas Savings
Below are a bunch of basic gas saving tips you probably will receive in most competent submissions added below for the sake of completeness
Save 3 gas by not reading
orders.length
- 3 gas per instance - 27 gas savedhttps://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L551-L552
orders.length
is read multiple times, because of that, especially for the loop, you should cache it in a memory variable to save 3 gas per instanceRefactor to:
https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L670-L671
https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L658
https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L647
https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L637
https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L627
https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L611
https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L594
https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L728
https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L742
Avoid default assignment - 3 gas per instance - 27 gas saved
Declaring
uint256 i = 0;
means doing an MSTORE of the value 0 Instead you could just declareuint256 i
to declare the variable without assigning it's default value, saving 3 gas per declarationhttps://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L556-L557
Instances: https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L670-L671
https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L658
https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L647
https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L637
https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L627
https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L611
https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L594
https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L728
https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L742
Cheaper For Loops - 25 to 80 gas per instance - 9 instances
You can get cheaper for loops (at least 25 gas, however can be up to 80 gas under certain conditions), by rewriting:
Instances: https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L670-L671
https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L658
https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L647
https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L637
https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L627
https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L611
https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L594
https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L728
https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L742
Simplify Value Comparison - Saves some bytecode and makes logic leaner
https://github.com/code-423n4/2022-06-putty/blob/3b6b844bc39e897bd0bbb69897f2deff12dc3893/contracts/src/PuttyV2.sol#L495-L496
Can be refactored with
As in both case they were both true or both false
Consequence of Smart Comparison Above - Saves 20 to 80 gas (avg is 60)
Because of the gas save above we can refactor the entire block to an if / else
Becomes
Which reduces bytecode size, and saves gas in most situations as you're avoiding up to 3 extra comparisons