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: 82/199
Findings: 2
Award: $43.63
π 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
Issue | Instances | ||
---|---|---|---|
[Lβ01] | For immutable variables, Zero address and zero value checks are missing in constructor | 10 | |
[Lβ02] | Withdraw to zero address should be avoided | 1 |
Issue | Instances | ||
---|---|---|---|
[Nβ01] | For internal functions, follow Solidity standard naming conventions | 21 | |
[Nβ02] | Solidity compiler version should be exactly same in all smart contracts | 10 | |
[Nβ03] | Use named parameters for mapping type declarations | 8 | |
[Nβ04] | Use scientific notation (e.g. 1e18) rather than exponentiation (e.g. 10**18) | 4 |
Zero address and zero value checks should be used in the constructors, to avoid the risk of setting a storage variable as zero address and zero value at the time of deployment. There are 10 instances of this issue.
In Position.sol contract constructor, Below are immutable variables.
File: contracts/Position.sol 24 uint256 public immutable challengePeriod; // challenge period in seconds --- 29 uint256 public immutable start; // timestamp when minting can start 30 uint256 public immutable expiration; // timestamp at which the position expires 31 32 address public immutable original; // originals point to themselves, clone to their origin 33 address public immutable hub; // the hub this position was created by 34 IFrankencoin public immutable zchf; // currency 35 IERC20 public override immutable collateral; // collateral 36 uint256 public override immutable minimumCollateral; // prevent dust amounts 37 38 uint32 public immutable mintingFeePPM; 39 uint32 public immutable reserveContribution; // in ppm
File: contracts/Position.sol 50 constructor(address _owner, address _hub, address _zchf, address _collateral, 51 uint256 _minCollateral, uint256 _initialLimit, uint256 initPeriod, uint256 _duration, 52 uint256 _challengePeriod, uint32 _mintingFeePPM, uint256 _liqPrice, uint32 _reservePPM) { 53 require(initPeriod >= 3 days); // must be at least three days, recommended to use higher values 54 setOwner(_owner); 55 original = address(this); 56 hub = _hub; 57 price = _liqPrice; 58 zchf = IFrankencoin(_zchf); 59 collateral = IERC20(_collateral); 60 mintingFeePPM = _mintingFeePPM; 61 reserveContribution = _reservePPM; 62 minimumCollateral = _minCollateral; 63 challengePeriod = _challengePeriod; 64 start = block.timestamp + initPeriod; // one week time to deny the position 65 cooldown = start; 66 expiration = start + _duration; 67 limit = _initialLimit; 68 69 emit PositionOpened(_owner, original, _zchf, address(collateral), _liqPrice); 70 }
Immutable variables once set during deployment can not be changed, It is better not to take risk and do the address and value validation so that error should not happen. Below is the recommended code.
File: 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(_hub != address(0), "invalid address"); + require(_zchf != address(0), "invalid address"); + require(_collateral != address(0), "invalid address"); + require(_minCollateral != 0, "invalid amount"); + require(_challengePeriod != 0, "invalid period"); + require(_mintingFeePPM != 0, "invalid fee"); + require(_reservePPM != 0, "invalid amount"); 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); } }
In MintingHub.sol contract constructor, Below are immutable variables.
File: contracts/MintingHub.sol 28 IPositionFactory private immutable POSITION_FACTORY; // position contract to clone --- 30 IFrankencoin public immutable zchf; // currency
File: contracts/MintingHub.sol 54 constructor(address _zchf, address factory) { 55 zchf = IFrankencoin(_zchf); 56 POSITION_FACTORY = IPositionFactory(factory); 57 }
Immutable variables once set during deployment can not be changed, It is better not to take risk and do the address and value validation so that error should not happen. Below is the recommended code.
File: contracts/MintingHub.sol constructor(address _zchf, address factory) { + require(_zchf != address(0), "invalid address"); + require(factory != address(0), "invalid address"); zchf = IFrankencoin(_zchf); POSITION_FACTORY = IPositionFactory(factory); }
In Frankencoin.sol contract constructor, Below are immutable variables.
File: contracts/Frankencoin.sol 26 uint256 public immutable MIN_APPLICATION_PERIOD; // for example 10 days --- 31 IReserve override public immutable reserve;
File: contracts/Frankencoin.sol 59 constructor(uint256 _minApplicationPeriod) ERC20(18){ 60 MIN_APPLICATION_PERIOD = _minApplicationPeriod; 61 reserve = new Equity(this); 62 }
File: contracts/Frankencoin.sol constructor(uint256 _minApplicationPeriod) ERC20(18){ + require(_minApplicationPeriod != 0, "invalid period"); MIN_APPLICATION_PERIOD = _minApplicationPeriod; reserve = new Equity(this); }
Withdraw collateral or tokens must be avoided to address(0) as if mistakenly sent to address(0), it can not be recovered. withdraw() can be accessed by onlyOwner but address(0) transfer can not be rule out as onlyOwner can also transfer by mistake to address(0). There is 1 instance of this issue.
File: contracts/Position.sol 249 function withdraw(address token, address target, uint256 amount) external onlyOwner { 250 if (token == address(collateral)){ 251 withdrawCollateral(target, amount); 252 } else { 253 IERC20(token).transfer(target, amount); 254 } 255 }
Add zero address check validation in withdraw function to prevent this issue.
File: contracts/Position.sol function withdraw(address token, address target, uint256 amount) external onlyOwner { + require(target != address(0), "invalid address"); if (token == address(collateral)){ withdrawCollateral(target, amount); } else { IERC20(token).transfer(target, amount); } }
Proper use of _ as a function name prefix should be taken care and a common pattern is to prefix internal and private function names with _. This is partially incorporated in contracts. Below internal functions does not follow this pattern which leads to confusion while reading code and affects overall readability of the code.
There are 21 instances of this issue:
File: contracts/Position.sol 169 function collateralBalance() internal view returns (uint256){
File: contracts/Position.sol 193 function mintInternal(address target, uint256 amount, uint256 collateral_) internal {
File: contracts/Position.sol 202 function restrictMinting(uint256 period) internal {
File: contracts/Position.sol 232 function repayInternal(uint256 burnable) internal {
File: contracts/Position.sol 240 function notifyRepaidInternal(uint256 amount) internal {
File: contracts/Position.sol 268 function internalWithdrawCollateral(address target, uint256 amount) internal returns (uint256) {
File: contracts/Position.sol 282 function checkCollateral(uint256 collateralReserve, uint256 atPrice) internal view {
File: contracts/Position.sol 286 function emitUpdate() internal {
File: contracts/MintingHub.sol 188 function minBid(Challenge storage challenge) internal view returns (uint256) {
File: contracts/MintingHub.sol 287 function returnCollateral(Challenge storage challenge, bool postpone) internal {
File: contracts/Equity.sol 144 function adjustTotalVotes(address from, uint256 amount, uint256 roundingLoss) internal {
File: contracts/Equity.sol 157 function adjustRecipientVoteAnchor(address to, uint256 amount) internal returns (uint256){
File: contracts/Equity.sol 172 function anchorTime() internal view returns (uint64){
File: contracts/Equity.sol 225 function canVoteFor(address delegate, address owner) internal view returns (bool) {
File: contracts/Equity.sol 266 function calculateSharesInternal(uint256 capitalBefore, uint256 investment) internal view returns (uint256) {
File: contracts/Frankencoin.sol 102 function allowanceInternal(address owner, address spender) internal view override returns (uint256) {
File: contracts/ERC20.sol 97 function allowanceInternal(address owner, address spender) internal view virtual returns (uint256) {
File: contracts/Ownable.sol 39 function setOwner(address newOwner) internal {
File: contracts/PositionFactory.sol 37 function createClone(address target) internal returns (address result) {
File: contracts/StablecoinBridge.sol 49 function mintInternal(address target, uint256 amount) internal {
File: contracts/StablecoinBridge.sol 67 function burnInternal(address zchfHolder, address target, uint256 amount) internal {
Solidity compiler version should be exactly same in all smart contracts. Different Solidity compiler versions are used, the following contracts have mix versions.
There are 10 instances of this issue.
File: contracts/Position.sol 2 pragma solidity ^0.8.0;
File: contracts/Equity.sol 4 pragma solidity >=0.8.0 <0.9.0;
File: contracts/ERC20.sol 14 pragma solidity ^0.8.0;
File: contracts/MathUtil.sol 3 pragma solidity >=0.8.0 <0.9.0;
File: contracts/ERC20PermitLight.sol 5 pragma solidity ^0.8.0;
File: contracts/Frankencoin.sol 2 pragma solidity ^0.8.0;
File: contracts/MintingHub.sol 2 pragma solidity ^0.8.0;
File: contracts/StablecoinBridge.sol 2 pragma solidity ^0.8.0;
File: contracts/PositionFactory.sol 2 pragma solidity ^0.8.0;
File: contracts/Ownable.sol 9 pragma solidity ^0.8.0;
Versions must be consistent with each other.
Consider using named parameters in mappings (e.g. mapping(address account => uint256 balance)) to improve readability. This feature is present since Solidity 0.8.18. There are 8 instances of this issue.
File: contracts/MintingHub.sol 37 mapping (address /** col */ => mapping (address => uint256)) public pendingReturns;
File: contracts/Equity.sol 83 mapping (address => address) public delegates; --- 88 mapping (address => uint64) private voteAnchor; // 40 Bit for the block number, 24 Bit sub-block time resolution
File: contracts/Frankencoin.sol 45 mapping (address => uint256) public minters; --- 50 mapping (address => address) public positions;
File: contracts/ERC20.sol 43 mapping (address => uint256) private _balances; 44 45 mapping (address => mapping (address => uint256)) private _allowances;
File: contracts/ERC20PermitLight.sol 15 mapping(address => uint256) public nonces;
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 There are 4 instances of this issue:
File: contracts/Equity.sol 253 require(totalSupply() < 2**128, "total supply exceeded");
File: contracts/Frankencoin.sol 25 uint256 public constant MIN_FEE = 1000 * (10**18);
File: contracts/MathUtil.sol 10 uint256 internal constant ONE_DEC18 = 10**18;
File: contracts/MintingHub.sol 20 uint256 public constant OPENING_FEE = 1000 * 10**18;
#0 - hansfriese
2023-05-18T05:45:20Z
N2, N4 are in automated findings
#1 - c4-judge
2023-05-18T05:45:25Z
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
21.0255 USDC - $21.03
Issue | Instances | ||
---|---|---|---|
[Gβ01] | Avoid using state variable in emit | 1 | |
[Gβ02] | Use nested if and, avoid multiple check combinations | 4 | |
[Gβ03] | Zero value check in mint function, can save gas for minter | 1 |
While emitting events, Passing parameter variables will cost less as compared to passing the storage values. There are 1 instances of this issues.
File: contracts/Position.sol 50 constructor(address _owner, address _hub, address _zchf, address _collateral, 51 uint256 _minCollateral, uint256 _initialLimit, uint256 initPeriod, uint256 _duration, 52 uint256 _challengePeriod, uint32 _mintingFeePPM, uint256 _liqPrice, uint32 _reservePPM) { 53 require(initPeriod >= 3 days); // must be at least three days, recommended to use higher values 54 setOwner(_owner); 55 original = address(this); 56 hub = _hub; 57 price = _liqPrice; 58 zchf = IFrankencoin(_zchf); 59 collateral = IERC20(_collateral); 60 mintingFeePPM = _mintingFeePPM; 61 reserveContribution = _reservePPM; 62 minimumCollateral = _minCollateral; 63 challengePeriod = _challengePeriod; 64 start = block.timestamp + initPeriod; // one week time to deny the position 65 cooldown = start; 66 expiration = start + _duration; 67 limit = _initialLimit; 68 69 emit PositionOpened(_owner, original, _zchf, address(collateral), _liqPrice); 70 }
By incorporating below recommendation, Some gas can be saved.
File: 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); + emit PositionOpened(_owner, original, _zchf, _collateral, _liqPrice); }
Using nested if is cheaper than using && multiple check combinations. There are more advantages, such as easier to read code and better coverage reports. There are 4 instances of this issue.
File: contracts/Position.sol 294 if (size < minimumCollateral && size < collateralBalance()) revert ChallengeTooSmall();
File: contracts/Frankencoin.sol 84 if (_applicationPeriod < MIN_APPLICATION_PERIOD && totalSupply() > 0) revert PeriodTooShort(); 85 if (_applicationFee < MIN_FEE && totalSupply() > 0) revert FeeTooLow();
File: contracts/Frankencoin.sol 267 if (!isMinter(msg.sender) && !isMinter(positions[msg.sender])) revert NotMinter();
mint() does not have zero value check which can waste lots of gas while called by minter for minting. It is recommended to have zero value check to prevent the wastage of gas. There is 1 instance of this issue.
File: contracts/Frankencoin.sol 165 function mint(address _target, uint256 _amount, uint32 _reservePPM, uint32 _feesPPM) override external minterOnly { 166 uint256 usableMint = (_amount * (1000_000 - _feesPPM - _reservePPM)) / 1000_000; // rounding down is fine 167 _mint(_target, usableMint); 168 _mint(address(reserve), _amount - usableMint); // rest goes to equity as reserves or as fees 169 minterReserveE6 += _amount * _reservePPM; // minter reserve must be kept accurately in order to ensure we can get back to exactly 0 170 }
Add zero value check in mint() function
File: contracts/Frankencoin.sol function mint(address _target, uint256 _amount, uint32 _reservePPM, uint32 _feesPPM) override external minterOnly { + require(_amount != 0, "invalid amount"); uint256 usableMint = (_amount * (1000_000 - _feesPPM - _reservePPM)) / 1000_000; // rounding down is fine _mint(_target, usableMint); _mint(address(reserve), _amount - usableMint); // rest goes to equity as reserves or as fees minterReserveE6 += _amount * _reservePPM; // minter reserve must be kept accurately in order to ensure we can get back to exactly 0 }
#0 - 0xA5DF
2023-04-27T15:27:39Z
G3 isn't significant and would probably just cause trouble
#1 - c4-judge
2023-05-16T13:53:02Z
hansfriese marked the issue as grade-b