Frankencoin - JCN's results

A decentralized and fully collateralized stablecoin.

General Information

Platform: Code4rena

Start Date: 12/04/2023

Pot Size: $60,500 USDC

Total HM: 21

Participants: 199

Period: 7 days

Judge: hansfriese

Total Solo HM: 5

Id: 231

League: ETH

Frankencoin

Findings Distribution

Researcher Performance

Rank: 32/199

Findings: 1

Award: $273.34

Gas:
grade-a

🌟 Selected for report: 0

🚀 Solo Findings: 0

Awards

273.342 USDC - $273.34

Labels

bug
G (Gas Optimization)
grade-a
high quality report
G-28

External Links

Summary

A majority of the optimizations were benchmarked via the protocol's tests, i.e. using the following config: solc version 0.8.13, optimizer on, and 200 runs. Optimizations that were not benchmarked are explained via EVM gas costs and opcodes.

Below are the overall average gas savings for the following tested functions, with all the optimizations applied:

FunctionBeforeAfterAvg Gas Savings
Equity.transfer8566285527135
Frankencoin.mint117324116933391
Frankencoin.transfer4035940224135
MintingHub.bid99969970842885
MintingHub.end1283031217526551
MintingHub.launchChallenge211011210751260
Position.repay87902827945108
Position.withdrawCollateral73058690713987
StablecoinBridge.mint9821997904315
TestToken.transfer5174951614135

Total gas saved across all listed functions: 19902

Notes:

  • The Gas report output, after all optimizations have been applied, can be found at the end of the report.
  • The final diffs for each contract, with all the optimizations applied, can be found here.
  • Some code snippets may be truncated to save space. Code snippets may also be accompanied by @audit tags in comments to aid in explaining the issue.

Gas Optimizations

NumberIssueInstances
G-01A mapping is more efficient than an array-
G-02State variables can be cached instead of re-reading them from storage34
G-03Refactor code to avoid re-reading from storage4
G-04Cache return value from external call to avoid an unnecessary CALL/STATICCALL1
G-05Refactor event to avoid emitting unnecessary storage values that don't change-
G-06Use unchecked for expressions that can not underflow due to previous if/require statement2

A mapping is more efficient than an array

Fetching data from an array is more expensive than fetching data from a mapping. Fetching data from an array will require iterating over the array until you reach your desired data. When using a mapping you only need to know the key in order to fetch the data from the exact slot it is stored in. Source

https://github.com/code-423n4/2023-04-frankencoin/blob/main/contracts/MintingHub.sol#L31

Gas Savings for MintingHub.bid, obtained via protocol's tests: Avg 2133 gas

MinMaxAvg# calls
Before76367123570999694
After74234121437978364
File: contracts/MintingHub.sol
31:    Challenge[] public challenges; // list of open challenges
diff --git a/contracts/MintingHub.sol b/contracts/MintingHub.sol
index 663b205..1585927 100644
--- a/contracts/MintingHub.sol
+++ b/contracts/MintingHub.sol
@@ -28,7 +28,6 @@ contract MintingHub {
     IPositionFactory private immutable POSITION_FACTORY; // position contract to clone

     IFrankencoin public immutable zchf; // currency
-    Challenge[] public challenges; // list of open challenges

     /**
      * Map to remember pending postponed collateral returns.
@@ -36,6 +35,10 @@ contract MintingHub {
      */
     mapping (address /** col */ => mapping (address => uint256)) public pendingReturns;

+    mapping(uint256 => Challenge) public challenges;
+
+    uint256 public challengesCount;
+
     struct Challenge {
         address challenger; // the address from which the challenge was initiated
         IPosition position; // the position that was challenged
@@ -140,8 +143,9 @@ contract MintingHub {
     function launchChallenge(address _positionAddr, uint256 _collateralAmount) external validPos(_positionAddr) returns (uint256) {
         IPosition position = IPosition(_positionAddr);
         IERC20(position.collateral()).transferFrom(msg.sender, address(this), _collateralAmount);
-        uint256 pos = challenges.length;
-        challenges.push(Challenge(msg.sender, position, _collateralAmount, block.timestamp + position.challengePeriod(), address(0x0), 0));
+        uint256 pos = challengesCount;
+        challenges[pos] = Challenge(msg.sender, position, _collateralAmount, block.timestamp + position.challengePeriod(), address(0x0), 0);
+        challengesCount = pos + 1;
         position.notifyChallengeStarted(_collateralAmount);
         emit ChallengeStarted(msg.sender, address(position), _collateralAmount, pos);
         return pos;
@@ -170,9 +174,10 @@ contract MintingHub {
         uint256 min = IPosition(challenge.position).minimumCollateral();
         require(challenge.size >= min);
         require(copy.size >= min);
-
-        uint256 pos = challenges.length;
-        challenges.push(copy);
+
+        uint256 pos = challengesCount;
+        challenges[pos] = copy;
+        challengesCount = pos + 1;
         emit ChallengeStarted(challenge.challenger, address(challenge.position), challenge.size, _challengeNumber);
         emit ChallengeStarted(copy.challenger, address(copy.position), copy.size, pos);
         return pos;

State variables can be cached instead of re-reading them from storage

Caching of a state variable replaces each Gwarmaccess (100 gas) with a much cheaper stack read.

Total Instances: 34

https://github.com/code-423n4/2023-04-frankencoin/blob/main/contracts/Position.sol#L76-L86

Cache _mint * ONE_DEC18 / _coll to save 1 SLOAD

File: contracts/Position.sol
76:    function initializeClone(address owner, uint256 _price, uint256 _limit, uint256 _coll, uint256 _mint) external onlyHub {
77:        if(_coll < minimumCollateral) revert InsufficientCollateral();
78:        setOwner(owner);
79:        
80:        price = _mint * ONE_DEC18 / _coll; 
81:        if (price > _price) revert InsufficientCollateral(); // @audit: unnecessary sload, use expression in above line
82:        limit = _limit;
83:        mintInternal(owner, _mint, _coll);
84:
85:        emit PositionOpened(owner, original, address(zchf), address(collateral), _price);
86:    }
diff --git a/contracts/Position.sol b/contracts/Position.sol
index 3e18534..a81a034 100644
--- a/contracts/Position.sol
+++ b/contracts/Position.sol
@@ -77,8 +77,9 @@ contract Position is Ownable, IPosition, MathUtil {
         if(_coll < minimumCollateral) revert InsufficientCollateral();
         setOwner(owner);

-        price = _mint * ONE_DEC18 / _coll;
-        if (price > _price) revert InsufficientCollateral();
+        uint256 _newPrice = _mint * ONE_DEC18 / _coll;
+        price = _newPrice;
+        if (_newPrice > _price) revert InsufficientCollateral();
         limit = _limit;
         mintInternal(owner, _mint, _coll);

https://github.com/code-423n4/2023-04-frankencoin/blob/main/contracts/Position.sol#L97-L101

Cache limit to save 1 SLOAD

File: contracts/Position.sol
97:    function reduceLimitForClone(uint256 _minimum) external noChallenge noCooldown alive onlyHub returns (uint256) {
98:        uint256 reduction = (limit - minted - _minimum)/2; // this will fail with an underflow if minimum is too high // @audit: 1st sload
99:        limit -= reduction + _minimum; // @audit: 2nd sload
100:       return reduction + _minimum;
101:   }
diff --git a/contracts/Position.sol b/contracts/Position.sol
index 3e18534..269d1f4 100644
--- a/contracts/Position.sol
+++ b/contracts/Position.sol
@@ -95,8 +95,9 @@ contract Position is Ownable, IPosition, MathUtil {
      * @return limit for the clone
      */
     function reduceLimitForClone(uint256 _minimum) external noChallenge noCooldown alive onlyHub returns (uint256) {
-        uint256 reduction = (limit - minted - _minimum)/2; // this will fail with an underflow if minimum is too high
-        limit -= reduction + _minimum;
+        uint256 _limit = limit;
+        uint256 reduction = (_limit - minted - _minimum)/2; // this will fail with an underflow if minimum is too high
+        limit = _limit - (reduction + _minimum);
         return reduction + _minimum;
     }

https://github.com/code-423n4/2023-04-frankencoin/blob/main/contracts/Position.sol#L132-L152

Cache minted to save 3 SLOADs

File: contracts/Position.sol
132:    function adjust(uint256 newMinted, uint256 newCollateral, uint256 newPrice) public onlyOwner {
133:        if (newPrice != price){
134:            adjustPrice(newPrice);
135:        }
136:        uint256 colbal = collateralBalance();
137:        if (newCollateral > colbal){
138:            collateral.transferFrom(msg.sender, address(this), newCollateral - colbal);
139:        }
140:        // Must be called after collateral deposit, but before withdrawal
141:        if (newMinted < minted){ // @audit: 1st sload
142:            zchf.burnFrom(msg.sender, minted - newMinted, reserveContribution); // @audit: 2nd sload
143:            minted = newMinted;
144:        }
145:        if (newCollateral < colbal){
146:            withdrawCollateral(msg.sender, colbal - newCollateral);
147:        }
148:        // Must be called after collateral withdrawal
149:        if (newMinted > minted){ // @audit: 3rd sload
150:            mint(msg.sender, newMinted - minted); // @audit: 4th sload
151:        }
152:    }
diff --git a/contracts/Position.sol b/contracts/Position.sol
index 3e18534..76de4c2 100644
--- a/contracts/Position.sol
+++ b/contracts/Position.sol
@@ -138,16 +138,17 @@ contract Position is Ownable, IPosition, MathUtil {
             collateral.transferFrom(msg.sender, address(this), newCollateral - colbal);
         }
         // Must be called after collateral deposit, but before withdrawal
-        if (newMinted < minted){
-            zchf.burnFrom(msg.sender, minted - newMinted, reserveContribution);
+        uint256 _minted = minted;
+        if (newMinted < _minted){
+            zchf.burnFrom(msg.sender, _minted - newMinted, reserveContribution);
             minted = newMinted;
         }
         if (newCollateral < colbal){
             withdrawCollateral(msg.sender, colbal - newCollateral);
         }
         // Must be called after collateral withdrawal
-        if (newMinted > minted){
-            mint(msg.sender, newMinted - minted);
+        if (newMinted > _minted){
+            mint(msg.sender, newMinted - _minted);
         }
     }

https://github.com/code-423n4/2023-04-frankencoin/blob/main/contracts/Position.sol#L193-L200

Cache minted to save 1 SLOAD

File: contracts/Position.sol
193:    function mintInternal(address target, uint256 amount, uint256 collateral_) internal {
194:        if (minted + amount > limit) revert LimitExceeded(); // @audit: 1st sload
195:        zchf.mint(target, amount, reserveContribution, calculateCurrentFee());
196:        minted += amount; // @audit: 2nd sload
197:
198:        checkCollateral(collateral_, price);
199:        emitUpdate();
200:    }
diff --git a/contracts/Position.sol b/contracts/Position.sol
index 3e18534..1fdb814 100644
--- a/contracts/Position.sol
+++ b/contracts/Position.sol
@@ -191,9 +191,10 @@ contract Position is Ownable, IPosition, MathUtil {
     error LimitExceeded();

     function mintInternal(address target, uint256 amount, uint256 collateral_) internal {
-        if (minted + amount > limit) revert LimitExceeded();
+        uint256 _minted = minted;
+        if (_minted + amount > limit) revert LimitExceeded();
         zchf.mint(target, amount, reserveContribution, calculateCurrentFee());
-        minted += amount;
+        minted = _minted + amount;

         checkCollateral(collateral_, price);
         emitUpdate();

https://github.com/code-423n4/2023-04-frankencoin/blob/main/contracts/Position.sol#L240-L243

Cache minted to save 1 SLOAD

File: contracts/Position.sol
240:    function notifyRepaidInternal(uint256 amount) internal {
241:        if (amount > minted) revert RepaidTooMuch(amount - minted); // @audit: 1st sload
242:        minted -= amount; // @audit: 2nd sload
243:    }
diff --git a/contracts/Position.sol b/contracts/Position.sol
index 3e18534..f63cc33 100644
--- a/contracts/Position.sol
+++ b/contracts/Position.sol
@@ -238,8 +238,9 @@ contract Position is Ownable, IPosition, MathUtil {
     error RepaidTooMuch(uint256 excess);

     function notifyRepaidInternal(uint256 amount) internal {
-        if (amount > minted) revert RepaidTooMuch(amount - minted);
-        minted -= amount;
+        uint256 _minted = minted;
+        if (amount > _minted) revert RepaidTooMuch(amount - _minted);
+        minted = _minted - amount;
     }

https://github.com/code-423n4/2023-04-frankencoin/blob/main/contracts/Position.sol#L347-L354

Cache minted to save 1 SLOAD

File: contracts/Position.sol
347:        uint256 volumeZCHF = _mulD18(price, _size); // How much could have minted with the challenged amount of the collateral
348:        // The owner does not have to repay (and burn) more than the owner actually minted.  
349:        uint256 repayment = minted < volumeZCHF ? minted : volumeZCHF; // how much must be burned to make things even // @audit: 1st & 2nd sload
350:
351:        notifyRepaidInternal(repayment); // we assume the caller takes care of the actual repayment
352:        internalWithdrawCollateral(_bidder, _size); // transfer collateral to the bidder and emit update
353:        return (owner, _bid, volumeZCHF, repayment, reserveContribution);
354:    }
diff --git a/contracts/Position.sol b/contracts/Position.sol
index 3e18534..4d6196a 100644
--- a/contracts/Position.sol
+++ b/contracts/Position.sol
@@ -346,7 +346,8 @@ contract Position is Ownable, IPosition, MathUtil {
         // So the owner cannot maliciously decrease the price to make volume fall below the proportionate repayment.
         uint256 volumeZCHF = _mulD18(price, _size); // How much could have minted with the challenged amount of the collateral
         // The owner does not have to repay (and burn) more than the owner actually minted.
-        uint256 repayment = minted < volumeZCHF ? minted : volumeZCHF; // how much must be burned to make things even
+        uint256 _minted = minted;
+        uint256 repayment = _minted < volumeZCHF ? _minted : volumeZCHF; // how much must be burned to make things even

         notifyRepaidInternal(repayment); // we assume the caller takes care of the actual repayment
         internalWithdrawCollateral(_bidder, _size); // transfer collateral to the bidder and emit update

https://github.com/code-423n4/2023-04-frankencoin/blob/main/contracts/MintingHub.sol#L156-L179

Cache challenge.challenger, challenge.position, challenge.bid, and challenge.size to save 8 SLOADs

File: contracts/MintingHub.sol
156:    function splitChallenge(uint256 _challengeNumber, uint256 splitOffAmount) external returns (uint256) {
157:        Challenge storage challenge = challenges[_challengeNumber];
158:        require(challenge.challenger != address(0x0)); // @audit: 1st sload
159:        Challenge memory copy = Challenge(
160:            challenge.challenger, // @audit: 2nd sload
161:            challenge.position, // @audit: 1st sload
162:            splitOffAmount,
163:            challenge.end,
164:            challenge.bidder,
165:            (challenge.bid * splitOffAmount) / challenge.size // @audit: 1st sloads
166:        );
167:        challenge.bid -= copy.bid; // @audit: 2nd sload
168:        challenge.size -= copy.size; // @audit: 2nd sload
169:
170:        uint256 min = IPosition(challenge.position).minimumCollateral(); // @audit: 2nd sload
171:        require(challenge.size >= min); // @audit: 3rd sload
172:        require(copy.size >= min);
173:
174:        uint256 pos = challenges.length;
175:        challenges.push(copy);
176:        emit ChallengeStarted(challenge.challenger, address(challenge.position), challenge.size, _challengeNumber); // @audit: 3rd sload & 4th sload
177:        emit ChallengeStarted(copy.challenger, address(copy.position), copy.size, pos);
178:        return pos;
179:    }
diff --git a/contracts/MintingHub.sol b/contracts/MintingHub.sol
index 663b205..d0c10f7 100644
--- a/contracts/MintingHub.sol
+++ b/contracts/MintingHub.sol
@@ -155,25 +155,29 @@ contract MintingHub {
      */
     function splitChallenge(uint256 _challengeNumber, uint256 splitOffAmount) external returns (uint256) {
         Challenge storage challenge = challenges[_challengeNumber];
-        require(challenge.challenger != address(0x0));
+        address _challenger = challenge.challenger;
+        require(_challenger != address(0x0));
+        IPosition _position = challenge.position;
+        uint256 _bid = challenge.bid;
+        uint256 _size = challenge.size;
         Challenge memory copy = Challenge(
-            challenge.challenger,
-            challenge.position,
+            _challenger,
+            _position,
             splitOffAmount,
             challenge.end,
             challenge.bidder,
-            (challenge.bid * splitOffAmount) / challenge.size
+            (_bid * splitOffAmount) / _size
         );
-        challenge.bid -= copy.bid;
-        challenge.size -= copy.size;
+        challenge.bid = _bid - copy.bid;
+        challenge.size = _size - copy.size;

-        uint256 min = IPosition(challenge.position).minimumCollateral();
-        require(challenge.size >= min);
+        uint256 min = IPosition(_position).minimumCollateral();
+        require(_size >= min);
         require(copy.size >= min);

         uint256 pos = challenges.length;
         challenges.push(copy);
-        emit ChallengeStarted(challenge.challenger, address(challenge.position), challenge.size, _challengeNumber);
+        emit ChallengeStarted(_challenger, address(_position), _size, _challengeNumber);
         emit ChallengeStarted(copy.challenger, address(copy.position), copy.size, pos);
         return pos;
     }

https://github.com/code-423n4/2023-04-frankencoin/blob/main/contracts/MintingHub.sol#L199-L229

Cache challenge.end, challenge.size, challenge.bid, and challenge.position to save 6 SLOADs

File: contracts/MintingHub.sol
199:    function bid(uint256 _challengeNumber, uint256 _bidAmountZCHF, uint256 expectedSize) external {
200:        Challenge storage challenge = challenges[_challengeNumber];
201:        if (block.timestamp >= challenge.end) revert TooLate(); // @audit: 1st sload
202:        if (expectedSize != challenge.size) revert UnexpectedSize(); // @audit: 1st sload
203:        if (challenge.bid > 0) { // @audit: 1st sload
204:            zchf.transfer(challenge.bidder, challenge.bid); // return old bid // @audit: 2nd sload
205:        }
206:        emit NewBid(_challengeNumber, _bidAmountZCHF, msg.sender);
207:        // ask position if the bid was high enough to avert the challenge
208:        if (challenge.position.tryAvertChallenge(challenge.size, _bidAmountZCHF)) { // @audit: 1st sload & 2nd sload
209:            // bid was high enough, let bidder buy collateral from challenger
210:            zchf.transferFrom(msg.sender, challenge.challenger, _bidAmountZCHF);
211:            challenge.position.collateral().transfer(msg.sender, challenge.size); // @audit: 2nd sload & 3rd sload
212:            emit ChallengeAverted(address(challenge.position), _challengeNumber); // @audit: 3rd sload
213:            delete challenges[_challengeNumber];
214:        } else {
215:            // challenge is not averted, update bid
216:            if (_bidAmountZCHF < minBid(challenge)) revert BidTooLow(_bidAmountZCHF, minBid(challenge));
217:            uint256 earliestEnd = block.timestamp + 30 minutes;
218:            if (earliestEnd >= challenge.end) { // @audit: 2nd sload
219:                // bump remaining time like ebay does when last minute bids come in
220:                // An attacker trying to postpone the challenge forever must increase the bid by 0.5%
221:                // every 30 minutes, or double it every three days, making the attack hard to sustain
222:                // for a prolonged period of time.
223:                challenge.end = earliestEnd;
224:            }
225:            zchf.transferFrom(msg.sender, address(this), _bidAmountZCHF);
226:            challenge.bid = _bidAmountZCHF; // @audit: 3rd sload
227:            challenge.bidder = msg.sender;
228:        }
229:    }
diff --git a/contracts/MintingHub.sol b/contracts/MintingHub.sol
index 663b205..d4aa22f 100644
--- a/contracts/MintingHub.sol
+++ b/contracts/MintingHub.sol
@@ -198,24 +198,28 @@ contract MintingHub {
      */
     function bid(uint256 _challengeNumber, uint256 _bidAmountZCHF, uint256 expectedSize) external {
         Challenge storage challenge = challenges[_challengeNumber];
-        if (block.timestamp >= challenge.end) revert TooLate();
-        if (expectedSize != challenge.size) revert UnexpectedSize();
-        if (challenge.bid > 0) {
-            zchf.transfer(challenge.bidder, challenge.bid); // return old bid
+        uint256 _end = challenge.end;
+        if (block.timestamp >= _end) revert TooLate();
+        uint256 _size = challenge.size;
+        if (expectedSize != _size) revert UnexpectedSize();
+        uint256 _bid = challenge.bid;
+        if (_bid > 0) {
+            zchf.transfer(challenge.bidder, _bid); // return old bid
         }
         emit NewBid(_challengeNumber, _bidAmountZCHF, msg.sender);
         // ask position if the bid was high enough to avert the challenge
-        if (challenge.position.tryAvertChallenge(challenge.size, _bidAmountZCHF)) {
+        IPosition _position = challenge.position;
+        if (_position.tryAvertChallenge(_size, _bidAmountZCHF)) {
             // bid was high enough, let bidder buy collateral from challenger
             zchf.transferFrom(msg.sender, challenge.challenger, _bidAmountZCHF);
-            challenge.position.collateral().transfer(msg.sender, challenge.size);
-            emit ChallengeAverted(address(challenge.position), _challengeNumber);
+            _position.collateral().transfer(msg.sender, _size);
+            emit ChallengeAverted(address(_position), _challengeNumber);
             delete challenges[_challengeNumber];
         } else {
             // challenge is not averted, update bid
             if (_bidAmountZCHF < minBid(challenge)) revert BidTooLow(_bidAmountZCHF, minBid(challenge));
             uint256 earliestEnd = block.timestamp + 30 minutes;
-            if (earliestEnd >= challenge.end) {
+            if (earliestEnd >= _end) {
                 // bump remaining time like ebay does when last minute bids come in
                 // An attacker trying to postpone the challenge forever must increase the bid by 0.5%
                 // every 30 minutes, or double it every three days, making the attack hard to sustain

https://github.com/code-423n4/2023-04-frankencoin/blob/main/contracts/MintingHub.sol#L252-L276

Cache challenge.challenger, challenge.bidder, challenge.bid, and challenge.position to save 7 SLOADs

File: contracts/MintingHub.sol
252:    function end(uint256 _challengeNumber, bool postponeCollateralReturn) public {
253:        Challenge storage challenge = challenges[_challengeNumber];
254:        require(challenge.challenger != address(0x0)); // @audit: 1st sload
255:        require(block.timestamp >= challenge.end, "period has not ended");
256:        // challenge must have been successful, because otherwise it would have immediately ended on placing the winning bid
257:        returnCollateral(challenge, postponeCollateralReturn);
258:        // notify the position that will send the collateral to the bidder. If there is no bid, send the collateral to msg.sender
259:        address recipient = challenge.bidder == address(0x0) ? msg.sender : challenge.bidder; // @audit: 1st and 2nd sload
260:        (address owner, uint256 effectiveBid, uint256 volume, uint256 repayment, uint32 reservePPM) = challenge.position.notifyChallengeSucceeded(recipient, challenge.bid, challenge.size); // @audit: 1st sloads
261:        if (effectiveBid < challenge.bid) { // @audit: 2nd sload
262:            // overbid, return excess amount
263:            IERC20(zchf).transfer(challenge.bidder, challenge.bid - effectiveBid); // @audit: 3rd sload & 2nd sload
264:        }
265:        uint256 reward = (volume * CHALLENGER_REWARD) / 1000_000;
266:        uint256 fundsNeeded = reward + repayment;
267:        if (effectiveBid > fundsNeeded){
268:            zchf.transfer(owner, effectiveBid - fundsNeeded);
269:        } else if (effectiveBid < fundsNeeded){
270:            zchf.notifyLoss(fundsNeeded - effectiveBid); // ensure we have enough to pay everything
271:        }
272:        zchf.transfer(challenge.challenger, reward); // pay out the challenger reward // @audit: 2nd sload
273:        zchf.burn(repayment, reservePPM); // Repay the challenged part
274:        emit ChallengeSucceeded(address(challenge.position), challenge.bid, _challengeNumber); // @audit: 2nd sload & 4th sload
275:        delete challenges[_challengeNumber];
276:    }
diff --git a/contracts/MintingHub.sol b/contracts/MintingHub.sol
index 663b205..d52d678 100644
--- a/contracts/MintingHub.sol
+++ b/contracts/MintingHub.sol
@@ -251,16 +251,27 @@ contract MintingHub {
      */
     function end(uint256 _challengeNumber, bool postponeCollateralReturn) public {
         Challenge storage challenge = challenges[_challengeNumber];
-        require(challenge.challenger != address(0x0));
+        address _challenger = challenge.challenger;
+        require(_challenger != address(0x0));
         require(block.timestamp >= challenge.end, "period has not ended");
         // challenge must have been successful, because otherwise it would have immediately ended on placing the winning bid
         returnCollateral(challenge, postponeCollateralReturn);
         // notify the position that will send the collateral to the bidder. If there is no bid, send the collateral to msg.sender
-        address recipient = challenge.bidder == address(0x0) ? msg.sender : challenge.bidder;
-        (address owner, uint256 effectiveBid, uint256 volume, uint256 repayment, uint32 reservePPM) = challenge.position.notifyChallengeSucceeded(recipient, challenge.bid, challenge.size);
-        if (effectiveBid < challenge.bid) {
-            // overbid, return excess amount
-            IERC20(zchf).transfer(challenge.bidder, challenge.bid - effectiveBid);
+        IPosition _position = challenge.position;
+        uint256 _bid = challenge.bid;
+        address owner;
+        uint256 effectiveBid;
+        uint256 volume;
+        uint256 repayment;
+        uint32 reservePPM;
+        { // @audit: needed due to `stack too deep error`
+            address _bidder = challenge.bidder;
+            address recipient = _bidder == address(0x0) ? msg.sender : _bidder;
+            (owner, effectiveBid, volume, repayment, reservePPM) = _position.notifyChallengeSucceeded(recipient, _bid, challenge.size);
+            if (effectiveBid < _bid) {
+                // overbid, return excess amount
+                IERC20(zchf).transfer(_bidder, _bid - effectiveBid);
+            }
         }
         uint256 reward = (volume * CHALLENGER_REWARD) / 1000_000;
         uint256 fundsNeeded = reward + repayment;
@@ -269,9 +280,9 @@ contract MintingHub {
         } else if (effectiveBid < fundsNeeded){
             zchf.notifyLoss(fundsNeeded - effectiveBid); // ensure we have enough to pay everything
         }
-        zchf.transfer(challenge.challenger, reward); // pay out the challenger reward
+        zchf.transfer(_challenger, reward); // pay out the challenger reward
         zchf.burn(repayment, reservePPM); // Repay the challenged part
-        emit ChallengeSucceeded(address(challenge.position), challenge.bid, _challengeNumber);
+        emit ChallengeSucceeded(address(_position), _bid, _challengeNumber);
         delete challenges[_challengeNumber];
     }

https://github.com/code-423n4/2023-04-frankencoin/blob/main/contracts/MintingHub.sol#L287-L296

Cache challenge.challenger and challenge.size to save 3 SLOADs

File: contracts/MintingHub.sol
287:    function returnCollateral(Challenge storage challenge, bool postpone) internal {
288:        if (postpone){
289:            // Postponing helps in case the challenger was blacklisted on the collateral token or otherwise cannot receive it at the moment.
290:            address collateral = address(challenge.position.collateral());
291:            pendingReturns[collateral][challenge.challenger] += challenge.size; // @audit: 1st and 2nd sload & 1st sload
292:            emit PostPonedReturn(collateral, challenge.challenger, challenge.size); // @audit: 3rd sload & 2nd sload
293:        } else {
294:            challenge.position.collateral().transfer(challenge.challenger, challenge.size); // return the challenger's collateral
295:        }
296:    }
diff --git a/contracts/MintingHub.sol b/contracts/MintingHub.sol
index 663b205..2e5b107 100644
--- a/contracts/MintingHub.sol
+++ b/contracts/MintingHub.sol
@@ -285,11 +285,14 @@ contract MintingHub {
     }

     function returnCollateral(Challenge storage challenge, bool postpone) internal {
+
         if (postpone){
+            address _challenger = challenge.challenger;
+            uint256 _size = challenge.size;
             // Postponing helps in case the challenger was blacklisted on the collateral token or otherwise cannot receive it at the moment.
             address collateral = address(challenge.position.collateral());
-            pendingReturns[collateral][challenge.challenger] += challenge.size;
-            emit PostPonedReturn(collateral, challenge.challenger, challenge.size);
+            pendingReturns[collateral][_challenger] += _size;
+            emit PostPonedReturn(collateral, _challenger, _size);
         } else {
             challenge.position.collateral().transfer(challenge.challenger, challenge.size); // return the challenger's collateral
         }

https://github.com/code-423n4/2023-04-frankencoin/blob/main/contracts/Frankencoin.sol#L293-L295

Cache minters[_minter] to save 1 SLOAD

Function is in a modifier that is used within state mutating functions

File: contracts/Frankencoin.sol
293:   function isMinter(address _minter) override public view returns (bool){
294:      return minters[_minter] != 0 && block.timestamp >= minters[_minter]; // @audit: 1st and 2nd sloads
295:   }
diff --git a/contracts/Frankencoin.sol b/contracts/Frankencoin.sol
index e9e87dc..93b224e 100644
--- a/contracts/Frankencoin.sol
+++ b/contracts/Frankencoin.sol
@@ -291,7 +291,8 @@ contract Frankencoin is ERC20PermitLight, IFrankencoin {
     * Returns true if the address is an approved minter.
     */
    function isMinter(address _minter) override public view returns (bool){
-      return minters[_minter] != 0 && block.timestamp >= minters[_minter];
+       uint256 _minter = minters[_minter];
+      return _minter != 0 && block.timestamp >= _minter;
    }

https://github.com/code-423n4/2023-04-frankencoin/blob/main/contracts/ERC20.sol#L151-L159

Cache _balances[sender] to save 1 SLOAD

File: contracts/ERC20.sol
151:    function _transfer(address sender, address recipient, uint256 amount) internal virtual {
152:        require(recipient != address(0));
153:        
154:        _beforeTokenTransfer(sender, recipient, amount);
155:        if (_balances[sender] < amount) revert ERC20InsufficientBalance(sender, _balances[sender], amount); // @audit: 1st sload
156:        _balances[sender] -= amount; // @audit: 2nd sload
157:        _balances[recipient] += amount;
158:        emit Transfer(sender, recipient, amount);
159:    }
diff --git a/contracts/ERC20.sol b/contracts/ERC20.sol
index 585f28b..e588b5b 100644
--- a/contracts/ERC20.sol
+++ b/contracts/ERC20.sol
@@ -152,8 +152,9 @@ abstract contract ERC20 is IERC20 {
         require(recipient != address(0));

         _beforeTokenTransfer(sender, recipient, amount);
-        if (_balances[sender] < amount) revert ERC20InsufficientBalance(sender, _balances[sender], amount);
-        _balances[sender] -= amount;
+        uint256 _senderBalance = _balances[sender];
+        if (_senderBalance < amount) revert ERC20InsufficientBalance(sender, _senderBalance, amount);
+        _balances[sender] = _senderBalance - amount;
         _balances[recipient] += amount;
         emit Transfer(sender, recipient, amount);
     }

Refactor code to avoid re-reading from storage

The internal functions below read storage slots that are previously read in the functions that invoke them. We can refactor the internal functions so we could pass cached storage variables as stack variables and avoid the extra storage reads that would otherwise take place in the internal functions.

Total Instances: 4

https://github.com/code-423n4/2023-04-frankencoin/blob/main/contracts/Position.sol#L227-L243

Gas Savings for Position.repay, obtained via protocol's tests: Avg 207 gas

MinMaxAvg# calls
Before8698088824879022
After8679688594876952
File: contracts/Position.sol
240:    function notifyRepaidInternal(uint256 amount) internal {
241:        if (amount > minted) revert RepaidTooMuch(amount - minted); 
242:        minted -= amount;
243:    }
diff --git a/contracts/Position.sol b/contracts/Position.sol
index 3e18534..030ce47 100644
--- a/contracts/Position.sol
+++ b/contracts/Position.sol
@@ -231,15 +231,16 @@ contract Position is Ownable, IPosition, MathUtil {

     function repayInternal(uint256 burnable) internal {
         uint256 actuallyBurned = IFrankencoin(zchf).burnWithReserve(burnable, reserveContribution);
-        notifyRepaidInternal(actuallyBurned);
-        emitUpdate();
+        uint256 _minted = minted;
+        notifyRepaidInternal(actuallyBurned, _minted);
+        emit MintingUpdate(collateralBalance(), price, _minted, limit);
     }

     error RepaidTooMuch(uint256 excess);

-    function notifyRepaidInternal(uint256 amount) internal {
-        if (amount > minted) revert RepaidTooMuch(amount - minted);
-        minted -= amount;
+    function notifyRepaidInternal(uint256 amount, uint256 _minted) internal {
+        if (amount > _minted) revert RepaidTooMuch(amount - _minted);
+        minted = _minted - amount;
     }

     /**
@@ -346,9 +347,10 @@ contract Position is Ownable, IPosition, MathUtil {
         // So the owner cannot maliciously decrease the price to make volume fall below the proportionate repayment.
         uint256 volumeZCHF = _mulD18(price, _size); // How much could have minted with the challenged amount of the collateral
         // The owner does not have to repay (and burn) more than the owner actually minted.
-        uint256 repayment = minted < volumeZCHF ? minted : volumeZCHF; // how much must be burned to make things even
+        uint256 _minted = minted;
+        uint256 repayment = _minted < volumeZCHF ? _minted : volumeZCHF; // how much must be burned to make things even

-        notifyRepaidInternal(repayment); // we assume the caller takes care of the actual repayment
+        notifyRepaidInternal(repayment, _minted); // we assume the caller takes care of the actual repayment
         internalWithdrawCollateral(_bidder, _size); // transfer collateral to the bidder and emit update
         return (owner, _bid, volumeZCHF, repayment, reserveContribution);
     }

https://github.com/code-423n4/2023-04-frankencoin/blob/main/contracts/Position.sol#L268-L284

Gas Savings for Position.withdrawCollateral, obtained via protocol's tests: Avg 211 gas

MinMaxAvg# calls
Before--730581
After--728471
File: contracts/Position.sol
268:    function internalWithdrawCollateral(address target, uint256 amount) internal returns (uint256) {
269:        IERC20(collateral).transfer(target, amount);
270:        uint256 balance = collateralBalance();
271:        if (balance < minimumCollateral){
272:            cooldown = expiration;
273:        }
274:        emitUpdate();
275:        return balance;
276:    }
277:
278:    /**
279:     * This invariant must always hold and must always be checked when any of the three
280:     * variables change in an adverse way.
281:     */
282:    function checkCollateral(uint256 collateralReserve, uint256 atPrice) internal view {
283:        if (collateralReserve * atPrice < minted * ONE_DEC18) revert InsufficientCollateral();
284:    }
diff --git a/contracts/Position.sol b/contracts/Position.sol
index 3e18534..cf14766 100644
--- a/contracts/Position.sol
+++ b/contracts/Position.sol
@@ -261,17 +261,19 @@ contract Position is Ownable, IPosition, MathUtil {
      * Withdrawing collateral below the minimum collateral amount formally closes the position.
      */
     function withdrawCollateral(address target, uint256 amount) public onlyOwner noChallenge noCooldown {
-        uint256 balance = internalWithdrawCollateral(target, amount);
-        checkCollateral(balance, price);
+        uint256 _price = price;
+        uint256 _minted = minted;
+        uint256 balance = internalWithdrawCollateral(target, amount, _price, _minted);
+        checkCollateral(balance, _price, _minted);
     }

-    function internalWithdrawCollateral(address target, uint256 amount) internal returns (uint256) {
+    function internalWithdrawCollateral(address target, uint256 amount, uint256 _price, uint256 _minted) internal returns (uint256) {
         IERC20(collateral).transfer(target, amount);
         uint256 balance = collateralBalance();
         if (balance < minimumCollateral){
             cooldown = expiration;
         }
-        emitUpdate();
+        emit MintingUpdate(collateralBalance(), _price, _minted, limit);
         return balance;
     }

@@ -279,8 +281,8 @@ contract Position is Ownable, IPosition, MathUtil {
      * This invariant must always hold and must always be checked when any of the three
      * variables change in an adverse way.
      */
-    function checkCollateral(uint256 collateralReserve, uint256 atPrice) internal view {
-        if (collateralReserve * atPrice < minted * ONE_DEC18) revert InsufficientCollateral();
+    function checkCollateral(uint256 collateralReserve, uint256 atPrice, uint256 _minted) internal view {
+        if (collateralReserve * atPrice < _minted * ONE_DEC18) revert InsufficientCollateral();
     }

     function emitUpdate() internal {
@@ -344,12 +346,14 @@ contract Position is Ownable, IPosition, MathUtil {
         // such that
         //    volumeZCHF = price * size / E18 >= minted * size / colbal
         // So the owner cannot maliciously decrease the price to make volume fall below the proportionate repayment.
-        uint256 volumeZCHF = _mulD18(price, _size); // How much could have minted with the challenged amount of the collateral
+        uint256 _price = price;
+        uint256 volumeZCHF = _mulD18(_price, _size); // How much could have minted with the challenged amount of the collateral
         // The owner does not have to repay (and burn) more than the owner actually minted.
-        uint256 repayment = minted < volumeZCHF ? minted : volumeZCHF; // how much must be burned to make things even
+        uint256 _minted = minted;
+        uint256 repayment = _minted < volumeZCHF ? _minted : volumeZCHF; // how much must be burned to make things even

         notifyRepaidInternal(repayment); // we assume the caller takes care of the actual repayment
-        internalWithdrawCollateral(_bidder, _size); // transfer collateral to the bidder and emit update
+        internalWithdrawCollateral(_bidder, _size, _price, _minted); // transfer collateral to the bidder and emit update
         return (owner, _bid, volumeZCHF, repayment, reserveContribution);
     }

https://github.com/code-423n4/2023-04-frankencoin/blob/main/contracts/MintingHub.sol#L188-L190

Gas Savings for MintingHub.bid, obtained via protocol's tests: Avg 159 gas

MinMaxAvg# calls
Before76367123570999694
After76157123463998104
File: contracts/MintingHub.sol
188:    function minBid(Challenge storage challenge) internal view returns (uint256) {
189:        return (challenge.bid * 1005) / 1000;
190:    }
diff --git a/contracts/MintingHub.sol b/contracts/MintingHub.sol
index 663b205..84aa645 100644
--- a/contracts/MintingHub.sol
+++ b/contracts/MintingHub.sol
@@ -179,14 +179,14 @@ contract MintingHub {
     }

     function minBid(uint256 challenge) public view returns (uint256) {
-        return minBid(challenges[challenge]);
+        return _minBid(challenges[challenge].bid);
     }

     /**
      * The minimum bid size for the next bid. It must be 0.5% higher than the previous bid.
      */
-    function minBid(Challenge storage challenge) internal view returns (uint256) {
-        return (challenge.bid * 1005) / 1000;
+    function _minBid(uint256 _bid) internal view returns (uint256) {
+        return (_bid * 1005) / 1000;
     }

     /**
@@ -200,8 +200,9 @@ contract MintingHub {
         Challenge storage challenge = challenges[_challengeNumber];
         if (block.timestamp >= challenge.end) revert TooLate();
         if (expectedSize != challenge.size) revert UnexpectedSize();
-        if (challenge.bid > 0) {
-            zchf.transfer(challenge.bidder, challenge.bid); // return old bid
+        uint256 _bid = challenge.bid;
+        if (_bid > 0) {
+            zchf.transfer(challenge.bidder, _bid); // return old bid
         }
         emit NewBid(_challengeNumber, _bidAmountZCHF, msg.sender);
         // ask position if the bid was high enough to avert the challenge
@@ -213,7 +214,7 @@ contract MintingHub {
             delete challenges[_challengeNumber];
         } else {
             // challenge is not averted, update bid
-            if (_bidAmountZCHF < minBid(challenge)) revert BidTooLow(_bidAmountZCHF, minBid(challenge));
+            if (_bidAmountZCHF < _minBid(_bid)) revert BidTooLow(_bidAmountZCHF, _minBid(_bid));
             uint256 earliestEnd = block.timestamp + 30 minutes;
             if (earliestEnd >= challenge.end) {
                 // bump remaining time like ebay does when last minute bids come in

https://github.com/code-423n4/2023-04-frankencoin/blob/main/contracts/MintingHub.sol#L287-L296

Gas Savings for MintingHub.end, obtained via protocol's tests: Avg 203 gas

MinMaxAvg# calls
Before--1283031
After--1281001
File: contracts/MintingHub.sol
287:    function returnCollateral(Challenge storage challenge, bool postpone) internal {
288:        if (postpone){
289:            // Postponing helps in case the challenger was blacklisted on the collateral token or otherwise cannot receive it at the moment.
290:            address collateral = address(challenge.position.collateral());
291:            pendingReturns[collateral][challenge.challenger] += challenge.size;
292:            emit PostPonedReturn(collateral, challenge.challenger, challenge.size);
293:        } else {
294:            challenge.position.collateral().transfer(challenge.challenger, challenge.size); // return the challenger's collateral
295:        }
296:    }
diff --git a/contracts/MintingHub.sol b/contracts/MintingHub.sol
index 663b205..5efb447 100644
--- a/contracts/MintingHub.sol
+++ b/contracts/MintingHub.sol
@@ -251,13 +251,23 @@ contract MintingHub {
      */
     function end(uint256 _challengeNumber, bool postponeCollateralReturn) public {
         Challenge storage challenge = challenges[_challengeNumber];
-        require(challenge.challenger != address(0x0));
+        address _challenger = challenge.challenger;
+        require(_challenger != address(0x0));
         require(block.timestamp >= challenge.end, "period has not ended");
         // challenge must have been successful, because otherwise it would have immediately ended on placing the winning bid
-        returnCollateral(challenge, postponeCollateralReturn);
-        // notify the position that will send the collateral to the bidder. If there is no bid, send the collateral to msg.sender
-        address recipient = challenge.bidder == address(0x0) ? msg.sender : challenge.bidder;
-        (address owner, uint256 effectiveBid, uint256 volume, uint256 repayment, uint32 reservePPM) = challenge.position.notifyChallengeSucceeded(recipient, challenge.bid, challenge.size);
+        IPosition _position = challenge.position;
+        address owner;
+        uint256 effectiveBid;
+        uint256 volume;
+        uint256 repayment;
+        uint32 reservePPM;
+        {
+            uint256 _size = challenge.size;
+            returnCollateral(_position, _challenger, _size, postponeCollateralReturn);
+            // notify the position that will send the collateral to the bidder. If there is no bid, send the collateral to msg.sender
+            address recipient = challenge.bidder == address(0x0) ? msg.sender : challenge.bidder;
+            (owner, effectiveBid, volume, repayment, reservePPM) = _position.notifyChallengeSucceeded(recipient, challenge.bid, _size);
+        }
         if (effectiveBid < challenge.bid) {
             // overbid, return excess amount
             IERC20(zchf).transfer(challenge.bidder, challenge.bid - effectiveBid);
@@ -284,14 +294,14 @@ contract MintingHub {
         IERC20(collateral).transfer(target, amount);
     }

-    function returnCollateral(Challenge storage challenge, bool postpone) internal {
+    function returnCollateral(IPosition _position, address _challenger, uint256 _size, bool postpone) internal {
         if (postpone){
             // Postponing helps in case the challenger was blacklisted on the collateral token or otherwise cannot receive it at the moment.
-            address collateral = address(challenge.position.collateral());
-            pendingReturns[collateral][challenge.challenger] += challenge.size;
-            emit PostPonedReturn(collateral, challenge.challenger, challenge.size);
+            address collateral = address(_position.collateral());
+            pendingReturns[collateral][_challenger] += _size;
+            emit PostPonedReturn(collateral, _challenger, _size);
         } else {
-            challenge.position.collateral().transfer(challenge.challenger, challenge.size); // return the challenger's collateral
+            _position.collateral().transfer(_challenger, _size); // return the challenger's collateral
         }
     }
 }

Cache return value from external call to avoid an unnecessary CALL/STATICCALL

External calls are expensive as they use the CALL/STATICCALL opcode (~100 gas). If you are calling the same external function more than once you should cache the return value to avoid an unnecessary CALL/STATICCALL.

https://github.com/code-423n4/2023-04-frankencoin/blob/main/contracts/Position.sol#L268-L276

Gas Savings for Position.withdrawCollateral, obtained via protocol's tests: Avg 998 gas

MinMaxAvg# calls
Before--730581
After--720601
File: contracts/Position.sol
268:    function internalWithdrawCollateral(address target, uint256 amount) internal returns (uint256) {
269:        IERC20(collateral).transfer(target, amount);
270:        uint256 balance = collateralBalance();
271:        if (balance < minimumCollateral){
272:            cooldown = expiration;
273:        }
274:        emitUpdate();
275:        return balance;
276:    }
diff --git a/contracts/Position.sol b/contracts/Position.sol
index 3e18534..ea69c6e 100644
--- a/contracts/Position.sol
+++ b/contracts/Position.sol
@@ -271,7 +271,7 @@ contract Position is Ownable, IPosition, MathUtil {
         if (balance < minimumCollateral){
             cooldown = expiration;
         }
-        emitUpdate();
+        emit MintingUpdate(balance, price, minted, limit);
         return balance;
     }

Refactor event to avoid emitting unnecessary storage values that don't change

The limit storage variable is only modified in the constructor, the initializeClone function, and the reduceLimitForClone function. Likewise, the price storage variable is only modified in the constructor, the initilizeClone function, and the adjustPrice function. Instead of using the MintingUpdate event (which emits the limit and price storage variables) in functions that do not modify limit and price, we can create a new event that opts out of emitting those excessive storage variables.

Note: Only repay and withdrawCollateral undergo this optimization since those are the only functions that are benchmarked in the tests. This optimization can also be done for other functions that call emitUpdate and do not modify the limit and price storage variables.

https://github.com/code-423n4/2023-04-frankencoin/blob/main/contracts/Position.sol#L232-L236

Gas Savings for Position.repay, obtained via protocol's tests: Avg 4297 gas

MinMaxAvg# calls
Before--879022
After--836052

https://github.com/code-423n4/2023-04-frankencoin/blob/main/contracts/Position.sol#L268-L276

Gas Savings for Position.withdrawCollateral, obtained via protocol's tests: Avg 2775 gas

MinMaxAvg# calls
Before--730581
After--702831
File: contracts/Position.sol
232:    function repayInternal(uint256 burnable) internal {
233:        uint256 actuallyBurned = IFrankencoin(zchf).burnWithReserve(burnable, reserveContribution);
234:        notifyRepaidInternal(actuallyBurned);
235:        emitUpdate();
236:    }

268:    function internalWithdrawCollateral(address target, uint256 amount) internal returns (uint256) {
269:        IERC20(collateral).transfer(target, amount);
270:        uint256 balance = collateralBalance();
271:        if (balance < minimumCollateral){
272:            cooldown = expiration;
273:        }
274:        emitUpdate();
275:        return balance;
276:    }
diff --git a/contracts/Position.sol b/contracts/Position.sol
index 3e18534..7e5f7f9 100644
--- a/contracts/Position.sol
+++ b/contracts/Position.sol
@@ -40,6 +40,7 @@ contract Position is Ownable, IPosition, MathUtil {

     event PositionOpened(address indexed owner, address original, address zchf, address collateral, uint256 price);
     event MintingUpdate(uint256 collateral, uint256 price, uint256 minted, uint256 limit);
+    event Update(uint256 collateral, uint256 minted);
     event PositionDenied(address indexed sender, string message); // emitted if closed by governance

     error InsufficientCollateral();
@@ -232,7 +233,7 @@ contract Position is Ownable, IPosition, MathUtil {
     function repayInternal(uint256 burnable) internal {
         uint256 actuallyBurned = IFrankencoin(zchf).burnWithReserve(burnable, reserveContribution);
         notifyRepaidInternal(actuallyBurned);
-        emitUpdate();
+        emit Update(collateralBalance(), minted);
     }

     error RepaidTooMuch(uint256 excess);
@@ -271,7 +272,7 @@ contract Position is Ownable, IPosition, MathUtil {
         if (balance < minimumCollateral){
             cooldown = expiration;
         }
-        emitUpdate();
+        emit Update(collateralBalance(), minted);
         return balance;
     }

Use unchecked for expressions that can not underflow due to previous if/require statement

Wrapping expressions in unchecked blocks will tell the solidity compiler to exclude the extra opcodes needed to check for underflows.

Total Instances: 2

https://github.com/code-423n4/2023-04-frankencoin/blob/main/contracts/Position.sol#L240-L243

Gas Savings for Position.repay, obtained via protocol's tests: Avg 66 gas

MinMaxAvg# calls
Before--879022
After--878362
File: contracts/Position.sol
240:    function notifyRepaidInternal(uint256 amount) internal {
241:        if (amount > minted) revert RepaidTooMuch(amount - minted);
242:        minted -= amount;
243:    }
diff --git a/contracts/Position.sol b/contracts/Position.sol
index 3e18534..b251af4 100644
--- a/contracts/Position.sol
+++ b/contracts/Position.sol
@@ -239,7 +239,9 @@ contract Position is Ownable, IPosition, MathUtil {

     function notifyRepaidInternal(uint256 amount) internal {
         if (amount > minted) revert RepaidTooMuch(amount - minted);
-        minted -= amount;
+        unchecked {
+            minted -= amount;
+        }
     }

https://github.com/code-423n4/2023-04-frankencoin/blob/main/contracts/MintingHub.sol#L261-L271

Gas Savings for MintingHub.end, obtained via protocol's tests: Avg 80 gas

MinMaxAvg# calls
Before--1283031
After--1282231
File: contracts/MintingHub.sol
261:        if (effectiveBid < challenge.bid) {
262:            // overbid, return excess amount
263:            IERC20(zchf).transfer(challenge.bidder, challenge.bid - effectiveBid);
264:        }
265:        uint256 reward = (volume * CHALLENGER_REWARD) / 1000_000;
266:        uint256 fundsNeeded = reward + repayment;
267:        if (effectiveBid > fundsNeeded){
268:            zchf.transfer(owner, effectiveBid - fundsNeeded);
269:        } else if (effectiveBid < fundsNeeded){
270:            zchf.notifyLoss(fundsNeeded - effectiveBid); // ensure we have enough to pay everything
271:        }
diff --git a/contracts/MintingHub.sol b/contracts/MintingHub.sol
index 663b205..6854a9e 100644
--- a/contracts/MintingHub.sol
+++ b/contracts/MintingHub.sol
@@ -260,14 +260,20 @@ contract MintingHub {
         (address owner, uint256 effectiveBid, uint256 volume, uint256 repayment, uint32 reservePPM) = challenge.position.notifyChallengeSucceeded(recipient, challenge.bid, challenge.size);
         if (effectiveBid < challenge.bid) {
             // overbid, return excess amount
-            IERC20(zchf).transfer(challenge.bidder, challenge.bid - effectiveBid);
+            unchecked {
+                IERC20(zchf).transfer(challenge.bidder, challenge.bid - effectiveBid);
+            }
         }
         uint256 reward = (volume * CHALLENGER_REWARD) / 1000_000;
         uint256 fundsNeeded = reward + repayment;
         if (effectiveBid > fundsNeeded){
-            zchf.transfer(owner, effectiveBid - fundsNeeded);
+            unchecked {
+                zchf.transfer(owner, effectiveBid - fundsNeeded);
+            }
         } else if (effectiveBid < fundsNeeded){
-            zchf.notifyLoss(fundsNeeded - effectiveBid); // ensure we have enough to pay everything
+            unchecked {
+                zchf.notifyLoss(fundsNeeded - effectiveBid); // ensure we have enough to pay everything
+            }
         }

GasReport output, with all optimizations applied

·-------------------------------------------------|---------------------------|-------------|-----------------------------·
|              Solc version: 0.8.13               ·  Optimizer enabled: true  ·  Runs: 200  ·  Block limit: 30000000 gas  │
··················································|···························|·············|······························
|  Methods                                                                                                                │
·····················|····························|·············|·············|·············|···············|··············
|  Contract          ·  Method                    ·  Min        ·  Max        ·  Avg        ·  # calls      ·  usd (avg)  │
·····················|····························|·············|·············|·············|···············|··············
|  Equity            ·  delegateVoteTo            ·      45534  ·      45546  ·      45540  ·            2  ·          -  │
·····················|····························|·············|·············|·············|···············|··············
|  Equity            ·  redeem                    ·          -  ·          -  ·      66819  ·            1  ·          -  │
·····················|····························|·············|·············|·············|···············|··············
|  Equity            ·  transfer                  ·          -  ·          -  ·      85527  ·            1  ·          -  │
·····················|····························|·············|·············|·············|···············|··············
|  Frankencoin       ·  approve                   ·          -  ·          -  ·      46228  ·            1  ·          -  │
·····················|····························|·············|·············|·············|···············|··············
|  Frankencoin       ·  burn                      ·          -  ·          -  ·      33770  ·            1  ·          -  │
·····················|····························|·············|·············|·············|···············|··············
|  Frankencoin       ·  denyMinter                ·          -  ·          -  ·      39074  ·            2  ·          -  │
·····················|····························|·············|·············|·············|···············|··············
|  Frankencoin       ·  mint                      ·          -  ·          -  ·     116933  ·            1  ·          -  │
·····················|····························|·············|·············|·············|···············|··············
|  Frankencoin       ·  suggestMinter             ·      56607  ·      77697  ·      62144  ·            8  ·          -  │
·····················|····························|·············|·············|·············|···············|··············
|  Frankencoin       ·  transfer                  ·      34528  ·      51616  ·      40224  ·            3  ·          -  │
·····················|····························|·············|·············|·············|···············|··············
|  Frankencoin       ·  transferAndCall           ·      65972  ·     156127  ·     113385  ·            4  ·          -  │
·····················|····························|·············|·············|·············|···············|··············
|  MintingHub        ·  bid                       ·      73363  ·     120804  ·      97084  ·            4  ·          -  │
·····················|····························|·············|·············|·············|···············|··············
|  MintingHub        ·  clonePosition             ·          -  ·          -  ·     274904  ·            2  ·          -  │
·····················|····························|·············|·············|·············|···············|··············
|  MintingHub        ·  end                       ·          -  ·          -  ·     121752  ·            1  ·          -  │
·····················|····························|·············|·············|·············|···············|··············
|  MintingHub        ·  launchChallenge           ·          -  ·          -  ·     210751  ·            2  ·          -  │
·····················|····························|·············|·············|·············|···············|··············
|  MintingHub        ·  openPosition              ·          -  ·          -  ·    1737410  ·            2  ·          -  │
·····················|····························|·············|·············|·············|···············|··············
|  MintingHubTest    ·  bidNearEndOfChallenge     ·          -  ·          -  ·     140715  ·            1  ·          -  │
·····················|····························|·············|·············|·············|···············|··············
|  MintingHubTest    ·  challengeExpiredPosition  ·          -  ·          -  ·     272504  ·            1  ·          -  │
·····················|····························|·············|·············|·············|···············|··············
|  MintingHubTest    ·  endChallenges             ·          -  ·          -  ·     469531  ·            1  ·          -  │
·····················|····························|·············|·············|·············|···············|··············
|  MintingHubTest    ·  endLastChallenge          ·          -  ·          -  ·     119344  ·            1  ·          -  │
·····················|····························|·············|·············|·············|···············|··············
|  MintingHubTest    ·  initiateAndDenyPosition   ·          -  ·          -  ·    1831819  ·            1  ·          -  │
·····················|····························|·············|·············|·············|···············|··············
|  MintingHubTest    ·  initiateEquity            ·          -  ·          -  ·     365145  ·            1  ·          -  │
·····················|····························|·············|·············|·············|···············|··············
|  MintingHubTest    ·  initiatePosition          ·          -  ·          -  ·    1849030  ·            1  ·          -  │
·····················|····························|·············|·············|·············|···············|··············
|  MintingHubTest    ·  letAliceMint              ·          -  ·          -  ·     311827  ·            1  ·          -  │
·····················|····························|·············|·············|·············|···············|··············
|  MintingHubTest    ·  letBobChallenge           ·          -  ·          -  ·     705114  ·            1  ·          -  │
·····················|····························|·············|·············|·············|···············|··············
|  MintingHubTest    ·  restructure               ·          -  ·          -  ·     183340  ·            1  ·          -  │
·····················|····························|·············|·············|·············|···············|··············
|  MintingHubTest    ·  testExcessiveChallenge    ·          -  ·          -  ·     290328  ·            1  ·          -  │
·····················|····························|·············|·············|·············|···············|··············
|  MintingHubTest    ·  testWithdraw              ·          -  ·          -  ·     169588  ·            1  ·          -  │
·····················|····························|·············|·············|·············|···············|··············
|  Position          ·  repay                     ·      82439  ·      83148  ·      82794  ·            2  ·          -  │
·····················|····························|·············|·············|·············|···············|··············
|  Position          ·  withdrawCollateral        ·          -  ·          -  ·      69071  ·            1  ·          -  │
·····················|····························|·············|·············|·············|···············|··············
|  StablecoinBridge  ·  burn                      ·          -  ·          -  ·      72239  ·            1  ·          -  │
·····················|····························|·············|·············|·············|···············|··············
|  StablecoinBridge  ·  burn                      ·          -  ·          -  ·      55545  ·            1  ·          -  │
·····················|····························|·············|·············|·············|···············|··············
|  StablecoinBridge  ·  mint                      ·      75906  ·     110106  ·      97904  ·            6  ·          -  │
·····················|····························|·············|·············|·············|···············|··············
|  TestToken         ·  approve                   ·      29093  ·      46205  ·      44878  ·           13  ·          -  │
·····················|····························|·············|·············|·············|···············|··············
|  TestToken         ·  mint                      ·      51294  ·      68394  ·      64118  ·            8  ·          -  │
·····················|····························|·············|·············|·············|···············|··············
|  TestToken         ·  transfer                  ·          -  ·          -  ·      51614  ·            2  ·          -  │
·····················|····························|·············|·············|·············|···············|··············
|  TestToken         ·  transferAndCall           ·          -  ·          -  ·      52939  ·            1  ·          -  │
·····················|····························|·············|·············|·············|···············|··············

#0 - c4-pre-sort

2023-04-27T12:43:31Z

0xA5DF marked the issue as high quality report

#1 - c4-judge

2023-05-16T15:20:56Z

hansfriese marked the issue as grade-b

#2 - 0xJCN

2023-05-19T01:49:21Z

Hi @hansfriese ,

I would like to encourage you to revisit this issue. I believe it should receive a grade A and I will explain why down below. If you still decide to maintain the current grade after reading this, I would very much appreciate some advice on how I can improve my submisions and provide more value in the future.

  • All of the instances presented (with the exception of Issue 2, where savings are explained via opcodes) are benchmarked.
  • All instances contain diffs to illustrate the necessary refactoring needed for the findings.
  • All instances contain explanations.
  • All of the instances (with the exception of Issue 6) are high impact, meaning they save >= 100 gas.
  • This report only contains one finding (Issue 6) that is not high impact (results in savings < 100 gas).
  • This report contains no false findings.
  • A table illustrating the exact gas savings the sponsor would see for all functions, after all optimizations are applied, is shown.
  • A gist illustrating all the final changes to needed to take place for all contracts is provided.
  • The final gas report with all changes from the final diffs is provided at the end of the report so the sponsor can see the exact savings they should expect to see from this report.

In the spirit of fairness (as well as maintaining and encouraging high quality reports) I have explained inaccuracies/falsities with reports that received a grade A below. I have seen previous contests in which reports that contain multiple false findings receive high grades. I have decided now to take the time to point this out so that, moving forward, we can ensure the validity of these reports for the sponsor. In addition, seeing as we are receiving compensation for our time and effort, I wanted to be fair to myself as it is difficult for me to observe the reports below and understand what I am 'missing' that resulted in my report being graded lower.

Issue #788

  • Note that a majority of findings do not illustrate how much gas will be saved.
  • G-01 is false as the protocol uses solidity 0.8.13 to compile their contracts for their tests.
  • G-03 does not save gas since the functions mentioned are pure. For reference, view functions also cost no gas.
  • G-05 contains a large amount of false instances as minimumCollateral, original, collateral, zchf, start, mintingFeePPM, expiration, and hub are immutables and therefore do not need to be cached since they are not read from storage. In addition, QUORUM, MINIMUM_EQUITY, MIN_APPLICATION_PERIOD, MIN_FEE, and INFINITY are constants, not storage variables.
  • G-06 is false as all the variables mentioned are constants, not storage variables.
  • G-08 is unclear as the variables mentioned change for each iteration and therefore are dependent on i in the loop. Declaring the variables inside the loop will discard them afterwards, while declaring the variable outside the loop will result in the stack variables remaining on the stack and may result in extra stack manipulation for other variables.
  • G-11 contains a large amount of false instances as the functions mentioned are only called once, not twice, and therefore do not need to be cached. The only instances that are correct are the instances mentioned for these lines of code: https://github.com/code-423n4/2023-04-frankencoin/blob/main/contracts/Frankencoin.sol#L84-L85
  • G-12 is false as the struct copy is being created in memory to be added to an array and emitted in an event, it is not being read from storage. Changing the location to storage would result in an error.

Issue #427

  • Note that the instances do not include diffs to illustrate the necessary refactoring needed to make for the gas savings to take effect and some issues do not contain explanations or explain how much gas is saved.
  • G-04 is false as public functions cost the same as external functions. In addition, these funtions are view functions and therefore do not cost gas.
  • G-05 is false as the instance points to a memory struct being creating in a function, not a function's argument.
  • G-08 is false as none of the instances reference bools used in storage.
  • G-10 does not save gas.
  • G-11 is false as using an extra indexed parameter will result in an extra Glogtopic (375 gas), while using non-indexed will result in Glogdata (8 gas) * each byte in data. I've seen this finding in many reports, but let it be known there is no such "rule" for emitting 3 indexed paremeters. Most of the time it will cost more gas.
  • G-14 contains multiple false instances which, similar to issue #788, reference constants and immutables instead of storage variables.

Issue #298

  • Note that no findings contain explanations, nor do they illustrate how much gas would be saved for each instance.
  • G-01 is false as all instances point to return values, not function arguments
  • G-02 is false and lacks an explanation. Let it be known that this description is copied verbatim from a report that I wrote for NeoTokyo, which was entirely specific to the architecture of that codebase and does not translate to every mapping.
  • G-03 does not contain any explanations nor does it point to the storage variables that should be cached. Only two arbitrary blocks of code are provided.
  • G-05 lacks the same information and explanation as the finding G-03.
  • G-08 lacks an explanation and is false. Please see explanation provided above for issue 427, [G-11]

Issue #128

  • Note that some findings do not specify how much gas would be saved for each instance and no instances contain diffs to illustrate the necessary refactoring needed for the optimizations.
  • G-02 is false. Please see explanation provided above for issue 788, [G-12]
  • G-03 is false. Please see explanation provided above for issue 427, [G-11]
  • G-10 please see explanation provided above for issue 788, [G-08]
  • G-13 is false as solidity versions >=0.8.10 will not include those contract existence checks. Since the protocol's repo uses version 0.8.13, the existence checks are already excluded.
  • G-19 should not be conflated with using bools for storage. Using bools for stack/memory results in negligible gas costs.
  • Let it be known that the lookout and sponsor also had conflicts with the following findings in this report: G-02, G-07, G-11, G-14, G-17.

#3 - hansfriese

2023-05-19T11:34:17Z

I am so impressed with this comparison. We all know that the quality of this report is high. It's not fair if a quality report is underestimated by low quality reports. I will revisit as you encouraged.

#4 - hansfriese

2023-05-22T06:11:31Z

Your comparison is good, @0xJCN. I reviewed all of them again, and I downgraded #298. I also agree that #788, #427, #128 include many false findings, the quality of those reports are not satisfactory. But they still contain enough correct findings, so they deserve grade-a. This report mentions fewer categories of optimizations, but the best quality can be a huge plus. I will upgrade this to grade-a.

#5 - c4-judge

2023-05-22T06:12:21Z

hansfriese marked the issue as grade-a

#6 - 0xJCN

2023-05-22T06:43:46Z

Your comparison is good, @0xJCN. I reviewed all of them again, and I downgraded #298. I also agree that #788, #427, #128 include many false findings, the quality of those reports are not satisfactory. But they still contain enough correct findings, so they deserve grade-a. This report mentions fewer categories of optimizations, but the best quality can be a huge plus. I will upgrade this to grade-a.

Hi @hansfriese ,

Thank you for recognizing the quality of this report. I would just like to make one more note. Respectfully, by maintaining the other grades for the reports we discussed, you may be encouraging wardens to submit low quality/unsatisfactory reports that contain multiple false findings. The automated report already contains some false positives that the sponsor would need to sort through. In my opinion, it would not be beneficial to allow the sponsor to also have too deal with other reports which contain multiple false findings. I believe low quality reports should be discouraged, not rewarded. I may not agree with your final decision, mostly because I do not understand the rationale, but I appreciate you taking the time to revisit the issues I addressed.

#7 - hansfriese

2023-05-22T07:54:21Z

@0xJCN, Good point. Those false findings make us unhappy, and I also considered downgrading all of them. I accepted 3 of them again after careful review.

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