The code can be optimized by minimising the number of SLOADs. SLOADs are expensive (100 gas) compared to MLOADs/MSTOREs (3 gas). In the paragraphs below, please see the @audit-issue tags in the pieces of code's comments for more information about SLOADs that could be saved by caching the mentioned storage variables in memory variables.
Unchecking arithmetics operations that can't underflow/overflow
Solidity version 0.8+ comes with implicit overflow and underflow checks on unsigned integers. When an overflow or an underflow isn't possible (as an example, when a comparison is made before the arithmetic operation, or the operation doesn't depend on user input), some gas can be saved by using an unchecked block: https://docs.soliditylang.org/en/v0.8.10/control-structures.html#checked-or-unchecked-arithmetic
@audit tags
The code is annotated at multiple places with //@audit comments to pinpoint the issues. Please, pay attention to them for more details.
Summary
One pattern that was often seen is caching structs in memory when it's not needed. A copy in memory of a storage struct will trigger as many SLOADs as there are slots. If the struct's fields are only read once, or if the number of storage reading would be inferior to the number of slots: don't cache the struct in memory.
220: Position memory _taker = positions[maker];//@audit 3 SLOADs vs 2 enough
...
233: _taker.size,
234: _taker.openNotional
Therefore, use 220: Position storage _taker = positions[maker];
Do not cache makers[maker] in memory
Similarly, a copy in memory for Maker costs 7 SLOADs:
48: struct Maker {
49: uint vUSD;
50: uint vAsset;
51: uint dToken;
52: int pos; // position
53: int posAccumulator; // value of global.posAccumulator until which pos has been updated
54: int lastPremiumFraction;
55: int lastPremiumPerDtoken;
56: }
Here, caching the first 5 fields in memory is enough.
This line can't underflow for obvious mathematical reasons (_balance substracting at most itself). Therefore, it should be wrapped in an unchecked block
File: Oracle.sol
function getUnderlyingTwapPrice()
Unchecked block L81
This line can't underflow due to L76-L79. Therefore, it should be wrapped in an unchecked block
File: Interfaces.sol
struct Collateral
Tight packing structs to save slots
While this file is out of scope, it deeply impacts MarginAccount.sol.
I suggest going from:
Here, we need Collateral storage coll = supportedCollateral[idx];. Copying the struct in memory costs 3 SLOADs.
function _transferOutVusd()
Unchecked block L588
This line can't underflow due to L583. Therefore, it should be wrapped in an unchecked block
File: VUSD.sol
function processWithdrawals()
53: function processWithdrawals() external {
54: uint reserve = reserveToken.balanceOf(address(this));
55: require(reserve >= withdrawals[start].amount, 'Cannot process withdrawals at this time: Not enough balance'); //@audit start SLOAD 1
56: uint i = start;//@audit start SLOAD 2
57: while (i < withdrawals.length && (i - start) <= maxWithdrawalProcesses) { //@audit uncheck whole //@audit start SLOAD 3
58: Withdrawal memory withdrawal = withdrawals[i]; //@audit-ok
59: if (reserve < withdrawal.amount) {
60: break;
61: }
62: reserveToken.safeTransfer(withdrawal.usr, withdrawal.amount);
63: reserve -= withdrawal.amount; //@audit uncheck (see L59-L61)
64: i += 1;
65: }
66: start = i;
67: }
Unchecked block L57-L65
The whole while-loop can't underflow. Therefore, it should be wrapped in an unchecked block
Cache start in memory
Cache start in memory as initialStart and use it L55 + L57 (compare i to it in the while-loop)
General recommendations
Variables
No need to explicitly initialize variables with default values
If a variable is not set/initialized, it is assumed to have the default value (0 for uint, false for bool, address(0) for address...). Explicitly initializing it with its default value is an anti-pattern and wastes gas.
As an example: for (uint256 i = 0; i < numIterations; ++i) { should be replaced with for (uint256 i; i < numIterations; ++i) {
Instances include:
ClearingHouse.sol:122: for (uint i = 0; i < amms.length; i++) {
ClearingHouse.sol:130: for (uint i = 0; i < amms.length; i++) {
ClearingHouse.sol:170: for (uint i = 0; i < amms.length; i++) {
ClearingHouse.sol:194: for (uint i = 0; i < amms.length; i++) { // liquidate all positions
ClearingHouse.sol:251: for (uint i = 0; i < amms.length; i++) {
ClearingHouse.sol:263: for (uint i = 0; i < amms.length; i++) {
ClearingHouse.sol:277: for (uint i = 0; i < amms.length; i++) {
InsuranceFund.sol:52: uint shares = 0;
MarginAccount.sol:31: uint constant VUSD_IDX = 0;
MarginAccount.sol:331: for (uint i = 0; i < idxs.length; i++) {
MarginAccount.sol:521: for (uint i = 0; i < assets.length; i++) {
MarginAccount.sol:552: for (uint i = 0; i < _collaterals.length; i++) {
MarginAccountHelper.sol:13: uint constant VUSD_IDX = 0;
I suggest removing explicit initializations for default values.
Pre-increments cost less gas compared to post-increments
Comparisons
> 0 is less efficient than != 0 for unsigned integers (with proof)
!= 0 costs less gas compared to > 0 for unsigned integers in require statements with the optimizer enabled (6 gas)
Proof: While it may seem that > 0 is cheaper than !=, this is only true without the optimizer enabled and outside a require statement. If you enable the optimizer at 10k AND you're in a require statement, this will save gas. You can see this tweet for more proofs: https://twitter.com/gzeon/status/1485428085885640706
> 0 in require statements are used in the following location(s):
I suggest you change > 0 with != 0 in require statements. Also, enable the Optimizer.
For-Loops
An array's length should be cached to save gas in for-loops
Reading array length at each iteration of the loop takes 6 gas (3 for mload and 3 to place memory_offset) in the stack.
Caching the array length in the stack saves around 3 gas per iteration.
Here, I suggest storing the array's length in a variable before the for-loop, and use it instead:
ClearingHouse.sol:122: for (uint i = 0; i < amms.length; i++) {
ClearingHouse.sol:130: for (uint i = 0; i < amms.length; i++) {
ClearingHouse.sol:170: for (uint i = 0; i < amms.length; i++) {
ClearingHouse.sol:194: for (uint i = 0; i < amms.length; i++) { // liquidate all positions
ClearingHouse.sol:251: for (uint i = 0; i < amms.length; i++) {
ClearingHouse.sol:263: for (uint i = 0; i < amms.length; i++) {
ClearingHouse.sol:277: for (uint i = 0; i < amms.length; i++) {
MarginAccount.sol:331: for (uint i = 0; i < idxs.length; i++) {
MarginAccount.sol:373: for (uint i = 1 /* skip vusd */; i < assets.length; i++) {
MarginAccount.sol:521: for (uint i = 0; i < assets.length; i++) {
MarginAccount.sol:552: for (uint i = 0; i < _collaterals.length; i++) {
++i costs less gas compared to i++
++i costs less gas compared to i++ for unsigned integer, as pre-increment is cheaper (about 5 gas per iteration)
i++ increments i and returns the initial value of i. Which means:
uint i = 1;
i++; // == 1 but i == 2
But ++i returns the actual incremented value:
uint i = 1;
++i; // == 2 and i == 2 too, so no need for a temporary variable
In the first case, the compiler has to create a temporary variable (when used) for returning 1 instead of 2
Instances include:
ClearingHouse.sol:122: for (uint i = 0; i < amms.length; i++) {
ClearingHouse.sol:130: for (uint i = 0; i < amms.length; i++) {
ClearingHouse.sol:170: for (uint i = 0; i < amms.length; i++) {
ClearingHouse.sol:194: for (uint i = 0; i < amms.length; i++) { // liquidate all positions
ClearingHouse.sol:251: for (uint i = 0; i < amms.length; i++) {
ClearingHouse.sol:263: for (uint i = 0; i < amms.length; i++) {
ClearingHouse.sol:277: for (uint i = 0; i < amms.length; i++) {
MarginAccount.sol:331: for (uint i = 0; i < idxs.length; i++) {
MarginAccount.sol:373: for (uint i = 1 /* skip vusd */; i < assets.length; i++) {
MarginAccount.sol:521: for (uint i = 0; i < assets.length; i++) {
MarginAccount.sol:552: for (uint i = 0; i < _collaterals.length; i++) {
I suggest using ++i instead of i++ to increment the value of an uint variable.
Increments can be unchecked
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.
ClearingHouse.sol:122: for (uint i = 0; i < amms.length; i++) {
ClearingHouse.sol:130: for (uint i = 0; i < amms.length; i++) {
ClearingHouse.sol:170: for (uint i = 0; i < amms.length; i++) {
ClearingHouse.sol:194: for (uint i = 0; i < amms.length; i++) { // liquidate all positions
ClearingHouse.sol:251: for (uint i = 0; i < amms.length; i++) {
ClearingHouse.sol:263: for (uint i = 0; i < amms.length; i++) {
ClearingHouse.sol:277: for (uint i = 0; i < amms.length; i++) {
MarginAccount.sol:331: for (uint i = 0; i < idxs.length; i++) {
MarginAccount.sol:373: for (uint i = 1 /* skip vusd */; i < assets.length; i++) {
MarginAccount.sol:521: for (uint i = 0; i < assets.length; i++) {
MarginAccount.sol:552: for (uint i = 0; i < _collaterals.length; i++) {
The code would go from:
for (uint256 i; i < numIterations; i++) {
// ...
}
to:
for (uint256 i; i < numIterations;) {
// ...
unchecked { ++i; }
}
The risk of overflow is inexistant for a uint256 here.
Arithmetics
Shift Right instead of Dividing by 2
A division by 2 can be calculated by shifting one to the right.
While the DIV opcode uses 5 gas, the SHR opcode only uses 3 gas. Furthermore, Solidity's division operation also includes a division-by-0 prevention which is bypassed using shifting.
Starting from Solidity v0.8.4, there is a convenient and gas-efficient way to explain to users why an operation failed through the use of custom errors. Until now, you could already use strings to give more information about failures (e.g., revert("Insufficient funds.");), but they are rather expensive, especially when it comes to deploy cost, and it is difficult to use dynamic information in them.
Custom errors are defined using the error statement, which can be used inside and outside of contracts (including interfaces and libraries).
Gas Report
Table of Contents:
calldata
instead ofmemory
forstring _name
positions[trader]
in memorypositions[trader]
in memorypositions[maker]
in memorymakers[maker]
in memorypositions[trader]
in memorymakers[trader]
in memorypositions[trader]
in memorymakers[trader]
in memorypositions[trader]
in memorypositions[trader]
in memorypositions[trader]
in memoryreserveSnapshots[snapshotIndex]
in memoryreserveSnapshots.length
in memorysupportedCollateral[idx]
in memorystart
in memory> 0
is less efficient than!= 0
for unsigned integers (with proof)++i
costs less gas compared toi++
Foreword
@audit
tagsSummary
File: AMM.sol
function initialize()
Use
calldata
instead ofmemory
forstring _name
An external function passing a readonly variable should mark it as
calldata
and notmemory
function openPosition()
Do not cache
positions[trader]
in memoryAs a copy in memory of a struct makes as many SLOADs as there are slots, here a copy costs 3 SLOADs:
However, only the
size
field is read twice. Therefore, only this field should get cached:int256 _size = positions[trader].size;
function liquidatePosition()
Do not cache
positions[trader]
in memorySimilar to Do not cache
positions[trader]
in memory.However, only the
size
field is read 3 times. Therefore, only this field should get cached:int256 _size = positions[trader].size;
function removeLiquidity()
Do not cache
positions[maker]
in memorySimilar to Do not cache
positions[trader]
in memory. However, here, even the fields shouldn't get cached, as they are read only once:Therefore, use
220: Position storage _taker = positions[maker];
Do not cache
makers[maker]
in memorySimilarly, a copy in memory for
Maker
costs 7 SLOADs:Here, caching the first 5 fields in memory is enough.
function getNotionalPositionAndUnrealizedPnl()
Do not cache
positions[trader]
in memoryHere, we need
Position storage _taker = positions[trader];
Do not cache
makers[trader]
in memoryHere, we need
Maker storage _maker = makers[trader];
function getPendingFundingPayment()
Do not cache
positions[trader]
in memoryHere, we need
Position storage _taker = positions[trader];
Do not cache
makers[trader]
in memoryHere, we need
Maker storage _maker = makers[trader];
function getTakerNotionalPositionAndUnrealizedPnl()
Do not cache
positions[trader]
in memoryHere, we need to cache these fields:
size
andopenNotional
function _emitPositionChanged()
Do not cache
positions[trader]
in memoryHere, we need
Position storage _taker = positions[trader];
function _openReversePosition()
Do not cache
positions[trader]
in memoryHere, we need to cache the
size
fieldUnchecked block L597
This line can't underflow due to the condition L596. Therefore, it should be wrapped in an
unchecked
blockfunction _calcTwap()
Do not cache
reserveSnapshots[snapshotIndex]
in memoryHere, we need to cache the
timestamp
field. Copying the struct in memory costs 3 SLOADs.Cache
reserveSnapshots.length
in memoryThis would save 1 SLOAD
Unchecked block L684
This line can't underflow due to the condition L680-L682. Therefore, it should be wrapped in an
unchecked
blockUse the cache for calculation
As we already have
currentSnapshot = reserveSnapshots[snapshotIndex];
: use it here:currentPrice = currentSnapshot.lastPrice;
File: ClearingHouse.sol
function _disperseLiquidationFee()
Unchecked block L214
This line can't underflow due to the condition L212. Therefore, it should be wrapped in an
unchecked
blockFile: InsuranceFund.sol
function pricePerShare()
Unchecked block L97
This line can't underflow for obvious mathematical reasons (
_balance
substracting at most itself). Therefore, it should be wrapped in anunchecked
blockFile: Oracle.sol
function getUnderlyingTwapPrice()
Unchecked block L81
This line can't underflow due to L76-L79. Therefore, it should be wrapped in an
unchecked
blockFile: Interfaces.sol
struct Collateral
Tight packing structs to save slots
While this file is out of scope, it deeply impacts MarginAccount.sol. I suggest going from:
to
To save 1 slot per array element in MarginAccount.sol's storage
File: MarginAccount.sol
function _getLiquidationInfo()
Do not cache
supportedCollateral[idx]
in memoryHere, we need
Collateral storage coll = supportedCollateral[idx];
. Copying the struct in memory costs 3 SLOADs.function _transferOutVusd()
Unchecked block L588
This line can't underflow due to L583. Therefore, it should be wrapped in an
unchecked
blockFile: VUSD.sol
function processWithdrawals()
Unchecked block L57-L65
The whole while-loop can't underflow. Therefore, it should be wrapped in an
unchecked
blockCache
start
in memoryCache
start
in memory asinitialStart
and use it L55 + L57 (comparei
to it in the while-loop)General recommendations
Variables
No need to explicitly initialize variables with default values
If a variable is not set/initialized, it is assumed to have the default value (
0
foruint
,false
forbool
,address(0)
for address...). Explicitly initializing it with its default value is an anti-pattern and wastes gas.As an example:
for (uint256 i = 0; i < numIterations; ++i) {
should be replaced withfor (uint256 i; i < numIterations; ++i) {
Instances include:
I suggest removing explicit initializations for default values.
Pre-increments cost less gas compared to post-increments
Comparisons
> 0
is less efficient than!= 0
for unsigned integers (with proof)!= 0
costs less gas compared to> 0
for unsigned integers inrequire
statements with the optimizer enabled (6 gas)Proof: While it may seem that
> 0
is cheaper than!=
, this is only true without the optimizer enabled and outside a require statement. If you enable the optimizer at 10k AND you're in arequire
statement, this will save gas. You can see this tweet for more proofs: https://twitter.com/gzeon/status/1485428085885640706> 0
in require statements are used in the following location(s):I suggest you change
> 0
with!= 0
in require statements. Also, enable the Optimizer.For-Loops
An array's length should be cached to save gas in for-loops
Reading array length at each iteration of the loop takes 6 gas (3 for mload and 3 to place memory_offset) in the stack.
Caching the array length in the stack saves around 3 gas per iteration.
Here, I suggest storing the array's length in a variable before the for-loop, and use it instead:
++i
costs less gas compared toi++
++i
costs less gas compared toi++
for unsigned integer, as pre-increment is cheaper (about 5 gas per iteration)i++
incrementsi
and returns the initial value ofi
. Which means:But
++i
returns the actual incremented value:In the first case, the compiler has to create a temporary variable (when used) for returning
1
instead of2
Instances include:
I suggest using
++i
instead ofi++
to increment the value of an uint variable.Increments can be unchecked
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.
ethereum/solidity#10695
Instances include:
The code would go from:
to:
The risk of overflow is inexistant for a
uint256
here.Arithmetics
Shift Right instead of Dividing by 2
A division by 2 can be calculated by shifting one to the right.
While the
DIV
opcode uses 5 gas, theSHR
opcode only uses 3 gas. Furthermore, Solidity's division operation also includes a division-by-0 prevention which is bypassed using shifting.I suggest replacing
/ 2
with>> 1
here:Errors
Reduce the size of error messages (Long revert Strings)
Shortening revert strings to fit in 32 bytes will decrease deployment time gas and will decrease runtime gas when the revert condition is met.
Revert strings that are longer than 32 bytes require at least one additional mstore, along with additional overhead for computing memory offset, etc.
Revert strings > 32 bytes are here:
I suggest shortening the revert strings to fit in 32 bytes, or that using custom errors as described next.
Use Custom Errors instead of Revert Strings to save Gas
Custom errors from Solidity 0.8.4 are cheaper than revert strings (cheaper deployment cost and runtime cost when the revert condition is met)
Source: https://blog.soliditylang.org/2021/04/21/custom-errors/:
Custom errors are defined using the
error
statement, which can be used inside and outside of contracts (including interfaces and libraries).Instances include:
I suggest replacing revert strings with custom errors.