Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Gas Optimizations #52

Open
code423n4 opened this issue Jun 13, 2022 · 1 comment
Open

Gas Optimizations #52

code423n4 opened this issue Jun 13, 2022 · 1 comment
Labels
bug Something isn't working G (Gas Optimization)

Comments

@code423n4
Copy link
Contributor

code423n4 commented Jun 13, 2022

report submitted via email on 2022/06/13 at 08:43 UTC

Gas Report

Table of Contents

Caching storage variables in memory to save gas

IMPACT

Anytime you are reading from storage more than once, it is cheaper in gas cost to cache the variable in memory: a SLOAD cost 100gas, while MLOAD and MSTORE cost 3 gas.

In particular, in for loops, when using the length of a storage array as the condition being checked after each loop, caching the array length in memory can yield significant gas savings if the array length is high

PROOF OF CONCEPT

Instances include:

ConnextPriceOracle.sol

scope: getTokenPrice()

  • v1PriceOracle is read twice:

line 93
line 94

ProposedOwnableUpgradeable.sol

scope: renounceRouterOwnership()

  • _routerOwnershipTimestamp is read twice:

line 175
line 178

scope: renounceAssetOwnership()

  • _routerOwnershipTimestamp is read twice:

line 217
line 220

scope: renounceOwnership()

  • _routerOwnershipTimestamp is read twice:

line 255
line 258

  • proposed is read twice:

line 262
line 265

scope: acceptProposedOwner()

  • proposed is read twice:

line 274
line 286

SponsorVault.sol

scope: reimburseLiquidityFees()

  • tokenExchanges[_token] is read twice:

line 203
line 205

scope: reimburseRelayerFees()

  • gasTokenOracle is read twice:

line 243
line 244

  • relayerFeeCap is read twice, but only if sponsoredFee is greater relayerFeeCap than:

line 256

ProposedOwnable.sol

scope: renounceOwnership()

  • _proposedOwnershipTimestamp is read twice:

line 125
line 128

  • _proposedO is read twice:

line 132
line 135

TOOLS USED

Manual Analysis

MITIGATION

cache these storage variables in memory

Calldata instead of memory for RO function parameters

PROBLEM

If a reference type function parameter is read-only, it is cheaper in gas to use calldata instead of memory.
Calldata is a non-modifiable, non-persistent area where function arguments are stored, and behaves mostly like memory.

Try to use calldata as a data location because it will avoid copies and also makes sure that the data cannot be modified.

PROOF OF CONCEPT

Instances include:

BridgeFacet.sol

scope: handle()

bytes memory _message

scope: _reconcile()

bytes memory _message

scope: _getTransferId()

ConnextMessage.TokenId memory _canonical

scope: _reconcileProcessMessage()

bytes memory _message

PromiseRouter.sol

scope: handle()

bytes memory _message

RelayerFeeRouter.sol

scope: handle()

bytes memory _message

AssetLogic.sol

scope: swapToLocalAssetIfNeeded()

ConnextMessage.TokenId memory _canonical

ConnextMessage.sol

scope: formatTokenId()

TokenId memory _tokenId

scope: formatDetailsHash()

string memory _name
string memory _symbol

LibCrossDomainProperty.sol

scope: parseDomainAndSenderBytes()

bytes memory _property

LibDiamond.sol

scope: proposeDiamondCut()

IDiamondCut.FacetCut[] memory _diamondCut
bytes memory _calldata

scope: rescindDiamondCut()

IDiamondCut.FacetCut[] memory _diamondCut
bytes memory _calldata

scope: diamondCut()

IDiamondCut.FacetCut[] memory _diamondCut
bytes memory _calldata

scope: addFunctions()

bytes4[] memory _functionSelectors

scope: replaceFunctions()

bytes4[] memory _functionSelectors

scope: removeFunctions()

bytes4[] memory _functionSelectors

scope: initializeDiamondCut()

bytes memory _calldata

TOOLS USED

Manual Analysis

MITIGATION

Replace memory with calldata

Comparison Operators

IMPACT

In the EVM, there is no opcode for >= or <=.
When using greater than or equal, two operations are performed: > and =.

Using strict comparison operators hence saves gas

PROOF OF CONCEPT

Instances include:

ProposedOwnableFacet.sol

(block.timestamp - s._routerOwnershipTimestamp) <= _delay
(block.timestamp - s._assetOwnershipTimestamp) <= _delay
(block.timestamp - s._proposedOwnershipTimestamp) <= _delay
(block.timestamp - s._proposedOwnershipTimestamp) <= _delay

RoutersFacet.sol

block.timestamp - s.routerPermissionInfo.proposedRouterTimestamp[router] <= _delay

getSwapToken.sol

index >= s.swapStorages[canonicalId].pooledTokens.length
index >= s.swapStorages[canonicalId].balances.length
_pooledTokens.length <= 1
_a >= AmplificationUtils.MAX_A
_fee >= SwapUtils.MAX_SWAP_FEE
_adminFee >= SwapUtils.MAX_ADMIN_FEE

ProposedOwnableUpgradeable.sol

(block.timestamp - _routerOwnershipTimestamp) <= _delay
(block.timestamp - _assetOwnershipTimestamp) <= _delay
(block.timestamp - _proposedOwnershipTimestamp) <= _delay
(block.timestamp - _proposedOwnershipTimestamp) <= _delay

SponsorVault.sol

currentBalance >= amountIn

StableSwap.sol

_pooledTokens.length <= 32
decimals[i] <= SwapUtils.POOL_PRECISION_DECIMALS
block.timestamp <= deadline

ProposedOwnable.sol

(block.timestamp - _proposedOwnershipTimestamp) <= _delay
(block.timestamp - _proposedOwnershipTimestamp) <= _delay

AmplificationUtils.sol

block.timestamp >= self.initialATime.add(1 days)
futureTime_ >= block.timestamp.add(MIN_RAMP_TIME)
futureAPrecise.mul(MAX_A_CHANGE) >= initialAPrecise
futureAPrecise <= initialAPrecise.mul(MAX_A_CHANGE)

AssetLogic.sol

_maxIn >= ipool.calculateSwapInv(tokenIndexIn, tokenIndexOut, _amountOut)
_amountIn <= _maxIn

MathUtils.sol

difference(a, b) <= 1

SwapUtils.sol

tokenAmount <= xp[tokenIndex]
amount <= totalSupply
dx <= tokenFrom.balanceOf(msg.sender)
dy >= minDy
dy <= self.balances[tokenIndexTo]
dx <= maxDx
dx <= tokenFrom.balanceOf(msg.sender)
dx <= tokenFrom.balanceOf(msg.sender)
dy >= minDy
dy <= self.balances[tokenIndexTo]
dx <= maxDx
toMint >= minToMint
amount <= lpToken.balanceOf(msg.sender)
amounts[i] >= minAmounts[i]
tokenAmount <= lpToken.balanceOf(msg.sender)
dy >= minAmount
maxBurnAmount <= v.lpToken.balanceOf(msg.sender)https://github.com/code-423n4/2022-06-connext/blob/4dd6149748b635f95460d4c3924c7e3fb6716967/contracts/contracts/core/connext/libraries/SwapUtils.sol#L1007\
tokenAmount <= maxBurnAmount
newAdminFee <= MAX_ADMIN_FEE
newSwapFee <= MAX_SWAP_FEE

PromiseMessage.sol

_len <= LENGTH_RETURNDATA_START
_len >= MIN_CLAIM_LEN

TOOLS USED

Manual Analysis

MITIGATION

Replace <= with <, and >= with >. Do not forget to increment/decrement the compared variable

example:

-(block.timestamp - s._proposedOwnershipTimestamp) <= _delay
+(block.timestamp - s._proposedOwnershipTimestamp) < _delay - 1;

However, if 1 is negligible compared to the value of the variable, we can omit the increment.

example:

-(block.timestamp - s._proposedOwnershipTimestamp) <= _delay;
+(block.timestamp - s._proposedOwnershipTimestamp) <= _delay;

Constructor parameters should be avoided when possible

IMPACT

Constructor parameters are expensive. The contract deployment will be cheaper in gas if they are hard coded instead of using constructor parameters: it costs approximately 400 less gas in deployment per variable hardcoded.

PROOF OF CONCEPT

Instances include:

ConnextPriceOracle.sol

wrapped = _wrapped;

Executor.sol

connext = _connext

TOOLS USED

Manual Analysis

MITIGATION

Hardcode variables with their initial value instead of writing it during contract deployment with constructor parameters.

Custom Errors

IMPACT

Custom errors from Solidity 0.8.4 are cheaper than revert strings (cheaper deployment cost and runtime cost when the revert condition is met) while providing the same amount of information, as explained here

Custom errors are defined using the error statement

PROOF OF CONCEPT

Instances include:

BaseConnextFacet.sol

require(s._status != _ENTERED, "ReentrancyGuard: reentrant call")
require(_remote != bytes32(0), "!remote")

ConnextPriceOracle.sol

require(baseTokenPrice > 0, "invalid base token")

LPToken.sol

require(amount != 0, "LPToken: cannot mint 0")
require(to != address(this), "LPToken: cannot send to itself")

StableSwap.sol

require(_pooledTokens.length > 1, "_pooledTokens.length <= 1")
require(_pooledTokens.length <= 32, "_pooledTokens.length > 32")
require(_pooledTokens.length == decimals.length, "_pooledTokens decimals mismatch")
require(tokenIndexes[address(_pooledTokens[i])] == 0 && _pooledTokens[0] != _pooledTokens[i],"Duplicate tokens")
require(address(_pooledTokens[i]) != address(0), "The 0 address isn't an ERC-20")
require(decimals[i] <= SwapUtils.POOL_PRECISION_DECIMALS, "Token decimals exceeds max")
require(_a < AmplificationUtils.MAX_A, "_a exceeds maximum")
require(_fee < SwapUtils.MAX_SWAP_FEE, "_fee exceeds maximum")
require(_adminFee < SwapUtils.MAX_ADMIN_FEE, "_adminFee exceeds maximum")
require(lpToken.initialize(lpTokenName, lpTokenSymbol), "could not init lpToken clone")
require(index < swapStorage.pooledTokens.length, "Out of range")
require(address(getToken(index)) == tokenAddress, "Token does not exist")
require(index < swapStorage.pooledTokens.length, "Index out of range")

AmplificationUtils.sol

require(block.timestamp >= self.initialATime.add(1 days), "Wait 1 day before starting ramp")
require(futureTime_ >= block.timestamp.add(MIN_RAMP_TIME), "Insufficient ramp time")
require(futureA_ > 0 && futureA_ < MAX_A, "futureA_ must be > 0 and < MAX_A")
require(futureAPrecise.mul(MAX_A_CHANGE) >= initialAPrecise, "futureA_ is too small")
require(futureAPrecise <= initialAPrecise.mul(MAX_A_CHANGE), "futureA_ is too large")
require(self.futureATime > block.timestamp, "Ramp is already stopped")

ConnextMessage.sol

require(isValidAction(_action), "!action")

LibDiamond.sol

require(msg.sender == diamondStorage().contractOwner, "LibDiamond: Must be contract owner")
require(diamondStorage().acceptanceTimes[keccak256(abi.encode(_diamondCut))] < block.timestamp,"LibDiamond: delay not elapsed")
require(_functionSelectors.length > 0, "LibDiamondCut: No selectors in facet to cut")
require(_facetAddress != address(0), "LibDiamondCut: Add facet can't be address(0)")
require(oldFacetAddress == address(0), "LibDiamondCut: Can't add function that already exists")
require(_functionSelectors.length > 0, "LibDiamondCut: No selectors in facet to cut")
require(_facetAddress != address(0), "LibDiamondCut: Add facet can't be address(0)")
require(oldFacetAddress != _facetAddress, "LibDiamondCut: Can't replace function with same function")
require(_functionSelectors.length > 0, "LibDiamondCut: No selectors in facet to cut")
require(_facetAddress == address(0), "LibDiamondCut: Remove facet address must be address(0)")
require(_facetAddress != address(0), "LibDiamondCut: Can't remove function that doesn't exist")
require(_facetAddress != address(this), "LibDiamondCut: Can't remove immutable function")
require(_calldata.length == 0, "LibDiamondCut: _init is address(0) but_calldata is not empty")
require(_calldata.length > 0, "LibDiamondCut: _calldata is empty but _init is not address(0)")
require(contractSize > 0, _errorMessage)

SwapUtils.sol

require(tokenIndex < xp.length, "Token index out of range")
require(tokenAmount <= xp[tokenIndex], "Withdraw exceeds available")
require(tokenIndex < numTokens, "Token not found"
require(numTokens == precisionMultipliers.length, "Balances must match multipliers")
require(tokenIndexFrom != tokenIndexTo, "Can't compare token to itself")
require(tokenIndexFrom < numTokens && tokenIndexTo < numTokens, "Tokens must be in pool")
require(tokenIndexFrom < xp.length && tokenIndexTo < xp.length, "Token index out of range")
require(tokenIndexFrom < xp.length && tokenIndexTo < xp.length, "Token index out of range")
require(amount <= totalSupply, "Cannot exceed total supply")
require(index < self.pooledTokens.length, "Token index out of range")
require(dx <= tokenFrom.balanceOf(msg.sender), "Cannot swap more than you own")
require(dy >= minDy, "Swap didn't result in min tokens")
require(dy <= self.balances[tokenIndexTo], "Cannot get more than pool balance")
require(dx <= maxDx, "Swap needs more than max tokens")
require(dx <= tokenFrom.balanceOf(msg.sender), "Cannot swap more than you own")
require(dx == tokenFrom.balanceOf(address(this)).sub(beforeBalance), "not support fee token")
require(dx <= tokenFrom.balanceOf(msg.sender), "Cannot swap more than you own")
require(dy >= minDy, "Swap didn't result in min tokens")
require(dy <= self.balances[tokenIndexTo], "Cannot get more than pool balance")
require(dx <= maxDx, "Swap didn't result in min tokens")
require(amounts.length == pooledTokens.length, "Amounts must match pooled tokens")
require(v.totalSupply != 0 || amounts[i] > 0, "Must supply all tokens in pool")
require(v.d1 > v.d0, "D should increase")
require(toMint >= minToMint, "Couldn't mint min requested")
require(amount <= lpToken.balanceOf(msg.sender), ">LP.balanceOf")
require(minAmounts.length == pooledTokens.length, "minAmounts must match poolTokens")
require(amounts[i] >= minAmounts[i], "amounts[i] < minAmounts[i]")
require(tokenAmount <= lpToken.balanceOf(msg.sender), ">LP.balanceOf")
require(tokenIndex < pooledTokens.length, "Token not found")
require(dy >= minAmount, "dy < minAmount")
require(amounts.length == pooledTokens.length, "Amounts should match pool tokens")
require(maxBurnAmount <= v.lpToken.balanceOf(msg.sender) && maxBurnAmount != 0, ">LP.balanceOf")
require(tokenAmount != 0, "Burnt amount cannot be zero")
require(tokenAmount <= maxBurnAmount, "tokenAmount > maxBurnAmount")
require(newAdminFee <= MAX_ADMIN_FEE, "Fee is too high")
require(newSwapFee <= MAX_SWAP_FEE, "Fee is too high")

TOOLS USED

Manual Analysis

MITIGATION

Replace require and revert statements with custom errors.

For instance, in StableSwap.sol:

Replace

require(_pooledTokens.length > 1, "_pooledTokens.length <= 1")

with

if (_pooledTokens.length < 2) {
		revert IncorrectPoolTokenLength();
}

and define the custom error in the contract

error IncorrectPoolTokenLength();

Default value initialization

IMPACT

If a variable is not set/initialized, it is assumed to have the default value (0, false, 0x0 etc depending on the data type).
Explicitly initializing it with its default value is an anti-pattern and wastes gas.

PROOF OF CONCEPT

Instances include:

StableSwapFaucet.sol

uint8 i = 0

ConnextPriceOracle.sol

uint256 i = 0

StableSwap.sol

uint8 i = 0

Encoding.sol

uint8 i = 0

SwapUtils.sol

uint256 i = 0
uint256 i = 0
uint256 i = 0
uint256 i = 0
uint256 i = 0
uint256 j = 0
uint256 i = 0
uint256 i = 0
uint256 i = 0
uint256 i = 0
uint256 i = 0
uint256 i = 0
uint256 i = 0
uint256 i = 0
uint256 i = 0
uint256 i = 0
uint256 i = 0
uint256 i = 0

RelayerFeeMessage.sol

uint256 i = 0

TOOLS USED

Manual Analysis

MITIGATION

Remove explicit initialization for default values.

Event emitting of local variable

PROBLEM

When emitting an event, using a local variable instead of a storage variable saves gas.

PROOF OF CONCEPT

Instances include:

ProposedOwnableFacet.sol

emit RouterOwnershipRenunciationProposed(s._routerOwnershipTimestamp)
emit AssetOwnershipRenunciationProposed(s._assetOwnershipTimestamp)
emit OwnershipProposed(s._proposed)

ProposedOwnableUpgradeable.sol

emit RouterOwnershipRenunciationProposed(_routerOwnershipTimestamp)
emit AssetOwnershipRenunciationProposed(_assetOwnershipTimestamp)

ProposedOwnable.sol

emit OwnershipProposed(_proposed)

TOOLS USED

Manual Analysis

MITIGATION

In all instances, the storage variable can be replaced by the function argument that is written to that storage variable.

Group logic in modifier

PROBLEM

You can group repetitive require statements into a modifier to save on deployment costs. There is however a tradeoff to doing this: declaring the logic inline is cheaper than having it in a modifier when you call the function that uses that modifier.

PROOF OF CONCEPT

Instances include:

StableSwap.sol

require(index < swapStorage.pooledTokens.length, "Out of range")
require(index < swapStorage.pooledTokens.length, "Index out of range")

LibDiamonds.sol

require(_functionSelectors.length > 0, "LibDiamondCut: No selectors in facet to cut")
require(_functionSelectors.length > 0, "LibDiamondCut: No selectors in facet to cut")
require(_functionSelectors.length > 0, "LibDiamondCut: No selectors in facet to cut")

SwapUtils.sol

require(dx <= tokenFrom.balanceOf(msg.sender), "Cannot swap more than you own")
require(dx <= tokenFrom.balanceOf(msg.sender), "Cannot swap more than you own")

require(tokenIndexFrom < xp.length && tokenIndexTo < xp.length, "Token index out of range")
require(tokenIndexFrom < xp.length && tokenIndexTo < xp.length, "Token index out of range")

TOOLS USED

Manual Analysis

MITIGATION

You can create modifiers for repetitive require statements and apply them to the functions where the require statement mentioned are called. Again, this one is more of a suggestion because there is a trade-off.

Immutable variables can save gas

PROBLEM

If a storage variable is never modified, marking it as immutable can save gas, as it is saved as a constant instead of a storage variable, saving a SLOAD operation (100 gas) every time the variable is read.

PROOF OF CONCEPT

Instances include:

ConnextPriceOracle.sol

wrapped is initialized once and can not be modified afterwards. It is read once in every call to getTokenPrice.

TOOLS USED

Manual Analysis

MITIGATION

add the immutable keyword to address public wrapped

Prefix increments

IMPACT

Prefix increments are cheaper than postfix increments: it returns the incremented variable instead of returning a temporary variable storing the initial value of the variable. It saves 5 gas per iteration

PROOF OF CONCEPT

Instances include:

BridgeFacet.sol

i++
i++
i++

DiamondLoupeFacet.sol

i++

RelayerFacet.sol

i++
i++

StableSwapFacet.sol

i++

ConnextPriceOracle.sol

i++

StableSwap.sol

i++

LibDiamond.sol

facetIndex++
selectorIndex++
selectorPosition++
selectorIndex++
selectorPosition++
selectorIndex++

SwapUtils.sol

i++
i++
i++
i++
i++
j++
i++
i++
i++
i++
i++
i++
i++
i++
i++
i++
i++
i++

RelayerFeeMessage.sol

i++

TOOLS USED

Manual Analysis

MITIGATION

change variable++ to ++variable.

Redundant libraries

IMPACT

As of Solidity 0.8.0, underflow and overflow checks are performed by default on additions and subtractions. Using an external library for these computations should be avoided as it will waste gas.

PROOF OF CONCEPT

Several contracts in scope use the SafeMath library of OpenZeppelin and its functions sub and add

ConnextPriceOracle.sol
AmplificationUtils.sol
SwapUtils.sol

TOOLS USED

Manual Analysis

MITIGATION

Use the native additions and subtractions.

Require instead of AND

IMPACT

Require statements including conditions with the && operator can be broken down in multiple require statements to save gas.

PROOF OF CONCEPT

Instances include:

StableSwap.sol

require(tokenIndexes[address(_pooledTokens[i])] == 0 && _pooledTokens[0] != _pooledTokens[i],"Duplicate tokens")

AmplificationUtils.sol

require(futureA_ > 0 && futureA_ < MAX_A, "futureA_ must be > 0 and < MAX_A")

SwapUtils.sol

require(tokenIndexFrom < numTokens && tokenIndexTo < numTokens, "Tokens must be in pool")
require(tokenIndexFrom < xp.length && tokenIndexTo < xp.length, "Token index out of range")
require(tokenIndexFrom < xp.length && tokenIndexTo < xp.length, "Token index out of range")
require(maxBurnAmount <= v.lpToken.balanceOf(msg.sender) && maxBurnAmount != 0, ">LP.balanceOf")

TOOLS USED

Manual Analysis

MITIGATION

Split the require statement in several statements. For instance:

-require(futureA_ > 0 && futureA_ < MAX_A, "futureA_ must be > 0 and < MAX_A")
+require(futureA_ > 0)
+require(futureA_ < MAX_A)

To improve the savings, you can also use custom errors instead of require statements

Tight Variable Packing

PROBLEM

Solidity contracts have contiguous 32 bytes (256 bits) slots used in storage.
By arranging the variables, it is possible to minimize the number of slots used within a contract's storage and therefore reduce deployment costs.

address type variables are each of 20 bytes size (way less than 32 bytes). However, they here take up a whole 32 bytes slot (they are contiguous).

As bool type variables are of size 1 byte, there's a slot here that can get saved by moving them closer to an address

PROOF OF CONCEPT

Instances include:

ProposedOwnableUpgradeable.sol

address private _owner;

address private _proposed;
uint256 private _proposedOwnershipTimestamp;

bool private _routerOwnershipRenounced;
uint256 private _routerOwnershipTimestamp;

bool private _assetOwnershipRenounced;

TOOLS USED

Manual Analysis

MITIGATION

Place _routerOwnershipRenounced after _owner and _assetOwnershipRenounced _proposed to save two storage slots

address private _owner;
+bool private _routerOwnershipRenounced;
address private _proposed;
+bool private _assetOwnershipRenounced;
uint256 private _proposedOwnershipTimestamp;

uint256 private _routerOwnershipTimestamp;

Unchecked arithmetic

IMPACT

The default "checked" behavior costs more gas when adding/diving/multiplying, because under-the-hood those checks are implemented as a series of opcodes that, prior to performing the actual arithmetic, check for under/overflow and revert if it is detected.

if it can statically be determined there is no possible way for your arithmetic to under/overflow (such as a condition in an if statement), surrounding the arithmetic in an unchecked block will save gas. This is particularly true in for loops, as it saves some gas at each iteration.

PROOF OF CONCEPT

Instances include:

BridgeFacet.sol

availableAmount - backUnbackedAmount, cannot overflow because of the condition line 1099

DiamondLoupeFacet.sol

i++

StableSwapFacet.sol

i++

ConnextPriceOracle.sol

i++

StableSwap.sol

i++

LibDiamond.sol

facetIndex++
selectorIndex++
selectorIndex++
selectorIndex++

SwapUtils.sol

i++
i++
i++
i++
i++
j++
i++
i++
i++
i++
i++
i++
i++
i++
i++
i++
i++
i++

TOOLS USED

Manual Analysis

MITIGATION

Place the arithmetic operations in an unchecked block

Unnecessary computation

IMPACT

When emitting an event that includes a new and an old value, it is cheaper in gas to avoid caching the old value in memory. Instead, emit the event, then save the new value in storage.

PROOF OF CONCEPT

Instances include:

ConnextPriceOracle.sol

line 169

address oldAdmin = admin;
admin = newAdmin;

emit NewAdmin(oldAdmin, newAdmin)

LibDiamond.sol

line 55

address previousOwner = ds.contractOwner;
ds.contractOwner = _newOwner;
emit OwnershipTransferred(previousOwner, _newOwner)

ProposedOwnable.sol

line 162

address oldOwner = _owner;
_owner = newOwner;
_proposedOwnershipTimestamp = 0;
_proposed = address(0);
emit OwnershipTransferred(oldOwner, newOwner)

TOOLS USED

Manual Analysis

MITIGATION

Emit the old storage variable before writing the new value.

e.g:

emit NewAdmin(admin, newAdmin)
admin = newAdmin
@code423n4 code423n4 added bug Something isn't working G (Gas Optimization) labels Jun 13, 2022
code423n4 added a commit that referenced this issue Jun 13, 2022
@kartoonjoy
Copy link
Collaborator

Added the file per the email submission for joestakey. Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working G (Gas Optimization)
Projects
None yet
Development

No branches or pull requests

2 participants