Platform: Code4rena
Start Date: 24/02/2022
Pot Size: $30,000 USDC
Total HM: 0
Participants: 28
Period: 3 days
Judge: Jack the Pug
Id: 95
League: ETH
Rank: 8/28
Findings: 2
Award: $1,085.16
🌟 Selected for report: 1
🚀 Solo Findings: 0
🌟 Selected for report: hickuphh3
Also found by: 0x1f8b, 0xwags, Dravee, IllIllI, Omik, Rhynorater, Ruhum, TerrierLover, cccz, cmichel, csanuragjain, defsec, gzeon, jayjonah8, kenta, kirk-baird, kyliek, leastwood, minhquanym, pedroais, peritoflores, robee, securerodd
725.8343 USDC - $725.83
Table of Contents:
@audit
tagsThe code is annotated at multiple places with
//@audit
comments to pinpoint the issues. Please, pay attention to them for more details.
proxy/ERC1967Proxy.sol:4:pragma solidity ^0.8.0; proxy/Proxy.sol:4:pragma solidity ^0.8.0; upgradeability/draft-IERC1822.sol:4:pragma solidity ^0.8.0; upgradeability/ERC1967Upgrade.sol:4:pragma solidity 0.8.11; upgradeability/UUPSUpgradeable.sol:4:pragma solidity 0.8.11; util/Address.sol:4:pragma solidity ^0.8.0; util/Context.sol:4:pragma solidity 0.8.11; util/ECRecover.sol:26:pragma solidity 0.8.11; util/EIP712.sol:25:pragma solidity 0.8.11; util/IERC20.sol:4:pragma solidity ^0.8.0; util/SafeERC20.sol:4:pragma solidity ^0.8.0; util/StorageSlot.sol:3:pragma solidity ^0.8.0; v1/AbstractFiatTokenV1.sol:25:pragma solidity 0.8.11; v1/Blocklistable.sol:25:pragma solidity 0.8.11; v1/EIP2612.sol:25:pragma solidity 0.8.11; v1/EIP3009.sol:25:pragma solidity 0.8.11; v1/EIP712Domain.sol:25:pragma solidity 0.8.11; v1/FiatTokenV1.sol:25:pragma solidity 0.8.11; v1/Ownable.sol:4:pragma solidity 0.8.11; v1/Pausable.sol:26:pragma solidity 0.8.11; v1/Rescuable.sol:25:pragma solidity 0.8.11; v2/FiatTokenV2.sol:25:pragma solidity 0.8.11; v2/FiatTokenV2test.sol:3:pragma solidity 0.8.11;
initialize()
methods in FiatTokenV1
and FiatTokenV2
are front-runnable and call the default implementation of the custom Ownable
's _transferOwnership
, which doesn't use access control@return
comments were missing on functionsFiatTokenV1
and FiatTokenV2
should be grouped in a struct32: modifier onlyProxy() { 33: require( 34: address(this) != __self, 35: "Function must be called through delegatecall" // missing "UUPSUpgradeable: " prefix (in all of the solution;, a prefix is used) 36: ); 37: require( 38: _getImplementation() == __self, 39: "Function must be called through active proxy" // missing "UUPSUpgradeable: " prefix 40: ); 41: _; 42: }
UUPSUpgradeable:
prefix on revert string Function must be called through delegatecall
UUPSUpgradeable:
prefix on revert string Function must be called through active proxy
In the whole solution, revert strings are prefixed with the contract's name. Here, it's missing.
64: /** 65: * @dev Checks if account is blocklisted 66: * @param _account The address to check //@audit missing @return bool 67: */ 68: function isBlocklisted(address _account) external view returns (bool) { 69: return blocklisted[_account]; 70: }
@return bool
For maps that use the same key value: having separate fields is error prone (like in case of deletion or future new fields).
In this contract, those 2 maps use the same key:
File: FiatTokenV1.sol 59: mapping(address => bool) internal minters; 60: mapping(address => uint256) internal minterAllowed;
Proof:
333: minters[minter] = true; 334: minterAllowed[minter] = minterAllowedAmount; ... 349: minters[minter] = false; 350: minterAllowed[minter] = 0;
I'd suggest these 2 related data get grouped in a struct, let's name it MinterInfo
:
struct MinterInfo { bool minters; uint256 minterAllowed; }
And it would be used as a state variable in this manner:
mapping(address => MinterInfo) minterInfo;
It would be less error-prone, more readable, and it would be possible to delete all related fields with a simple delete minterInfo[address]
.
Even if the contract is Ownable
and the default owner is the deployer:
File: Ownable.sol 28: constructor() { 29: _transferOwnership(_msgSender()); 30: }
there's no access-control implemented on initialize()
:
File: FiatTokenV1.sol 40: contract FiatTokenV1 is 41: Ownable, ... 068: function initialize( 069: string memory tokenName, 070: string memory tokenSymbol, 071: string memory tokenCurrency, 072: uint8 tokenDecimals, 073: address newMasterMinter, 074: address newPauser, 075: address newBlocklister, 076: address newOwner 077: ) public { 078: require(!initialized, "FiatToken: contract is already initialized"); 079: require( 080: newMasterMinter != address(0), 081: "FiatToken: new masterMinter is the zero address" 082: ); 083: require( 084: newPauser != address(0), 085: "FiatToken: new pauser is the zero address" 086: ); 087: require( 088: newBlocklister != address(0), 089: "FiatToken: new blocklister is the zero address" 090: ); 091: require( 092: newOwner != address(0), 093: "FiatToken: new owner is the zero address" 094: ); 095: 096: name = tokenName; 097: symbol = tokenSymbol; 098: currency = tokenCurrency; 099: decimals = tokenDecimals; 100: masterMinter = newMasterMinter; 101: pauser = newPauser; 102: blocklister = newBlocklister; 103: _transferOwnership(newOwner); 104: blocklisted[address(this)] = true; 105: DOMAIN_SEPARATOR = EIP712.makeDomainSeparator(name, "1"); 106: CHAIN_ID = block.chainid; 107: NAME = name; 108: VERSION = "1"; 109: initialized = true; 110: }
Furthermore, as this method calls L103: _transferOwnership(newOwner);
, it's possible for the front-runner to claim ownership, as this is calling the default implementation without access-control:
File: Ownable.sol 56: /** 57: * @dev Transfers ownership of the contract to a new account (`newOwner`). 58: * Internal function without access restriction. 59: */ 60: function _transferOwnership(address newOwner) internal virtual { 61: address oldOwner = _owner; 62: _owner = newOwner; 63: emit OwnershipTransferred(oldOwner, newOwner); 64: }
I suggest adding the onlyOwner
modifier to the initialize()
method.
There's exactly the same issue on FiatTokenV2.sol
@return uint256
163: /** 164: * @dev Get minter allowance for an account 165: * @param minter The address of the minter //@audit missing @return uint256 166: */ 167: function minterAllowance(address minter) external view returns (uint256) { 168: return minterAllowed[minter]; 169: }
@return bool
171: /** 172: * @dev Checks if account is a minter 173: * @param account The address to check //@audit missing @return bool 174: */ 175: function isMinter(address account) external view returns (bool) { 176: return minters[account]; 177: }
@return uint256
202: /** 203: * @dev Get token balance of an account 204: * @param account address The account //@audit missing @return uint256 205: */ 206: function balanceOf(address account) 207: external 208: view 209: override 210: returns (uint256) 211: { 212: return balances[account]; 213: }
The same suggestion as in FiatTokenV1.sol
applies here for grouping minters
and minterAllowed
in a struct.
Same as FiatTokenV1.sol
: Front-Runnable initialize()
@return uint256
File: FiatTokenV2.sol 170: /** 171: * @dev Get minter allowance for an account 172: * @param minter The address of the minter //@audit missing @return uint256 173: */ 174: function minterAllowance(address minter) external view returns (uint256) { 175: return minterAllowed[minter]; 176: }
@return bool
File: FiatTokenV2.sol 178: /** 179: * @dev Checks if account is a minter 180: * @param account The address to check //@audit missing @return bool 181: */ 182: function isMinter(address account) external view returns (bool) { 183: return minters[account]; 184: }
@return uint256
File: FiatTokenV2.sol 209: /** 210: * @dev Get token balance of an account 211: * @param account address The account //@audit missing @return uint256 212: */ 213: function balanceOf(address account) 214: external 215: view 216: override 217: returns (uint256) 218: { 219: return balances[account]; 220: }
File: FiatTokenV2.sol 248: function _approve( 249: address owner, 250: address spender, 251: uint256 value 252: ) internal override { 253: require(owner != address(0), "ERC20: approve from the zero address"); //@audit replace prefix from "ERC20:" to "FiatToken:" 254: require(spender != address(0), "ERC20: approve to the zero address"); //@audit replace prefix from "ERC20:" to "FiatToken:" 255: allowed[owner][spender] = value; 256: emit Approval(owner, spender, value); 257: }
ERC20: approve from the zero address
should be FiatToken: approve from the zero address
ERC20: approve to the zero address
should be FiatToken: approve to the zero address
File: FiatTokenV2.sol 280: require( 281: value <= allowed[from][msg.sender], 282: "ERC20: transfer amount exceeds allowance" //@audit replace prefix from "ERC20:" to "FiatToken:" 283: );
ERC20: transfer amount exceeds allowance
should be FiatToken: transfer amount exceeds allowance
File: FiatTokenV2.sol 314: function _transfer( 315: address from, 316: address to, 317: uint256 value 318: ) internal override { 319: require(from != address(0), "ERC20: transfer from the zero address"); //@audit replace prefix from "ERC20:" to "FiatToken:" 320: require(to != address(0), "ERC20: transfer to the zero address"); //@audit replace prefix from "ERC20:" to "FiatToken:" 321: require( 322: value <= balances[from], 323: "ERC20: transfer amount exceeds balance" //@audit replace prefix from "ERC20:" to "FiatToken:" 324: ); 325: 326: balances[from] = balances[from] - value; 327: balances[to] = balances[to] + value; 328: emit Transfer(from, to, value); 329: }
ERC20: transfer from the zero address
should be FiatToken: transfer from the zero address
ERC20: transfer to the zero address
should be FiatToken: transfer to the zero address
ERC20: transfer amount exceeds balance
should be FiatToken: transfer amount exceeds balance
File: FiatTokenV2.sol 280: require( 281: value <= allowed[from][msg.sender], 282: "ERC20: transfer amount exceeds allowance" //@audit replace prefix from "ERC20:" to "FiatToken:" 283: );
ERC20: decreased allowance below zero
should be FiatToken: decreased allowance below zero
File: FiatTokenV2.sol 610: modifier onlyWhitelister() { 611: require( 612: msg.sender == whitelister, 613: "Whitelistable: caller is not the whitelister" //@audit replace prefix from "Whitelistable:" to "FiatToken:" 614: ); 615: _; 616: }
Whitelistable: caller is not the whitelister
should be FiatToken: caller is not the whitelister
File: FiatTokenV2.sol 623: modifier checkWhitelist(address _account, uint256 _value) { 624: if (_value > 100000 * 10 ** 18) { 625: require( 626: whitelisted[_account], 627: "Whitelistable: account is not whitelisted"//@audit replace prefix from "Whitelistable:" to "FiatToken:" 628: ); 629: } 630: _; 631: }
Whitelistable: account is not whitelisted
should be FiatToken: account is not whitelisted
File: FiatTokenV2.sol 633: /** 634: * @dev Checks if account is whitelisted 635: * @param _account The address to check //@audit missing @return bool 636: */ 637: function isWhitelisted(address _account) external view returns (bool) { 638: return whitelisted[_account]; 639: }
@return bool
Whitelistable: new whitelister is the zero address
should be FiatToken: new whitelister is the zero address
47: /** 48: * @dev Transfers ownership of the contract to a new account (`newOwner`). 49: * Can only be called by the current owner. 50: */ 51: function transferOwnership(address newOwner) public virtual onlyOwner { 52: require(newOwner != address(0), "Ownable: new owner is the zero address"); 53: _transferOwnership(newOwner); //@audit should be 2 step 54: } 55: 56: /** 57: * @dev Transfers ownership of the contract to a new account (`newOwner`). 58: * Internal function without access restriction. 59: */ 60: function _transferOwnership(address newOwner) internal virtual { 61: address oldOwner = _owner; 62: _owner = newOwner; 63: emit OwnershipTransferred(oldOwner, newOwner); 64: }
The current ownership transfer process checks the new owner is not the zero address and proceeds to write the new owner's address into the owner's state variable. If the nominated EOA account is not a valid account, it is entirely possible the owner may accidentally transfer ownership to an uncontrolled account, breaking all functions with the onlyOwner() modifier. Lack of two-step procedure for critical operations leaves them error-prone. if the address is incorrect, the new address will take on the functionality of the new role immediately.
I suggest implementing a two step process where the owner nominates an account and the nominated account needs to call an acceptOwnership() function for the transfer of ownership to fully succeed. This ensures the nominated EOA account is a valid and active account.
#0 - thurendous
2022-03-07T11:19:08Z
Wrong revert string prefix (1)
This one is valid and we will fix it!
#1 - thurendous
2022-03-28T08:00:27Z
Wrong revert string prefix (1)
Fixed
#2 - thurendous
2022-03-28T08:13:53Z
The pragmas used are not the same everywhere:
fixed
#3 - thurendous
2022-03-28T08:17:48Z
Final change can be viewed here.
🌟 Selected for report: Dravee
Also found by: 0x1f8b, IllIllI, Omik, Rhynorater, TerrierLover, Tomio, defsec, gzeon, kenta, pedroais, peritoflores, rfa, robee, securerodd, sorrynotsorry, ye0lde
359.3323 USDC - $359.33
Table of Contents:
The code can be optimized by minimising the number of SLOADs. SLOADs are expensive (100 gas) compared to MLOADs/MSTOREs (3 gas). In the paragraphs below, please see the
@audit-issue
tags in the pieces of code's comments for more information about SLOADs that could be saved by caching the mentioned storage variables in memory variables.
Solidity version 0.8+ comes with implicit overflow and underflow checks on unsigned integers. When an overflow or an underflow isn't possible (as an example, when a comparison is made before the arithmetic operation, or the operation doesn't depend on user input), some gas can be saved by using an
unchecked
block: https://docs.soliditylang.org/en/v0.8.10/control-structures.html#checked-or-unchecked-arithmetic
@audit
tagsThe code is annotated at multiple places with
//@audit
comments to pinpoint the issues. Please, pay attention to them for more details.
File: ERC1967Proxy.sol 30: function _implementation() internal view virtual override returns (address impl) { 31: return ERC1967Upgrade._getImplementation(); //@audit unused named returns 32: }
Removing unused named returns variables can reduce gas usage (MSTOREs/MLOADs) and improve code clarity. To save gas and improve code quality: consider using only one of those.
File: UUPSUpgradeable.sol 095: function upgradeToAndCall(address newImplementation, bytes memory data) //@audit can be calldata 096: external 097: payable 098: virtual 099: onlyProxy 100: { 101: _authorizeUpgrade(newImplementation); 102: _upgradeToAndCallUUPS(newImplementation, data, true); 103: }
calldata
instead of memory
for external functions where the function argument is read-onlyHere, bytes memory data
should be bytes calldata data
File: Blocklistable.sol 90: function updateBlocklister(address _newBlocklister) external onlyOwner { 91: require( 92: _newBlocklister != address(0), 93: "Blocklistable: new blocklister is the zero address" 94: ); 95: blocklister = _newBlocklister; 96: emit BlocklisterChanged(blocklister); //@audit emitting storage value 97: }
blocklister
vs _newBlocklister
)I suggest going from
96: emit BlocklisterChanged(blocklister);
to
96: emit BlocklisterChanged(_newBlocklister);
File: EIP3009.sol 219: function _requireValidAuthorization( 220: address authorizer, 221: bytes32 nonce, 222: uint256 validAfter, 223: uint256 validBefore 224: ) private view { 225: require( 226: block.timestamp > validAfter, //@audit make it inclusive 227: "FEIP3009: authorization is not yet valid" 228: ); 229: require( 230: block.timestamp < validBefore,//@audit make it inclusive 231: "EIP3009: authorization is expired" 232: ); 233: _requireUnusedAuthorization(authorizer, nonce); 234: }
It's possible to save 3 gas on block.timestamp > validAfter
by making it inclusive: block.timestamp >= validAfter
. I believe it wouldn't change much functionally here.
It's possible to save 3 gas on block.timestamp < validBefore
by making it inclusive: block.timestamp <= validBefore
. I believe it wouldn't change much functionally here.
tokenName
instead of name
This can save 2 SLOADs (around 200 gas). Replace:
File: FiatTokenV1.sol 96: name = tokenName; ... 105: DOMAIN_SEPARATOR = EIP712.makeDomainSeparator(name, "1"); //@audit name SLOAD 106: CHAIN_ID = block.chainid; 107: NAME = name;//@audit name SLOAD
with
105: DOMAIN_SEPARATOR = EIP712.makeDomainSeparator(tokenName, "1"); //@audit tokenName MLOAD ... 107: NAME = tokenName;//@audit tokenName MLOAD
Additionally, EIP712.makeDomainSeparator()
signature is as such:
File: EIP712.sol 40: function makeDomainSeparator(string memory name, string memory version)
As the first argument is a memory
and not a storage
, passing the storage name
instead of the memory tokenName
would imply another copy in memory
File: FiatTokenV1.sol 127: function mint(address _to, uint256 _amount) ... 139: require( 140: _amount <= mintingAllowedAmount, ... 146: minterAllowed[msg.sender] = mintingAllowedAmount - _amount;//@audit should be unchecked (see L140)
This line can't underflow due to L140. Therefore, it should be wrapped in an unchecked
block.
File: FiatTokenV1.sol 258: function transferFrom( ... 271: require( 272: value <= allowed[from][msg.sender], //@audit allowed[from][msg.sender] SLOAD 1 273: "ERC20: transfer amount exceeds allowance" 274: ); 275: _transfer(from, to, value); 276: allowed[from][msg.sender] = allowed[from][msg.sender] - value; //@audit should be unchecked (see L272) //@audit allowed[from][msg.sender] SLOAD 2
This line can't underflow due to L272. Therefore, it should be wrapped in an unchecked
block.
allowed[from][msg.sender]
Caching this in memory can save around 1 SLOAD.
This is similar to an already implemented optimization in L361: function burn()
for uint256 balance = balances[msg.sender]
File: FiatTokenV1.sol 304: function _transfer( ... 311: require( 312: value <= balances[from],//@audit SLOAD 1 313: "ERC20: transfer amount exceeds balance" 314: ); 315: 316: balances[from] = balances[from] - value; //@audit should be unchecked (see L312) //@audit SLOAD 2 317: balances[to] = balances[to] + value;
This line can't underflow due to L312. Therefore, it should be wrapped in an unchecked
block.
balances[from]
Caching this in memory can save around 1 SLOAD.
This is similar to an already implemented optimization in L361: function burn()
for uint256 balance = balances[msg.sender]
File: FiatTokenV1.sol 361: function burn(uint256 _amount) ... 369: require(balance >= _amount, "FiatToken: burn amount exceeds balance"); 370: 371: totalSupply_ = totalSupply_ - _amount; 372: balances[msg.sender] = balance - _amount; //@audit should be unchecked (see L369)
This line can't underflow due to L369. Therefore, it should be wrapped in an unchecked
block.
File: FiatTokenV1.sol 377: function updateMasterMinter(address _newMasterMinter) external onlyOwner { 378: require( 379: _newMasterMinter != address(0), 380: "FiatToken: new masterMinter is the zero address" 381: ); 382: masterMinter = _newMasterMinter; 383: emit MasterMinterChanged(masterMinter); //@audit emitting storage value 384: }
masterMinter
vs _newMasterMinter
)I suggest going from
383: emit MasterMinterChanged(masterMinter);
to
383: emit MasterMinterChanged(_newMasterMinter);
File: FiatTokenV1.sol 440: function _decreaseAllowance( ... 445: require( 446: decrement <= allowed[owner][spender],//@audit SLOAD 1 447: "ERC20: decreased allowance below zero" 448: ); 449: _approve(owner, spender, allowed[owner][spender] - decrement); //@audit should be unchecked (see L446) //@audit SLOAD 2 450: }
This line can't underflow due to L446. Therefore, it should be wrapped in an unchecked
block.
allowed[owner][spender]
Caching this in memory can save around 1 SLOAD.
This is similar to an already implemented optimization in L361: function burn()
for uint256 balance = balances[msg.sender]
tokenName
instead of name
This can save 2 SLOADs (around 200 gas).
The explanation is the same as Use tokenName
instead of name
File: FiatTokenV2.sol 133: function mint(address _to, uint256 _amount) ... 146: require( 147: _amount <= mintingAllowedAmount, ... 153: minterAllowed[msg.sender] = mintingAllowedAmount - _amount; //@audit should be unchecked (see L147)
This line can't underflow due to L147. Therefore, it should be wrapped in an unchecked
block.
File: FiatTokenV2.sol 266: function transferFrom( ... 280: require( 281: value <= allowed[from][msg.sender],//@audit allowed[from][msg.sender] SLOAD 1 ... 285: allowed[from][msg.sender] = allowed[from][msg.sender] - value; //@audit should be unchecked (see L281) //@audit allowed[from][msg.sender] SLOAD 2
This line can't underflow due to L281. Therefore, it should be wrapped in an unchecked
block.
allowed[from][msg.sender]
Caching this in memory can save around 1 SLOAD.
File: FiatTokenV2.sol 314: function _transfer( ... 321: require( 322: value <= balances[from],//@audit SLOAD 1 323: "ERC20: transfer amount exceeds balance" ... 326: balances[from] = balances[from] - value; //@audit should be unchecked (see L322) //@audit SLOAD 2
This line can't underflow due to L322. Therefore, it should be wrapped in an unchecked
block.
balances[from]
Caching this in memory can save around 1 SLOAD.
File: FiatTokenV2.sol 371: function burn(uint256 _amount) ... 379: require(balance >= _amount, "FiatToken: burn amount exceeds balance"); ... 382: balances[msg.sender] = balance - _amount; //@audit should be unchecked (see L379)
This line can't underflow due to L379. Therefore, it should be wrapped in an unchecked
block.
File: FiatTokenV2.sol 387: function updateMasterMinter(address _newMasterMinter) external onlyOwner { 388: require( 389: _newMasterMinter != address(0), 390: "FiatToken: new masterMinter is the zero address" 391: ); 392: masterMinter = _newMasterMinter; 393: emit MasterMinterChanged(masterMinter); 394: }
masterMinter
vs _newMasterMinter
)I suggest going from
393: emit MasterMinterChanged(masterMinter);
to
393: emit MasterMinterChanged(_newMasterMinter);
File: FiatTokenV2.sol 451: function _decreaseAllowance( 452: address owner, 453: address spender, 454: uint256 decrement 455: ) internal override { 456: require( 457: decrement <= allowed[owner][spender],//@audit SLOAD 1 458: "ERC20: decreased allowance below zero" 459: ); 460: _approve(owner, spender, allowed[owner][spender] - decrement); //@audit should be unchecked (see L457) //@audit SLOAD 2 461: }
This line can't underflow due to L457. Therefore, it should be wrapped in an unchecked
block.
allowed[owner][spender]
Caching this in memory can save around 1 SLOAD.
File: FiatTokenV2.sol 659: function updateWhitelister(address _newWhitelister) external onlyOwner { 660: require( 661: _newWhitelister != address(0), 662: "Whitelistable: new whitelister is the zero address" 663: ); 664: whitelister = _newWhitelister; 665: emit WhitelisterChanged(whitelister); //@audit emitting storage value 666: }
whitelister
vs _newWhitelister
)I suggest going from
665: emit WhitelisterChanged(whitelister);
to
665: emit WhitelisterChanged(_newWhitelister);
If a variable is not set/initialized, it is assumed to have the default value (0
for uint
, false
for bool
, address(0)
for address...). Explicitly initializing it with its default value is an anti-pattern and wastes gas.
As an example: for (uint256 i = 0; i < numIterations; ++i) {
should be replaced with for (uint256 i; i < numIterations; ++i) {
Instances include:
v1/FiatTokenV1.sol:58: uint256 internal totalSupply_ = 0; v1/Pausable.sol:49: bool public paused = false; v2/FiatTokenV2.sol:58: uint256 internal totalSupply_ = 0;
I suggest removing explicit initializations for default values.
> 0
is less efficient than != 0
for unsigned integers (with proof)!= 0
costs less gas compared to > 0
for unsigned integers in require
statements with the optimizer enabled (6 gas)
Proof: While it may seem that > 0
is cheaper than !=
, this is only true without the optimizer enabled and outside a require statement. If you enable the optimizer at 10k AND you're in a require
statement, this will save gas. You can see this tweet for more proofs: https://twitter.com/gzeon/status/1485428085885640706
I suggest changing > 0
with != 0
here:
v1/FiatTokenV1.sol:136: require(_amount > 0, "FiatToken: mint amount not greater than 0"); v1/FiatTokenV1.sol:368: require(_amount > 0, "FiatToken: burn amount not greater than 0"); v2/FiatTokenV2.sol:143: require(_amount > 0, "FiatToken: mint amount not greater than 0"); v2/FiatTokenV2.sol:378: require(_amount > 0, "FiatToken: burn amount not greater than 0");
Also, please enable the Optimizer.
Shortening revert strings to fit in 32 bytes will decrease deployment time gas and will decrease runtime gas when the revert condition is met.
Revert strings that are longer than 32 bytes require at least one additional mstore, along with additional overhead for computing memory offset, etc.
Strings > 32 bytes:
upgradeability/ERC1967Upgrade.sol:45: require(Address.isContract(newImplementation), "ERC1967: new implementation is not a contract"); upgradeability/ERC1967Upgrade.sol:92: require(slot == _IMPLEMENTATION_SLOT, "ERC1967Upgrade: unsupported proxiableUUID"); upgradeability/ERC1967Upgrade.sol:94: revert("ERC1967Upgrade: new implementation is not UUPS"); upgradeability/UUPSUpgradeable.sol:35: "Function must be called through delegatecall" upgradeability/UUPSUpgradeable.sol:39: "Function must be called through active proxy" upgradeability/UUPSUpgradeable.sol:51: "UUPSUpgradeable: must not be called through delegatecall" util/Address.sol:59: require(success, "Address: unable to send value, recipient may have reverted"); util/Address.sol:114: return functionCallWithValue(target, data, value, "Address: low-level call with value failed"); util/Address.sol:129: require(address(this).balance >= value, "Address: insufficient balance for call"); util/Address.sol:143: return functionStaticCall(target, data, "Address: low-level static call failed"); util/Address.sol:157: require(isContract(target), "Address: static call to non-contract"); util/Address.sol:170: return functionDelegateCall(target, data, "Address: low-level delegate call failed"); util/Address.sol:184: require(isContract(target), "Address: delegate call to non-contract"); util/ECRecover.sol:62: revert("ECRecover: invalid signature 's' value"); util/ECRecover.sol:66: revert("ECRecover: invalid signature 'v' value"); util/SafeERC20.sol:55: "SafeERC20: approve from non-zero to non-zero allowance" util/SafeERC20.sol:76: require(oldAllowance >= value, "SafeERC20: decreased allowance below zero"); util/SafeERC20.sol:96: require(abi.decode(returndata, (bool)), "SafeERC20: ERC20 operation did not succeed"); util/StorageSlot.sol:23: * require(Address.isContract(newImplementation), "ERC1967: new implementation is not a contract"); v1/Blocklistable.sol:47: "Blocklistable: caller is not the blocklister" v1/Blocklistable.sol:59: "Blocklistable: account is blocklisted" v1/Blocklistable.sol:93: "Blocklistable: new blocklister is the zero address" v1/EIP3009.sol:145: require(to == msg.sender, "EIP3009: caller must be the payee"); v1/EIP3009.sol:208: "EIP3009: authorization is used or canceled" v1/EIP3009.sol:227: "FEIP3009: authorization is not yet valid" v1/EIP3009.sol:231: "EIP3009: authorization is expired" v1/FiatTokenV1.sol:78: require(!initialized, "FiatToken: contract is already initialized"); v1/FiatTokenV1.sol:81: "FiatToken: new masterMinter is the zero address" v1/FiatTokenV1.sol:85: "FiatToken: new pauser is the zero address" v1/FiatTokenV1.sol:89: "FiatToken: new blocklister is the zero address" v1/FiatTokenV1.sol:93: "FiatToken: new owner is the zero address" v1/FiatTokenV1.sol:116: require(minters[msg.sender], "FiatToken: caller is not a minter"); v1/FiatTokenV1.sol:135: require(_to != address(0), "FiatToken: mint to the zero address"); v1/FiatTokenV1.sol:136: require(_amount > 0, "FiatToken: mint amount not greater than 0"); v1/FiatTokenV1.sol:141: "FiatToken: mint amount exceeds minterAllowance" v1/FiatTokenV1.sol:158: "FiatToken: caller is not the masterMinter" v1/FiatTokenV1.sol:245: require(owner != address(0), "ERC20: approve from the zero address"); v1/FiatTokenV1.sol:246: require(spender != address(0), "ERC20: approve to the zero address"); v1/FiatTokenV1.sol:273: "ERC20: transfer amount exceeds allowance" v1/FiatTokenV1.sol:309: require(from != address(0), "ERC20: transfer from the zero address"); v1/FiatTokenV1.sol:310: require(to != address(0), "ERC20: transfer to the zero address"); v1/FiatTokenV1.sol:313: "ERC20: transfer amount exceeds balance" v1/FiatTokenV1.sol:368: require(_amount > 0, "FiatToken: burn amount not greater than 0"); v1/FiatTokenV1.sol:369: require(balance >= _amount, "FiatToken: burn amount exceeds balance"); v1/FiatTokenV1.sol:380: "FiatToken: new masterMinter is the zero address" v1/FiatTokenV1.sol:447: "ERC20: decreased allowance below zero" v1/Ownable.sol:52: require(newOwner != address(0), "Ownable: new owner is the zero address"); v1/Pausable.sol:63: require(msg.sender == pauser, "Pausable: caller is not the pauser"); v1/Pausable.sol:89: "Pausable: new pauser is the zero address" v1/Rescuable.sol:50: require(msg.sender == _rescuer, "Rescuable: caller is not the rescuer"); v1/Rescuable.sol:75: "Rescuable: new rescuer is the zero address" v2/FiatTokenV2.sol:84: require(!initialized, "FiatToken: contract is already initialized"); v2/FiatTokenV2.sol:87: "FiatToken: new masterMinter is the zero address" v2/FiatTokenV2.sol:91: "FiatToken: new pauser is the zero address" v2/FiatTokenV2.sol:95: "FiatToken: new blocklister is the zero address" v2/FiatTokenV2.sol:99: "FiatToken: new owner is the zero address" v2/FiatTokenV2.sol:122: require(minters[msg.sender], "FiatToken: caller is not a minter"); v2/FiatTokenV2.sol:142: require(_to != address(0), "FiatToken: mint to the zero address"); v2/FiatTokenV2.sol:143: require(_amount > 0, "FiatToken: mint amount not greater than 0"); v2/FiatTokenV2.sol:148: "FiatToken: mint amount exceeds minterAllowance" v2/FiatTokenV2.sol:165: "FiatToken: caller is not the masterMinter" v2/FiatTokenV2.sol:253: require(owner != address(0), "ERC20: approve from the zero address"); v2/FiatTokenV2.sol:254: require(spender != address(0), "ERC20: approve to the zero address"); v2/FiatTokenV2.sol:282: "ERC20: transfer amount exceeds allowance" v2/FiatTokenV2.sol:319: require(from != address(0), "ERC20: transfer from the zero address"); v2/FiatTokenV2.sol:320: require(to != address(0), "ERC20: transfer to the zero address"); v2/FiatTokenV2.sol:323: "ERC20: transfer amount exceeds balance" v2/FiatTokenV2.sol:378: require(_amount > 0, "FiatToken: burn amount not greater than 0"); v2/FiatTokenV2.sol:379: require(balance >= _amount, "FiatToken: burn amount exceeds balance"); v2/FiatTokenV2.sol:390: "FiatToken: new masterMinter is the zero address" v2/FiatTokenV2.sol:458: "ERC20: decreased allowance below zero" v2/FiatTokenV2.sol:613: "Whitelistable: caller is not the whitelister" v2/FiatTokenV2.sol:627: "Whitelistable: account is not whitelisted" v2/FiatTokenV2.sol:662: "Whitelistable: new whitelister is the zero address"
I suggest shortening the revert strings to fit in 32 bytes, or that using custom errors as described next.
Custom errors from Solidity 0.8.4 are cheaper than revert strings (cheaper deployment cost and runtime cost when the revert condition is met)
Source: https://blog.soliditylang.org/2021/04/21/custom-errors/:
Starting from Solidity v0.8.4, there is a convenient and gas-efficient way to explain to users why an operation failed through the use of custom errors. Until now, you could already use strings to give more information about failures (e.g.,
revert("Insufficient funds.");
), but they are rather expensive, especially when it comes to deploy cost, and it is difficult to use dynamic information in them.
Custom errors are defined using the error
statement, which can be used inside and outside of contracts (including interfaces and libraries).
Instances include:
78 results - 14 files contracts/upgradeability/ERC1967Upgrade.sol: 45: require(Address.isContract(newImplementation), "ERC1967: new implementation is not a contract"); 92: require(slot == _IMPLEMENTATION_SLOT, "ERC1967Upgrade: unsupported proxiableUUID"); contracts/upgradeability/UUPSUpgradeable.sol: 33: require( 37: require( 49: require( contracts/util/Address.sol: 56: require(address(this).balance >= amount, "Address: insufficient balance"); 59: require(success, "Address: unable to send value, recipient may have reverted"); 129: require(address(this).balance >= value, "Address: insufficient balance for call"); 130: require(isContract(target), "Address: call to non-contract"); 157: require(isContract(target), "Address: static call to non-contract"); 184: require(isContract(target), "Address: delegate call to non-contract"); contracts/util/ECRecover.sol: 71: require(signer != address(0), "ECRecover: invalid signature"); contracts/util/SafeERC20.sol: 53: require( 76: require(oldAllowance >= value, "SafeERC20: decreased allowance below zero"); 96: require(abi.decode(returndata, (bool)), "SafeERC20: ERC20 operation did not succeed"); contracts/util/StorageSlot.sol: 23: * require(Address.isContract(newImplementation), "ERC1967: new implementation is not a contract"); contracts/v1/Blocklistable.sol: 45: require( 57: require( 91: require( contracts/v1/EIP2612.sol: 70: require(deadline >= block.timestamp, "EIP2612: permit is expired"); 80: require( contracts/v1/EIP3009.sol: 111: require( 145: require(to == msg.sender, "EIP3009: caller must be the payee"); 157: require( 188: require( 206: require( 225: require( 229: require( contracts/v1/FiatTokenV1.sol: 78: require(!initialized, "FiatToken: contract is already initialized"); 79: require( 83: require( 87: require( 91: require( 116: require(minters[msg.sender], "FiatToken: caller is not a minter"); 135: require(_to != address(0), "FiatToken: mint to the zero address"); 136: require(_amount > 0, "FiatToken: mint amount not greater than 0"); 139: require( 156: require( 245: require(owner != address(0), "ERC20: approve from the zero address"); 246: require(spender != address(0), "ERC20: approve to the zero address"); 271: require( 309: require(from != address(0), "ERC20: transfer from the zero address"); 310: require(to != address(0), "ERC20: transfer to the zero address"); 311: require( 368: require(_amount > 0, "FiatToken: burn amount not greater than 0"); 369: require(balance >= _amount, "FiatToken: burn amount exceeds balance"); 378: require( 445: require( contracts/v1/Ownable.sol: 43: require(owner() == _msgSender(), "Ownable: caller is not the owner"); 52: require(newOwner != address(0), "Ownable: new owner is the zero address"); contracts/v1/Pausable.sol: 55: require(!paused, "Pausable: paused"); 63: require(msg.sender == pauser, "Pausable: caller is not the pauser"); 87: require( contracts/v1/Rescuable.sol: 50: require(msg.sender == _rescuer, "Rescuable: caller is not the rescuer"); 73: require( contracts/v2/FiatTokenV2.sol: 84: require(!initialized, "FiatToken: contract is already initialized"); 85: require( 89: require( 93: require( 97: require( 122: require(minters[msg.sender], "FiatToken: caller is not a minter"); 142: require(_to != address(0), "FiatToken: mint to the zero address"); 143: require(_amount > 0, "FiatToken: mint amount not greater than 0"); 146: require( 163: require( 253: require(owner != address(0), "ERC20: approve from the zero address"); 254: require(spender != address(0), "ERC20: approve to the zero address"); 280: require( 319: require(from != address(0), "ERC20: transfer from the zero address"); 320: require(to != address(0), "ERC20: transfer to the zero address"); 321: require( 378: require(_amount > 0, "FiatToken: burn amount not greater than 0"); 379: require(balance >= _amount, "FiatToken: burn amount exceeds balance"); 388: require( 456: require( 611: require( 625: require( 660: require(
I suggest replacing revert strings with custom errors.
#0 - thurendous
2022-03-02T04:18:52Z
function _implementation() File: UUPSUpgradeable.sol
We used OpenZeppelin's library and it is like this. Should we change this?
File: Blocklistable.sol Emitting a storage value (blocklister vs _newBlocklister)
We will fix this.
File: EIP3009.sol Non-strict inequalities are cheaper than strict ones (1)
We are not sure about this. Is this true?
File: FiatTokenV1.sol function initialize()
This can be true. But the initialize function is rarely called. We keep it this way.
File: FiatTokenV1.sol File: FiatTokenV2.sol Unchecked block L146 Unchecked block L276 Unchecked block L316 Unchecked block L372 Unchecked block L449 Cache allowed[from][msg.sender] Cache balances[from]
We maybe will fix this after checking in detail.
function updateMasterMinter() Emitting a storage value (masterMinter vs _newMasterMinter)
We will fix this.
0 is less efficient than != 0 for unsigned integers (with proof)
Thank you for the proof.
Reduce the size of error messages (Long revert Strings)
We maybe will fix this after checking in detail. Do you suggest do this strongly?
#1 - thurendous
2022-03-29T03:00:31Z
No need to explicitly initialize variables with default values Cache allowedfrom Cache allowed[from][msg.sender] Cache balances[from] Cache allowed[owner][spender]
Above issues were fixed and thanks!
Final change can be viewed here.