Gas savings are estimated using the gas report of existing forge test --gas-report tests (the sum of all deployment costs and the sum of the costs of calling methods) and may vary depending on the implementation of the fix.
Issue
Instances
Estimated gas(deployments)
Estimated gas(min method call)
Estimated gas(avg method call)
Estimated gas(max method call)
1
State variables only set in the constructor should be declared immutable
2
117 275
104
110
110
2
Use function instead of modifiers
4
115 926
162
-264
-481
3
Duplicated require()/revert() checks should be refactored to a modifier or function
11
114 932
-59
-284
-398
4
Multiple address mappings can be combined into a single mapping of an address to a struct, where appropriate
5
24 227
254
533
-6 726
5
Expression can be unchecked when overflow is not possible
6
20 220
410
4 630
1354
6
State variables can be packed into fewer storage slots
1
-5 008
1 911
15 525
20 972
7
refactoring similar statements
1
18 422
-18
-11
6
8
Better algorithm for underflow check
3
12 613
656
8 332
3 741
9
x = x + y is cheaper than x += y
12
11 214
180
468
616
10
internal functions only called once can be inlined to save gas
1
5 207
67
47
24
11
State variables should be cached in stack variables rather than re-reading them from storage
2
5 007
478
1 117
1 423
Overall gas savings
48
416 802 (6,58%)
3 423 (0,34%)
15 773 (0,82%)
18 283 (0,72%)
Total: 48 instances over 11 issues
1. State variables only set in the constructor should be declared immutable (2 instances)
Deployment. Gas Saved: 117 275
Minimum Method Call. Gas Saved: 104
Average Method Call. Gas Saved: 110
Maximum Method Call. Gas Saved: 110
Overall gas change: -678 (-0.723%)
Avoids a Gsset (20000 gas) in the constructor, and replaces each Gwarmacces (100 gas) with a PUSH32 (3 gas).
diff --git a/src/DBR.sol b/src/DBR.sol
index aab6daf..50428cd 100644
--- a/src/DBR.sol
+++ b/src/DBR.sol
@@ -41,16 +41,16 @@ contract DolaBorrowingRights {
41, 41: INITIAL_DOMAIN_SEPARATOR = computeDomainSeparator();
42, 42: }
43, 43:
- 44 :- modifier onlyOperator {
+ 44:+ function onlyOperator() private view {
45, 45: require(msg.sender == operator, "ONLY OPERATOR");
- 46 :- _;
47, 46: }
48, 47:
49, 48: /**
50, 49: @notice Sets pending operator of the contract. Operator role must be claimed by the new oprator. Only callable by Operator.
51, 50: @param newOperator_ The address of the newOperator
52, 51: */
- 53 :- function setPendingOperator(address newOperator_) public onlyOperator {
+ 52:+ function setPendingOperator(address newOperator_) public {
+ 53:+ onlyOperator();
54, 54: pendingOperator = newOperator_;
55, 55: }
56, 56:
@@ -59,7 +59,8 @@ contract DolaBorrowingRights {
59, 59: At 10000, the cost of replenishing 1 DBR is 1 DOLA in debt. Only callable by Operator.
60, 60: @param newReplenishmentPriceBps_ The new replen
61, 61: */
- 62 :- function setReplenishmentPriceBps(uint newReplenishmentPriceBps_) public onlyOperator {
+ 62:+ function setReplenishmentPriceBps(uint newReplenishmentPriceBps_) public {
+ 63:+ onlyOperator();
63, 64: require(newReplenishmentPriceBps_ > 0, "replenishment price must be over 0");
64, 65: replenishmentPriceBps = newReplenishmentPriceBps_;
65, 66: }
@@ -78,7 +79,8 @@ contract DolaBorrowingRights {
78, 79: @notice Add a minter to the set of addresses allowed to mint DBR tokens. Only callable by Operator.
79, 80: @param minter_ The address of the new minter.
80, 81: */
- 81 :- function addMinter(address minter_) public onlyOperator {
+ 82:+ function addMinter(address minter_) public {
+ 83:+ onlyOperator();
82, 84: minters[minter_] = true;
83, 85: emit AddMinter(minter_);
84, 86: }
@@ -87,7 +89,8 @@ contract DolaBorrowingRights {
87, 89: @notice Removes a minter from the set of addresses allowe to mint DBR tokens. Only callable by Operator.
88, 90: @param minter_ The address to be removed from the minter set.
89, 91: */
- 90 :- function removeMinter(address minter_) public onlyOperator {
+ 92:+ function removeMinter(address minter_) public {
+ 93:+ onlyOperator();
91, 94: minters[minter_] = false;
92, 95: emit RemoveMinter(minter_);
93, 96: }
@@ -96,7 +99,8 @@ contract DolaBorrowingRights {
96, 99: @dev markets can be added but cannot be removed. A removed market would result in unrepayable debt for some users.
97, 100: @param market_ The address of the new market contract to be added.
98, 101: */
- 99 :- function addMarket(address market_) public onlyOperator {
+ 102:+ function addMarket(address market_) public {
+ 103:+ onlyOperator();
100, 104: markets[market_] = true;
101, 105: emit AddMarket(market_);
102, 106: }
diff --git a/src/DBR.sol b/src/DBR.sol
index aab6daf..625c422 100644
--- a/src/DBR.sol
+++ b/src/DBR.sol
@@ -46,6 +46,10 @@ contract DolaBorrowingRights {
46, 46: _;
47, 47: }
48, 48:
+ 49:+ function is_balance_sufficient(address _user, uint256 amount) private view {
+ 50:+ require(balanceOf(_user) >= amount, "Insufficient balance");
+ 51:+ }
+ 52:+
49, 53: /**
50, 54: @notice Sets pending operator of the contract. Operator role must be claimed by the new oprator. Only callable by Operator.
51, 55: @param newOperator_ The address of the newOperator
@@ -168,7 +172,7 @@ contract DolaBorrowingRights {
168, 172: @return Always returns true, will revert if not successful.
169, 173: */
170, 174: function transfer(address to, uint256 amount) public virtual returns (bool) {
- 171 :- require(balanceOf(msg.sender) >= amount, "Insufficient balance");
+ 175:+ is_balance_sufficient(msg.sender, amount);
172, 176: balances[msg.sender] -= amount;
173, 177: unchecked {
174, 178: balances[to] += amount;
@@ -192,7 +196,7 @@ contract DolaBorrowingRights {
192, 196: ) public virtual returns (bool) {
193, 197: uint256 allowed = allowance[from][msg.sender];
194, 198: if (allowed != type(uint256).max) allowance[from][msg.sender] = allowed - amount;
- 195 :- require(balanceOf(from) >= amount, "Insufficient balance");
+ 199:+ is_balance_sufficient(from, amount);
196, 200: balances[from] -= amount;
197, 201: unchecked {
198, 202: balances[to] += amount;
@@ -370,7 +374,7 @@ contract DolaBorrowingRights {
370, 374: @param amount Amount of DBR to be burned.
371, 375: */
372, 376: function _burn(address from, uint256 amount) internal virtual {
- 373 :- require(balanceOf(from) >= amount, "Insufficient balance");
+ 377:+ is_balance_sufficient(from, amount);
374, 378: balances[from] -= amount;
375, 379: unchecked {
376, 380: _totalSupply -= amount;
4. Multiple address mappings can be combined into a single mapping of an address to a struct, where appropriate (5 instances)
Deployment. Gas Saved: 24 227
Minimum Method Call. Gas Saved: 254
Average Method Call. Gas Saved: 533
Maximum Method Call. Gas Saved: -6 726
Overall gas change: -1 371 (20.741%)
Saves a storage slot for the mapping. Depending on the circumstances and sizes of types, can avoid a Gsset (20000 gas) per mapping combined. Reads and subsequent writes can also be cheaper when a function requires both values and they both fit in the same storage slot. Finally, if both fields are accessed in the same function, can save ~42 gas per access due to not having to recalculate the key's keccak256 hash (Gkeccak256 - 30 gas) and that calculation's associated stack operations.
diff --git a/src/Market.sol b/src/Market.sol
index 9585b85..293bbb6 100644
--- a/src/Market.sol
+++ b/src/Market.sol
@@ -518,7 +518,7 @@ contract Market {
518, 518: @notice Function for incrementing the nonce of the msg.sender, making their latest signed message unusable.
519, 519: */
520, 520: function invalidateNonce() public {
- 521 :- nonces[msg.sender]++;
+ 521:+ unchecked { nonces[msg.sender]++; }
522, 522: }
523, 523:
524, 524: /**
6. State variables can be packed into fewer storage slots (1 instance)
Deployment. Gas Saved: -5 008
Minimum Method Call. Gas Saved: 1 911
Average Method Call. Gas Saved: 15 525
Maximum Method Call. Gas Saved: 20 972
Overall gas change: -62 419 (-69.524%)
If variables occupying the same slot are both written the same function or by the constructor, avoids a separate Gsset (20000 gas). Reads of the variables can also be cheaper
diff --git a/src/DBR.sol b/src/DBR.sol
index aab6daf..bff9fef 100644
--- a/src/DBR.sol
+++ b/src/DBR.sol
@@ -104,37 +104,39 @@ contract DolaBorrowingRights {
104, 104: /**
105, 105: @notice Get the total supply of DBR tokens.
106, 106: @dev The total supply is calculated as the difference between total DBR minted and total DBR accrued.
- 107 :- @return uint representing the total supply of DBR.
+ 107:+ @return ret uint representing the total supply of DBR.
108, 108: */
- 109 :- function totalSupply() public view returns (uint) {
- 110 :- if(totalDueTokensAccrued > _totalSupply) return 0;
- 111 :- return _totalSupply - totalDueTokensAccrued;
+ 109:+ function totalSupply() public view returns (uint ret) {
+ 110:+ unchecked { ret = _totalSupply - totalDueTokensAccrued; }
+ 111:+ if(ret > _totalSupply) return 0;
112, 112: }
113, 113:
114, 114: /**
115, 115: @notice Get the DBR balance of an address. Will return 0 if the user has zero DBR or a deficit.
116, 116: @dev The balance of a user is calculated as the difference between the user's balance and the user's accrued DBR debt + due DBR debt.
117, 117: @param user Address of the user.
- 118 :- @return uint representing the balance of the user.
+ 118:+ @return ret uint representing the balance of the user.
119, 119: */
- 120 :- function balanceOf(address user) public view returns (uint) {
+ 120:+ function balanceOf(address user) public view returns (uint ret) {
121, 121: uint debt = debts[user];
122, 122: uint accrued = (block.timestamp - lastUpdated[user]) * debt / 365 days;
- 123 :- if(dueTokensAccrued[user] + accrued > balances[user]) return 0;
- 124 :- return balances[user] - dueTokensAccrued[user] - accrued;
+ 123:+ uint mid = dueTokensAccrued[user] + accrued;
+ 124:+ unchecked { ret = balances[user] - mid; }
+ 125:+ if(ret > balances[user]) return 0;
125, 126: }
126, 127:
127, 128: /**
128, 129: @notice Get the DBR deficit of an address. Will return 0 if th user has zero DBR or more.
129, 130: @dev The deficit of a user is calculated as the difference between the user's accrued DBR deb + due DBR debt and their balance.
130, 131: @param user Address of the user.
- 131 :- @return uint representing the deficit of the user.
+ 132:+ @return ret uint representing the deficit of the user.
132, 133: */
- 133 :- function deficitOf(address user) public view returns (uint) {
+ 134:+ function deficitOf(address user) public view returns (uint ret) {
134, 135: uint debt = debts[user];
135, 136: uint accrued = (block.timestamp - lastUpdated[user]) * debt / 365 days;
- 136 :- if(dueTokensAccrued[user] + accrued < balances[user]) return 0;
- 137 :- return dueTokensAccrued[user] + accrued - balances[user];
+ 137:+ uint mid = dueTokensAccrued[user] + accrued;
+ 138:+ unchecked { ret = mid - balances[user]; }
+ 139:+ if(mid < ret) return 0;
138, 140: }
139, 141:
140, 142: /**
9. x = x + y is cheaper than x += y (12 instances)
Gas Optimizations
Gas savings are estimated using the gas report of existing
forge test --gas-report
tests (the sum of all deployment costs and the sum of the costs of calling methods) and may vary depending on the implementation of the fix.x = x + y
is cheaper thanx += y
internal
functions only called once can be inlined to save gasTotal: 48 instances over 11 issues
1. State variables only set in the constructor should be declared immutable (2 instances)
Deployment. Gas Saved: 117 275
Minimum Method Call. Gas Saved: 104
Average Method Call. Gas Saved: 110
Maximum Method Call. Gas Saved: 110
Overall gas change: -678 (-0.723%)
Avoids a Gsset (20000 gas) in the constructor, and replaces each Gwarmacces (100 gas) with a PUSH32 (3 gas).
- src/DBR.sol:11, 12
NOTE: name and symbol must be within 32 bytes
2. Use function instead of modifiers (4 instances)
Deployment. Gas Saved: 115 926
Minimum Method Call. Gas Saved: 162
Average Method Call. Gas Saved: -264
Maximum Method Call. Gas Saved: -481
Overall gas change: 734 (2.459%)
- src/BorrowController.sol:17
- src/DBR.sol:44
- src/Market.sol:92
- src/Oracle.sol:35
3. Duplicated require()/revert() checks should be refactored to a modifier or function ( instances)
Deployment. Gas Saved: 114 932
Minimum Method Call. Gas Saved: -59
Average Method Call. Gas Saved: -284
Maximum Method Call. Gas Saved: -398
Overall gas change: -2 665 (-12.599%)
- src/Fed.sol:49, 58, 67, 76, 87, 88, 104, 105
- src/DBR.sol:171, 195, 373
4. Multiple address mappings can be combined into a single mapping of an address to a struct, where appropriate (5 instances)
Deployment. Gas Saved: 24 227
Minimum Method Call. Gas Saved: 254
Average Method Call. Gas Saved: 533
Maximum Method Call. Gas Saved: -6 726
Overall gas change: -1 371 (20.741%)
Saves a storage slot for the mapping. Depending on the circumstances and sizes of types, can avoid a Gsset (20000 gas) per mapping combined. Reads and subsequent writes can also be cheaper when a function requires both values and they both fit in the same storage slot. Finally, if both fields are accessed in the same function, can save ~42 gas per access due to not having to recalculate the key's keccak256 hash (Gkeccak256 - 30 gas) and that calculation's associated stack operations.
- src/DBR.sol:19, 23, 26, 27, 28
5. Expression can be unchecked when overflow is not possible (6 instances)
Deployment. Gas Saved: 20 220
Minimum Method Call. Gas Saved: 410
Average Method Call. Gas Saved: 4 630
Maximum Method Call. Gas Saved: 1 354
Overall gas change: -6 233 (-5.326%)
- src/DBR.sol:110, 124, 137, 259
- src/Fed.sol:124
- src/Market.sol:521
6. State variables can be packed into fewer storage slots (1 instance)
Deployment. Gas Saved: -5 008
Minimum Method Call. Gas Saved: 1 911
Average Method Call. Gas Saved: 15 525
Maximum Method Call. Gas Saved: 20 972
Overall gas change: -62 419 (-69.524%)
If variables occupying the same slot are both written the same function or by the constructor, avoids a separate Gsset (20000 gas). Reads of the variables can also be cheaper
uint256(32), mapping(32), address(20), bool(1)
- src/Market.sol:53
7. refactoring similar statements (1 instance)
Deployment. Gas Saved: 18 422
Minimum Method Call. Gas Saved: -18
Average Method Call. Gas Saved: -11
Maximum Method Call. Gas Saved: 6
Overall gas change: 4 876 (7.739%)
- src/Market.sol:213
8. Better algorithm for underflow check (3 instances)
Deployment. Gas Saved: 12 613
Minimum Method Call. Gas Saved: 656
Average Method Call. Gas Saved: 8 332
Maximum Method Call. Gas Saved: 3 741
Overall gas change: -18 048 (-15.981%)
- src/DBR.sol:110, 123, 136
9.
x = x + y
is cheaper thanx += y
(12 instances)Deployment. Gas Saved: 11 214
Minimum Method Call. Gas Saved: 180
Average Method Call. Gas Saved: 468
Maximum Method Call. Gas Saved: 616
Overall gas change: -5 325 (-1.318%)
- src/DBR.sol:174, 196, 289, 360, 362, 376
- src/Market.sol:395, 397, 535, 568, 598, 600
10.
internal
functions only called once can be inlined to save gas (1 instance)Deployment. Gas Saved: 5 207
Minimum Method Call. Gas Saved: 67
Average Method Call. Gas Saved: 47
Maximum Method Call. Gas Saved: 24
Overall gas change: -137 (-0.154%)
- src/DBR.sol:341
11. State variables should be cached in stack variables rather than re-reading them from storage (2 instances)
Deployment. Gas Saved: 5 007
Minimum Method Call. Gas Saved: 478
Average Method Call. Gas Saved: 1 117
Maximum Method Call. Gas Saved: 1 423
Overall gas change: -6 231 (-1.618%)
- src/DBR.sol:286
- src/Market.sol:391
99. Overall gas savings
Deployment. Gas Saved: 416 802
Minimum Method Call. Gas Saved: 3 423
Average Method Call. Gas Saved: 15 773
Maximum Method Call. Gas Saved: 18 283
Overall gas change: -84 866 (-67.204%)