Abracadabra Mimswap - Bozho's results

General Information

Platform: Code4rena

Start Date: 07/03/2024

Pot Size: $63,000 USDC

Total HM: 20

Participants: 36

Period: 5 days

Judge: cccz

Total Solo HM: 11

Id: 349

League: BLAST

Abracadabra Money

Findings Distribution

Researcher Performance

Rank: 25/36

Findings: 2

Award: $184.90

🌟 Selected for report: 0

🚀 Solo Findings: 0

Awards

15.328 USDC - $15.33

Labels

bug
grade-b
QA (Quality Assurance)
sufficient quality report
Q-16

External Links

Low

NumberIssueInstances
L‑1Avoid using tx.origin5
L‑2Consider bounding input array length1
L‑3Consider implementing two-step procedure for updating protocol addresses8
L‑4constructor/initialize function lacks parameter validation6
L‑5Contracts use infinite approvals with no means to revoke2
L‑6Critical functions should be controlled by time locks12
L‑7decimals() is not a part of the ERC-20 standard9
L‑8Division by zero not prevented9
L‑9Do not use deprecated library functions3
L‑10External calls in an un-bounded for-loop may result in a DOS2
L‑11Functions calling contracts/addresses with transfer hooks are missing reentrancy guards31
L‑12latestAnswer() is deprecated4
L‑13Low level calls to custom addresses1
L‑14Missing checks for address(0) when assigning values to address state variables2
L‑15Missing contract-existence checks before low-level calls1
L‑16Missing zero address check in functions with address parameters97
L‑17Multiplication on the result of a division4
L‑18Owner or single user with a role can renounce while system is paused2
L‑19SafeTransferLib does not ensure that the token contract exists5
L‑20Setters should prevent re-setting of the same value5
L‑21Some tokens may revert when zero value transfers are made26
L‑22State variables not capped at reasonable values1
L‑23symbol() is not a part of the ERC-20 standard2
L‑24TransferOwnership should be two step1
L‑25unchecked blocks with additions/multiplications may overflow1
L‑26Unused/empty receive()/fallback() function2
L‑27Using >/>= without specifying an upper bound is unsafe20

NonCritical

NumberIssueInstances
NC‑12**<n> - 1 should be re-written as type(uint<n>).max2
NC‑2Adding a return statement when the function defines a named return variable, is redundant1
NC‑3address shouldn't be hard-coded5
NC‑4Avoid external calls in modifiers3
NC‑5Avoid mutating function/modifier parameters8
NC‑6Complicated functions should have explicit comments1
NC‑7Consider disallowing transfers to address(this)1
NC‑8Consider making contracts Upgradeable19
NC‑9Consider providing a ranged getter for array state variables2
NC‑10Consider using delete rather than assigning zero to clear values2
NC‑11Consider using named function arguments14
NC‑12Consider using named mappings3
NC‑13Constants in comparisons should appear on the left side42
NC‑14constants should be defined rather than using magic numbers22
NC‑15constants/immutables redefined elsewhere31
NC‑16constructor should emit an event15
NC‑17Contracts containing only utility functions should be made into libraries1
NC‑18Contracts should each be defined in separate files7
NC‑19Contracts should have all public/external functions exposed by interfaces14
NC‑20Contracts/libraries/interfaces should each be defined in separate files7
NC‑21Control structures do not follow the Solidity Style Guide15
NC‑22Do not use underscore at the end of variable name75
NC‑23else-block not required4
NC‑24Empty bytes check is missing9
NC‑25Enum values should be used instead of constant array indexes3
NC‑26Events are missing sender information26
NC‑27Events should be emitted before external calls38
NC‑28Events that mark critical parameter changes should contain both the old and the new value11
NC‑29Expressions for constant values should use immutable rather than constant6
NC‑30Extraneous whitespace3
NC‑31For loops in public or external functions should be avoided due to high gas costs and possible DOS2
NC‑32Function ordering in the contract does not follow the Solidity style guide100
NC‑33Functions contain the same code11
NC‑34Functions should be named in mixedCase style23
NC‑35High cyclomatic complexity15
NC‑36if-statement can be converted to a ternary9
NC‑37Imports could be organized more systematically7
NC‑38Inconsistent spacing in comments17
NC‑39Large multiples of ten should use scientific notation3
NC‑40Large numeric literals should use underscores for readability8
NC‑41Layout order does not comply with best practices61
NC‑42Lines are too long54
NC‑43Long functions should be refactored into multiple, smaller, functions5
NC‑44Make use of Solidity's using keyword97
NC‑45Misplaced SPDX identifier5
NC‑46Mixed usage of int/uint with int256/uint2562
NC‑47Multiple mappings with same keys can be combined into a single struct mapping for readability17
NC‑48Named imports of parent contracts are missing2
NC‑49Names of structs, events, enums and errors should use CapWords style2
NC‑50Non-external/public function names should begin with an underscore25
NC‑51Not using the latest versions of project dependencies1
NC‑52Not using the named return variables anywhere in the function is confusing19
NC‑53Outdated Solidity version20
NC‑54Overly complicated arithmetic3
NC‑55Overridden function has no body1
NC‑56Parameter change does not emit event2
NC‑57Polymorphic functions make security audits more time-consuming and error-prone4
NC‑58Prefer skip over revert model in iteration2
NC‑59public functions not called by the contract should be declared external instead17
NC‑60Public functions shouldn't have a preceding _ in their name1
NC‑61Public state variables shouldn't have a preceding _ in their name13
NC‑62Returning a struct instead of a bunch of variables is better11
NC‑63Some contracts can be abstract1
NC‑64Some events are never emitted3
NC‑65Some variables have a implicit default visibility1
NC‑66Top-level declarations should be separated by at least two lines168
NC‑67Typos1
NC‑68Unnecessary cast29
NC‑69Unnecessary struct attribute prefix2
NC‑70Unspecific compiler version pragma20
NC‑71Unused error definition6
NC‑72Unused event definition3
NC‑73Unused modifier definition2
NC‑74Use a single file for system wide constants15
NC‑75Use a struct to encapsulate multiple function parameters25
NC‑76Use CamelCase for contract and library names3
NC‑77Use delete instead of assigning values to false1
NC‑78Use scientific notation (e.g. 1e18) rather than exponentiation (e.g. 10**18)5
NC‑79Use UPPER_CASE for immutable16
NC‑80Variable names that consist of all capital letters should be reserved for constant/immutable variables22
NC‑81Variables need not be initialized to zero2
NC‑82Variables should be named in mixedCase style164

FULL REPORT @ https://gist.github.com/notbozho/d220e0f58fe1756d458f277cbf12f0f5



[L‑1] Avoid using tx.origin

tx.origin is a global variable in Solidity that returns the address of the account that sent the transaction. Using the variable could make a contract vulnerable if an authorized account calls a malicious contract. You can impersonate a user using a third party contract. This can make it easier to create a vault on behalf of another user with an external administrator (by receiving it as an argument).

<details> <summary><i>There are 5 instances of this issue:</i></summary>
📁 File: src/mimswap/MagicLP.sol

250:         (receiveQuoteAmount, mtFee, newRState, newBaseTarget) = querySellBase(tx.origin, baseInput); 

273:         (receiveBaseAmount, mtFee, newRState, newQuoteTarget) = querySellQuote(tx.origin, quoteInput); 

311:                 tx.origin, 

333:                 tx.origin, 

250, 273, 311, 333

📁 File: src/mimswap/periphery/Factory.sol

82:         address creator = tx.origin; 

82

</details>

[L‑2] Consider bounding input array length

The functions below take in an unbounded array, and make function calls for entries in the array. While the function will revert if it eventually runs out of gas, it may be a nicer user experience to require() that the length of the array is below some reasonable maximum, so that the user doesn't have to use up a full transaction's gas only to see that the transaction reverts.

<i>There is one instance of this issue:</i>

📁 File: src/blast/BlastOnboarding.sol

/// @audit tokens.length not bounded
165:         for (uint256 i = 0; i < tokens.length; i++) { 
166:             if (!supportedTokens[tokens[i]]) {
167:                 revert ErrUnsupportedToken();
168:             }
169:             if (registry.nativeYieldTokens(tokens[i])) {
170:                 BlastYields.claimAllTokenYields(tokens[i], feeTo);
171:             }
172:         }

165


[L‑3] Consider implementing two-step procedure for updating protocol addresses

A copy-paste error or a typo may end up bricking protocol functionality, or sending tokens to an address with no known private key. Consider implementing a two-step procedure for updating protocol addresses, where the recipient is set as pending, and must 'accept' the assignment by making an affirmative call. A straight forward way of doing this would be to have the target contracts implement EIP-165, and to have the 'set' functions ensure that the recipient is of the right interface type.

<details> <summary><i>There are 8 instances of this issue:</i></summary>
📁 File: src/blast/BlastBox.sol

/// @audit line 77
72:     function setFeeTo(address feeTo_) external onlyOwner { 
73:         if (feeTo_ == address(0)) {
74:             revert ErrZeroAddress();
75:         }
76: 
77:         feeTo = feeTo_;
78:         emit LogFeeToChanged(feeTo_);
79:     }

72

📁 File: src/blast/BlastGovernor.sol

/// @audit line 45
40:     function setFeeTo(address _feeTo) external onlyOwner { 
41:         if(_feeTo == address(0)) {
42:             revert ErrZeroAddress();
43:         }
44:         
45:         feeTo = _feeTo;
46:         emit LogFeeToChanged(_feeTo);
47:     }

40

📁 File: src/blast/BlastMagicLP.sol

/// @audit line 85
80:     function setFeeTo(address feeTo_) external onlyImplementation onlyImplementationOwner { 
81:         if (feeTo_ == address(0)) {
82:             revert ErrZeroAddress();
83:         }
84: 
85:         feeTo = feeTo_;
86:         emit LogFeeToChanged(feeTo_);
87:     }

80

📁 File: src/blast/BlastOnboarding.sol

/// @audit line 152
147:     function setFeeTo(address feeTo_) external onlyOwner { 
148:         if (feeTo_ == address(0)) {
149:             revert ErrZeroAddress();
150:         }
151: 
152:         feeTo = feeTo_;
153:         emit LogFeeToChanged(feeTo_);
154:     }

/// @audit line 191
190:     function setBootstrapper(address bootstrapper_) external onlyOwner { 
191:         bootstrapper = bootstrapper_;
192:         emit LogBootstrapperChanged(bootstrapper_);
193:     }

147, 190

📁 File: src/mimswap/auxiliary/FeeRateModel.sol

/// @audit line 51
46:     function setMaintainer(address maintainer_) external onlyOwner { 
47:         if (maintainer_ == address(0)) {
48:             revert ErrZeroAddress();
49:         }
50: 
51:         maintainer = maintainer_;
52:         emit LogMaintainerChanged(maintainer_);
53:     }

/// @audit line 58
57:     function setImplementation(address implementation_) public onlyOwner { 
58:         implementation = implementation_;
59:         emit LogImplementationChanged(implementation_);
60:     }

46, 57

📁 File: src/mimswap/periphery/Factory.sol

/// @audit line 101
96:     function setLpImplementation(address implementation_) external onlyOwner { 
97:         if (implementation_ == address(0)) {
98:             revert ErrZeroAddress();
99:         }
100: 
101:         implementation = implementation_;
102:         emit LogSetImplementation(implementation_);
103:     }

96

</details>

#0 - c4-pre-sort

2024-03-15T15:00:37Z

141345 marked the issue as sufficient quality report

#1 - 141345

2024-03-16T01:06:12Z

110 Bozho l r nc 3 0 57

L 1 i L 2 n L 3 i L 4 l L 5 n L 6 l L 7 i L 8 i L 9 i L 10 i L 11 l L 12 d dup of https://github.com/code-423n4/2024-03-abracadabra-money-findings/issues/82 L 13 i L 14 i L 15 i L 16 i L 17 n L 18 i L 19 n L 20 n L 21 n L 22 i L 23 i L 24 i L 25 n L 26 n L 27 n

#2 - c4-judge

2024-03-29T16:50:03Z

thereksfour marked the issue as grade-a

#3 - c4-judge

2024-04-06T06:58:59Z

thereksfour marked the issue as grade-b

Findings Information

🌟 Selected for report: hihen

Also found by: 0x11singh99, Bozho, Sathish9098, albahaca, clara, dharma09, oualidpro, pfapostol, slvDev

Labels

bug
G (Gas Optimization)
grade-a
high quality report
sponsor disputed
G-10

Awards

169.5732 USDC - $169.57

External Links

NumberIssueInstancesEstimated Gas Saved
GAS‑1++i costs less gas than i++/i += 1 (same for --i vs i--/i -+ 1)15
GAS‑2++i/i++ should be unchecked when it is not possible for them to overflow160
GAS‑3>=/<= costs less gas than >/<82246
GAS‑4address(this) should be cached when used multiple times3-
GAS‑5Avoid fetching a low-level call's return data by using assembly1159
GAS‑6Avoid unnecessary public variables491,078,000
GAS‑7Avoid updating storage when the value hasn't changed1118,700
GAS‑8Avoid zero to non-zero storage writes where possible13287,300
GAS‑9Consider activating via-ir for deploying0250
GAS‑10Consider caching repeated computations2120
GAS‑11Consider pre-calculating the address of address(this)49-
GAS‑12Consider using OpenZeppelin's EnumerateSet instead of nested mappings44,000
GAS‑13Consider using Solady's gas optimized lib for Math39-
GAS‑14Constructors can be marked payable16336
GAS‑15Counting down in for statements is more gas efficient8128
GAS‑16Declare variables outside of loops15225
GAS‑17Do not calculate constants4-
GAS‑18do-while is cheaper than for-loops when the initial check can be skipped82,040
GAS‑19Don't transfer with zero amount to save gas26520
GAS‑20Emitting storage values instead of the memory one.8800
GAS‑21Empty blocks should be removed or emit something3-
GAS‑22Function names can be optimized111,408
GAS‑23Functions not used internally could be marked external to save gas15-
GAS‑24Initializers can be marked payable363
GAS‑25internal functions only used once can be inlined so save gas390
GAS‑26Low-level call can be optimized with assembly1159
GAS‑27Mappings are cheaper to use than storage arrays510,500
GAS‑28Merge events to save gas93,375
GAS‑29Multiple accesses of the same mapping/array key/index should be cached23966
GAS‑30Multiple address/ID mappings can be combined into a single mapping of an address/ID to a struct9180,000
GAS‑31Nested for loops should be avoided due to high gas costs resulting from O^2 time complexity1-
GAS‑32Nesting if-statements is cheaper than using &&11330
GAS‑33Newer versions of solidity are more gas efficient20-
GAS‑34Only emit event in setter function if the state variable was changed13-
GAS‑35Optimize Deployment Size by Fine-tuning IPFS Hash20212,000
GAS‑36Order of initial checks in a function can be optimized24,200
GAS‑37Pre-increments/pre-decrements are cheaper than +1/-116
GAS‑38Remove or replace unused state variables780,150
GAS‑39Simple checks for zero can be done using assembly to save gas74444
GAS‑40Simplify modulo operations1101
GAS‑41Sort Solidity operations using short-circuit mode36-
GAS‑42Stack variable is only used once2884
GAS‑43State variable read in a loop7679
GAS‑44State variable written in a loop211,588
GAS‑45State variables can be packed into fewer storage slots by truncating timestamp bytes1140,000
GAS‑46State variables can be reordered to fit into fewer storage slots120,000
GAS‑47State variables should be cached in stack variables rather than re-reading them from storage535,300
GAS‑48Structs can be packed into fewer storage slots by truncating timestamp bytes120,000
GAS‑49Superfluous event fields270
GAS‑50unchecked {} can be used on the division of two uints in order to save gas362,160
GAS‑51Update OpenZeppelin dependency to the latest version8-
GAS‑52Usage of uints/ints smaller than 32 bytes (256 bits) incurs overhead1272
GAS‑53Use Array.unsafeAccess() to avoid repeated array length checks1021,000
GAS‑54Use assembly for math equations61,560
GAS‑55Use assembly for small keccak256 hashes, in order to save gas180
GAS‑56Use assembly scratch space to build calldata for external calls408,800
GAS‑57Use assembly to perform efficient back-to-back calls2600
GAS‑58Use assembly to validate msg.sender224
GAS‑59Use assembly to write address/contract storage values381,900
GAS‑60Use calldata instead of memory for function arguments that do not get mutated2600
GAS‑61Use constants instead of type(uint<n>).max / .min624
GAS‑62Use if statements instead of ternary operators6-
GAS‑63Use immutable when you have storage variable that is not going to change2-
GAS‑64Use modifiers rather than invoking functions to perform checks7280
GAS‑65Use of emit inside a loop51,875
GAS‑66Use scratch space when building emitted events with two data arguments361,368
GAS‑67Use the inputs/results of assignments rather than re-reading state variables282,716
GAS‑68Use unchecked for divisions on constant or immutable values6360
GAS‑69Using bools for storage incurs overhead351,300
GAS‑70Using constants instead of enum can save gas2-
GAS‑71Using globals directly is cheaper than assigning them to variables15
GAS‑72Using storage instead of memory for structs/arrays saves gas36,300
GAS‑73x + y is more efficient than using += for state variables (likewise for -=)358,680

❗ Disclaimer: Gas totals are estimates based on data from the Ethereum Yellowpaper. The estimates use the lower bounds of ranges and count two iterations of each for-loop. All values above are runtime, not deployment, values; deployment values are listed in the individual issue descriptions.


THE FULL REPORT IS @ https://gist.github.com/notbozho/07d533d869f7cf2921a83e8d092a5b9a


[GAS‑1] ++i costs less gas than i++/i += 1 (same for --i vs i--/i -+ 1)

Gas saved per Instance: ~5

<i>There is one instance of this issue:</i>

📁 File: src/blast/BlastOnboarding.sol

165:         for (uint256 i = 0; i < tokens.length; i++) {

165


[GAS‑2] ++i/i++ should be unchecked when it is not possible for them to overflow

The unchecked keyword is new in solidity version 0.8.0, so this only applies to that version or higher, which these instances are. This saves 30-40 gas per loop

Gas saved per Instance: ~60

<i>There is one instance of this issue:</i>

📁 File: src/blast/BlastOnboarding.sol

165:         for (uint256 i = 0; i < tokens.length; i++) {

165


[GAS‑3] >=/<= costs less gas than >/<

The compiler uses opcodes GT and ISZERO for code that uses >, but only requires LT for >=. A similar behaviour applies for >, which uses opcodes LT and ISZERO, but only requires GT for <=.

Gas saved per Instance: ~3 (Total: ~246)

<details> <summary><i>There are 82 instances of this issue:</i></summary>
📁 File: src/blast/BlastOnboarding.sol

114:         if (caps[token] > 0 && totals[token].total > caps[token]) {

165:         for (uint256 i = 0; i < tokens.length; i++) {

114, 165

📁 File: src/blast/BlastOnboardingBoot.sol

108:         if (totalPoolShares < minAmountOut) {

108

📁 File: src/mimswap/MagicLP.sol

108:         if (i == 0 || i > MAX_I) {

111:         if (k > MAX_K) {

114:         if (lpFeeRate < MIN_LP_FEE_RATE || lpFeeRate > MAX_LP_FEE_RATE) {

139:         if (_RState_ == uint32(PMMPricing.RState.BELOW_ONE) && _BASE_RESERVE_ < _BASE_TARGET_) {

144:         if (_RState_ == uint32(PMMPricing.RState.ABOVE_ONE) && _QUOTE_RESERVE_ < _QUOTE_TARGET_) {

294:         if (data.length > 0) {

302:         if (baseBalance < _BASE_RESERVE_ && quoteBalance < _QUOTE_RESERVE_) {

308:         if (baseBalance < _BASE_RESERVE_) {

315:             if (uint256(_BASE_RESERVE_) - baseBalance > receiveBaseAmount) {

330:         if (quoteBalance < _QUOTE_RESERVE_) {

337:             if (uint256(_QUOTE_RESERVE_) - quoteBalance > receiveQuoteAmount) {

381:             shares = quoteBalance < DecimalMath.mulFloor(baseBalance, _I_) ? DecimalMath.divFloor(quoteBalance, _I_) : baseBalance;

395:         } else if (baseReserve > 0 && quoteReserve > 0) {

399:             uint256 mintRatio = quoteInputRatio < baseInputRatio ? quoteInputRatio : baseInputRatio;

421:         if (deadline < block.timestamp) {

424:         if (shareAmount > balanceOf(msg.sender)) {

441:         if (baseAmount < baseMinAmount || quoteAmount < quoteMinAmount) {

450:         if (data.length > 0) {

480:         if (_BASE_RESERVE_ < minBaseReserve || _QUOTE_RESERVE_ < minQuoteReserve) {

483:         if (newI == 0 || newI > MAX_I) {

486:         if (newK > MAX_K) {

489:         if (newLpFeeRate < MIN_LP_FEE_RATE || newLpFeeRate > MAX_LP_FEE_RATE) {

508:         if (baseBalance > type(uint112).max || quoteBalance > type(uint112).max) {

532:         if (baseBalance > type(uint112).max || quoteBalance > type(uint112).max) {

549:         if (timeElapsed > 0 && _BASE_RESERVE_ != 0 && _QUOTE_RESERVE_ != 0) {

582:         if (amount > 0) {

588:         if (amount > 0) {

108, 111, 114, 139, 144, 294, 302, 308, 315, 330, 337, 381, 395, 399, 421, 424, 441, 450, 480, 483, 486, 489, 508, 532, 549, 582, 588

📁 File: src/mimswap/libraries/Math.sol

22:         if (remainder > 0) {

32:         while (z < y) {

141:             return DecimalMath.mulFloor(i, delta) > V1 ? V1 : DecimalMath.mulFloor(i, delta);

200:         if (V2 > V1) {

22, 32, 141, 200

📁 File: src/mimswap/libraries/PMMPricing.sol

50:             if (payBaseAmount < backToOnePayBase) {

54:                 if (receiveQuoteAmount > backToOneReceiveQuote) {

86:             if (payQuoteAmount < backToOnePayQuote) {

89:                 if (receiveBaseAmount > backToOneReceiveBase) {

50, 54, 86, 89

📁 File: src/mimswap/periphery/Router.sol

48:         if (block.timestamp > deadline) {

101:         shares = quoteInAmount < DecimalMath.mulFloor(baseInAmount, i) ? DecimalMath.divFloor(quoteInAmount, i) : baseInAmount;

138:             shares = quoteBalance < DecimalMath.mulFloor(baseBalance, i) ? DecimalMath.divFloor(quoteBalance, i) : baseBalance;

147:         } else if (baseReserve > 0 && quoteReserve > 0) {

220:         if (msg.value > wethAdjustedAmount) {

502:         if (shares < minimumShares) {

523:             uint256 shares = quoteInAmount < DecimalMath.mulFloor(baseInAmount, i) ? DecimalMath.divFloor(quoteInAmount, i) : baseInAmount;

527:             if (quoteReserve > 0 && baseReserve > 0) {

544:         for (uint256 i = 0; i < iterations; ) {

566:         if (amountOut < minimumOut) {

573:         if (amountOut < minimumOut) {

581:         if (amountOut < minimumOut) {

590:         if (pathLength > 256) {

602:         if (quoteDecimals - baseDecimals > MAX_BASE_QUOTE_DECIMALS_DIFFERENCE) {

48, 101, 138, 147, 220, 502, 523, 527, 544, 566, 573, 581, 590, 602

📁 File: src/oracles/aggregators/MagicLpAggregator.sol

40:         uint256 minAnswer = baseAnswerNomalized < quoteAnswerNormalized ? baseAnswerNomalized : quoteAnswerNormalized;

40

📁 File: src/staking/LockingMultiRewards.sol

123:         if (_lockDuration < MIN_LOCK_DURATION) {

127:         if (_rewardsDuration < MIN_REWARDS_DURATION) {

326:         if (tokenAddress == stakingToken && tokenAmount > stakingToken.balanceOf(address(this)) - stakingTokenBalance) {

374:         if (_remainingRewardTime < minRemainingTime) {

379:         if (block.timestamp < reward.periodFinish) {

384:         if (amount < _remainingRewardTime) {

405:         for (uint256 i; i < users.length; ) {

417:             if (index == lastLockIndex[user] && locks.length > 1) {

422:             if (locks[index].unlockTime > block.timestamp) {

490:         if (lockCount == 0 || _userLocks[user][_lastLockIndex].unlockTime < _nextUnlockTime) {

497:             if (amount < minLockAmount) {

547:         for (uint256 i; i < rewardTokens.length; ) {

561:         for (uint256 i; i < rewardTokens.length; ) {

575:         for (uint256 i; i < rewardTokens.length; ) {

580:             for (uint256 j; j < users.length; ) {

614:         for (uint256 i; i < rewardTokens.length; ) {

623:             if (i < rewardItemLength) {

636:                     if (amount > 0) {

123, 127, 326, 374, 379, 384, 405, 417, 422, 490, 497, 547, 561, 575, 580, 614, 623, 636

</details>

[GAS‑4] address(this) should be cached when used multiple times

<details> <summary><i>There are 3 instances of this issue:</i></summary>
📁 File: src/blast/BlastOnboardingBoot.sol

/// @audit 'address(this)' used 3 times
96:     function bootstrap(uint256 minAmountOut) external onlyOwner onlyState(State.Closed) returns (address, address, uint256) {

96

📁 File: src/mimswap/MagicLP.sol

/// @audit 'address(this)' used 3 times
413:     function sellShares(
414:         uint256 shareAmount,
415:         address to,
416:         uint256 baseMinAmount,
417:         uint256 quoteMinAmount,
418:         bytes calldata data,
419:         uint256 deadline
420:     ) external nonReentrant returns (uint256 baseAmount, uint256 quoteAmount) {

413

📁 File: src/mimswap/periphery/Router.sol

/// @audit 'address(this)' used 3 times
274:     function removeLiquidityETH(
275:         address lp,
276:         address to,
277:         uint256 sharesIn,
278:         uint256 minimumETHAmount,
279:         uint256 minimumTokenAmount,
280:         uint256 deadline
281:     ) external returns (uint256 ethAmountOut, uint256 tokenAmountOut) {

274

</details>

[GAS‑5] Avoid fetching a low-level call's return data by using assembly

Even if you don't assign the call's second return value, it still gets copied to memory. Use assembly instead to prevent this and save 159 gas:

Gas saved per Instance: ~159

<i>There is one instance of this issue:</i>

📁 File: src/blast/BlastGovernor.sol

54:         (success, result) = to.call{value: value}(data);

54


[GAS‑6] Avoid unnecessary public variables

Public state variables in Solidity automatically generate getter functions, increasing contract size and potentially leading to higher deployment and interaction costs. To optimize gas usage and contract efficiency, minimize the use of public variables unless external access is necessary. Instead, use internal or private visibility combined with explicit getter functions when required. This practice not only reduces contract size but also provides better control over data access and manipulation, enhancing security and readability. Prioritize lean, efficient contracts to ensure cost-effectiveness and better performance on the blockchain.

Gas saved per Instance: ~22,000 (Total: ~1,078,000)

<details> <summary><i>There are 49 instances of this issue:</i></summary>
📁 File: src/blast/BlastBox.sol

20:     BlastTokenRegistry public immutable registry;

22:     address public feeTo;

20, 22

📁 File: src/blast/BlastGovernor.sol

11:     address public feeTo;

11

📁 File: src/blast/BlastMagicLP.sol

17:     BlastTokenRegistry public immutable registry;

17

📁 File: src/blast/BlastOnboarding.sol

30:     State public state;
31:     address public bootstrapper;
32:     address public feeTo;
33:     BlastTokenRegistry public registry;

30

📁 File: src/blast/BlastOnboardingBoot.sol

24:     address public pool;
25:     Router public router;
26:     IFactory public factory;
27:     uint256 public totalPoolShares;
28:     bool public ready;
29:     LockingMultiRewards public staking;

24

📁 File: src/mimswap/MagicLP.sol

61:     MagicLP public immutable implementation;

70:     address public _BASE_TOKEN_;
71:     address public _QUOTE_TOKEN_;
72:     uint112 public _BASE_RESERVE_;
73:     uint112 public _QUOTE_RESERVE_;
74:     uint32 public _BLOCK_TIMESTAMP_LAST_;
75:     uint256 public _BASE_PRICE_CUMULATIVE_LAST_;
76:     uint112 public _BASE_TARGET_;
77:     uint112 public _QUOTE_TARGET_;
78:     uint32 public _RState_;
79:     IFeeRateModel public _MT_FEE_RATE_MODEL_;
80:     uint256 public _LP_FEE_RATE_;
81:     uint256 public _K_;
82:     uint256 public _I_;

61, 70

📁 File: src/mimswap/auxiliary/FeeRateModel.sol

19:     address public maintainer;
20:     address public implementation;

19

📁 File: src/mimswap/periphery/Factory.sol

32:     address public implementation;
33:     IFeeRateModel public maintainerFeeRateModel;

32

📁 File: src/mimswap/periphery/Router.sol

33:     IWETH public immutable weth;
34:     IFactory public immutable factory;

33

📁 File: src/oracles/aggregators/MagicLpAggregator.sol

10:     IMagicLP public immutable pair;
11:     IAggregator public immutable baseOracle;
12:     IAggregator public immutable quoteOracle;
13:     uint8 public immutable baseDecimals;
14:     uint8 public immutable quoteDecimals;

10

📁 File: src/staking/LockingMultiRewards.sol

83:     uint256 public immutable maxLocks;
84:     uint256 public immutable lockingBoostMultiplerInBips;
85:     uint256 public immutable rewardsDuration;
86:     uint256 public immutable lockDuration;
87:     address public immutable stakingToken;

98:     address[] public rewardTokens;

100:     uint256 public lockedSupply; // all locked boosted deposits
101:     uint256 public unlockedSupply; // all unlocked unboosted deposits
102:     uint256 public minLockAmount; // minimum amount allowed to lock
103:     uint256 public stakingTokenBalance; // total staking token balance

83, 98, 100

</details>

[GAS‑7] Avoid updating storage when the value hasn't changed

If the old value is equal to the new value, not re-storing the value will avoid a Gsreset (2900 gas), potentially at the expense of a Gcoldsload (2100 gas) or a Gwarmaccess (100 gas)

Gas saved per Instance: ~1,700 (Total: ~18,700)

<details> <summary><i>There are 11 instances of this issue:</i></summary>
📁 File: src/blast/BlastBox.sol

72:     function setFeeTo(address feeTo_) external onlyOwner {
73:         if (feeTo_ == address(0)) {
74:             revert ErrZeroAddress();
75:         }
76:
77:         feeTo = feeTo_;
78:         emit LogFeeToChanged(feeTo_);
79:     }

72

📁 File: src/blast/BlastGovernor.sol

40:     function setFeeTo(address _feeTo) external onlyOwner {
41:         if(_feeTo == address(0)) {
42:             revert ErrZeroAddress();
43:         }
44:
45:         feeTo = _feeTo;
46:         emit LogFeeToChanged(_feeTo);
47:     }

40

📁 File: src/blast/BlastMagicLP.sol

80:     function setFeeTo(address feeTo_) external onlyImplementation onlyImplementationOwner {
81:         if (feeTo_ == address(0)) {
82:             revert ErrZeroAddress();
83:         }
84:
85:         feeTo = feeTo_;
86:         emit LogFeeToChanged(feeTo_);
87:     }

80

📁 File: src/blast/BlastOnboarding.sol

147:     function setFeeTo(address feeTo_) external onlyOwner {
148:         if (feeTo_ == address(0)) {
149:             revert ErrZeroAddress();
150:         }
151:
152:         feeTo = feeTo_;
153:         emit LogFeeToChanged(feeTo_);
154:     }

190:     function setBootstrapper(address bootstrapper_) external onlyOwner {
191:         bootstrapper = bootstrapper_;
192:         emit LogBootstrapperChanged(bootstrapper_);
193:     }

147, 190

📁 File: src/blast/BlastOnboardingBoot.sol

137:     function setStaking(LockingMultiRewards _staking) external onlyOwner {
138:         if (ready) {
139:             revert ErrCannotChangeOnceReady();
140:         }
141:
142:         staking = _staking;
143:         emit LogStakingChanged(address(_staking));
144:     }

146:     function setReady(bool _ready) external onlyOwner onlyState(State.Closed) {
147:         ready = _ready;
148:         emit LogReadyChanged(ready);
149:     }

137, 146

📁 File: src/mimswap/auxiliary/FeeRateModel.sol

46:     function setMaintainer(address maintainer_) external onlyOwner {
47:         if (maintainer_ == address(0)) {
48:             revert ErrZeroAddress();
49:         }
50:
51:         maintainer = maintainer_;
52:         emit LogMaintainerChanged(maintainer_);
53:     }

57:     function setImplementation(address implementation_) public onlyOwner {
58:         implementation = implementation_;
59:         emit LogImplementationChanged(implementation_);
60:     }

46, 57

📁 File: src/mimswap/periphery/Factory.sol

96:     function setLpImplementation(address implementation_) external onlyOwner {
97:         if (implementation_ == address(0)) {
98:             revert ErrZeroAddress();
99:         }
100:
101:         implementation = implementation_;
102:         emit LogSetImplementation(implementation_);
103:     }

105:     function setMaintainerFeeRateModel(IFeeRateModel maintainerFeeRateModel_) external onlyOwner {
106:         if (address(maintainerFeeRateModel_) == address(0)) {
107:             revert ErrZeroAddress();
108:         }
109:
110:         maintainerFeeRateModel = maintainerFeeRateModel_;
111:         emit LogSetMaintainerFeeRateModel(maintainerFeeRateModel_);
112:     }

96, 105

</details>

[GAS‑8] Avoid zero to non-zero storage writes where possible

Changing a storage variable from zero to non-zero costs 22,100 gas in total. (20,000 gas for a zero to non-zero write and 2,100 for a cold storage access)

Consider using non-zero architecture to avoid high gas costs for zero to non-zero storage writes.

Gas saved per Instance: ~22,100 (Total: ~287,300)

<details> <summary><i>There are 13 instances of this issue:</i></summary>
📁 File: src/mimswap/MagicLP.sol

121:         _I_ = i;
122:         _K_ = k;
123:         _LP_FEE_RATE_ = lpFeeRate;

121

📁 File: src/staking/LockingMultiRewards.sol

163:         unlockedSupply -= amount;

178:         unlockedSupply -= amount;

181:         stakingTokenBalance -= amount;

319:         minLockAmount = _minLockAmount;

380:             amount += _remainingRewardTime * reward.rewardRate;

441:             unlockedSupply += amount;
442:             lockedSupply -= amount;

465:         stakingTokenBalance += amount;

473:             unlockedSupply += amount;

486:         lockedSupply += amount;

163, 178, 181, 319, 380, 441, 465, 473, 486

</details>

[GAS‑9] Consider activating via-ir for deploying

The IR-based code generator was introduced with an aim to not only allow code generation to be more transparent and auditable but also to enable more powerful optimization passes that span across functions.You can enable it on the command line using --via-ir or with the option {"viaIR": true}.This will take longer to compile, but you can just simple test it before deploying and if you got a better benchmark then you can add --via-ir to your deploy commandMore on: https://docs.soliditylang.org/en/v0.8.17/ir-breaking-changes.html


[GAS‑10] Consider caching repeated computations

The result of repeated computations can be cached to avoid calculating it multiple times and wasting gas.

Gas saved per Instance: ~60 (Total: ~120)

<details> <summary><i>There are 2 instances of this issue:</i></summary>
📁 File: src/mimswap/libraries/Math.sol

/// @audit DecimalMath.ONE-k is seen 3 times
131:     function _SolveQuadraticFunctionForTrade(uint256 V0, uint256 V1, uint256 delta, uint256 i, uint256 k) internal pure returns (uint256) {
132:         if (V0 == 0) {
133:             revert ErrIsZero();
134:         }
135:
136:         if (delta == 0) {
137:             return 0;
138:         }
139:
140:         if (k == 0) {
141:             return DecimalMath.mulFloor(i, delta) > V1 ? V1 : DecimalMath.mulFloor(i, delta);
142:         }
143:
144:         if (k == DecimalMath.ONE) {
145:             // if k==1
146:             // Q2=Q1/(1+ideltaBQ1/Q0/Q0)
147:             // temp = ideltaBQ1/Q0/Q0
148:             // Q2 = Q1/(1+temp)
149:             // Q1-Q2 = Q1*(1-1/(1+temp)) = Q1*(temp/(1+temp))
150:             // uint256 temp = i.mul(delta).mul(V1).div(V0.mul(V0));
151:             uint256 temp;
152:             uint256 idelta = i * delta;
153:             if (idelta == 0) {
154:                 temp = 0;
155:             } else if ((idelta * V1) / idelta == V1) {
156:                 temp = (idelta * V1) / (V0 * V0);
157:             } else {
158:                 temp = (((delta * V1) / V0) * i) / V0;
159:             }
160:             return (V1 * temp) / (temp + DecimalMath.ONE);
161:         }
162:
163:         // calculate -b value and sig
164:         // b = kQ0^2/Q1-i*deltaB-(1-k)Q1
165:         // part1 = (1-k)Q1 >=0
166:         // part2 = kQ0^2/Q1-i*deltaB >=0
167:         // bAbs = abs(part1-part2)
168:         // if part1>part2 => b is negative => bSig is false
169:         // if part2>part1 => b is positive => bSig is true
170:         uint256 part2 = (((k * V0) / V1) * V0) + (i * delta); // kQ0^2/Q1-i*deltaB
171:         uint256 bAbs = (DecimalMath.ONE - k) * V1; // (1-k)Q1
172:
173:         bool bSig;
174:         if (bAbs >= part2) {
175:             bAbs = bAbs - part2;
176:             bSig = false;
177:         } else {
178:             bAbs = part2 - bAbs;
179:             bSig = true;
180:         }
181:         bAbs = bAbs / DecimalMath.ONE;
182:
183:         // calculate sqrt
184:         uint256 squareRoot = DecimalMath.mulFloor((DecimalMath.ONE - k) * 4, DecimalMath.mulFloor(k, V0) * V0); // 4(1-k)kQ0^2
185:         squareRoot = sqrt((bAbs * bAbs) + squareRoot); // sqrt(b*b+4(1-k)kQ0*Q0)
186:
187:         // final res
188:         uint256 denominator = (DecimalMath.ONE - k) * 2; // 2(1-k)
189:         uint256 numerator;
190:         if (bSig) {
191:             numerator = squareRoot - bAbs;
192:             if (numerator == 0) {
193:                 revert ErrIsZero();
194:             }
195:         } else {
196:             numerator = bAbs + squareRoot;
197:         }
198:
199:         uint256 V2 = DecimalMath.divCeil(numerator, denominator);
200:         if (V2 > V1) {
201:             return 0;
202:         } else {
203:             return V1 - V2;
204:         }
205:     }

131

📁 File: src/mimswap/libraries/PMMPricing.sol

/// @audit (DecimalMath.ONE-state.K)+DecimalMath.mulFloor(state.K,R) is seen 2 times
203:     function getMidPrice(PMMState memory state) internal pure returns (uint256) {
204:         if (state.R == RState.BELOW_ONE) {
205:             uint256 R = DecimalMath.divFloor((state.Q0 * state.Q0) / state.Q, state.Q);
206:             R = (DecimalMath.ONE - state.K) + DecimalMath.mulFloor(state.K, R);
207:             return DecimalMath.divFloor(state.i, R);
208:         } else {
209:             uint256 R = DecimalMath.divFloor((state.B0 * state.B0) / state.B, state.B);
210:             R = (DecimalMath.ONE - state.K) + DecimalMath.mulFloor(state.K, R);
211:             return DecimalMath.mulFloor(state.i, R);
212:         }
213:     }

203

</details>

[GAS‑11] Consider pre-calculating the address of address(this)

It can be more gas-efficient to use a hardcoded address instead of the address(this) expression, especially if you need to use the same address multiple times in your contract.

The reason for this, is that using address(this) requires an additional EXTCODESIZE operation to retrieve the contract’s address from its bytecode, which can increase the gas cost of your contract. By pre-calculating and using a hardcoded address, you can avoid this additional operation and reduce the overall gas cost of your contract.

<details> <summary><i>There are 49 instances of this issue:</i></summary>
📁 File: src/blast/BlastBox.sol

99:         BlastYields.configureDefaultClaimables(address(this));

99

📁 File: src/blast/BlastGovernor.sol

21:         BlastYields.configureDefaultClaimables(address(this));

21

📁 File: src/blast/BlastMagicLP.sol

99:         BlastYields.configureDefaultClaimables(address(this));

99

📁 File: src/blast/BlastOnboarding.sol

59:         BlastYields.configureDefaultClaimables(address(this));

102:         token.safeTransferFrom(msg.sender, address(this), amount);

59, 102

📁 File: src/blast/BlastOnboardingBoot.sol

106:         (pool, totalPoolShares) = router.createPool(MIM, USDB, FEE_RATE, I, K, address(this), baseAmount, quoteAmount);

117:         staking = new LockingMultiRewards(pool, 30_000, 7 days, 13 weeks, address(this));
118:         staking.setOperator(address(this), true);

106, 117

📁 File: src/blast/BlastWrappers.sol

51:         if (governor_ == address(this)) {

60:         if (_governor == address(this)) {

51, 60

📁 File: src/blast/libraries/BlastYields.sol

21:         if (IERC20Rebasing(token).getConfiguration(address(this)) == YieldMode.CLAIMABLE) {

26:         emit LogBlastTokenClaimableEnabled(address(this), token);

31:         emit LogBlastNativeClaimableEnabled(address(this));

39:         return claimMaxGasYields(address(this), recipient);

52:         return claimAllNativeYields(address(this), recipient);

71:         amount = IERC20Rebasing(token).claim(recipient, IERC20Rebasing(token).getClaimableAmount(address(this)));

21, 26, 31, 39, 52, 71

📁 File: src/mimswap/MagicLP.sol

229:         return _BASE_TOKEN_.balanceOf(address(this)) - uint256(_BASE_RESERVE_);

233:         return _QUOTE_TOKEN_.balanceOf(address(this)) - uint256(_QUOTE_RESERVE_);

245:         uint256 baseBalance = _BASE_TOKEN_.balanceOf(address(this));

262:         _setReserve(baseBalance, _QUOTE_TOKEN_.balanceOf(address(this)));

268:         uint256 quoteBalance = _QUOTE_TOKEN_.balanceOf(address(this));

285:         _setReserve(_BASE_TOKEN_.balanceOf(address(this)), quoteBalance);

298:         uint256 baseBalance = _BASE_TOKEN_.balanceOf(address(this));
299:         uint256 quoteBalance = _QUOTE_TOKEN_.balanceOf(address(this));

361:         uint256 baseBalance = _BASE_TOKEN_.balanceOf(address(this));
362:         uint256 quoteBalance = _QUOTE_TOKEN_.balanceOf(address(this));

427:         if (to == address(this)) {

431:         uint256 baseBalance = _BASE_TOKEN_.balanceOf(address(this));
432:         uint256 quoteBalance = _QUOTE_TOKEN_.balanceOf(address(this));

505:         uint256 baseBalance = _BASE_TOKEN_.balanceOf(address(this));
506:         uint256 quoteBalance = _QUOTE_TOKEN_.balanceOf(address(this));

529:         baseBalance = _BASE_TOKEN_.balanceOf(address(this));
530:         quoteBalance = _QUOTE_TOKEN_.balanceOf(address(this));

568:         uint256 baseBalance = _BASE_TOKEN_.balanceOf(address(this));
569:         uint256 quoteBalance = _QUOTE_TOKEN_.balanceOf(address(this));

615:         if (address(this) == address(implementation)) {

622:         if (address(this) != address(implementation)) {

229, 233, 245, 262, 268, 285, 298, 361, 427, 431, 505, 529, 568, 615, 622

📁 File: src/mimswap/periphery/Factory.sol

77:                 address(this)

77

📁 File: src/mimswap/periphery/Router.sol

92:         address(weth).safeTransferFrom(address(this), clone, msg.value);

269:         lp.safeTransferFrom(msg.sender, address(this), sharesIn);

282:         lp.safeTransferFrom(msg.sender, address(this), sharesIn);

289:                 address(this),

298:                 address(this),

398:         amountOut = _swap(address(this), path, directions, minimumOut);

444:         amountOut = _sellBase(lp, address(this), minimumOut);

490:         amountOut = _sellQuote(lp, address(this), minimumOut);

92, 269, 282, 289, 298, 398, 444, 490

📁 File: src/staking/LockingMultiRewards.sol

326:         if (tokenAddress == stakingToken && tokenAmount > stakingToken.balanceOf(address(this)) - stakingTokenBalance) {

367:         rewardToken.safeTransferFrom(msg.sender, address(this), amount);

464:         stakingToken.safeTransferFrom(msg.sender, address(this), amount);

326, 367, 464

</details>

[GAS‑12] Consider using OpenZeppelin's EnumerateSet instead of nested mappings

Nested mappings and multi-dimensional arrays in Solidity operate through a process of double hashing, wherein the original storage slot and the first key are concatenated and hashed, and then this hash is again concatenated with the second key and hashed. This process can be quite gas expensive due to the double-hashing operation and subsequent storage operation (sstore).

A possible optimization involves manually concatenating the keys followed by a single hash operation and an sstore. However, this technique introduces the risk of storage collision, especially when there are other nested hash maps in the contract that use the same key types. Because Solidity is unaware of the number and structure of nested hash maps in a contract, it follows a conservative approach in computing the storage slot to avoid possible collisions.

OpenZeppelin's EnumerableSet provides a potential solution to this problem. It creates a data structure that combines the benefits of set operations with the ability to enumerate stored elements, which is not natively available in Solidity. EnumerableSet handles the element uniqueness internally and can therefore provide a more gas-efficient and collision-resistant alternative to nested mappings or multi-dimensional arrays in certain scenarios.

Gas saved per Instance: ~1,000 (Total: ~4,000)

<details> <summary><i>There are 4 instances of this issue:</i></summary>
📁 File: src/blast/BlastOnboarding.sol

41:     mapping(address user => mapping(address token => Balances)) public balances;

41

📁 File: src/mimswap/periphery/Factory.sol

35:     mapping(address base => mapping(address quote => address[] pools)) public pools;

35

📁 File: src/staking/LockingMultiRewards.sol

94:     mapping(address user => mapping(address token => uint256 amount)) public userRewardPerTokenPaid;
95:     mapping(address user => mapping(address token => uint256 amount)) public rewards;

94

</details>

[GAS‑13] Consider using Solady's gas optimized lib for Math

Utilizing gas-optimized math functions from libraries like Solady can lead to more efficient smart contracts. This is particularly beneficial in contracts where these operations are frequently used.

<details> <summary><i>There are 39 instances of this issue:</i></summary>
📁 File: src/blast/BlastOnboardingBoot.sol

163:         return (userLocked * totalPoolShares) / totalLocked;

163

📁 File: src/mimswap/MagicLP.sol

125:         _BLOCK_TIMESTAMP_LAST_ = uint32(block.timestamp % 2 ** 32);

435:         baseAmount = (baseBalance * shareAmount) / totalShares;
436:         quoteAmount = (quoteBalance * shareAmount) / totalShares;

513:             _BASE_TARGET_ = uint112((uint256(_BASE_TARGET_) * baseBalance) / uint256(_BASE_RESERVE_));

517:             _QUOTE_TARGET_ = uint112((uint256(_QUOTE_TARGET_) * quoteBalance) / uint256(_QUOTE_RESERVE_));

546:         uint32 blockTimestamp = uint32(block.timestamp % 2 ** 32);

125, 435, 513, 517, 546

📁 File: src/mimswap/libraries/DecimalMath.sol

24:         return (target * d) / ONE;

32:         return (target * ONE) / d;

54:             p = (p * p) / ONE;

56:                 p = (p * target) / ONE;

24, 32, 54, 56

📁 File: src/mimswap/libraries/Math.sol

21:         uint256 remainder = a - quotient * b;

30:         uint256 z = x / 2 + 1;

34:             z = (x / z + z) / 2;

56:         uint256 fairAmount = i * (V1 - V2); // i*delta

62:         uint256 V0V0V1V2 = DecimalMath.divFloor((V0 * V0) / V1, V2);

64:         return (((DecimalMath.ONE - k) + penalty) * fairAmount) / DecimalMath.ONE2;

93:         uint256 ki = (4 * k) * i;

96:         } else if ((ki * delta) / ki == delta) {
97:             _sqrt = sqrt(((ki * delta) / V1) + DecimalMath.ONE2);

99:             _sqrt = sqrt(((ki / V1) * delta) + DecimalMath.ONE2);

101:         uint256 premium = DecimalMath.divFloor(_sqrt - DecimalMath.ONE, k * 2) + DecimalMath.ONE;

155:             } else if ((idelta * V1) / idelta == V1) {
156:                 temp = (idelta * V1) / (V0 * V0);

158:                 temp = (((delta * V1) / V0) * i) / V0;

160:             return (V1 * temp) / (temp + DecimalMath.ONE);

170:         uint256 part2 = (((k * V0) / V1) * V0) + (i * delta); // kQ0^2/Q1-i*deltaB
171:         uint256 bAbs = (DecimalMath.ONE - k) * V1; // (1-k)Q1

184:         uint256 squareRoot = DecimalMath.mulFloor((DecimalMath.ONE - k) * 4, DecimalMath.mulFloor(k, V0) * V0); // 4(1-k)kQ0^2
185:         squareRoot = sqrt((bAbs * bAbs) + squareRoot); // sqrt(b*b+4(1-k)kQ0*Q0)

188:         uint256 denominator = (DecimalMath.ONE - k) * 2; // 2(1-k)

21, 30, 34, 56, 62, 64, 93, 96, 99, 101, 155, 158, 160, 170, 184, 188

📁 File: src/mimswap/periphery/Router.sol

257:         baseAmountOut = (baseBalance * sharesIn) / totalShares;
258:         quoteAmountOut = (quoteBalance * sharesIn) / totalShares;

379:         if ((directions >> lastLpIndex) & 1 == 0) {

257, 379

📁 File: src/staking/LockingMultiRewards.sol

236:         return unlockedSupply + ((lockedSupply * lockingBoostMultiplerInBips) / BIPS);

241:         return bal.unlocked + ((bal.locked * lockingBoostMultiplerInBips) / BIPS);

258:         return (block.timestamp / rewardsDuration) * rewardsDuration;

283:         uint256 pendingRewardsPerToken = (timeElapsed * _rewardData[rewardToken].rewardRate * 1e18) / totalSupply_;

294:         return ((balance_ * pendingUserRewardsPerToken) / 1e18) + rewards[user][rewardToken];

236, 241, 258, 283, 294

</details>

[GAS‑14] Constructors can be marked payable

Payable functions cost less gas to execute, since the compiler does not have to add extra checks to ensure that a payment wasn't provided. A constructor can safely be marked as payable, since only the deployer would be able to pass funds, and the project itself would not pass any funds.

Gas saved per Instance: ~21 (Total: ~336)

<details> <summary><i>There are 16 instances of this issue:</i></summary>
📁 File: src/blast/BlastBox.sol

24:     constructor(IERC20 weth_, BlastTokenRegistry registry_, address feeTo_) DegenBox(weth_) {

24

📁 File: src/blast/BlastDapp.sol

7:     constructor() {

7

📁 File: src/blast/BlastGovernor.sol

15:     constructor(address feeTo_, address _owner) OperatableV2(_owner) {

15

📁 File: src/blast/BlastMagicLP.sol

23:     constructor(BlastTokenRegistry registry_, address feeTo_, address owner_) MagicLP(owner_) {

23

📁 File: src/blast/BlastOnboarding.sol

58:     constructor() Owned(msg.sender) {

84:     constructor(BlastTokenRegistry registry_, address feeTo_) {

58, 84

📁 File: src/blast/BlastTokenRegistry.sol

12:     constructor(address _owner) Owned(_owner) {}

12

📁 File: src/blast/BlastWrappers.sol

20:     constructor(IWETH weth_, IFactory factory, address governor_) Router(weth_, factory) {

29:     constructor(

47:     constructor(address box_, address mim_, address governor_) CauldronV4(IBentoBoxV1(box_), IERC20(mim_)) {

20, 29, 47

📁 File: src/mimswap/MagicLP.sol

84:     constructor(address owner_) Owned(owner_) {

84

📁 File: src/mimswap/auxiliary/FeeRateModel.sol

22:     constructor(address maintainer_, address owner_) Owned(owner_) {

22

📁 File: src/mimswap/periphery/Factory.sol

38:     constructor(address implementation_, IFeeRateModel maintainerFeeRateModel_, address owner_) Owned(owner_) {

38

📁 File: src/mimswap/periphery/Router.sol

38:     constructor(IWETH weth_, IFactory factory_) {

38

📁 File: src/oracles/aggregators/MagicLpAggregator.sol

21:     constructor(IMagicLP pair_, IAggregator baseOracle_, IAggregator quoteOracle_) {

21

📁 File: src/staking/LockingMultiRewards.sol

112:     constructor(

112

</details>

[GAS‑15] Counting down in for statements is more gas efficient

Counting down is more gas efficient than counting up because neither we are making zero variable to non-zero variable and also we will get gas refund in the last transaction when making non-zero to zero variable. More info

Gas saved per Instance: ~16 (Total: ~128)

<details> <summary><i>There are 8 instances of this issue:</i></summary>
📁 File: src/blast/BlastOnboarding.sol

165:         for (uint256 i = 0; i < tokens.length; i++) {

165

📁 File: src/mimswap/periphery/Router.sol

/// @audit line 556
544:         for (uint256 i = 0; i < iterations; ) {
545:             if (directions & 1 == 0) {
546:                 // Sell base
547:                 IMagicLP(path[i]).sellBase(address(path[i + 1]));
548:             } else {
549:                 // Sell quote
550:                 IMagicLP(path[i]).sellQuote(address(path[i + 1]));
551:             }
552:
553:             directions >>= 1;
554:
555:             unchecked {
556:                 ++i;
557:             }
558:         }

544

📁 File: src/staking/LockingMultiRewards.sol

/// @audit line 450
405:         for (uint256 i; i < users.length; ) {
406:             address user = users[i];
407:             Balances storage bal = _balances[user];
408:             LockedBalance[] storage locks = _userLocks[user];
409:
410:             if (locks.length == 0) {
411:                 revert ErrNoLocks();
412:             }
413:
414:             uint256 index = lockIndexes[i];
415:
416:             // Prevents processing `lastLockIndex` out of order
417:             if (index == lastLockIndex[user] && locks.length > 1) {
418:                 revert ErrInvalidLockIndex();
419:             }
420:
421:             // prohibit releasing non-expired locks
422:             if (locks[index].unlockTime > block.timestamp) {
423:                 revert ErrLockNotExpired();
424:             }
425:
426:             uint256 amount = locks[index].amount;
427:             uint256 lastIndex = locks.length - 1;
428:
429:             /// Last lock index changed place with the one we just swapped.
430:             if (lastLockIndex[user] == lastIndex) {
431:                 lastLockIndex[user] = index;
432:             }
433:
434:             if (index != lastIndex) {
435:                 locks[index] = locks[lastIndex];
436:                 emit LogLockIndexChanged(user, lastIndex, index);
437:             }
438:
439:             locks.pop();
440:
441:             unlockedSupply += amount;
442:             lockedSupply -= amount;
443:
444:             bal.unlocked += amount;
445:             bal.locked -= amount;
446:
447:             emit LogUnlocked(user, amount, index);
448:
449:             unchecked {
450:                 ++i;
451:             }
452:         }

/// @audit line 550
547:         for (uint256 i; i < rewardTokens.length; ) {
548:             _updateRewardsGlobal(rewardTokens[i], totalSupply_);
549:             unchecked {
550:                 ++i;
551:             }
552:         }

/// @audit line 566
561:         for (uint256 i; i < rewardTokens.length; ) {
562:             address token = rewardTokens[i];
563:             _udpateUserRewards(user, balance, token, _updateRewardsGlobal(token, totalSupply_));
564:
565:             unchecked {
566:                 ++i;
567:             }
568:         }

/// @audit line 590
575:         for (uint256 i; i < rewardTokens.length; ) {
576:             address token = rewardTokens[i];
577:             uint256 rewardPerToken_ = _updateRewardsGlobal(token, totalSupply_);
578:
579:             // Record each user's rewards
/// @audit line 585
580:             for (uint256 j; j < users.length; ) {
581:                 address user = users[j];
582:                 _udpateUserRewards(user, balanceOf(user), token, rewardPerToken_);
583:
584:                 unchecked {
585:                     ++j;
586:                 }
587:             }
588:
589:             unchecked {
590:                 ++i;
591:             }
592:         }

/// @audit line 654
614:         for (uint256 i; i < rewardTokens.length; ) {
615:             address rewardToken = rewardTokens[i];
616:             uint256 rewardAmount = rewards[user][rewardToken];
617:
618:             // in all scenario, reset the reward amount immediately
619:             rewards[user][rewardToken] = 0;
620:
621:             // don't assume the rewardTokens array is always the same length as the items array
622:             // as new reward tokens can be added by the owner
623:             if (i < rewardItemLength) {
624:                 RewardLockItem storage item = _rewardLock.items[i];
625:
626:                 // expired lock, claim existing unlocked rewards if any
627:                 if (expired) {
628:                     uint256 amount = item.amount;
629:
630:                     // since this current lock is expired and that item index
631:                     // matches the reward index, override the current amount
632:                     // with the new locked amount.
633:                     item.amount = rewardAmount;
634:
635:                     // use cached amount
636:                     if (amount > 0) {
637:                         rewardToken.safeTransfer(user, amount);
638:                         emit LogRewardPaid(user, rewardToken, amount);
639:                     }
640:                 } else {
641:                     // not expired, just add to the existing lock
642:                     item.amount += rewardAmount;
643:                 }
644:             }
645:             // new reward token, create a new lock item
646:             // could mean it's adding to an existing lock or creating a new one
647:             else {
648:                 _userRewardLock[user].items.push(RewardLockItem({token: rewardToken, amount: rewardAmount}));
649:             }
650:
651:             emit LogRewardLocked(user, rewardToken, rewardAmount);
652:
653:             unchecked {
654:                 ++i;
655:             }
656:         }

405, 547, 561, 575, 614

</details>

[GAS‑16] Declare variables outside of loops

Variables should be declared outside of loops, and get overriden with each iteration of loop, By doing so we save gas cost for memory variable declaration in each iteration.

Gas saved per Instance: ~15 (Total: ~225)

<i>There are 15 instaces of this issue:</i>

📁 File: src/staking/LockingMultiRewards.sol

/// @audit loop from line 405 to line 452
406:             address user = users[i];
/// @audit loop from line 405 to line 452
407:             Balances storage bal = _balances[user];
/// @audit loop from line 405 to line 452
408:             LockedBalance[] storage locks = _userLocks[user];

/// @audit loop from line 405 to line 452
414:             uint256 index = lockIndexes[i];

/// @audit loop from line 405 to line 452
426:             uint256 amount = locks[index].amount;
/// @audit loop from line 405 to line 452
427:             uint256 lastIndex = locks.length - 1;

/// @audit loop from line 561 to line 568
562:             address token = rewardTokens[i];

/// @audit loop from line 575 to line 592
576:             address token = rewardTokens[i];
/// @audit loop from line 575 to line 592
577:             uint256 rewardPerToken_ = _updateRewardsGlobal(token, totalSupply_);

/// @audit loop from line 575 to line 592
/// @audit loop from line 580 to line 587
581:                 address user = users[j];

/// @audit loop from line 614 to line 656
615:             address rewardToken = rewardTokens[i];
/// @audit loop from line 614 to line 656
616:             uint256 rewardAmount = rewards[user][rewardToken];

/// @audit loop from line 614 to line 656
624:                 RewardLockItem storage item = _rewardLock.items[i];

/// @audit loop from line 614 to line 656
628:                     uint256 amount = item.amount;

406, 414, 426, 562, 576, 581, 615, 624, 628


[GAS‑17] Do not calculate constants

Due to how constant variables are implemented (replacements at compile-time), an expression assigned to a constant variable is recomputed each time that the variable is used, which wastes some gas.

<details> <summary><i>There are 4 instances of this issue:</i></summary>
📁 File: src/mimswap/MagicLP.sol

63:     uint256 public constant MAX_I = 10 ** 36;
64:     uint256 public constant MAX_K = 10 ** 18;

63

📁 File: src/mimswap/libraries/DecimalMath.sol

20:     uint256 internal constant ONE = 10 ** 18;
21:     uint256 internal constant ONE2 = 10 ** 36;

20

</details>

[GAS‑18] do-while is cheaper than for-loops when the initial check can be skipped

Using do-while loops instead of for loops can be more gas-efficient. Even if you add an if condition to account for the case where the loop doesn't execute at all, a do-while loop can still be cheaper in terms of gas.

Gas saved per Instance: ~255 (Total: ~2,040)

<details> <summary><i>There are 8 instances of this issue:</i></summary>
📁 File: src/blast/BlastOnboarding.sol

165:         for (uint256 i = 0; i < tokens.length; i++) {

165

📁 File: src/mimswap/periphery/Router.sol

544:         for (uint256 i = 0; i < iterations; ) {

544

📁 File: src/staking/LockingMultiRewards.sol

405:         for (uint256 i; i < users.length; ) {

547:         for (uint256 i; i < rewardTokens.length; ) {

561:         for (uint256 i; i < rewardTokens.length; ) {

575:         for (uint256 i; i < rewardTokens.length; ) {

580:             for (uint256 j; j < users.length; ) {

614:         for (uint256 i; i < rewardTokens.length; ) {

405, 547, 561, 575, 580, 614

</details>

[GAS‑19] Don't transfer with zero amount to save gas

In Solidity, unnecessary operations can waste gas. For example, a transfer function without a zero amount check uses gas even if called with a zero amount, since the contract state remains unchanged. Implementing a zero amount check avoids these unnecessary function calls, saving gas and improving efficiency.

Gas saved per Instance: ~20 (Total: ~520)

<details> <summary><i>There are 26 instances of this issue:</i></summary>
📁 File: src/blast/BlastOnboarding.sol

/// @audit check for zero amount on the 'amount' variable
102:         token.safeTransferFrom(msg.sender, address(this), amount);

/// @audit check for zero amount on the 'amount' variable
138:         token.safeTransfer(msg.sender, amount);

/// @audit check for zero amount on the 'amount' variable
210:         token.safeTransfer(to, amount);

102, 138, 210

📁 File: src/mimswap/MagicLP.sol

/// @audit check for zero amount on the 'amount' variable
466:         token.safeTransfer(to, amount);

466

📁 File: src/mimswap/periphery/Router.sol

/// @audit check for zero amount on the 'baseInAmount' variable
68:         baseToken.safeTransferFrom(msg.sender, clone, baseInAmount);
/// @audit check for zero amount on the 'quoteInAmount' variable
69:         quoteToken.safeTransferFrom(msg.sender, clone, quoteInAmount);

/// @audit check for zero amount on the 'tokenInAmount' variable
91:         token.safeTransferFrom(msg.sender, clone, tokenInAmount);

/// @audit check for zero amount on the 'baseAdjustedInAmount' variable
172:         IMagicLP(lp)._BASE_TOKEN_().safeTransferFrom(msg.sender, lp, baseAdjustedInAmount);
/// @audit check for zero amount on the 'quoteAdjustedInAmount' variable
173:         IMagicLP(lp)._QUOTE_TOKEN_().safeTransferFrom(msg.sender, lp, quoteAdjustedInAmount);

/// @audit check for zero amount on the 'baseInAmount' variable
186:         IMagicLP(lp)._BASE_TOKEN_().safeTransferFrom(msg.sender, lp, baseInAmount);
/// @audit check for zero amount on the 'quoteInAmount' variable
187:         IMagicLP(lp)._QUOTE_TOKEN_().safeTransferFrom(msg.sender, lp, quoteInAmount);

/// @audit check for zero amount on the 'wethAdjustedAmount' variable
217:         address(weth).safeTransfer(lp, wethAdjustedAmount);

/// @audit check for zero amount on the 'tokenAdjustedAmount' variable
224:         token.safeTransferFrom(msg.sender, lp, tokenAdjustedAmount);

/// @audit check for zero amount on the 'tokenInAmount' variable
246:         token.safeTransferFrom(msg.sender, lp, tokenInAmount);

/// @audit check for zero amount on the 'sharesIn' variable
269:         lp.safeTransferFrom(msg.sender, address(this), sharesIn);

/// @audit check for zero amount on the 'sharesIn' variable
282:         lp.safeTransferFrom(msg.sender, address(this), sharesIn);

/// @audit check for zero amount on the 'tokenAmountOut' variable
311:         token.safeTransfer(to, tokenAmountOut);

/// @audit check for zero amount on the 'amountIn' variable
328:             IMagicLP(firstLp)._BASE_TOKEN_().safeTransferFrom(msg.sender, address(firstLp), amountIn);

/// @audit check for zero amount on the 'amountIn' variable
330:             IMagicLP(firstLp)._QUOTE_TOKEN_().safeTransferFrom(msg.sender, address(firstLp), amountIn);

/// @audit check for zero amount on the 'amountIn' variable
393:             IMagicLP(firstLp)._BASE_TOKEN_().safeTransferFrom(msg.sender, firstLp, amountIn);

/// @audit check for zero amount on the 'amountIn' variable
395:             IMagicLP(firstLp)._QUOTE_TOKEN_().safeTransferFrom(msg.sender, firstLp, amountIn);

/// @audit check for zero amount on the 'amountIn' variable
411:         IMagicLP(lp)._BASE_TOKEN_().safeTransferFrom(msg.sender, lp, amountIn);

/// @audit check for zero amount on the 'amountIn' variable
443:         IMagicLP(lp)._BASE_TOKEN_().safeTransferFrom(msg.sender, lp, amountIn);

/// @audit check for zero amount on the 'amountIn' variable
456:         IMagicLP(lp)._QUOTE_TOKEN_().safeTransferFrom(msg.sender, lp, amountIn);

/// @audit check for zero amount on the 'amountIn' variable
489:         IMagicLP(lp)._QUOTE_TOKEN_().safeTransferFrom(msg.sender, lp, amountIn);

68, 91, 172, 186, 217, 224, 246, 269, 282, 311, 328, 330, 393, 395, 411, 443, 456, 489

📁 File: src/staking/LockingMultiRewards.sol

/// @audit check for zero amount on the 'amount' variable
367:         rewardToken.safeTransferFrom(msg.sender, address(this), amount);

367

</details>

[GAS‑20] Emitting storage values instead of the memory one.

Emitted values should not be read from storage again. Instead, the existing values from memory should be used.

Gas saved per Instance: ~100 (Total: ~800)

<details> <summary><i>There are 8 instances of this issue:</i></summary>
📁 File: src/blast/BlastOnboardingBoot.sol

/// @audit pool, staking, totalPoolShares
124:         emit LogLiquidityBootstrapped(pool, address(staking), totalPoolShares);

/// @audit ready
148:         emit LogReadyChanged(ready);

124, 148

📁 File: src/mimswap/MagicLP.sol

/// @audit _BASE_TOKEN_, _QUOTE_TOKEN_
264:         emit Swap(address(_BASE_TOKEN_), address(_QUOTE_TOKEN_), baseInput, receiveQuoteAmount, msg.sender, to);

/// @audit _QUOTE_TOKEN_, _BASE_TOKEN_
287:         emit Swap(address(_QUOTE_TOKEN_), address(_BASE_TOKEN_), quoteInput, receiveBaseAmount, msg.sender, to);

/// @audit _QUOTE_TOKEN_, _BASE_TOKEN_
325:             emit Swap(address(_QUOTE_TOKEN_), address(_BASE_TOKEN_), quoteInput, receiveBaseAmount, msg.sender, assetTo);

/// @audit _BASE_TOKEN_, _QUOTE_TOKEN_
347:             emit Swap(address(_BASE_TOKEN_), address(_QUOTE_TOKEN_), baseInput, receiveQuoteAmount, msg.sender, assetTo);

264, 287, 325, 347

📁 File: src/mimswap/periphery/Factory.sol

/// @audit maintainerFeeRateModel
88:         emit LogCreated(clone, baseToken_, quoteToken_, creator, lpFeeRate_, maintainerFeeRateModel, i_, k_);

88

📁 File: src/staking/LockingMultiRewards.sol

/// @audit minLockAmount
318:         emit LogSetMinLockAmount(minLockAmount, _minLockAmount);

318

</details>

[GAS‑21] Empty blocks should be removed or emit something

Some functions don't have a body: consider commenting why, or add some logic. Otherwise, refactor the code and remove these functions.

<details> <summary><i>There are 3 instances of this issue:</i></summary>
📁 File: src/blast/BlastGovernor.sol

13:     receive() external payable {}

13

📁 File: src/mimswap/MagicLP.sol

601:     function _afterInitialized() internal virtual {}

601

📁 File: src/mimswap/periphery/Router.sol

36:     receive() external payable {}

36

</details>

[GAS‑22] Function names can be optimized

Function that are public/external and public state variable names can be optimized to save gas.

Method IDs that have two leading zero bytes can save 128 gas each during deployment, and renaming functions to have lower method IDs will save 22 gas per call, per sorted position shifted. Reference

Gas saved per Instance: ~128 (Total: ~1,408)

<details> <summary><i>There are 11 instances of this issue:</i></summary>
📁 File: src/blast/BlastBox.sol

/// @audit optimized order: _onBeforeDeposit(), setTokenEnabled(), setFeeTo(), callBlastPrecompile(), claimTokenYields(), claimGasYields(), _configure(), isOwner()
13: contract BlastBox is DegenBox, OperatableV3 {

13

📁 File: src/blast/BlastGovernor.sol

/// @audit optimized order: setFeeTo(), claimNativeYields(), callBlastPrecompile(), execute(), claimMaxGasYields()
7: contract BlastGovernor is OperatableV2 {

7

📁 File: src/blast/BlastMagicLP.sol

/// @audit optimized order: setFeeTo(), callBlastPrecompile(), updateTokenClaimables(), setOperator(), version(), claimTokenYields(), claimGasYields(), _afterInitialized(), _updateTokenClaimables()
10: contract BlastMagicLP is MagicLP {

10

📁 File: src/blast/BlastOnboarding.sol

/// @audit optimized order: claimTokenYields(), open(), setFeeTo(), withdraw(), callBlastPrecompile(), setTokenSupported(), pause(), setCap(), setBootstrapper(), close(), unpause(), deposit(), claimGasYields(), lock(), rescue(), _implementation()
64: contract BlastOnboarding is BlastOnboardingData, Proxy {

64

📁 File: src/blast/BlastOnboardingBoot.sol

/// @audit optimized order: previewTotalPoolShares(), setReady(), initialize(), setStaking(), claimable(), bootstrap(), claim(), _claimable()
34: contract BlastOnboardingBoot is BlastOnboardingBootDataV1 {

34

📁 File: src/mimswap/MagicLP.sol

/// @audit optimized order: sync(), getPMMStateForCall(), getMidPrice(), sellQuote(), flashLoan(), ratioSync(), sellBase(), sellShares(), init(), getPMMState(), symbol(), querySellBase(), correctRState(), getQuoteInput(), querySellQuote(), getBaseInput(), version(), buyShares(), getUserFeeRate(), decimals(), setParameters(), rescue(), getReserves(), name(), _resetTargetAndReserve(), _twapUpdate(), _setReserve(), _sync(), _transferBaseOut(), _transferQuoteOut(), _mint(), _afterInitialized()
25: contract MagicLP is ERC20, ReentrancyGuard, Owned {

25

📁 File: src/mimswap/auxiliary/FeeRateModel.sol

/// @audit optimized order: getFeeRate(), setImplementation(), setMaintainer()
13: contract FeeRateModel is Owned {

13

📁 File: src/mimswap/periphery/Factory.sol

/// @audit optimized order: getPoolCount(), create(), removePool(), getUserPoolCount(), setLpImplementation(), addPool(), predictDeterministicAddress(), setMaintainerFeeRateModel(), _addPool(), _computeSalt()
11: contract Factory is Owned {

11

📁 File: src/mimswap/periphery/Router.sol

/// @audit optimized order: removeLiquidityETH(), swapTokensForTokens(), addLiquidityETH(), sellQuoteETHForTokens(), sellBaseTokensForTokens(), swapTokensForETH(), createPool(), addLiquidityUnsafe(), swapETHForTokens(), addLiquidityETHUnsafe(), createPoolETH(), previewRemoveLiquidity(), sellBaseTokensForETH(), previewAddLiquidity(), removeLiquidity(), previewCreatePool(), sellQuoteTokensForTokens(), sellBaseETHForTokens(), addLiquidity(), sellQuoteTokensForETH(), _addLiquidity(), _adjustAddLiquidity(), _swap(), _sellBase(), _sellQuote(), _validatePath(), _validateDecimals()
12: contract Router {

12

📁 File: src/oracles/aggregators/MagicLpAggregator.sol

/// @audit optimized order: decimals(), _getReserves(), latestRoundData(), latestAnswer()
9: contract MagicLpAggregator is IAggregator {

9

📁 File: src/staking/LockingMultiRewards.sol

/// @audit optimized order: nextUnlockTime(), rewardPerToken(), lock(), remainingEpochTime(), stakeFor(), unlocked(), locked(), userLocksLength(), rewardTokensLength(), nextEpoch(), stake(), userRewardLock(), notifyRewardAmount(), withdrawWithRewards(), processExpiredLocks(), addReward(), epoch(), pause(), balanceOf(), lastTimeRewardApplicable(), recover(), _rewardPerToken(), rewardData(), unpause(), withdraw(), balances(), earned(), totalSupply(), setMinLockAmount(), rewardsForDuration(), getRewards(), userLocks(), _earned(), _stakeFor(), _createLock(), _updateRewardsGlobal(), _udpateUserRewards(), _updateRewards(), _updateRewardsForUser(), _updateRewardsForUsers(), _getRewards()
14: contract LockingMultiRewards is OperatableV2, Pausable {

14

</details>

[GAS‑23] Functions not used internally could be marked external to save gas

Before Solidity version 0.6.9, external functions are cheaper than public ones

<details> <summary><i>There are 15 instances of this issue:</i></summary>
📁 File: src/blast/BlastWrappers.sol

59:     function init(bytes calldata data) public payable override {

59

📁 File: src/mimswap/MagicLP.sol

155:     function name() public view override returns (string memory) {

159:     function symbol() public pure override returns (string memory) {

163:     function decimals() public view override returns (uint8) {

228:     function getBaseInput() public view returns (uint256 input) {

232:     function getQuoteInput() public view returns (uint256 input) {

470:     function setParameters(
471:         address assetTo,
472:         uint256 newLpFeeRate,
473:         uint256 newI,
474:         uint256 newK,
475:         uint256 baseOutAmount,
476:         uint256 quoteOutAmount,
477:         uint256 minBaseReserve,
478:         uint256 minQuoteReserve
479:     ) public nonReentrant onlyImplementationOwner {

155, 159, 163, 228, 232, 470

📁 File: src/mimswap/auxiliary/FeeRateModel.sol

57:     function setImplementation(address implementation_) public onlyOwner {

57

📁 File: src/mimswap/periphery/Factory.sol

65:     function predictDeterministicAddress(
66:         address creator,
67:         address baseToken_,
68:         address quoteToken_,
69:         uint256 lpFeeRate_,
70:         uint256 i_,
71:         uint256 k_
72:     ) public view returns (address) {

65

📁 File: src/staking/LockingMultiRewards.sol

150:     function stake(uint256 amount, bool lock_) public whenNotPaused {

155:     function lock(uint256 amount) public whenNotPaused {

265:     function remainingEpochTime() public view returns (uint256) {

288:     function earned(address user, address rewardToken) public view returns (uint256) {

300:     function addReward(address rewardToken) public onlyOwner {

361:     function notifyRewardAmount(address rewardToken, uint256 amount, uint minRemainingTime) public onlyOperators {

150, 155, 265, 288, 300, 361

</details>

[GAS‑24] Initializers can be marked payable

Gas saved per Instance: ~21 (Total: ~63)

<details> <summary><i>There are 3 instances of this issue:</i></summary>
📁 File: src/blast/BlastOnboardingBoot.sol

129:     function initialize(Router _router) external onlyOwner {

129

📁 File: src/mimswap/MagicLP.sol

91:     function init(
92:         address baseTokenAddress,
93:         address quoteTokenAddress,
94:         uint256 lpFeeRate,
95:         address mtFeeRateModel,
96:         uint256 i,
97:         uint256 k
98:     ) external {

91

</details>

[GAS‑25] internal functions only used once can be inlined so save gas

If a internal function is only used once it doesn't make sense to modularise it unless the function which does call the function would be overly long and complex otherwise

Gas saved per Instance: ~30 (Total: ~90)

<details> <summary><i>There are 3 instances of this issue:</i></summary>
📁 File: src/mimswap/MagicLP.sol

528:     function _resetTargetAndReserve() internal returns (uint256 baseBalance, uint256 quoteBalance) {

528

📁 File: src/staking/LockingMultiRewards.sol

544:     function _updateRewards() internal {

572:     function _updateRewardsForUsers(address[] memory users) internal {

544, 572

</details>

[GAS‑26] Low-level call can be optimized with assembly

returnData is copied to memory even if the variable is not utilized: the proper way to handle this is through a low level assembly call and save 159 gas.

 // before (bool success,) = payable(receiver).call{gas: gas, value: value}("");
//after bool success; assembly { success := call(gas, receiver, value, 0, 0, 0, 0) }

Gas saved per Instance: ~159

<i>There is one instance of this issue:</i>

📁 File: src/blast/BlastGovernor.sol

54:         (success, result) = to.call{value: value}(data);

54


[GAS‑27] Mappings are cheaper to use than storage arrays

When using storage arrays, solidity adds an internal lookup of the array's length (a Gcoldsload 2100 gas) to ensure you don't read past the array's end. You can avoid this lookup by using a mapping and storing the number of entries in a separate storage variable. In cases where you have sentinel values (e.g. 'zero' means invalid), you can avoid length checks

Gas saved per Instance: ~2,100 (Total: ~10,500)

<details> <summary><i>There are 5 instances of this issue:</i></summary>
📁 File: src/mimswap/periphery/Factory.sol

35:     mapping(address base => mapping(address quote => address[] pools)) public pools;
36:     mapping(address creator => address[] pools) public userPools;

35

📁 File: src/staking/LockingMultiRewards.sol

74:         RewardLockItem[] items;

91:     mapping(address user => LockedBalance[] locks) private _userLocks;

98:     address[] public rewardTokens;

74, 91, 98

</details>

[GAS‑28] Merge events to save gas

Consolidating multiple event emissions into a single event in Solidity can result in significant gas savings. Each event emission in Ethereum involves a gas cost, specifically for the topics logged with the event. By merging sequential events into a singular event, you can save on the Glogtopic cost, which is incurred for each topic of each event. This approach can save around 375 gas per additional topic. This strategy is particularly beneficial in functions where multiple related events are emitted in sequence. However, it's crucial to balance gas optimization with the clarity and utility of the event data for off-chain consumers.

Gas saved per Instance: ~375 (Total: ~3,375)

<details> <summary><i>There are 9 instances of this issue:</i></summary>
📁 File: src/mimswap/MagicLP.sol

/// @audit 'Swap', 'RChange'
244:     function sellBase(address to) external nonReentrant returns (uint256 receiveQuoteAmount) {
245:         uint256 baseBalance = _BASE_TOKEN_.balanceOf(address(this));
246:         uint256 baseInput = baseBalance - uint256(_BASE_RESERVE_);
247:         uint256 mtFee;
248:         uint256 newBaseTarget;
249:         PMMPricing.RState newRState;
250:         (receiveQuoteAmount, mtFee, newRState, newBaseTarget) = querySellBase(tx.origin, baseInput);
251:
252:         _transferQuoteOut(to, receiveQuoteAmount);
253:         _transferQuoteOut(_MT_FEE_RATE_MODEL_.maintainer(), mtFee);
254:
255:         // update TARGET
256:         if (_RState_ != uint32(newRState)) {
257:             _BASE_TARGET_ = newBaseTarget.toUint112();
258:             _RState_ = uint32(newRState);
259:             emit RChange(newRState);
260:         }
261:
262:         _setReserve(baseBalance, _QUOTE_TOKEN_.balanceOf(address(this)));
263:
264:         emit Swap(address(_BASE_TOKEN_), address(_QUOTE_TOKEN_), baseInput, receiveQuoteAmount, msg.sender, to);
265:     }

/// @audit 'Swap', 'RChange'
267:     function sellQuote(address to) external nonReentrant returns (uint256 receiveBaseAmount) {
268:         uint256 quoteBalance = _QUOTE_TOKEN_.balanceOf(address(this));
269:         uint256 quoteInput = quoteBalance - uint256(_QUOTE_RESERVE_);
270:         uint256 mtFee;
271:         uint256 newQuoteTarget;
272:         PMMPricing.RState newRState;
273:         (receiveBaseAmount, mtFee, newRState, newQuoteTarget) = querySellQuote(tx.origin, quoteInput);
274:
275:         _transferBaseOut(to, receiveBaseAmount);
276:         _transferBaseOut(_MT_FEE_RATE_MODEL_.maintainer(), mtFee);
277:
278:         // update TARGET
279:         if (_RState_ != uint32(newRState)) {
280:             _QUOTE_TARGET_ = newQuoteTarget.toUint112();
281:             _RState_ = uint32(newRState);
282:             emit RChange(newRState);
283:         }
284:
285:         _setReserve(_BASE_TOKEN_.balanceOf(address(this)), quoteBalance);
286:
287:         emit Swap(address(_QUOTE_TOKEN_), address(_BASE_TOKEN_), quoteInput, receiveBaseAmount, msg.sender, to);
288:     }

/// @audit 'FlashLoan', 'Swap', 'Swap', 'RChange', 'RChange'
290:     function flashLoan(uint256 baseAmount, uint256 quoteAmount, address assetTo, bytes calldata data) external nonReentrant {
291:         _transferBaseOut(assetTo, baseAmount);
292:         _transferQuoteOut(assetTo, quoteAmount);
293:
294:         if (data.length > 0) {
295:             ICallee(assetTo).FlashLoanCall(msg.sender, baseAmount, quoteAmount, data);
296:         }
297:
298:         uint256 baseBalance = _BASE_TOKEN_.balanceOf(address(this));
299:         uint256 quoteBalance = _QUOTE_TOKEN_.balanceOf(address(this));
300:
301:         // no input -> pure loss
302:         if (baseBalance < _BASE_RESERVE_ && quoteBalance < _QUOTE_RESERVE_) {
303:             revert ErrFlashLoanFailed();
304:         }
305:
306:         // sell quote case
307:         // quote input + base output
/// @audit 'Swap', 'RChange'
308:         if (baseBalance < _BASE_RESERVE_) {
309:             uint256 quoteInput = quoteBalance - uint256(_QUOTE_RESERVE_);
310:             (uint256 receiveBaseAmount, uint256 mtFee, PMMPricing.RState newRState, uint256 newQuoteTarget) = querySellQuote(
311:                 tx.origin,
312:                 quoteInput
313:             );
314:
315:             if (uint256(_BASE_RESERVE_) - baseBalance > receiveBaseAmount) {
316:                 revert ErrFlashLoanFailed();
317:             }
318:
319:             _transferBaseOut(_MT_FEE_RATE_MODEL_.maintainer(), mtFee);
320:             if (_RState_ != uint32(newRState)) {
321:                 _QUOTE_TARGET_ = newQuoteTarget.toUint112();
322:                 _RState_ = uint32(newRState);
323:                 emit RChange(newRState);
324:             }
325:             emit Swap(address(_QUOTE_TOKEN_), address(_BASE_TOKEN_), quoteInput, receiveBaseAmount, msg.sender, assetTo);
326:         }
327:
328:         // sell base case
329:         // base input + quote output
/// @audit 'Swap', 'RChange'
330:         if (quoteBalance < _QUOTE_RESERVE_) {
331:             uint256 baseInput = baseBalance - uint256(_BASE_RESERVE_);
332:             (uint256 receiveQuoteAmount, uint256 mtFee, PMMPricing.RState newRState, uint256 newBaseTarget) = querySellBase(
333:                 tx.origin,
334:                 baseInput
335:             );
336:
337:             if (uint256(_QUOTE_RESERVE_) - quoteBalance > receiveQuoteAmount) {
338:                 revert ErrFlashLoanFailed();
339:             }
340:
341:             _transferQuoteOut(_MT_FEE_RATE_MODEL_.maintainer(), mtFee);
342:             if (_RState_ != uint32(newRState)) {
343:                 _BASE_TARGET_ = newBaseTarget.toUint112();
344:                 _RState_ = uint32(newRState);
345:                 emit RChange(newRState);
346:             }
347:             emit Swap(address(_BASE_TOKEN_), address(_QUOTE_TOKEN_), baseInput, receiveQuoteAmount, msg.sender, assetTo);
348:         }
349:
350:         _sync();
351:
352:         emit FlashLoan(msg.sender, assetTo, baseAmount, quoteAmount);
353:     }

244, 267, 290

📁 File: src/staking/LockingMultiRewards.sol

/// @audit 'LogUnlocked', 'LogLockIndexChanged'
397:     function processExpiredLocks(address[] memory users, uint256[] calldata lockIndexes) external onlyOperators {
398:         if (users.length != lockIndexes.length) {
399:             revert ErrLengthMismatch();
400:         }
401:
402:         _updateRewardsForUsers(users);
403:
404:         // Release all expired users' locks
/// @audit 'LogUnlocked', 'LogLockIndexChanged'
405:         for (uint256 i; i < users.length; ) {
406:             address user = users[i];
407:             Balances storage bal = _balances[user];
408:             LockedBalance[] storage locks = _userLocks[user];
409:
410:             if (locks.length == 0) {
411:                 revert ErrNoLocks();
412:             }
413:
414:             uint256 index = lockIndexes[i];
415:
416:             // Prevents processing `lastLockIndex` out of order
417:             if (index == lastLockIndex[user] && locks.length > 1) {
418:                 revert ErrInvalidLockIndex();
419:             }
420:
421:             // prohibit releasing non-expired locks
422:             if (locks[index].unlockTime > block.timestamp) {
423:                 revert ErrLockNotExpired();
424:             }
425:
426:             uint256 amount = locks[index].amount;
427:             uint256 lastIndex = locks.length - 1;
428:
429:             /// Last lock index changed place with the one we just swapped.
430:             if (lastLockIndex[user] == lastIndex) {
431:                 lastLockIndex[user] = index;
432:             }
433:
434:             if (index != lastIndex) {
435:                 locks[index] = locks[lastIndex];
436:                 emit LogLockIndexChanged(user, lastIndex, index);
437:             }
438:
439:             locks.pop();
440:
441:             unlockedSupply += amount;
442:             lockedSupply -= amount;
443:
444:             bal.unlocked += amount;
445:             bal.locked -= amount;
446:
447:             emit LogUnlocked(user, amount, index);
448:
449:             unchecked {
450:                 ++i;
451:             }
452:         }
453:     }

/// @audit 'LogRewardLockCreated', 'LogRewardLocked', 'LogRewardPaid'
597:     function _getRewards(address user) internal {
598:         RewardLock storage _rewardLock = _userRewardLock[user];
599:
600:         // first ever lock is always expired because `unlockTime` is 0
601:         // unlock time is aligned to epoch
602:         bool expired = _rewardLock.unlockTime <= block.timestamp;
603:
604:         // cache the length here since the loop will be modifying the array
605:         uint256 rewardItemLength = _rewardLock.items.length;
606:
607:         // expired lock
608:         // existing lock items will be reused
609:         if (expired) {
610:             _rewardLock.unlockTime = nextEpoch();
611:             emit LogRewardLockCreated(user, _rewardLock.unlockTime);
612:         }
613:
/// @audit 'LogRewardLocked', 'LogRewardPaid'
614:         for (uint256 i; i < rewardTokens.length; ) {
615:             address rewardToken = rewardTokens[i];
616:             uint256 rewardAmount = rewards[user][rewardToken];
617:
618:             // in all scenario, reset the reward amount immediately
619:             rewards[user][rewardToken] = 0;
620:
621:             // don't assume the rewardTokens array is always the same length as the items array
622:             // as new reward tokens can be added by the owner
623:             if (i < rewardItemLength) {
624:                 RewardLockItem storage item = _rewardLock.items[i];
625:
626:                 // expired lock, claim existing unlocked rewards if any
627:                 if (expired) {
628:                     uint256 amount = item.amount;
629:
630:                     // since this current lock is expired and that item index
631:                     // matches the reward index, override the current amount
632:                     // with the new locked amount.
633:                     item.amount = rewardAmount;
634:
635:                     // use cached amount
636:                     if (amount > 0) {
637:                         rewardToken.safeTransfer(user, amount);
638:                         emit LogRewardPaid(user, rewardToken, amount);
639:                     }
640:                 } else {
641:                     // not expired, just add to the existing lock
642:                     item.amount += rewardAmount;
643:                 }
644:             }
645:             // new reward token, create a new lock item
646:             // could mean it's adding to an existing lock or creating a new one
647:             else {
648:                 _userRewardLock[user].items.push(RewardLockItem({token: rewardToken, amount: rewardAmount}));
649:             }
650:
651:             emit LogRewardLocked(user, rewardToken, rewardAmount);
652:
653:             unchecked {
654:                 ++i;
655:             }
656:         }
657:     }

397, 597

</details>

#0 - c4-pre-sort

2024-03-15T14:58:01Z

141345 marked the issue as high quality report

#1 - 141345

2024-03-16T00:21:17Z

35 Bozho l r nc 3 1 50

G 1 i G 2 i G 3 i G 4 n G 5 n G 6 n G 7 n G 8 n G 9 n G 10 n G 11 n G 12 n G 13 n G 14 n G 15 n G 16 n G 17 n G 18 n G 19 n G 20 n G 21 n G 22 n G 23 n G 24 i G 25 i G 26 n G 27 n G 28 n G 29 n G 30 n G 31 i G 32 n G 33 n G 34 i G 35 n G 36 n G 37 i G 38 n G 39 i G 40 n G 41 n G 42 n G 43 l G 44 i G 45 r G 46 l G 47 l G 48 i G 49 n G 50 n G 51 n G 52 n G 53 n G 54 i G 55 i G 56 i G 57 i G 58 i G 59 i G 60 n G 61 n G 62 n G 63 n G 64 n G 65 n G 66 n G 67 n G 68 n G 69 i G 70 n G 71 n G 72 n G 73 i

#2 - 0xCalibur

2024-03-17T14:24:23Z

no factor

#3 - c4-sponsor

2024-03-28T16:58:05Z

0xCalibur (sponsor) disputed

#4 - c4-judge

2024-03-29T16:36:30Z

thereksfour marked the issue as selected for report

#5 - c4-judge

2024-04-06T07:47:29Z

thereksfour marked the issue as not selected for report

#6 - c4-judge

2024-04-06T08:00:37Z

thereksfour marked the issue as grade-a

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax © 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter