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
Rank: 30/199
Findings: 2
Award: $295.94
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: juancito
Also found by: 0xAgro, 0xNorman, 0xSmartContract, 0xStalin, 0xTheC0der, 0xWaitress, 0xhacksmithh, 0xnev, 3dgeville, 8olidity, Arz, Aymen0909, BGSecurity, BRONZEDISC, Bauchibred, Bauer, BenRai, ChainHunters, ChrisTina, CodeFoxInc, DedOhWale, DishWasher, EloiManuel, IceBear, Inspex, Jorgect, Kaysoft, LeoGold, LewisBroadhurst, Madalad, MiloTruck, MohammedRizwan, Nyx, Polaris_tow, RaymondFam, SaharDevep, SanketKogekar, Sathish9098, SolidityATL, Udsen, W0RR1O, aria, ayden, berlin-101, bin2chen, catellatech, codeslide, crc32, decade, descharre, evmboi32, eyexploit, fatherOfBlocks, georgits, giovannidisiena, joestakey, karanctf, kodyvim, ltyu, lukris02, m9800, matrix_0wl, mov, mrpathfindr, nadin, niser93, p0wd3r, parlayan_yildizlar_takimi, pavankv, pontifex, qpzm, ravikiranweb3, rbserver, santipu_, shealtielanz, slvDev, tnevler, wonjun, xmxanuel, yixxas
22.6007 USDC - $22.60
FILE: 2023-04-frankencoin/contracts/Position.sol 80: price = _mint * ONE_DEC18 / _coll; 98: uint256 reduction = (limit - minted - _minimum)/2; 122: return totalMint * (1000_000 - reserveContribution - calculateCurrentFee()) / 1000_000; 124: return totalMint * (1000_000 - reserveContribution) / 1000_000; 187: return uint32(mintingFeePPM - mintingFeePPM * (time - start) / (exp - start)); 239: return 1000000 * amountExcludingReserve / (1000000 - adjustedReservePPM); // 41 / (1-18%) = 50
FILE: FILE: 2023-04-frankencoin/contracts/Frankencoin.sol 118: return minterReserveE6 / 1000000;
FILE: 2023-04-frankencoin/contracts/Equity.sol 109: return VALUATION_FACTOR * zchf.equity() * ONE_DEC18 / totalSupply(); 161: voteAnchor[to] = uint64(anchorTime() - recipientVotes / newbalance); 173: return uint64(block.number << BLOCK_TIME_RESOLUTION_BITS);
FILE: 2023-04-frankencoin/contracts/MintingHub.sol 189: return (challenge.bid * 1005) / 1000;
Using the SafeCast library can help prevent unexpected errors in your Solidity code and make your contracts more secure
FILE: 2023-04-frankencoin/contracts/Position.sol 187: return uint32(mintingFeePPM - mintingFeePPM * (time - start) / (exp - start));
FILE: 2023-04-frankencoin/contracts/Equity.sol 146: totalVotesAtAnchor = uint192(totalVotes() - roundingLoss - lostVotes); 161: voteAnchor[to] = uint64(anchorTime() - recipientVotes / newbalance);
Consider using OpenZeppelin’s SafeCast library to prevent unexpected overflows when casting from uint256.
See this issue from a prior contest for details
FILE: 2023-04-frankencoin/contracts/ERC20PermitLight.sol function DOMAIN_SEPARATOR() public view returns (bytes32) { return keccak256( abi.encode( //keccak256("EIP712Domain(uint256 chainId,address verifyingContract)"); bytes32(0x47e79534a245952e8b16893a336b85a3d9ea9fa8c573f3d803afb92a79469218), block.chainid, address(this) ) );
The pragma version used are: 0.8.0
The minimum required version must be 0.8.17; otherwise, contracts will be affected by the following important bug fixes:
0.8.14:
ABI Encoder: When ABI-encoding values from calldata that contain nested arrays, correctly validate the nested array length against calldatasize() in all cases. Override Checker: Allow changing data location for parameters only when overriding external functions.
0.8.15
Code Generation: Avoid writing dirty bytes to storage when copying bytes arrays. Yul Optimizer: Keep all memory side-effects of inline assembly blocks.
0.8.16
Code Generation: Fix data corruption that affected ABI-encoding of calldata values represented by tuples: structs at any nesting level; argument lists of external functions, events and errors; return value lists of external functions. The 32 leading bytes of the first dynamically-encoded value in the tuple would get zeroed when the last component contained a statically-encoded array.
0.8.17 Yul Optimizer: Prevent the incorrect removal of storage writes before calls to Yul functions that conditionally terminate the external EVM call. Apart from these, there are several minor bug fixes and improvements
FILE: 2023-04-frankencoin/contracts/ERC20.sol 12: pragma solidity ^0.8.0; FILE: 2023-04-frankencoin/contracts/ERC20PermitLight.sol 5: pragma solidity ^0.8.0; FILE: 2023-04-frankencoin/contracts/StablecoinBridge.sol 2: pragma solidity ^0.8.0; FILE: 2023-04-frankencoin/contracts/StablecoinBridge.sol 2: pragma solidity ^0.8.0; FILE: 2023-04-frankencoin/contracts/PositionFactory.sol 2: pragma solidity ^0.8.0; FILE: 2023-04-frankencoin/contracts/MathUtil.sol 3: pragma solidity >=0.8.0 <0.9.0; FILE: 2023-04-frankencoin/contracts/Ownable.sol 9: pragma solidity ^0.8.0; FILE: 2023-04-frankencoin/contracts/Position.sol 2: pragma solidity ^0.8.0; FILE: 2023-04-frankencoin/contracts/MintingHub.sol 2: pragma solidity ^0.8.0; FILE: 2023-04-frankencoin/contracts/Equity.sol 4: pragma solidity >=0.8.0 <0.9.0; FILE: 2023-04-frankencoin/contracts/Frankencoin.sol 2: pragma solidity ^0.8.0;
Use abi.encode() instead which will pad items to 32 bytes, which will prevent hash collisions (e.g. abi.encodePacked(0x123,0x456) => 0x123456 => abi.encodePacked(0x1,0x23456), but abi.encode(0x123,0x456) => 0x0...1230...456). "Unless there is a compelling reason, abi.encode should be preferred". If there is only one argument to abi.encodePacked() it can often be cast to bytes() or bytes32() instead. If all arguments are strings and or bytes, bytes.concat() should be used instead
FILE: 2023-04-frankencoin/contracts/ERC20PermitLight.sol abi.encodePacked( "\x19\x01", DOMAIN_SEPARATOR(), keccak256( abi.encode( // keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"), bytes32(0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9), owner, spender, value, nonces[owner]++, deadline ) ) ) ), v, r, s );
Devoid of sanity/threshold/limit checks, critical parameters can be configured to invalid values, causing a variety of issues and breaking expected interactions within/between contracts. Consider adding proper uint256 validation as well as zero address checks for critical changes. A worst case scenario would render the contract needing to be re-deployed in the event of human/accidental errors that involve value assignments to immutable variables. If the validation procedure is unclear or too complex to implement on-chain, document the potential issues that could produce invalid values
FILE: 2023-04-frankencoin/contracts/Frankencoin.sol _minApplicationPeriod value is not checked before assigning to MIN_APPLICATION_PERIOD constructor(uint256 _minApplicationPeriod) ERC20(18){ MIN_APPLICATION_PERIOD = _minApplicationPeriod; reserve = new Equity(this); } function registerPosition(address _position) override external { if (!isMinter(msg.sender)) revert NotMinter(); positions[_position] = msg.sender; } function burn(uint256 _amount) external { _burn(msg.sender, _amount); } function isMinter(address _minter) override public view returns (bool){ return minters[_minter] != 0 && block.timestamp >= minters[_minter]; }
FILE: 2023-04-frankencoin/contracts/Equity.sol 112: function _beforeTokenTransfer(address from, address to, uint256 amount) override internal { 113: super._beforeTokenTransfer(from, to, amount);
FILE: 2023-04-frankencoin/contracts/MintingHub.sol function openPosition( address _collateralAddress, uint256 _minCollateral, uint256 _initialCollateral, uint256 _mintingMaximum, uint256 _initPeriodSeconds, uint256 _expirationSeconds, uint256 _challengeSeconds, uint32 _mintingFeePPM, uint256 _liqPrice, uint32 _reservePPM) public returns (address) {
Function calls made in unbounded loop are error-prone with potential resource exhaustion as it can trap the contract due to the gas limitations or failed transactions
FILE: 2023-04-frankencoin/contracts/Equity.sol for (uint i=0; i<helpers.length; i++){ address current = helpers[i]; for (uint256 i = 0; i<addressesToWipe.length; i++){ address current = addressesToWipe[0];
.transfer will relay 2300 gas and .call will relay all the gas. If the receive/fallback function from the recipient proxy contract has complex logic, using .transfer will fail, causing integration issues
FILE: 2023-04-frankencoin/contracts/Equity.sol 279: zchf.transfer(target, proceeds);
FILE: 2023-04-frankencoin/contracts/MintingHub.sol 294: challenge.position.collateral().transfer(challenge.challenger, challenge.size); 204: zchf.transfer(challenge.bidder, challenge.bid); // return old bid 211: challenge.position.collateral().transfer(msg.sender, challenge.size); 268: zchf.transfer(owner, effectiveBid - fundsNeeded);
FILE: 2023-04-frankencoin/contracts/Position.sol 253: IERC20(token).transfer(target, amount); 269: IERC20(collateral).transfer(target, amount);
At the start of the project, the system may need to be stopped or upgraded, I suggest you have a script beforehand and add it to the documentation. This can also be called an ” EMERGENCY STOP (CIRCUIT BREAKER) PATTERN “.
https://github.com/maxwoe/solidity_patterns/blob/master/security/EmergencyStop.sol
Here, the ecrecover() method doesn’t check the s range.
Homestead (EIP-2) added this limitation, however the precompile remained unaltered. The majority of libraries, including OpenZeppelin, do this check.
Since an order can only be confirmed once and its hash is saved, there doesn’t seem to be a serious danger in existing use cases
FILE: 2023-04-frankencoin/contracts/ERC20PermitLight.sol address recoveredAddress = ecrecover( keccak256( abi.encodePacked( "\x19\x01", DOMAIN_SEPARATOR(), keccak256( abi.encode( // keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"), bytes32(0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9), owner, spender, value, nonces[owner]++, deadline ) ) ) ), v, r, s );
It is not good practice to hardcode values, but if you are dealing with addresses much less, these can change between implementations, networks or projects, so it is convenient to remove these values from the source code
FILE: 2023-04-frankencoin/contracts/ERC20PermitLight.sol 41: bytes32(0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9), 66: bytes32(0x47e79534a245952e8b16893a336b85a3d9ea9fa8c573f3d803afb92a79469218),
owner value is not a constant value and can be changed with transferOwnership() function, before a function using setOwner state variable value in the project, transferOwnership function can be triggered by onlyOwner and operations can be blocked
FILE: 2023-04-frankencoin/contracts/Ownable.sol function transferOwnership(address newOwner) public onlyOwner { setOwner(newOwner); } /** * @dev Transfers ownership of the contract to a new account (`newOwner`). * Internal function without access restriction. */ function setOwner(address newOwner) internal { address oldOwner = owner; owner = newOwner; emit OwnershipTransferred(oldOwner, newOwner); }
FILE: 2023-04-frankencoin/contracts/Position.sol uint256 public immutable start; // timestamp when minting can start uint256 public immutable expiration; // timestamp at which the position expires address public immutable original; // originals point to themselves, clone to their origin address public immutable hub; // the hub this position was created by IFrankencoin public immutable zchf; // currency IERC20 public override immutable collateral; // collateral uint256 public override immutable minimumCollateral; // prevent dust amounts uint32 public immutable mintingFeePPM; uint32 public immutable reserveContribution; // in ppm
FILE: 2023-04-frankencoin/contracts/Frankencoin.sol 31: IReserve override public immutable reserve;
The above codes don’t follow Solidity’s standard naming convention,
internal and private functions : the mixedCase format starting with an underscore (_mixedCase starting with an underscore)
FILE: 2023-04-frankencoin/contracts/Position.sol 193: function mintInternal(address target, uint256 amount, uint256 collateral_) internal { 202: function restrictMinting(uint256 period) internal { 232: function repayInternal(uint256 burnable) internal { 240: function notifyRepaidInternal(uint256 amount) internal 268: function internalWithdrawCollateral(address target, uint256 amount) internal returns (uint256) { 282: function checkCollateral(uint256 collateralReserve, uint256 atPrice) internal view { 286: function emitUpdate() internal {
FILE: FILE: 2023-04-frankencoin/contracts/Frankencoin.sol 102: function allowanceInternal(address owner, address spender) internal view override returns (uint256) {
FILE: 2023-04-frankencoin/contracts/Equity.sol 144: function adjustTotalVotes(address from, uint256 amount, uint256 roundingLoss) internal { 157: function adjustRecipientVoteAnchor(address to, uint256 amount) internal returns (uint256){
While the compiler knows to optimize away the exponentiation, it’s still better coding practice to use idioms that do not require compiler optimization, if they exist
FILE: 2023-04-frankencoin/contracts/Frankencoin.sol 25: uint256 public constant MIN_FEE = 1000 * (10**18);
FILE: 2023-04-frankencoin/contracts/MintingHub.sol 20: uint256 public constant OPENING_FEE = 1000 * 10**18;
FILE: 2023-04-frankencoin/contracts/ERC20PermitLight.sol 32: unchecked {
FILE: 2023-04-frankencoin/contracts/ERC20PermitLight.sol 65: //keccak256("EIP712Domain(uint256 chainId,address verifyingContract)");
Some lines use // x and some use //x. The instances below point out the usages that don’t follow the majority, within each file
Some files use >=, some use ^. The instances below are examples of the method that has the fewest instances for a specific version. Note that using >= without also specifying <= will lead to failures to compile, or external project incompatability, when the major version changes and there are breaking-changes, so ^ should be preferred regardless of the instance counts
FILE: 2023-04-frankencoin/contracts/PositionFactory.sol 2: pragma solidity ^0.8.0; FILE: 2023-04-frankencoin/contracts/MathUtil.sol 3: pragma solidity >=0.8.0 <0.9.0;
There are units for seconds, minutes, hours, days, and weeks, and since they’re defined, they should be used
FILE: 2023-04-frankencoin/contracts/Equity.sol 51: uint8 private constant BLOCK_TIME_RESOLUTION_BITS = 24; 59: uint256 public constant MIN_HOLDING_DURATION = 90*7200 << BLOCK_TIME_RESOLUTION_BITS;
FILE : 2023-04-frankencoin/contracts/Ownable.sol function setOwner(address newOwner) internal { address oldOwner = owner; owner = newOwner; emit OwnershipTransferred(oldOwner, newOwner); }
The Solidity style guide recommends
Declare internal functions bellow the external/public functions
Consider defining in only one contract so that values cannot become out of sync when only one location is updated.
A cheap way to store constants in a single location is to create an internal constant in a library. If the variable is a local cache of another contract’s value, consider making the cache variable internal or private, which will require external users to query the contract with the source of truth, so that callers don’t get out of sync.
FILE: 2023-04-frankencoin/contracts/ERC20PermitLight.sol - 15: mapping(address => uint256) public nonces; + 15: mapping (address => uint256) public nonces;
It can’t be recovered if the tokens accidentally arrive at the contract address, which has happened to many popular projects, so I recommend adding a recovery code to your critical contracts
Add this code:
/** * @notice Sends ERC20 tokens trapped in contract to external address * @dev Onlyowner is allowed to make this function call * @param account is the receiving address * @param externalToken is the token being sent * @param amount is the quantity being sent * @return boolean value indicating whether the operation succeeded. * */ function rescueERC20(address account, address externalToken, uint256 amount) public onlyOwner returns (bool) { IERC20(externalToken).transfer(account, amount); return true; } }
The highest tier of smart contract behavior assurance is formal mathematical verification. All assertions that are made are guaranteed to be true across all inputs → The quality of your asserts is the quality of your verification
https://twitter.com/0xOwenThurm/status/1614359896350425088?t=dbG9gHFigBX85Rv29lOjIQ&s=19
If you use the constant first you support structures that veil programming errors. And one should only produce code either to add functionality, fix an programming error or trying to support structures to avoid programming errors (like design patterns).
https://www.moserware.com/2008/01/constants-on-left-are-better-but-this.html
Since this is a low level language that is more difficult to parse by readers, include extensive documentation, comments on the rationale behind its use, clearly explaining what each assembly instruction does.
This will make it easier for users to trust the code, for reviewers to validate the code, and for developers to build on or update the code.
Note that using Assembly removes several important security features of Solidity, which can make the code more insecure and more error-prone
FILE: 2023-04-frankencoin/contracts/PositionFactory.sol 39: assembly {
An important feature of Custom Error is that values such as address, tokenID, msg.value can be written inside the () sign, this kind of approach provides a serious advantage in debugging and examining the revert details of dapps such as tenderly
FILE: 2023-04-frankencoin/contracts/Position.sol 77: if(_coll < minimumCollateral) revert InsufficientCollateral(); 110: if (block.timestamp >= start) revert TooLate(); 194: if (minted + amount > limit) revert LimitExceeded(); 294: if (size < minimumCollateral && size < collateralBalance()) revert ChallengeTooSmall();
FILE: 2023-04-frankencoin/contracts/Position.sol 122: return totalMint * (1000_000 - reserveContribution - calculateCurrentFee()) / 1000_000; 124: return totalMint * (1000_000 - reserveContribution) / 1000_000;
FILE: 2023-04-frankencoin/contracts/MintingHub.sol 189: return (challenge.bid * 1005) / 1000; 217: uint256 earliestEnd = block.timestamp + 30 minutes; 265: uint256 reward = (volume * CHALLENGER_REWARD) / 1000_000;
FILE: 2023-04-frankencoin/contracts/Frankencoin.sol 118: return minterReserveE6 / 1000000; 166: uint256 usableMint = (_amount * (1000_000 - _feesPPM - _reservePPM)) / 1000_000; // rounding down is fine 205: uint256 theoreticalReserve = _reservePPM * mintedAmount / 1000000; 239: return 1000000 * amountExcludingReserve / (1000000 - adjustedReservePPM); // 41 / (1-18%) = 50
The normal if / else statement can be refactored in a shorthand way to write it:
Increases readability Shortens the overall SLOC
FILE: 2023-04-frankencoin/contracts/Position.sol 184: if (time >= exp){ return 0; } else { return uint32(mintingFeePPM - mintingFeePPM * (time - start) / (exp - start)); } 160: if (newPrice > price) { restrictMinting(3 days); } else { checkCollateral(collateralBalance(), newPrice); } 121: if (afterFees){ return totalMint * (1000_000 - reserveContribution - calculateCurrentFee()) / 1000_000; } else { return totalMint * (1000_000 - reserveContribution) / 1000_000; } 250: if (token == address(collateral)){ withdrawCollateral(target, amount); } else { IERC20(token).transfer(target, amount); }
FILE: 2023-04-frankencoin/contracts/MintingHub.sol 267: if (effectiveBid > fundsNeeded){ zchf.transfer(owner, effectiveBid - fundsNeeded); } else if (effectiveBid < fundsNeeded){ zchf.notifyLoss(fundsNeeded - effectiveBid); // ensure we have enough to pay everything }
FILE: 2023-04-frankencoin/contracts/Frankencoin.sol 141: if (balance <= minReserve){ return 0; } else { return balance - minReserve; } 207: if (currentReserve < minterReserve()){ // not enough reserves, owner has to take a loss return theoreticalReserve * currentReserve / minterReserve(); } else { return theoreticalReserve; }
time >= exp ? return 0 : return uint32(mintingFeePPM - mintingFeePPM * (time - start) / (exp - start));
Not all IERC20 implementations revert() when there’s a failure in approve(). The function signature has a boolean return value and they indicate errors that way instead. By not checking the return value, operations that should have marked as failed, may potentially go through without actually approving anything
FILE: 2023-04-frankencoin/contracts/ERC20.sol 109: _approve(msg.sender, spender, value); 132: _approve(sender, msg.sender, currentAllowance - amount);
#0 - c4-pre-sort
2023-04-27T11:04:37Z
0xA5DF marked the issue as low quality report
#1 - 0xA5DF
2023-04-27T11:06:51Z
Contains many false findings (e.g. L1, L3, L5) L2 is dupe of #393 though, and L7 partial dupe of #640
#2 - c4-judge
2023-05-17T04:10:38Z
hansfriese marked the issue as grade-b
🌟 Selected for report: c3phas
Also found by: 0xDACA, 0xRB, 0xSmartContract, 0xhacksmithh, 0xnev, Aymen0909, BenRai, Breeje, DishWasher, Erko, EvanW, JCN, MohammedRizwan, NoamYakov, Polaris_tow, Proxy, Rageur, Raihan, RaymondFam, ReyAdmirado, SAAJ, Sathish9098, Satyam_Sharma, Udsen, __141345__, aria, codeslide, decade, fatherOfBlocks, hunter_w3b, karanctf, matrix_0wl, nadin, naman1778, niser93, pavankv, petrichor, pfapostol, sebghatullah, slvDev, trysam2003, xmxanuel
273.342 USDC - $273.34
Instances(2)
Approximate gas saved: 500 gas
Caching will replace each Gwarmaccess (100 gas) with a much cheaper stack read. Less obvious fixes/optimizations include having local storage variables of mappings within state variable mappings or mappings within state variable structs, having local storage variables of structs within mappings, having local memory caches of state variable structs, or having local caches of state variable contracts/addresses.
FILE: 2023-04-frankencoin/contracts/Position.sol minted state variable should be cached with stack variable 141: if (newMinted < minted){ /// @audit minted cached zchf.burnFrom(msg.sender, minted - newMinted, reserveContribution); /// @audit minted cached minted = newMinted; } if (newCollateral < colbal){ withdrawCollateral(msg.sender, colbal - newCollateral); } // Must be called after collateral withdrawal if (newMinted > minted){ /// @audit minted cached mint(msg.sender, newMinted - minted); /// @audit minted cached } 241: if (amount > minted) revert RepaidTooMuch(amount - minted); minted -= amount; 349: uint256 repayment = minted < volumeZCHF ? minted : volumeZCHF;
Instances(2]1)
Approximate gas saved: 2100 gas
When fetching data from a storage location, assigning the data to a memory variable causes all fields of the struct/array to be read from storage, which incurs a Gcoldsload (2100 gas) for each field of the struct/array. If the fields are read from the new memory variable, they incur an additional MLOAD rather than a cheap stack read. Instead of declearing the variable with the memory keyword, declaring the variable with the storage keyword and caching any fields that need to be re-read in stack variables, will be much cheaper, only incuring the Gcoldsload for the fields actually read. The only time it makes sense to read the whole struct/array into a memory variable, is if the full struct/array is being returned by the function, is being passed to a function that requires memory, or if the array/struct is being read from another memory array/struct
FILE: 2023-04-frankencoin/contracts/MintingHub.sol 159: Challenge memory copy = Challenge(
Instances(12)
Need to declare 3 indexed fields for event parameters. If the event parameter is less than 3 should declare all event parameters indexed
FILE: 2023-04-frankencoin/contracts/Position.sol 41: event PositionOpened(address indexed owner, address original, address zchf, address collateral, uint256 price); 42: event MintingUpdate(uint256 collateral, uint256 price, uint256 minted, uint256 limit); 43: event PositionDenied(address indexed sender, string message); // emitted if closed by governance
FILE: 2023-04-frankencoin/contracts/MintingHub.sol 48: event ChallengeStarted(address indexed challenger, address indexed position, uint256 size, uint256 number); 49: event ChallengeAverted(address indexed position, uint256 number); 50: event ChallengeSucceeded(address indexed position, uint256 bid, uint256 number); 51: event NewBid(uint256 challengedId, uint256 bidAmount, address bidder); 52: event PostPonedReturn(address collateral, address indexed beneficiary, uint256 amount);
FILE: 2023-04-frankencoin/contracts/Equity.sol 90: event Delegation(address indexed from, address indexed to); // indicates a delegation 91: event Trade(address who, int amount, uint totPrice, uint newprice); // amount pos or neg for mint or redemption
FILE: 2023-04-frankencoin/contracts/Frankencoin.sol 52: event MinterApplied(address indexed minter, uint256 applicationPeriod, uint256 applicationFee, string message); 53: event MinterDenied(address indexed minter, string message);
Instances(6)
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.
FILE: 2023-04-frankencoin/contracts/Frankencoin.sol 45: mapping (address => uint256) public minters; 50: mapping (address => address) public positions;
FILE: 2023-04-frankencoin/contracts/Equity.sol 83: mapping (address => address) public delegates; 88: mapping (address => uint64) private voteAnchor;
FILE: 2023-04-frankencoin/contracts/ERC20.sol 43: mapping (address => uint256) private _balances; 45: mapping (address => mapping (address => uint256)) private _allowances;
Instances(24)
Devoid of sanity/threshold/limit checks, critical parameters can be configured to invalid values, causing a variety of issues and breaking expected interactions within/between contracts. Consider adding proper uint256 validation. A worst case scenario would render the contract needing to be re-deployed in the event of human/accidental errors that involve value assignments to immutable variables.
If any human/accidental errors happen need to redeploy the contract so this create the huge gas lose
FILE: 2023-04-frankencoin/contracts/Frankencoin.sol _minApplicationPeriod value is not checked before assigning to MIN_APPLICATION_PERIOD constructor(uint256 _minApplicationPeriod) ERC20(18){ MIN_APPLICATION_PERIOD = _minApplicationPeriod; reserve = new Equity(this); }
FILE: 2023-04-frankencoin/contracts/Position.sol constructor(address _owner, address _hub, address _zchf, address _collateral, uint256 _minCollateral, uint256 _initialLimit, uint256 initPeriod, uint256 _duration, uint256 _challengePeriod, uint32 _mintingFeePPM, uint256 _liqPrice, uint32 _reservePPM) { require(initPeriod >= 3 days); // must be at least three days, recommended to use higher values setOwner(_owner); original = address(this); hub = _hub; price = _liqPrice; zchf = IFrankencoin(_zchf); collateral = IERC20(_collateral); mintingFeePPM = _mintingFeePPM; reserveContribution = _reservePPM; minimumCollateral = _minCollateral; challengePeriod = _challengePeriod; start = block.timestamp + initPeriod; // one week time to deny the position cooldown = start; expiration = start + _duration; limit = _initialLimit; emit PositionOpened(_owner, original, _zchf, address(collateral), _liqPrice); }
FILE: 2023-04-frankencoin/contracts/MintingHub.sol constructor(address _zchf, address factory) { zchf = IFrankencoin(_zchf); POSITION_FACTORY = IPositionFactory(factory); }
FILE: 2023-04-frankencoin/contracts/Equity.sol constructor(Frankencoin zchf_) ERC20(18) { zchf = zchf_; }
FILE: 2023-04-frankencoin/contracts/StablecoinBridge.sol constructor(address other, address zchfAddress, uint256 limit_){ chf = IERC20(other); zchf = IFrankencoin(zchfAddress); horizon = block.timestamp + 52 weeks; limit = limit_; }
Instances(4)
Approximate Gas Saved: 36 gas
Using nested is cheaper than using && multiple check combinations. There are more advantages, such as easier to read code and better coverage reports.
As per Solidity reports possible to save 9 gas
FILE: FILE: 2023-04-frankencoin/contracts/Frankencoin.sol 84: if (_applicationPeriod < MIN_APPLICATION_PERIOD && totalSupply() > 0) revert PeriodTooShort(); 85: if (_applicationFee < MIN_FEE && totalSupply() > 0) revert FeeTooLow(); 267: if (!isMinter(msg.sender) && !isMinter(positions[msg.sender])) revert NotMinter();
FILE: 2023-04-frankencoin/contracts/Position.sol 294: if (size < minimumCollateral && size < collateralBalance()) revert ChallengeTooSmall();
Instances(1)
When we have a code expressionA || expressionB if expressionA is true then expressionB will not be evaluated and gas saved
FILE: FILE: 2023-04-frankencoin/contracts/Frankencoin.sol 106: } else if (isMinter(spender) || isMinter(isPosition(spender))){
Instances(2)
In Solidity, caching repeated function calls can be an effective way to optimize gas usage, especially when the function is called frequently with the same arguments
FILE: FILE: 2023-04-frankencoin/contracts/Frankencoin.sol totalSupply() value should be cached instead of calling multiple times 84: if (_applicationPeriod < MIN_APPLICATION_PERIOD && totalSupply() > 0) revert PeriodTooShort(); 85: if (_applicationFee < MIN_FEE && totalSupply() > 0) revert FeeTooLow();
FILE: 2023-04-frankencoin/contracts/Equity.sol anchorTime() function results should be cached function adjustTotalVotes(address from, uint256 amount, uint256 roundingLoss) internal { uint256 lostVotes = from == address(0x0) ? 0 : (anchorTime() - voteAnchor[from]) * amount; totalVotesAtAnchor = uint192(totalVotes() - roundingLoss - lostVotes); totalVotesAnchorTime = anchorTime(); }
Instances(10)
Checking non-zero transfer values can avoid an expensive external call and save gas. While this is done at some places, it’s not consistently done in the solution. I suggest adding a non-zero-value check here
FILE: FILE: 2023-04-frankencoin/contracts/Frankencoin.sol 87: _transfer(msg.sender, address(reserve), _applicationFee); 283: _transfer(address(reserve), msg.sender, _amount); 285: _transfer(address(reserve), msg.sender, reserveLeft); 254: _transfer(address(reserve), msg.sender, freedAmount - _amountExcludingReserve);
FILE: 2023-04-frankencoin/contracts/Equity.sol 279: zchf.transfer(target, proceeds);
FILE: 2023-04-frankencoin/contracts/MintingHub.sol 108: zchf.transferFrom(msg.sender, address(zchf.reserve()), OPENING_FEE); 129: existing.collateral().transferFrom(msg.sender, address(pos), _initialCollateral); 225: zchf.transferFrom(msg.sender, address(this), _bidAmountZCHF);
Instances(2)
In every iterations the new variables instance created this will consumes more gas . So just declare variables outside the loop and only use inside to save gas
FILE: 2023-04-frankencoin/contracts/Equity.sol 192: for (uint i=0; i<helpers.length; i++){ address current = helpers[i]; 312: for (uint256 i = 0; i<addressesToWipe.length; i++){ address current = addressesToWipe[0];
Instances(1)
FILE: ERC20.sol 240: function _beforeTokenTransfer(address from, address to, uint256 amount) virtual internal { }
Instances(3)
FILE: 2023-04-frankencoin/contracts/Position.sol modifier noCooldown() { if (block.timestamp <= cooldown) revert Hot(); _; } modifier noChallenge() { if (challengedAmount > 0) revert Challenged(); _; } modifier onlyHub() { if (msg.sender != address(hub)) revert NotHub(); _; }
Instances(5)
Approximate gas saved: 500 gas
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
FILE: 2023-04-frankencoin/contracts/Equity.sol 109: return VALUATION_FACTOR * zchf.equity() * ONE_DEC18 / totalSupply(); 243: uint256 equity = zchf.equity(); 279: zchf.transfer(target, proceeds); 292: uint256 capital = zchf.equity(); 310: require(zchf.equity() < MINIMUM_EQUITY);
Instances(3)
Short-circuiting is a solidity contract development model that uses OR/AND logic to sequence different cost operations. It puts low gas cost operations in the front and high gas cost operations in the back, so that if the front is low If the cost operation is feasible, you can skip (short-circuit) the subsequent high-cost Ethereum virtual machine operation.
//f(x) is a low gas cost operation //g(y) is a high gas cost operation //Sort operations with different gas costs as follows f(x) || g(y) f(x) && g(y)
FILE: 2023-04-frankencoin/contracts/Frankencoin.sol 84: if (_applicationPeriod < MIN_APPLICATION_PERIOD && totalSupply() > 0) revert PeriodTooShort(); 85: if (_applicationFee < MIN_FEE && totalSupply() > 0) revert FeeTooLow(); 267: if (!isMinter(msg.sender) && !isMinter(positions[msg.sender])) revert NotMinter();
Instances(3)
Saves 6 gas per instance
FILE: 2023-04-frankencoin/contracts/ERC20.sol 152: require(recipient != address(0)); 180: require(recipient != address(0));
FILE: 2023-04-frankencoin/contracts/ERC20PermitLight.sol 56: require(recoveredAddress != address(0) && recoveredAddress == owner, "INVALID_SIGNER");
Instances(7)
The normal if / else statement can be refactored in a shorthand way to write it:
Increases readability Shortens the overall SLOC
FILE: 2023-04-frankencoin/contracts/Position.sol 184: if (time >= exp){ return 0; } else { return uint32(mintingFeePPM - mintingFeePPM * (time - start) / (exp - start)); } 160: if (newPrice > price) { restrictMinting(3 days); } else { checkCollateral(collateralBalance(), newPrice); } 121: if (afterFees){ return totalMint * (1000_000 - reserveContribution - calculateCurrentFee()) / 1000_000; } else { return totalMint * (1000_000 - reserveContribution) / 1000_000; } 250: if (token == address(collateral)){ withdrawCollateral(target, amount); } else { IERC20(token).transfer(target, amount); }
FILE: 2023-04-frankencoin/contracts/MintingHub.sol 267: if (effectiveBid > fundsNeeded){ zchf.transfer(owner, effectiveBid - fundsNeeded); } else if (effectiveBid < fundsNeeded){ zchf.notifyLoss(fundsNeeded - effectiveBid); // ensure we have enough to pay everything }
FILE: 2023-04-frankencoin/contracts/Frankencoin.sol 141: if (balance <= minReserve){ return 0; } else { return balance - minReserve; } 207: if (currentReserve < minterReserve()){ // not enough reserves, owner has to take a loss return theoreticalReserve * currentReserve / minterReserve(); } else { return theoreticalReserve; }
time >= exp ? return 0 : return uint32(mintingFeePPM - mintingFeePPM * (time - start) / (exp - start));
Instances(1)
When writing conditional statements in smart contracts, it is generally best practice to order the conditions so that the less gas-consuming checks are performed first. This can help to optimize the gas usage of the contract and improve its overall efficiency
FILE: 2023-04-frankencoin/contracts/Frankencoin.sol 84: if (_applicationPeriod < MIN_APPLICATION_PERIOD && totalSupply() > 0) revert PeriodTooShort(); 85: if (_applicationFee < MIN_FEE && totalSupply() > 0) revert FeeTooLow(); 86: if (minters[_minter] != 0) revert AlreadyRegistered();
Instances(4)
If the functions are required by an interface, the contract should inherit from that interface and use the override keyword
FILE: 2023-04-frankencoin/contracts/MathUtil.sol 18: function _cubicRoot(uint256 _v) internal pure returns (uint256) { 39: function _power3(uint256 _x) internal pure returns(uint256) {
FILE: 2023-04-frankencoin/contracts/ERC20.sol 179: function _mint(address recipient, uint256 amount) internal virtual { 200: function _burn(address account, uint256 amount) internal virtual {
Instances(1)
Use uint256(1) and uint256(2) for true/false
FILE: 2023-04-frankencoin/contracts/MathUtil.sol 21: bool cond;
Instances(2)
One way to optimize your smart contract and reduce the gas cost is to avoid emitting constants. When you declare a constant in your contract, it is stored on the blockchain and takes up space. This can increase the cost of deploying your contract and make it more expensive to execute.
To avoid emitting constants, you can use inline assembly to perform arithmetic operations or bitwise operations. You can also use local variables instead of constants, and calculate the values you need at runtime
FILE: 2023-04-frankencoin/contracts/Position.sol original,collateral are immutable constants 69: emit PositionOpened(_owner, original, _zchf, address(collateral), _liqPrice); 85: emit PositionOpened(owner, original, address(zchf), address(collateral), _price);
#0 - 0xA5DF
2023-04-27T15:40:35Z
G7 isn't clear G11 seems false
#1 - c4-pre-sort
2023-04-27T15:40:39Z
0xA5DF marked the issue as high quality report
#2 - luziusmeisser
2023-05-03T04:24:24Z
G1 should be an optimization done by the compiler G2 leads to compiler error: "Type struct MintingHub.Challenge memory is not implicitly convertible to expected type struct MintingHub.Challenge storage pointer." G7, G14, G17 are basically the same and all wrong. The costs of checking a condition matters, but also the expected frequency. So the cost alone is not sufficient to decide which should come first.
Generally, many of these points are valid, but I value readability higher, so I do not implement them.
#3 - c4-sponsor
2023-05-03T04:24:31Z
luziusmeisser marked the issue as sponsor acknowledged
#4 - c4-judge
2023-05-16T15:29:05Z
hansfriese marked the issue as grade-a
#5 - noamyakov
2023-05-19T16:52:27Z
@hansfriese please review this report again. I think it should be graded as C because it has too many wrong optimizations.
Storage can't be used for new structs instances
Most of the instances are wrong because the value types don't fit in a single slot (for example uint256
and address
)
Not an optimization
The compiler already does that
Invalid, produces compilation error
Doesn't save gas
Doesn't save gas, code already follows this pattern of optimization
Not an optimization, increase execution cost
Doesn't save gas, code already follows this pattern of optimization
All the instances are wrong because they refer to virtual functions / library functions
The single instance doesn't use bool
for storage, it use it for a local variable
None of the instances emit constants
#6 - hansfriese
2023-05-22T05:15:40Z
G10, G11, G18, G19, G20 are invalid.