Issue | Instances | |
---|---|---|
GAS-1 | Use assembly to check for address(0) |
1 |
GAS-2 | Using bools for storage incurs overhead | 2 |
GAS-3 | Cache array length outside of loop | 5 |
GAS-4 | State variables should be cached in stack variables rather than re-reading them from storage | 6 |
GAS-5 | Use calldata instead of memory for function arguments that do not get mutated | 2 |
GAS-6 | Use Custom Errors | 20 |
GAS-7 | Don't initialize variables with default value | 4 |
GAS-8 | ++i costs less gas than i++ , especially when it's used in for -loops (--i /i-- too) |
18 |
GAS-9 | Use != 0 instead of > 0 for unsigned integer comparison | 18 |
GAS-10 | internal functions not called by the contract should be removed |
32 |
Saves 6 gas per instance
Instances (1):
File: models/Factory.sol
41: require(msg.sender == owner && _owner != address(0), "Factory:NOT_ALLOWED");
Use uint256(1) and uint256(2) for true/false to avoid a Gwarmaccess (100 gas), and to avoid Gsset (20000 gas) when changing from ‘false’ to ‘true’, after having been ‘true’ in the past. See source.
Instances (2):
File: libraries/BinMap.sol
75: function nextActive(mapping(int32 => uint256) storage binMap, int32 tick, bool isRight) internal view returns (int32 nextTick) {
File: models/Factory.sol
18: mapping(IPool => bool) public override isFactoryPool;
If not cached, the solidity compiler will always read the length of the array during each iteration. That is, if it is a storage array, this is an extra sload operation (100 additional extra gas for each iteration except for the first) and if it is a memory array, this is an extra mload operation (3 additional gas for each iteration except for the first).
Instances (5):
File: models/Pool.sol
127: for (uint256 i; i < params.length; i++) {
180: for (uint256 i; i < binIds.length; i++) {
193: for (uint256 i; i < params.length; i++) {
224: for (uint256 i; i < params.length; i++) {
File: models/PoolInspector.sol
101: for (uint256 i; i < bins.length; i++) {
[GAS-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 replaces 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.
Saves 100 gas per instance
Instances (6):
File: libraries/BinMap.sol
68: offset += uint16(uint32(NUMBER_OF_KINDS_32));
File: models/Factory.sol
80: emit PoolCreated(address(pool), _fee, _tickSpacing, _activeTick, _lookback, protocolFeeRatio, _tokenA, _tokenB);
File: models/Pool.sol
164: tokenAAmount = Math.toScale(tokenAAmount, tokenAScale, true);
165: tokenBAmount = Math.toScale(tokenBAmount, tokenBScale, true);
518: sqrtUpperTickPrice = BinMath.tickSqrtPrice(tickSpacing, tick + 1);
File: models/PositionMetadata.sol
22: return bytes(baseURI).length > 0 ? string(abi.encodePacked(baseURI, tokenId.toString())) : "";
Mark data types as calldata
instead of memory
where possible. This makes it so that the data is not automatically loaded into memory. If the data passed into the function does not need to be changed (like updating values in an array), it can be passed in as calldata
. The one exception to this is if the argument must later be passed into another function that takes an argument that specifies memory
storage.
Instances (2):
File: models/PositionMetadata.sol
13: constructor(string memory _baseURI) {
17: function setBaseURI(string memory _baseURI) external onlyOwner {
Source Instead of using error strings, to reduce deployment and runtime cost, you should use Custom Errors. This would save both deployment and runtime cost.
Instances (20):
File: libraries/Bin.sol
85: require(deltaLpToken != 0, "L");
140: require(activeBin.state.mergeId == 0, "N");
File: libraries/BinMath.sol
94: require(tick <= MAX_TICK, "X");
File: libraries/Cast.sol
6: require((y = uint128(x)) == x, "C");
File: libraries/SafeERC20Min.sol
18: require(abi.decode(returndata, (bool)), "T");
File: models/Factory.sol
27: require(_protocolFeeRatio <= ONE_3_DECIMAL_SCALE, "Factory:PROTOCOL_FEE_CANNOT_EXCEED_ONE");
34: require(msg.sender == owner, "Factory:NOT_ALLOWED");
35: require(_protocolFeeRatio <= ONE_3_DECIMAL_SCALE, "Factory:PROTOCOL_FEE_CANNOT_EXCEED_ONE");
41: require(msg.sender == owner && _owner != address(0), "Factory:NOT_ALLOWED");
59: require(_tokenA < _tokenB, "Factory:TOKENS_MUST_BE_SORTED_BY_ADDRESS");
60: require(_fee > 0 && _fee < 1e18, "Factory:FEE_OUT_OF_BOUNDS");
61: require(_tickSpacing > 0, "Factory:TICK_SPACING_OUT_OF_BOUNDS");
62: require(_lookback >= 3600 && _lookback <= uint16(type(int16).max), "Factory:LOOKBACK_OUT_OF_BOUNDS");
64: require(pools[_fee][_tickSpacing][_lookback][_tokenA][_tokenB] == IPool(address(0)), "Factory:POOL_ALREADY_EXISTS");
File: models/Pool.sol
87: require(msg.sender == position.ownerOf(tokenId) || msg.sender == position.getApproved(tokenId), "P");
93: require((currentState.status & LOCKED == 0) && (allowInEmergency || (currentState.status & EMERGENCY == 0)), "E");
168: require(previousABalance + tokenAAmount <= _tokenABalance() && previousBBalance + tokenBAmount <= _tokenBBalance(), "A");
303: require(previousBalance + amountIn <= (tokenAIn ? _tokenABalance() : _tokenBBalance()), "S");
File: models/PoolInspector.sol
67: revert("Invalid Swap");
File: models/Position.sol
43: require(_exists(tokenId), "Invalid Token ID");
Instances (4):
File: libraries/Constants.sol
6: uint256 constant MERGED_LP_BALANCE_TOKEN_ID = 0;
File: models/Pool.sol
31: uint8 constant NO_EMERGENCY_UNLOCKED = 0;
File: models/PoolInspector.sol
29: uint128 activeCounter = 0;
80: for (uint8 i = 0; i < NUMBER_OF_KINDS; i++) {
Saves 5 gas per loop
Instances (18):
File: models/Pool.sol
127: for (uint256 i; i < params.length; i++) {
180: for (uint256 i; i < binIds.length; i++) {
193: for (uint256 i; i < params.length; i++) {
224: for (uint256 i; i < params.length; i++) {
380: for (uint256 j; j < 2; j++) {
389: for (uint256 i; i <= moveData.binCounter; i++) {
396: moveData.mergeBinCounter++;
409: moveData.mergeBinCounter++;
414: for (uint256 i; i < moveData.mergeBinCounter; i++) {
523: for (uint256 i; i < NUMBER_OF_KINDS; i++) {
530: output.counter++;
540: for (uint256 i; i < NUMBER_OF_KINDS; i++) {
653: for (uint256 i; i < swapData.counter; i++) {
File: models/PoolInspector.sol
30: for (uint128 i = startBinIndex; i < binCounter; i++) {
34: activeCounter++;
49: depth++;
80: for (uint8 i = 0; i < NUMBER_OF_KINDS; i++) {
101: for (uint256 i; i < bins.length; i++) {
Instances (18):
File: libraries/Bin.sol
42: if (_existingReserveA > 0) {
45: if (deltaBOptimal > _deltaB && _existingReserveB > 0) {
File: libraries/BinMath.sol
52: if (x & 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF > 0) {
57: if (x & 0xFFFFFFFFFFFFFFFF > 0) {
62: if (x & 0xFFFFFFFF > 0) {
67: if (x & 0xFFFF > 0) {
72: if (x & 0xFF > 0) {
77: if (x & 0xF > 0) {
82: if (x & 0x3 > 0) {
87: if (x & 0x1 > 0) result -= 1;
115: if (_tick > 0) ratio = type(uint256).max / ratio;
File: libraries/SafeERC20Min.sol
17: if (returndata.length > 0) {
File: models/Factory.sol
60: require(_fee > 0 && _fee < 1e18, "Factory:FEE_OUT_OF_BOUNDS");
61: require(_tickSpacing > 0, "Factory:TICK_SPACING_OUT_OF_BOUNDS");
File: models/Pool.sol
277: while (delta.excess > 0) {
659: swapData.currentReserveB > 0 ? bin.state.reserveB : bin.state.reserveA,
660: swapData.currentReserveB > 0 ? swapData.currentReserveB : swapData.currentReserveA
File: models/PositionMetadata.sol
22: return bytes(baseURI).length > 0 ? string(abi.encodePacked(baseURI, tokenId.toString())) : "";
If the functions are required by an interface, the contract should inherit from that interface and use the override
keyword
Instances (32):
File: libraries/Bin.sol
56: function adjustAB(Bin.Instance storage self, bool tokenAIn, Delta.Instance memory delta, uint256 thisBinAmount, uint256 totalAmount) internal {
75: function addLiquidity(
126: function removeLiquidity(
File: libraries/BinMap.sol
25: function putTypeAtTick(mapping(int32 => uint256) storage binMap, uint8 kind, int32 tick) internal {
30: function removeTypeAtTick(mapping(int32 => uint256) storage binMap, uint8 kind, int32 tick) internal {
35: function getKindsAtTick(mapping(int32 => uint256) storage binMap, int32 tick) internal view returns (uint256 word) {
41: function getKindsAtTickRange(mapping(int32 => uint256) storage binMap, int32 startTick, int32 endTick, uint256 kindMask) internal view returns (Active[] memory activeList, uint256 counter) {
75: function nextActive(mapping(int32 => uint256) storage binMap, int32 tick, bool isRight) internal view returns (int32 nextTick) {
File: libraries/BinMath.sol
12: function msb(uint256 x) internal pure returns (uint8 result) {
48: function lsb(uint256 x) internal pure returns (uint8 result) {
91: function tickSqrtPrice(uint256 tickSpacing, int32 _tick) internal pure returns (uint256 _result) {
134: function getTickSqrtPriceAndL(uint256 _reserveA, uint256 _reserveB, uint256 _sqrtLowerTickPrice, uint256 _sqrtUpperTickPrice) internal pure returns (uint256 sqrtPrice, uint256 liquidity) {
File: libraries/Cast.sol
5: function toUint128(uint256 x) internal pure returns (uint128 y) {
File: libraries/Delta.sol
22: function combine(Instance memory self, Instance memory delta) internal pure {
34: function pastMaxPrice(Instance memory self) internal pure {
38: function sqrtEdgePrice(Instance memory self) internal pure returns (uint256 edge) {
42: function noSwapReset(Instance memory self) internal pure {
File: libraries/Math.sol
8: function max(uint256 x, uint256 y) internal pure returns (uint256) {
12: function min(uint256 x, uint256 y) internal pure returns (uint256) {
16: function max(int256 x, int256 y) internal pure returns (int256) {
20: function min(int256 x, int256 y) internal pure returns (int256) {
25: function clip128(uint128 x, uint128 y) internal pure returns (uint128) {
29: function clip(uint256 x, uint256 y) internal pure returns (uint256) {
33: function mulDiv(uint256 x, uint256 y, uint256 k, bool ceil) internal pure returns (uint256) {
38: function scale(uint8 decimals) internal pure returns (uint256) {
48: function toScale(uint256 amount, uint256 scaleFactor, bool ceil) internal pure returns (uint256) {
58: function fromScale(uint256 amount, uint256 scaleFactor) internal pure returns (uint256) {
68: function abs32(int32 x) internal pure returns (uint32) {
File: libraries/SafeERC20Min.sol
11: function safeTransfer(IERC20 token, address to, uint256 value) internal {
File: libraries/Twa.sol
11: function updateValue(IPool.TwaState storage self, int256 value) internal {
17: function floor(IPool.TwaState storage self) internal view returns (int32) {
35: function getTwaFloor(IPool.TwaState storage self) internal view returns (int32) {
Issue | Instances | |
---|---|---|
NC-1 | require() / revert() statements should have descriptive reason strings |
3 |
NC-2 | Event is missing indexed fields |
12 |
NC-3 | Functions not used internally could be marked external | 2 |
Instances (3):
File: models/Pool.sol
129: require(param.kind < NUMBER_OF_KINDS);
333: require(_protocolFeeRatio <= ONE_3_DECIMAL_SCALE);
342: require(msg.sender == factory.owner());
Index event fields make the field more quickly accessible to off-chain tools that parse events. However, note that each index field costs extra gas during emission, so it's not necessarily best to index the maximum allowed per event (three fields). Each event should use three indexed fields if there are three or more fields, and gas usage is not particularly of concern for the events in question. If there are fewer than three fields, all of the fields should be indexed.
Instances (12):
File: interfaces/IFactory.sol
9: event PoolCreated(address poolAddress, uint256 fee, uint256 tickSpacing, int32 activeTick, uint32 lookback, uint16 protocolFeeRatio, IERC20 tokenA, IERC20 tokenB);
10: event SetFactoryProtocolFeeRatio(uint16 protocolFeeRatio);
11: event SetFactoryOwner(address owner);
File: interfaces/IPool.sol
8: event Swap(address indexed sender, address indexed recipient, bool tokenAIn, bool exactOutput, uint256 amountIn, uint256 amountOut, int32 activeTick);
10: event AddLiquidity(address indexed sender, uint256 indexed tokenId, BinDelta[] binDeltas);
12: event MigrateBinsUpStack(address indexed sender, uint128[] binIds, uint32 maxRecursion);
14: event TransferLiquidity(uint256 fromTokenId, uint256 toTokenId, RemoveLiquidityParams[] params);
18: event BinMerged(uint128 indexed binId, uint128 reserveA, uint128 reserveB, uint128 mergeId);
20: event BinMoved(uint128 indexed binId, int128 previousTick, int128 newTick);
22: event ProtocolFeeCollected(uint256 protocolFee, bool isTokenA);
24: event SetProtocolFeeRatio(uint256 protocolFee);
File: interfaces/IPosition.sol
8: event SetMetadata(IPositionMetadata metadata);
Instances (2):
File: models/PoolInspector.sol
45: function getBinDepth(IPool pool, uint128 binId) public view returns (uint256 depth) {
File: models/Position.sol
38: function supportsInterface(bytes4 interfaceId) public view override(ERC721, ERC721Enumerable, IERC165) returns (bool) {
Issue | Instances | |
---|---|---|
GAS-1 | Use selfbalance() instead of address(this).balance |
3 |
GAS-2 | Use assembly to check for address(0) |
4 |
GAS-3 | Cache array length outside of loop | 1 |
GAS-4 | State variables should be cached in stack variables rather than re-reading them from storage | 2 |
GAS-5 | Use Custom Errors | 26 |
GAS-6 | Don't initialize variables with default value | 1 |
GAS-7 | ++i costs less gas than i++ , especially when it's used in for -loops (--i /i-- too) |
1 |
GAS-8 | Use != 0 instead of > 0 for unsigned integer comparison | 10 |
GAS-9 | internal functions not called by the contract should be removed |
12 |
Use assembly when getting a contract's balance of ETH.
You can use selfbalance()
instead of address(this).balance
when getting your contract's balance of ETH to save gas.
Additionally, you can use balance(address)
instead of address.balance()
when getting an external contract's balance of ETH.
Saves 15 gas when checking internal balance, 6 for external
Instances (3):
File: Router.sol
81: if (address(this).balance > 0) TransferHelper.safeTransferETH(msg.sender, address(this).balance);
81: if (address(this).balance > 0) TransferHelper.safeTransferETH(msg.sender, address(this).balance);
89: if (IWETH9(address(token)) == WETH9 && address(this).balance >= value) {
Saves 6 gas per instance
Instances (4):
File: Router.sol
122: if (recipient == address(0)) recipient = address(this);
170: if (recipient == address(0)) recipient = address(this);
271: if (address(pool) == address(0)) {
306: if (recipient == address(0)) recipient = address(this);
If not cached, the solidity compiler will always read the length of the array during each iteration. That is, if it is a storage array, this is an extra sload operation (100 additional extra gas for each iteration except for the first) and if it is a memory array, this is an extra mload operation (3 additional gas for each iteration except for the first).
Instances (1):
File: libraries/Multicall.sol
13: for (uint256 i = 0; i < data.length; i++) {
[GAS-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 replaces 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.
Saves 100 gas per instance
Instances (2):
File: Router.sol
224: tokenId = IPosition(position).tokenOfOwnerByIndex(msg.sender, 0);
272: pool = IFactory(factory).create(poolParams.fee, poolParams.tickSpacing, poolParams.lookback, poolParams.activeTick, poolParams.tokenA, poolParams.tokenB);
Source Instead of using error strings, to reduce deployment and runtime cost, you should use Custom Errors. This would save both deployment and runtime cost.
Instances (26):
File: Router.sol
55: require(IWETH9(msg.sender) == WETH9, "Not WETH9");
61: require(balanceWETH9 >= amountMinimum, "Insufficient WETH9");
72: require(balanceToken >= amountMinimum, "Insufficient token");
100: require(amountToPay > 0 && amountOut > 0, "In or Out Amount is Zero");
101: require(factory.isFactoryPool(IPool(msg.sender)), "Must call from a Factory Pool");
139: require(amountOut >= params.amountOutMinimum, "Too little received");
165: require(amountOut >= params.amountOutMinimum, "Too little received");
177: require(amountOutReceived == amountOut, "Requested amount not available");
188: require(amountIn <= params.amountInMaximum, "Too much requested");
197: require(amountIn <= params.amountInMaximum, "Too much requested");
234: require(tokenAAmount >= minTokenAAmount && tokenBAmount >= minTokenBAmount, "Too little added");
262: require(activeTick >= minActiveTick && activeTick <= maxActiveTick, "activeTick not in range");
304: require(msg.sender == position.ownerOf(tokenId), "P");
309: require(tokenAAmount >= minTokenAAmount && tokenBAmount >= minTokenBAmount, "Too little removed");
File: libraries/BytesLib.sol
13: require(_length + 31 >= _length, "slice_overflow");
14: require(_start + _length >= _start, "slice_overflow");
15: require(_bytes.length >= _start + _length, "slice_outOfBounds");
75: require(_start + 20 >= _start, "toAddress_overflow");
76: require(_bytes.length >= _start + 20, "toAddress_outOfBounds");
87: require(_start + 3 >= _start, "toUint24_overflow");
88: require(_bytes.length >= _start + 3, "toUint24_outOfBounds");
File: libraries/Deadline.sol
6: require(block.timestamp <= deadline, "Transaction too old");
File: libraries/TransferHelper.sol
15: require(success && (data.length == 0 || abi.decode(data, (bool))), "STF");
25: require(success && (data.length == 0 || abi.decode(data, (bool))), "ST");
35: require(success && (data.length == 0 || abi.decode(data, (bool))), "SA");
44: require(success, "STE");
Instances (1):
File: libraries/Multicall.sol
13: for (uint256 i = 0; i < data.length; i++) {
Saves 5 gas per loop
Instances (1):
File: libraries/Multicall.sol
13: for (uint256 i = 0; i < data.length; i++) {
Instances (10):
File: Router.sol
63: if (balanceWETH9 > 0) {
74: if (balanceToken > 0) {
81: if (address(this).balance > 0) TransferHelper.safeTransferETH(msg.sender, address(this).balance);
100: require(amountToPay > 0 && amountOut > 0, "In or Out Amount is Zero");
100: require(amountToPay > 0 && amountOut > 0, "In or Out Amount is Zero");
File: interfaces/IMulticall.sol
2: pragma solidity >=0.7.5;
File: interfaces/ISelfPermit.sol
2: pragma solidity >=0.7.5;
File: interfaces/external/IERC20PermitAllowed.sol
2: pragma solidity >=0.5.0;
File: libraries/SelfPermit.sol
2: pragma solidity >=0.5.0;
File: libraries/TransferHelper.sol
2: pragma solidity >=0.6.0;
If the functions are required by an interface, the contract should inherit from that interface and use the override
keyword
Instances (12):
File: libraries/BytesLib.sol
12: function slice(bytes memory _bytes, uint256 _start, uint256 _length) internal pure returns (bytes memory) {
74: function toAddress(bytes memory _bytes, uint256 _start) internal pure returns (address) {
86: function toUint24(bytes memory _bytes, uint256 _start) internal pure returns (uint24) {
File: libraries/Path.sol
27: function hasMultiplePools(bytes memory path) internal pure returns (bool) {
34: function numPools(bytes memory path) internal pure returns (uint256) {
44: function decodeFirstPool(bytes memory path) internal pure returns (IERC20 tokenIn, IERC20 tokenOut, IPool pool) {
53: function getFirstPool(bytes memory path) internal pure returns (bytes memory) {
60: function skipToken(bytes memory path) internal pure returns (bytes memory) {
File: libraries/TransferHelper.sol
13: function safeTransferFrom(address token, address from, address to, uint256 value) internal {
23: function safeTransfer(address token, address to, uint256 value) internal {
33: function safeApprove(address token, address to, uint256 value) internal {
42: function safeTransferETH(address to, uint256 value) internal {
Issue | Instances | |
---|---|---|
NC-1 | require() / revert() statements should have descriptive reason strings |
3 |
NC-2 | Constants should be defined rather than using magic numbers | 1 |
NC-3 | Functions not used internally could be marked external | 3 |
Instances (3):
File: Router.sol
106: require(msg.sender == address(pool));
205: require(factory.isFactoryPool(IPool(msg.sender)));
206: require(msg.sender == address(data.pool));
Instances (1):
File: libraries/BytesLib.sol
58: mstore(0x40, and(add(mc, 31), not(31)))
Instances (3):
File: Router.sol
59: function unwrapWETH9(uint256 amountMinimum, address recipient) public payable override {
70: function sweepToken(IERC20 token, uint256 amountMinimum, address recipient) public payable {
File: libraries/Multicall.sol
11: function multicall(bytes[] calldata data) public payable override returns (bytes[] memory results) {
Issue | Instances | |
---|---|---|
L-1 | Do not use deprecated library functions | 1 |
L-2 | Unsafe ERC20 operation(s) | 1 |
L-3 | Unspecific compiler version pragma | 5 |
Instances (1):
File: libraries/TransferHelper.sol
33: function safeApprove(address token, address to, uint256 value) internal {
Instances (1):
File: Router.sol
91: WETH9.transfer(recipient, value);
Instances (5):
File: interfaces/IMulticall.sol
2: pragma solidity >=0.7.5;
File: interfaces/ISelfPermit.sol
2: pragma solidity >=0.7.5;
File: interfaces/external/IERC20PermitAllowed.sol
2: pragma solidity >=0.5.0;
File: libraries/SelfPermit.sol
2: pragma solidity >=0.5.0;
File: libraries/TransferHelper.sol
2: pragma solidity >=0.6.0;