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 #224

Open
code423n4 opened this issue Apr 20, 2022 · 0 comments
Open

Gas Optimizations #224

code423n4 opened this issue Apr 20, 2022 · 0 comments
Labels
bug Something isn't working G (Gas Optimization)

Comments

@code423n4
Copy link
Contributor

Gas Report

Table of Contents

Caching storage variables in memory to save gas

PROBLEM

If 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.

PROOF OF CONCEPT

Instances include:

CitadelMinter.sol

scope: mintAndDistribute()

  • citadelToken is read twice
CitadelMinter.sol:178
CitadelMinter.sol:217

Funding.sol

scope: claimAssetToTreasury()

  • asset is read 3 times
	Funding.sol:339:
	Funding.sol:341:
	Funding.sol:343:

scope: updateCitadelPriceInAsset()

  • minCitadelPriceInAsset is read twice
	Funding.sol:428
	Funding.sol:434
  • maxCitadelPriceInAsset is read twice
	Funding.sol:429
	Funding.sol:435

KnightingRound.sol

scope: buy()

  • saleStart is read twice
	KnightingRound.sol:167
	KnightingRound.sol:169
  • totalTokenIn is read twice
		KnightingRound.sol:174
		KnightingRound.sol:198
  • guestlist is read twice
	KnightingRound.sol:178
	KnightingRound.sol:179

StakedCitadel.sol

scope: depositFor()

  • token is read 3 times
		StakedCitadel.sol:773
		StakedCitadel.sol:774
		StakedCitadel.sol:775

scope: _withdraw()

  • token is read 3 times
	StakedCitadel.sol:815
	StakedCitadel.sol:819
	StakedCitadel.sol:831

scope: _depositForWithAuthorization()

  • guestlist is read twice
	StakedCitadel.sol:793
	StakedCitadel.sol:795

scope: setStrategy()

  • strategy is read twice
	StakedCitadel.sol:505
	StakedCitadel.sol:507

scope: earn()

  • strategy is read twice
	StakedCitadel.sol:722
	StakedCitadel.sol:723

scope: withdraw()

  • vesting is read twice
	StakedCitadel.sol:830
	StakedCitadel.sol:831

SupplySchedule.sol

scope: getMintableDebug()

  • globalStartTimestamp is read (5 + endingEpoch - lastEpoch) times:
    number of reads depending on lastMintTimestamp as it is in a for loop
	SupplySchedule.sol:180
	SupplySchedule.sol:184
	SupplySchedule.sol:197
	SupplySchedule.sol:200
	SupplySchedule.sol:204
	SupplySchedule.sol:211
	SupplySchedule.sol:212

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:

CitadelToken.sol

scope: initialize()

CitadelToken.sol:22: string memory _name, string memory _symbol

GlobalAccessControl.sol

scope: initializeNewRole()

GlobalAccessControl.sol:107: string memory roleString

StakedCitadel.sol

scope: initialize()

StakedCitadel.sol:167: string memory _name, string memory _symbol, uint256[4] memory _feeConfig

scope: deposit()

StakedCitadel.sol:319: bytes32[] memory proof

scope: depositAll()

StakedCitadel.sol:341: bytes32[] memory proof

scope: depositFor()

StakedCitadel.sol:363: bytes32[] memory proof

scope: _depositWithAuthorization()

StakedCitadel.sol:780: bytes32[] memory proof

scope: _depositForWithAuthorization()

StakedCitadel.sol:788: bytes32[] memory proof

TOOLS USED

Manual Analysis

MITIGATION

Replace memory with calldata

Comparisons with zero for unsigned integers

PROBLEM

>0 is less gas efficient than !0 if you enable the optimizer at 10k AND you’re in a require statement.
Detailed explanation with the opcodes here

PROOF OF CONCEPT

Instances include:

CitadelMinter.sol

scope: _transferToFundingPools()

	CitadelMinter.sol:343

Funding.sol

scope: deposit()

	Funding.sol:170

scope: sweep()

	Funding.sol:322

scope: claimAssetToTreasury()

	Funding.sol:340

scope: updateCitadelPriceInAsset()

	Funding.sol:424

scope: updateCitadelPriceInAsset()

	Funding.sol:452

KnightingRound.sol

scope: function initialize()

	KnightingRound.sol:125
	KnightingRound.sol:129

scope: buy()

	KnightingRound.sol:172

scope: claim()

	KnightingRound.sol:215

scope: setSaleDuration()

	KnightingRound.sol:313

scope: setTokenOutPrice()

	KnightingRound.sol:332

scope: sweep()

	KnightingRound.sol:411

StakedCitadelVester.sol

scope: vest()

	StakedCitadelVester.sol:138

SupplySchedule.sol

scope: getEpochAtTimestamp()

	SupplySchedule.sol:61

scope: getMintable()

	SupplySchedule.sol:91

scope: getMintableDebug()

	SupplySchedule.sol:180

TOOLS USED

Manual Analysis

MITIGATION

Replace > 0 with !0

Comparison Operators

PROBLEM

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:

CitadelMinter.sol

	CitadelMinter.sol:272

Funding.sol

	Funding.sol:172
	Funding.sol:178
	Funding.sol:270
	Funding.sol:271

KnightingRound.sol

	KnightingRound.sol:120
	KnightingRound.sol:167
	KnightingRound.sol:173
	KnightingRound.sol:259
	KnightingRound.sol:275
	KnightingRound.sol:293

StakedCitadel.sol

	StakedCitadel.sol:190
	StakedCitadel.sol:194
	StakedCitadel.sol:198
	StakedCitadel.sol:202
	StakedCitadel.sol:523
	StakedCitadel.sol:535
	StakedCitadel.sol:550
	StakedCitadel.sol:588
	StakedCitadel.sol:613
	StakedCitadel.sol:630
	StakedCitadel.sol:651
	StakedCitadel.sol:666

StakedCitadelVester.sol

	StakedCitadelVester:111

SupplySchedule.sol

	SupplySchedule.sol:111
	SupplySchedule.sol:142
	SupplySchedule.sol:208

TOOLS USED

Manual Analysis

MITIGATION

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

example:

-require(citadelAmount_ >= _minCitadelOut, "minCitadelOut");
+require(citadelAmount_ > _minCitadelOut - 1, "minCitadelOut");

When the comparison is with a constant storage variable, you can also do the increment in the storage variable declaration

example:

-require(
            _feeConfig[1] <= PERFORMANCE_FEE_HARD_CAP,
            "performanceFeeStrategist too high"
        );
+require(
            _feeConfig[1] < PERFORMANCE_FEE_HARD_CAP_PLUS_ONE,
            "performanceFeeStrategist too high"
        );

and

-uint256 public constant PERFORMANCE_FEE_HARD_CAP = 3_000;
+uint256 public constant PERFORMANCE_FEE_HARD_CAP_PLUS_ONE = 3_001;

Custom Errors

PROBLEM

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:

CitadelMinter.sol

	CitadelMinter.sol:116
	CitadelMinter.sol:256
	CitadelMinter.sol:272
	CitadelMinter.sol:299
	CitadelMinter.sol:319
	CitadelMinter.sol:326
	CitadelMinter.sol:343
	CitadelMinter.sol:368
	CitadelMinter.sol:374

Funding.sol

	Funding.sol:113
	Funding.sol:117
	Funding.sol:170
	Funding.sol:171
	Funding.sol:178
	Funding.sol:270
	Funding.sol:271
	Funding.sol:296
	Funding.sol:322
	Funding.sol:323
	Funding.sol:340
	Funding.sol:361
	Funding.sol:388
	Funding.sol:424
	Funding.sol:425
	Funding.sol:452

GlobalAccessControl.sol

	GlobalAccessControl.sol:95
	GlobalAccessControl.sol:100
	GlobalAccessControl.sol:112
	GlobalAccessControl.sol:116

KnightingRound.sol

	KnightingRound.sol:120
	KnightingRound.sol:124
	KnightingRound.sol:128
	KnightingRound.sol:132
	KnightingRound.sol:167
	KnightingRound.sol:168
	KnightingRound.sol:172
	KnightingRound.sol:173
	KnightingRound.sol:179
	KnightingRound.sol:185
	KnightingRound.sol:210
	KnightingRound.sol:211
	KnightingRound.sol:215
	KnightingRound.sol:273
	KnightingRound.sol:274
	KnightingRound.sol:275
	KnightingRound.sol:293
	KnightingRound.sol:297
	KnightingRound.sol:312
	KnightingRound.sol:316
	KnightingRound.sol:331
	KnightingRound.sol:349
	KnightingRound.sol:384
	KnightingRound.sol:411

StakedCitadel.sol

	StakedCitadel.sol:180
	StakedCitadel.sol:181
	StakedCitadel.sol:182
	StakedCitadel.sol:183
	StakedCitadel.sol:184
	StakedCitadel.sol:185
	StakedCitadel.sol:186
	StakedCitadel.sol:187
	StakedCitadel.sol:190
	StakedCitadel.sol:194
	StakedCitadel.sol:198
	StakedCitadel.sol:202
	StakedCitadel.sol:262
	StakedCitadel.sol:270
	StakedCitadel.sol:441
	StakedCitadel.sol:487
	StakedCitadel.sol:502
	StakedCitadel.sol:506
	StakedCitadel.sol:523
	StakedCitadel.sol:535
	StakedCitadel.sol:550
	StakedCitadel.sol:562
	StakedCitadel.sol:574
	StakedCitadel.sol:588
	StakedCitadel.sol:613
	StakedCitadel.sol:630
	StakedCitadel.sol:650
	StakedCitadel.sol:666
	StakedCitadel.sol:700
	StakedCitadel.sol:718
	StakedCitadel.sol:768
	StakedCitadel.sol:769
	StakedCitadel.sol:770
	StakedCitadel.sol:794
	StakedCitadel.sol:809

StakedCitadelVester.sol

	StakedCitadelVester.sol:64
	StakedCitadelVester.sol:65
	StakedCitadelVester.sol:137
	StakedCitadelVester.sol:138

SupplySchedule.sol

	SupplySchedule.sol:60
	SupplySchedule.sol:90
	SupplySchedule.sol:94
	SupplySchedule.sol:137
	SupplySchedule.sol:141
	SupplySchedule.sol:155
	SupplySchedule.sol:179
	SupplySchedule.sol:183
	SupplySchedule.sol:187

TOOLS USED

Manual Analysis

MITIGATION

Replace require statements with custom errors.

Default value initialization

PROBLEM

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:

CitadelMinter.sol

CitadelMinter.sol:152: for (uint256 i = 0; i < numPools; i++) {
CitadelMinter.sol:180: uint256 lockingAmount = 0;
CitadelMinter.sol:181: uint256 stakingAmount = 0;
CitadelMinter.sol:182: uint256 fundingAmount = 0;

SupplySchedule.sol

SupplySchedule.sol:103: uint256 mintable = 0;
SupplySchedule.sol:192: uint256 mintable = 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:

Funding.sol

Funding.sol: 343

SupplySchedule.sol

SupplySchedule.sol: 197

TOOLS USED

Manual Analysis

MITIGATION

In both instances, the storage variable is read multiple times in the function and it is recommended to cache them into memory, then passing these cached variables in the emit statement, as explained in the cache paragraph

Inline functions

PROBLEM

When we define internal functions to perform computation:

  • The contract’s code size gets bigger
  • the function call consumes more gas than executing it as an inlined function (part of the code, without the function call)

When it does not affect readability, it is recommended to inline functions in order to save gas

PROOF OF CONCEPT

Instances include:

StakedCitadel.sol

StakedCitadel.sol: 780

TOOLS USED

Manual Analysis

MITIGATION

Inline this function where it is called:

StakedCitadel.sol: 310
StakedCitadel.sol: 323
StakedCitadel.sol: 330
StakedCitadel.sol: 342

Prefix increments

PROBLEM

Prefix increments are cheaper than postfix increments.

PROOF OF CONCEPT

Instances include:

CitadelMinter.sol

CitadelMinter.sol:152
CitadelMinter.sol:344

SupplySchedule.sol

SupplySchedule.sol:208

TOOLS USED

Manual Analysis

MITIGATION

change i++ to ++i

Short-circuiting

PROBLEM

Reading from storage costs 100 gas.
When checking conditions using the AND operator, we can save gas by not evaluating the second expression if the first one is false.

PROOF OF CONCEPT

Instances include:

CitadelMinter.sol

CitadelMinter.sol:266

TOOLS USED

Manual Analysis

MITIGATION

  • Remove the declaration of poolExists line 261 and inline the computation of fundingPools.contains(_pool) line 266 and 273.

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:

Funding.sol

Funding.sol:39:
bool public citadelPriceFlag; /// Flag citadel price for review by guardian if it exceeds min and max bounds;

uint256 public assetDecimalsNormalizationValue;

address public citadelPriceInAssetOracle;
address public saleRecipient;

TOOLS USED

Manual Analysis

MITIGATION

Place citadelPriceFlag after SaleRecipient to save one storage slot

uint256 public assetDecimalsNormalizationValue;

address public citadelPriceInAssetOracle;
address public saleRecipient;
+bool public citadelPriceFlag;

Uint256 instead of Uint8

PROBLEM

Each storage slot has 256 bits.
Whenever a uint smaller than 256 (or even a bool) is pulled from storage, the EVM casts it to a uint256.
Calculations are also unexceptionally performed in uint256 by the EVM.

PROOF OF CONCEPT

Instances include

KnightingRound.sol

KnightingRound.sol:69
KnightingRound.sol:70
KnightingRound.sol:76
KnightingRound.sol:162

TOOLS USED

Manual Analysis

MITIGATION

Replace uint8 with uint256

Unchecked arithmetic

PROBLEM

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

PROOF OF CONCEPT

Instances include:

CitadelMinter.sol

CitadelMinter.sol: 152: i bounded by numPools, overflow check unnecessary


CitadelMinter.sol: 344: i bounded by length, overflow check unnecessary


CitadelMinter.sol: 364: totalFundingPoolWeight is greater than or equal to currentPoolWeight, underflow check unnecessary

Funding.sol

Funding.sol: 212: MAX_BPS is less than the maximum discount, underflow check unnecessary

Funding.sol: 236: assetCumulativeFunded is less than assetCap, underflow check unnecessary

KnightingRound.sol

KnightingRound.sol: 198: totalTokenIn + _tokenInAmount is capped by tokenInLimit, overflow check unnecessary

KnightingRound.sol: 250: totalTokenIn is less than tokenInLimit, underflow check unnecessary

StakedCitadel.sol

StakedCitadel.sol: 817: b is less than r, underflow check unnecessary

StakedCitadel.sol: 821: b + diff capped by r (see line 821 and 817), overflow check unnecessary

StakedCitadel.sol: 827: _fee is less than r, underflow check unnecessary

SupplySchedule.sol

SupplySchedule.sol: 208: i is capped by endingEpoch, overflow check unnecessary

TOOLS USED

Manual Analysis

MITIGATION

Place the arithmetic operations in an unchecked block

@code423n4 code423n4 added bug Something isn't working G (Gas Optimization) labels Apr 20, 2022
code423n4 added a commit that referenced this issue Apr 20, 2022
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

1 participant