Frankencoin - NoamYakov'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: 31/199

Findings: 1

Award: $273.34

Gas:
grade-a

🌟 Selected for report: 0

šŸš€ Solo Findings: 0

Summary

Gas Optimizations

IssueInstances*Gas Saved**
[G‑01]Multiple address/ID mappings can be combined into a single mapping of an address/ID to a struct, where appropriate110,500
[G‑02]Avoid contract existence checks by using low level calls38At least 3800
[G‑03]Functions guaranteed to revert when called by normal users can be marked payable14At least 294
[G‑04]Multiple accesses of a mapping should use a local variable cache8At least 336
[G‑05]Multiple accesses of an array should use a local variable cache2-
[G‑06]State variables should be cached in stack variables rather than re-reading them from storage34At least 3,400
[G‑07]Add unchecked {} for subtractions where the operands cannot underflow because of a previous require() or if-statement16At least1360
[G‑08]The result of internal view function calls should be cached rather than re-calling the function1-

Total: 114 instances over 8 issues with at least 19,690 gas saved.

* Exluding any instance from the automated findings (in case the automated findings don't contain all instances).

** Gas totals use 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. Gas savings are calculated as the sum of all total savings of each public/external function (including any internal/external function call made by this function directly or indirectly) available for the end user. "At least" is written were I didn't calculated the savings of all the instances of an issue on each external call. For those instances which weren't taken into consideration, I calculated the raw savings of the instance - practically assuming that the ineffient code from that instance would be executed (directly or indirectly) on at least one public/external function available for the end user (and isn't dead code).

Gas Optimizations

[G‑01] Multiple address/ID mappings can be combined into a single mapping of an address/ID to a struct, where appropriate

Saves a storage slot for the mapping. Depending on the circumstances and sizes of types, can avoid a Gsset (20000 gas) per mapping combined. Reads and subsequent writes can also be cheaper when a function requires both values and they both fit in the same storage slot. Finally, if both fields are accessed in the same function, can save ~42 gas per access due to not having to recalculate the key's keccak256 hash (Gkeccak256 - 30 gas) and that calculation's associated stack operations.

There is 1 instance of this issue:

contracts\Equity.sol

/// @audit can be a mapping of an address to a struct of address and uint64
83      mapping (address => address) public delegates;
88      mapping (address => uint64) private voteAnchor; // 40 Bit for the block number, 24 Bit sub-block time resolution

[G‑02] Avoid contract existence checks by using low level calls

Prior to 0.8.10 the compiler inserted extra code, including EXTCODESIZE (100 gas), to check for contract existence for external function calls. In more recent solidity versions, the compiler will not insert these checks if the external call has a return value. Similar behavior can be achieved in earlier versions by using low-level calls, since low level calls never check for contract existence.

There are 38 instances of this issue:

contracts\Equity.sol

109         return VALUATION_FACTOR * zchf.equity() * ONE_DEC18 / totalSupply();

contracts\Equity.sol

243         uint256 equity = zchf.equity();

contracts\Equity.sol

263         return calculateSharesInternal(zchf.equity(), investment);

contracts\Equity.sol

279         zchf.transfer(target, proceeds);

contracts\Equity.sol

292         uint256 capital = zchf.equity();

contracts\Equity.sol

310         require(zchf.equity() < MINIMUM_EQUITY);

contracts\ERC20.sol

165             success = IERC677Receiver(recipient).onTokenTransfer(msg.sender, amount, data);

contracts\MintingHub.sol

93              POSITION_FACTORY.createNewPosition(
94                  msg.sender,
95                  address(zchf),
96                  _collateralAddress,
97                  _minCollateral,
98                  _mintingMaximum,
99                  _initPeriodSeconds,
100                 _expirationSeconds,
101                 _challengeSeconds,
102                 _mintingFeePPM,
103                 _liqPrice,
104                 _reservePPM
105             )

contracts\MintingHub.sol

108         zchf.transferFrom(msg.sender, address(zchf.reserve()), OPENING_FEE);

contracts\MintingHub.sol

110         IERC20(_collateralAddress).transferFrom(msg.sender, address(pos), _initialCollateral);

contracts\MintingHub.sol

116         require(zchf.isPosition(position) == address(this), "not our pos");

contracts\MintingHub.sol

126         uint256 limit = existing.reduceLimitForClone(_initialMint);

contracts\MintingHub.sol

127         address pos = POSITION_FACTORY.clonePosition(position);

contracts\MintingHub.sol

129         existing.collateral().transferFrom(msg.sender, address(pos), _initialCollateral);

contracts\MintingHub.sol

129         existing.collateral().transferFrom(msg.sender, address(pos), _initialCollateral);

contracts\MintingHub.sol

142         IERC20(position.collateral()).transferFrom(msg.sender, address(this), _collateralAmount);

contracts\MintingHub.sol

142         IERC20(position.collateral()).transferFrom(msg.sender, address(this), _collateralAmount);

contracts\MintingHub.sol

144         challenges.push(Challenge(msg.sender, position, _collateralAmount, block.timestamp + position.challengePeriod(), address(0x0), 0));

contracts\MintingHub.sol

170         uint256 min = IPosition(challenge.position).minimumCollateral();

contracts\MintingHub.sol

204             zchf.transfer(challenge.bidder, challenge.bid); // return old bid

contracts\MintingHub.sol

210             zchf.transferFrom(msg.sender, challenge.challenger, _bidAmountZCHF);

contracts\MintingHub.sol

225             zchf.transferFrom(msg.sender, address(this), _bidAmountZCHF);

contracts\MintingHub.sol

263             IERC20(zchf).transfer(challenge.bidder, challenge.bid - effectiveBid);

contracts\MintingHub.sol

268             zchf.transfer(owner, effectiveBid - fundsNeeded);

contracts\MintingHub.sol

272         zchf.transfer(challenge.challenger, reward); // pay out the challenger reward

contracts\MintingHub.sol

284         IERC20(collateral).transfer(target, amount);

contracts\Position.sol

111         IReserve(zchf.reserve()).checkQualified(msg.sender, helpers);

contracts\Position.sol

138             collateral.transferFrom(msg.sender, address(this), newCollateral - colbal);

contracts\Position.sol

142             zchf.burnFrom(msg.sender, minted - newMinted, reserveContribution);

contracts\Position.sol

170         return IERC20(collateral).balanceOf(address(this));

contracts\Position.sol

228         IERC20(zchf).transferFrom(msg.sender, address(this), amount);

contracts\Position.sol

233         uint256 actuallyBurned = IFrankencoin(zchf).burnWithReserve(burnable, reserveContribution);

contracts\Position.sol

253             IERC20(token).transfer(target, amount);

contracts\Position.sol

269         IERC20(collateral).transfer(target, amount);

contracts\PositionFactory.sol

32          Position clone = Position(createClone(existing.original()));

contracts\StablecoinBridge.sol

45          chf.transferFrom(msg.sender, address(this), amount);

contracts\StablecoinBridge.sol

51          require(chf.balanceOf(address(this)) <= limit, "limit");

contracts\StablecoinBridge.sol

69          chf.transfer(target, amount);

[G‑03] Functions guaranteed to revert when called by normal users can be marked payable

If a function modifier such as onlyOwner is used, the function will revert if a normal user tries to pay the function. Marking the function as payable will lower the gas cost for legitimate callers because the compiler will not include checks for whether a payment was provided. The extra opcodes avoided are CALLVALUE(2),DUP1(3),ISZERO(3),PUSH2(3),JUMPI(10),PUSH1(3),DUP1(3),REVERT(0),JUMPDEST(1),POP(2), which costs an average of about 21 gas per call to the function, in addition to the extra deployment cost.

There are 14 instances of this issue:

contracts\Equity.sol

241     function onTokenTransfer(address from, uint256 amount, bytes calldata) external returns (bool) {
242         require(msg.sender == address(zchf), "caller must be zchf");

contracts\Frankencoin.sol

172    function mint(address _target, uint256 _amount) override external minterOnly {

contracts\Frankencoin.sol

262    function burn(address _owner, uint256 _amount) override external minterOnly {

contracts\Position.sol

76      function initializeClone(address owner, uint256 _price, uint256 _limit, uint256 _coll, uint256 _mint) external onlyHub {

contracts\Position.sol

97      function reduceLimitForClone(uint256 _minimum) external noChallenge noCooldown alive onlyHub returns (uint256) {

contracts\Position.sol

132     function adjust(uint256 newMinted, uint256 newCollateral, uint256 newPrice) public onlyOwner {

contracts\Position.sol

159     function adjustPrice(uint256 newPrice) public onlyOwner noChallenge {

contracts\Position.sol

177     function mint(address target, uint256 amount) public onlyOwner noChallenge noCooldown alive {

contracts\Position.sol

227     function repay(uint256 amount) public onlyOwner {

contracts\Position.sol

249     function withdraw(address token, address target, uint256 amount) external onlyOwner {

contracts\Position.sol

263     function withdrawCollateral(address target, uint256 amount) public onlyOwner noChallenge noCooldown {

contracts\Position.sol

292     function notifyChallengeStarted(uint256 size) external onlyHub {

contracts\Position.sol

304     function tryAvertChallenge(uint256 _collateralAmount, uint256 _bidAmountZCHF) external onlyHub returns (bool) {

contracts\Position.sol

329     function notifyChallengeSucceeded(address _bidder, uint256 _bid, uint256 _size) external onlyHub returns (address, uint256, uint256, uint256, uint32) {

[G‑04] Multiple accesses of a mapping should use a local variable cache

The instances below point to the second+ access of a value inside a mapping, within a function. Caching a mapping's value in a local storage or calldata variable when the value is accessed multiple times, saves ~42 gas per access due to not having to recalculate the key's keccak256 hash (Gkeccak256 - 30 gas) and that calculation's associated stack operations.

There are 8 instances of this issue:

contracts\ERC20.sol

/// @audit _balances[sender] on line 155
155         if (_balances[sender] < amount) revert ERC20InsufficientBalance(sender, _balances[sender], amount);

contracts\ERC20.sol

/// @audit _balances[sender] on line 155
156         _balances[sender] -= amount;

contracts\Frankencoin.sol

/// @audit minters[_minter] on line 86
88        minters[_minter] = block.timestamp + _applicationPeriod;

contracts\Frankencoin.sol

/// @audit minters[_minter] on line 153
155       delete minters[_minter];

contracts\Frankencoin.sol

/// @audit minters[_minter] on line 294
294       return minters[_minter] != 0 && block.timestamp >= minters[_minter];

contracts\MintingHub.sol

/// @audit pendingReturns[collateral] on line 282
283         delete pendingReturns[collateral][msg.sender];

contracts\MintingHub.sol

/// @audit pendingReturns[collateral][msg.sender] on line 282
283         delete pendingReturns[collateral][msg.sender];

contracts\MintingHub.sol

/// @audit pendingReturns[collateral] on line 282
283         delete pendingReturns[collateral][msg.sender];

[G‑05] Multiple accesses of an array should use a local variable cache

The instances below point to the second+ access of a value inside an array, within a function. Caching an array's element avoids recalculating the array offsets into memory/calldata and avoids the boundaries check.

There are 2 instances of this issue:

contracts\MintingHub.sol

/// @audit challenges[_challengeNumber] on line 200
213             delete challenges[_challengeNumber];

contracts\MintingHub.sol

/// @audit challenges[_challengeNumber] on line 253
275         delete challenges[_challengeNumber];

[G‑06] State variables should be cached in stack variables rather than re-reading them from storage

The instances below point to the second+ access of a state variable within a function. Caching of a state variable replaces each Gwarmaccess (100 gas) with a much cheaper stack read. Other less obvious fixes/optimizations include having local memory caches of state variable structs, or having local caches of state variable contracts/addresses.

There are 34 instances of this issue:

contracts\ERC20.sol

/// @audit _balances[sender] on line 155
155         if (_balances[sender] < amount) revert ERC20InsufficientBalance(sender, _balances[sender], amount);

contracts\ERC20.sol

/// @audit _balances[sender] on line 155
156         _balances[sender] -= amount;

contracts\Frankencoin.sol

/// @audit minters[_minter] on line 294
294       return minters[_minter] != 0 && block.timestamp >= minters[_minter];

contracts\MintingHub.sol

/// @audit challenge.challenger on line 158
160             challenge.challenger,

contracts\MintingHub.sol

/// @audit challenge.bid on line 165
167         challenge.bid -= copy.bid;

contracts\MintingHub.sol

/// @audit challenge.size on line 165
168         challenge.size -= copy.size;

contracts\MintingHub.sol

/// @audit challenge.position on line 161
170         uint256 min = IPosition(challenge.position).minimumCollateral();

contracts\MintingHub.sol

/// @audit challenge.size on line 168
171         require(challenge.size >= min);

contracts\MintingHub.sol

/// @audit challenge.challenger on line 158
176         emit ChallengeStarted(challenge.challenger, address(challenge.position), challenge.size, _challengeNumber);

contracts\MintingHub.sol

/// @audit challenge.position on line 161
176         emit ChallengeStarted(challenge.challenger, address(challenge.position), challenge.size, _challengeNumber);

contracts\MintingHub.sol

/// @audit challenge.size on line 168
176         emit ChallengeStarted(challenge.challenger, address(challenge.position), challenge.size, _challengeNumber);

contracts\MintingHub.sol

/// @audit challenge.bid on line 203
204             zchf.transfer(challenge.bidder, challenge.bid); // return old bid

contracts\MintingHub.sol

/// @audit challenge.size on line 202
208         if (challenge.position.tryAvertChallenge(challenge.size, _bidAmountZCHF)) {

contracts\MintingHub.sol

/// @audit challenge.position on line 208
211             challenge.position.collateral().transfer(msg.sender, challenge.size);

contracts\MintingHub.sol

/// @audit challenge.size on line 202
211             challenge.position.collateral().transfer(msg.sender, challenge.size);

contracts\MintingHub.sol

/// @audit challenge.position on line 208
212             emit ChallengeAverted(address(challenge.position), _challengeNumber);

contracts\MintingHub.sol

/// @audit challenge.end on line 201
218             if (earliestEnd >= challenge.end) {

contracts\MintingHub.sol

/// @audit challenge.bidder on line 259
259         address recipient = challenge.bidder == address(0x0) ? msg.sender : challenge.bidder;

contracts\MintingHub.sol

/// @audit challenge.bid on line 260
261         if (effectiveBid < challenge.bid) {

contracts\MintingHub.sol

/// @audit challenge.bidder on line 259
263             IERC20(zchf).transfer(challenge.bidder, challenge.bid - effectiveBid);

contracts\MintingHub.sol

/// @audit challenge.bid on line 260
263             IERC20(zchf).transfer(challenge.bidder, challenge.bid - effectiveBid);

contracts\MintingHub.sol

/// @audit challenge.challenger on line 254
272         zchf.transfer(challenge.challenger, reward); // pay out the challenger reward

contracts\MintingHub.sol

/// @audit challenge.position on line 260
273         emit ChallengeSucceeded(address(challenge.position), challenge.bid, _challengeNumber);

contracts\MintingHub.sol

/// @audit challenge.bid on line 260
273         emit ChallengeSucceeded(address(challenge.position), challenge.bid, _challengeNumber);

contracts\MintingHub.sol

/// @audit challenge.challenger on line 291
292             emit PostPonedReturn(collateral, challenge.challenger, challenge.size);

contracts\MintingHub.sol

/// @audit challenge.size on line 291
292             emit PostPonedReturn(collateral, challenge.challenger, challenge.size);

contracts\Position.sol

/// @audit price on line 80
81          if (price > _price) revert InsufficientCollateral();

contracts\Position.sol

/// @audit limit on line 98
99          limit -= reduction + _minimum;

contracts\Position.sol

/// @audit minted on line 141
142             zchf.burnFrom(msg.sender, minted - newMinted, reserveContribution);

contracts\Position.sol

/// @audit minted on line 149
150             mint(msg.sender, newMinted - minted);

contracts\Position.sol

/// @audit minted on line 194
196         minted += amount;

contracts\Position.sol

/// @audit minted on line 241
241         if (amount > minted) revert RepaidTooMuch(amount - minted);

contracts\Position.sol

/// @audit minted on line 241
242         minted -= amount;

contracts\Position.sol

/// @audit minted on line 349
349         uint256 repayment = minted < volumeZCHF ? minted : volumeZCHF; // how much must be burned to make things even

[G‑07] Add unchecked {} for subtractions where the operands cannot underflow because of a previous require() or if-statement

require(a <= b); x = b - a => require(a <= b); unchecked { x = b - a }.

There are 16 instances of this issue:

contracts\Equity.sol

/// @audit equity >= amount because of the ternary condition on line 247
247         uint256 shares = equity <= amount ? 1000 * ONE_DEC18 : calculateSharesInternal(equity - amount, amount);

contracts\ERC20.sol

/// @audit currentAllowance >= amount because of the if condition on line 131
132             _approve(sender, msg.sender, currentAllowance - amount);

contracts\Frankencoin.sol

/// @audit balance >= minReserve because of the if condition on line 141
144         return balance - minReserve;

contracts\Frankencoin.sol

/// @audit _amount >= usableMint because of the calculation of usableMint on line 166
168       _mint(address(reserve), _amount - usableMint); // rest goes to equity as reserves or as fees

contracts\Frankencoin.sol

/// @audit _amount >= reserveLeft because of the if condition on line 282
286          _mint(msg.sender, _amount - reserveLeft);

contracts\MathUtil.sol

/// @audit xOld >= x because of the ternary condition on line 26
26              cond = xOld > x ? xOld - x > THRESH_DEC18 : x - xOld > THRESH_DEC18;

contracts\MathUtil.sol

/// @audit x >= xOld because of the ternary condition on line 26
26              cond = xOld > x ? xOld - x > THRESH_DEC18 : x - xOld > THRESH_DEC18;

contracts\MintingHub.sol

/// @audit challenge.bid >= effectiveBid because of the if condition on line 261
263             IERC20(zchf).transfer(challenge.bidder, challenge.bid - effectiveBid);

contracts\MintingHub.sol

/// @audit effectiveBid >= fundsNeeded because of the if condition on line 267
268             zchf.transfer(owner, effectiveBid - fundsNeeded);

contracts\MintingHub.sol

/// @audit fundsNeeded >= effectiveBid because of the if condition on line 269
270             zchf.notifyLoss(fundsNeeded - effectiveBid); // ensure we have enough to pay everything

contracts\Position.sol

/// @audit newCollateral >= colbal because of the if condition on line 137
138             collateral.transferFrom(msg.sender, address(this), newCollateral - colbal);

contracts\Position.sol

/// @audit minted >= newMinted because of the if condition on line 141
142             zchf.burnFrom(msg.sender, minted - newMinted, reserveContribution);

contracts\Position.sol

/// @audit colbal >= newCollateral because of the if condition on line 145
146             withdrawCollateral(msg.sender, colbal - newCollateral);

contracts\Position.sol

/// @audit newMinted >= minted because of the if condition on line 149
150             mint(msg.sender, newMinted - minted);

contracts\Position.sol

/// @audit amount >= minted because of the if condition on line 241
241         if (amount > minted) revert RepaidTooMuch(amount - minted);

contracts\Position.sol

/// @audit minted >= amount because of the if condition on line 241
242         minted -= amount;

[G‑08] The result of internal function calls should be cached rather than re-calling the function

The instances below point to the second+ call of the function within a single function.

There is 1 instance of this issue:

contracts\MintingHub.sol

/// @audit minBid(challenge) on line 216
216             if (_bidAmountZCHF < minBid(challenge)) revert BidTooLow(_bidAmountZCHF, minBid(challenge));

#0 - hansfriese

2023-05-16T13:30:40Z

3 is in automated findings

#1 - c4-judge

2023-05-16T13:30:47Z

hansfriese marked the issue as grade-b

#2 - noamyakov

2023-05-18T20:23:36Z

@hansfriese the instances listed under 3 aren't in the automated findings.

* Exluding any instance from the automated findings (in case the automated findings don't contain all instances).

The optimizations suggested in this gas report saves more gas than those suggested in the one that was selected for the report (#956). Please review this gas report again and reconsider your decision for the best gas report.

#3 - c3phas

2023-05-19T13:47:48Z

Saw your reference, Most of the instances on G-02 are also not valid, this is because the project is using compiler version 0.8.13 and the contracts are using a floating pragma ^0.8 meaning they would compile using 0.8.13

#4 - c3phas

2023-05-19T13:54:51Z

Also the judge may miss your comment during QA unless you point him towards it. Just pop on the discussion and inform him you left some comments

#5 - noamyakov

2023-05-19T14:03:11Z

Thank you @c3phas for your feedback.

I'm aware of that, but these optimizations are still valid because the goal is to optimize the project's code as it is. That's how it has always been in C4 competitions.

#6 - hansfriese

2023-05-22T05:57:13Z

After careful review of this report and other reports, this crosses the border of grade-a and one more plus is fewer false positives for this report.

#7 - c4-judge

2023-05-22T05:57:56Z

hansfriese marked the issue as grade-a

#8 - noamyakov

2023-05-23T12:11:36Z

@hansfriese thanks. Have you reconsidered your decision for the best gas report? Do you think report #956 is better?

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