Platform: Code4rena
Start Date: 11/11/2022
Pot Size: $36,500 USDC
Total HM: 5
Participants: 62
Period: 3 days
Judge: berndartmueller
Id: 181
League: ETH
Rank: 38/62
Findings: 2
Award: $64.77
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: 0xSmartContract
Also found by: 0x4non, 0xNazgul, 0xhacksmithh, Aymen0909, HE1M, IllIllI, Josiah, RaymondFam, Rolezn, Tricko, brgltd, c3phas, chrisdior4, joestakey, ladboy233, martin, neko_nyaa, rotcivegaf, tnevler, trustindistrust
42.5508 USDC - $42.55
File: /contracts/Exchange.sol 130: oracle = _oracle;
Recommended to use ECDSA from openzeppallin.
File: /contracts/Exchange.sol 524: address recoveredSigner = ecrecover(digest, v, r, s);
Contracts are allowed to override their parents' functions and change the visibility from external to public. Functions marked by external use call data to read arguments, where public will first allocate in local memory and then load them.
File: /contracts/Pool.sol 44: function withdraw(uint256 amount) public {
File: /contracts/Pool.sol 79: function balanceOf(address user) public view returns (uint256) {
File: /contracts/Pool.sol 83: function totalSupply() public view returns (uint256) {
ecrecover
precompile internally checks if the value is 27 or 28. No need to check in the client side.File:/contracts/Exchange.sol 523: require(v == 27 || v == 28, "Invalid v parameter");
See this pull request on the Ethereum Yellow paper
File: /contracts/Pool.sol 18: // TODO: set proper address before deployment
require()/revert() statements should have descriptive reason strings. https://github.com/code-423n4/2022-11-non-fungible/blob/323b7cbf607425dd81da96c0777c8b12e800305d/contracts/Exchange.sol#L240
File: /contracts/Exchange.sol 240: require(sell.order.side == Side.Sell); 291: require(msg.sender == order.trader);
File: /contracts/Pool.sol 45: require(_balances[msg.sender] >= amount); 48: require(success); 71: require(_balances[from] >= amount); 72: require(to != address(0));
File: /contracts/Exchange.sol 176: uint256 executionsLength = executions.length; 177: for (uint8 i=0; i < executionsLength; i++) { 178: bytes memory data = abi.encodeWithSelector(this._execute.selector, executions[i].sell, executions[i].buy); 179: (bool success,) = address(this).delegatecall(data); 180: } 181: _returnDust(remainingETH);
diff --git a/contracts/Exchange.sol b/contracts/Exchange.sol index ec27b1d..e623eec 100644 --- a/contracts/Exchange.sol +++ b/contracts/Exchange.sol @@ -171,15 +171,7 @@ contract Exchange is IExchange, ReentrancyGuarded, EIP712, OwnableUpgradeable, U whenOpen setupExecution { - /* - REFERENCE - uint256 executionsLength = executions.length; - for (uint8 i=0; i < executionsLength; i++) { - bytes memory data = abi.encodeWithSelector(this._execute.selector, executions[i].sell, executions[i].buy); - (bool success,) = address(this).delegatecall(data); - } - _returnDust(remainingETH); - */ + uint256 executionsLength = executions.length; for (uint8 i = 0; i < executionsLength; i++) { assembly {
File: /contracts/Exchange.sol //@audit: Missing @param _executionDelegate 323: function setExecutionDelegate(IExecutionDelegate _executionDelegate) //@audit: Missing @param _policyManager 332: function setPolicyManager(IPolicyManager _policyManager) //@audit: Missing @param _oracle 341: function setOracle(address _oracle) //@audit: Missing @param _blockRange 350: function setBlockRange(uint256 _blockRange)
The best-practice layout for a contract should follow the following order: state variables, events, modifiers, constructor and functions. In the following https://github.com/code-423n4/2022-11-non-fungible/blob/323b7cbf607425dd81da96c0777c8b12e800305d/contracts/Exchange.sol , some state variables are declared in between functions.
File: /contracts/Exchange.sol 146: bool public isInternal = false; 147: uint256 public remainingETH = 0;
Recommendation: Consider adopting recommended best-practice for code structure and layout.
See: https://docs.soliditylang.org/en/v0.8.15/style-guide.html#order-of-layout
#0 - c4-judge
2022-11-17T11:24:44Z
berndartmueller marked the issue as grade-b
🌟 Selected for report: ReyAdmirado
Also found by: 0x4non, 0xRoxas, 0xab00, Awesome, Aymen0909, Bnke0x0, Deivitto, Diana, IllIllI, Rahoz, RaymondFam, Rolezn, Sathish9098, ajtra, aphak5010, aviggiano, c3phas, carlitox477, ch0bu, cryptostellar5, erictee, lukris02, martin, rotcivegaf, saian, shark, trustindistrust, zaskoh
22.2155 USDC - $22.22
NB: Some functions have been truncated where neccessary to just show affected parts of the code Throught the report some places might be denoted with audit tags to show the actual place affected.
Here, the storage variables can be tightly packed
File: /contracts/Exchange.sol //@audit: move the bool isInternal next to address oracle 77: /* Variables */ 78: IExecutionDelegate public executionDelegate; 79: IPolicyManager public policyManager; 80: address public oracle; 81: uint256 public blockRange; 84: /* Storage */ 85: mapping(bytes32 => bool) public cancelledOrFilled; 86: mapping(address => uint256) public nonces; 145: /* External Functions */ 146: bool public isInternal = false; 147: uint256 public remainingETH = 0;
diff --git a/contracts/Exchange.sol b/contracts/Exchange.sol index ec27b1d..6fa9ede 100644 --- a/contracts/Exchange.sol +++ b/contracts/Exchange.sol @@ -78,6 +78,7 @@ contract Exchange is IExchange, ReentrancyGuarded, EIP712, OwnableUpgradeable, U IExecutionDelegate public executionDelegate; IPolicyManager public policyManager; address public oracle; + bool public isInternal = false; uint256 public blockRange; @@ -143,7 +144,6 @@ contract Exchange is IExchange, ReentrancyGuarded, EIP712, OwnableUpgradeable, U /* External Functions */ - bool public isInternal = false; uint256 public remainingETH = 0; /**
The code can be optimized by minimizing the number of SLOADs.
SLOADs are expensive (100 gas after the 1st one) compared to MLOADs/MSTOREs (3 gas each). Storage values read multiple times should instead be cached in memory the first time (costing 1 SLOAD) and then read from this cache to avoid multiple SLOADs.
File: /contracts/Exchange.sol 565: function _executeFundsTransfer( 572: if (msg.sender == buyer && paymentToken == address(0)) { 573: require(remainingETH >= price); 574: remainingETH -= price; 575: }
diff --git a/contracts/Exchange.sol b/contracts/Exchange.sol index ec27b1d..682a797 100644 --- a/contracts/Exchange.sol +++ b/contracts/Exchange.sol @@ -570,8 +570,9 @@ contract Exchange is IExchange, ReentrancyGuarded, EIP712, OwnableUpgradeable, U uint256 price ) internal { if (msg.sender == buyer && paymentToken == address(0)) { - require(remainingETH >= price); - remainingETH -= price; + uint256 _remainingETH = remainingETH; + require(_remainingETH >= price); + remainingETH = _remainingETH - price; }
Min | Max | Avg | |
---|---|---|---|
Before | - | - | 40433 |
After | - | - | 40315 |
File: /contracts/Pool.sol 44: function withdraw(uint256 amount) public { 45: require(_balances[msg.sender] >= amount); 46: _balances[msg.sender] -= amount;
diff --git a/contracts/Pool.sol b/contracts/Pool.sol index 3722082..bac2c33 100644 --- a/contracts/Pool.sol +++ b/contracts/Pool.sol @@ -42,8 +42,9 @@ contract Pool is IPool, OwnableUpgradeable, UUPSUpgradeable { * @param amount Amount to withdraw */ function withdraw(uint256 amount) public { - require(_balances[msg.sender] >= amount); - _balances[msg.sender] -= amount; + uint256 _senderBalance = _balances[msg.sender]; + require(_senderBalance >= amount); + _balances[msg.sender] = _senderBalance - amount; (bool success,) = payable(msg.sender).call{value: amount}(""); require(success); emit Transfer(address(0), msg.sender, amount);
File:/contracts/Pool.sol 70: function _transfer(address from, address to, uint256 amount) private { 71: require(_balances[from] >= amount); 72: require(to != address(0)); 73: _balances[from] -= amount;
diff --git a/contracts/Pool.sol b/contracts/Pool.sol index 3722082..9b8a022 100644 --- a/contracts/Pool.sol +++ b/contracts/Pool.sol @@ -68,9 +68,12 @@ contract Pool is IPool, OwnableUpgradeable, UUPSUpgradeable { } function _transfer(address from, address to, uint256 amount) private { - require(_balances[from] >= amount); + uint256 _balanceFrom = _balances[from]; + require(_balanceFrom >= amount); require(to != address(0)); - _balances[from] -= amount; + unchecked { + _balances[from] = _balanceFrom - amount; + } _balances[to] += amount; emit Transfer(from, to, amount);
Not inlining costs 20 to 40 gas because of two extra JUMP instructions and additional stack operations needed for function calls.
Affected code:
File: /contracts/Exchange.sol 440: function _validateUserAuthorization( 441: bytes32 orderHash, 442: address trader, 443: uint8 v, 444: bytes32 r, 445: bytes32 s, 446: SignatureVersion signatureVersion, 447: bytes calldata extraSignature 448: ) internal view returns (bool) {
The above is only called once on Line 399
File: /contracts/Exchange.sol 471: function _validateOracleAuthorization( 472: bytes32 orderHash, 473: SignatureVersion signatureVersion, 474: bytes calldata extraSignature, 475: uint256 blockNumber 476: ) internal view returns (bool) {
The above function is only called once on Line 416
File: /contracts/Exchange.sol 537: function _canMatchOrders(Order calldata sell, Order calldata buy) 538: internal 539: view 540: returns (uint256 price, uint256 tokenId, uint256 amount, AssetType assetType) 541: {
The above is only called once on Line 251
File: /contracts/Exchange.sol 565: function _executeFundsTransfer( 566: address seller, 567: address buyer, 568: address paymentToken, 569: Fee[] calldata fees, 570: uint256 price 571: ) internal {
The above is only called once on Line 257
File: /contracts/Exchange.sol 591: function _transferFees( 592: Fee[] calldata fees, 593: address paymentToken, 594: address from, 595: uint256 price 596: ) internal returns (uint256) {
The above is only called once on Line 578
File: /contracts/Exchange.sol 653: function _executeTokenTransfer( 654: address collection, 655: address from, 656: address to, 657: uint256 tokenId, 658: uint256 amount, 659: AssetType assetType 660: ) internal {
The above function is being called only once on Line 264
Here, the values emitted shouldn’t be read from storage. The existing memory values should be used instead:
File: /contracts/Exchange.sol 323: function setExecutionDelegate(IExecutionDelegate _executionDelegate) 324: external 325: onlyOwner 326: { 327: require(address(_executionDelegate) != address(0), "Address cannot be zero"); 328: executionDelegate = _executionDelegate; 329: emit NewExecutionDelegate(executionDelegate); 330: }
diff --git a/contracts/Exchange.sol b/contracts/Exchange.sol index ec27b1d..dbe7861 100644 --- a/contracts/Exchange.sol +++ b/contracts/Exchange.sol @@ -326,7 +326,7 @@ contract Exchange is IExchange, ReentrancyGuarded, EIP712, OwnableUpgradeable, U { require(address(_executionDelegate) != address(0), "Address cannot be zero"); executionDelegate = _executionDelegate; - emit NewExecutionDelegate(executionDelegate); + emit NewExecutionDelegate(_executionDelegate); }
File: /contracts/Exchange.sol 332: function setPolicyManager(IPolicyManager _policyManager) 333: external 334: onlyOwner 335: { 336: require(address(_policyManager) != address(0), "Address cannot be zero"); 337: policyManager = _policyManager; 338: emit NewPolicyManager(policyManager); 339: }
diff --git a/contracts/Exchange.sol b/contracts/Exchange.sol index ec27b1d..ab17632 100644 --- a/contracts/Exchange.sol +++ b/contracts/Exchange.sol @@ -335,7 +335,7 @@ contract Exchange is IExchange, ReentrancyGuarded, EIP712, OwnableUpgradeable, U { require(address(_policyManager) != address(0), "Address cannot be zero"); policyManager = _policyManager; - emit NewPolicyManager(policyManager); + emit NewPolicyManager(_policyManager); }
File: /contracts/Exchange.sol 341: function setOracle(address _oracle) 342: external 343: onlyOwner 344: { 345: require(_oracle != address(0), "Address cannot be zero"); 346: oracle = _oracle; 347: emit NewOracle(oracle); 348: }
diff --git a/contracts/Exchange.sol b/contracts/Exchange.sol index ec27b1d..5b7cfb1 100644 --- a/contracts/Exchange.sol +++ b/contracts/Exchange.sol @@ -344,7 +344,7 @@ contract Exchange is IExchange, ReentrancyGuarded, EIP712, OwnableUpgradeable, U { require(_oracle != address(0), "Address cannot be zero"); oracle = _oracle; - emit NewOracle(oracle); + emit NewOracle(_oracle); }
File: /contracts/Exchange.sol 350: function setBlockRange(uint256 _blockRange) 351: external 352: onlyOwner 353: { 354: blockRange = _blockRange; 355: emit NewBlockRange(blockRange); 356: }
diff --git a/contracts/Exchange.sol b/contracts/Exchange.sol index ec27b1d..34144dd 100644 --- a/contracts/Exchange.sol +++ b/contracts/Exchange.sol @@ -352,7 +352,7 @@ contract Exchange is IExchange, ReentrancyGuarded, EIP712, OwnableUpgradeable, U onlyOwner { blockRange = _blockRange; - emit NewBlockRange(blockRange); + emit NewBlockRange(_blockRange); }
File: /contracts/Exchange.sol 574: remainingETH -= price;
diff --git a/contracts/Exchange.sol b/contracts/Exchange.sol index ec27b1d..0fe488f 100644 --- a/contracts/Exchange.sol +++ b/contracts/Exchange.sol @@ -571,7 +571,7 @@ contract Exchange is IExchange, ReentrancyGuarded, EIP712, OwnableUpgradeable, U ) internal { if (msg.sender == buyer && paymentToken == address(0)) { require(remainingETH >= price); - remainingETH -= price; + remainingETH = remainingETH - price; }
Min | Max | Avg | |
---|---|---|---|
Before | 32939 | 50039 | 41489 |
After | 32835 | 49935 | 41385 |
File: /contracts/Exchange.sol 315: function incrementNonce() external { 316: nonces[msg.sender] += 1; 317: emit NonceIncremented(msg.sender, nonces[msg.sender]); 318: }
diff --git a/contracts/Exchange.sol b/contracts/Exchange.sol index ec27b1d..bf5f114 100644 --- a/contracts/Exchange.sol +++ b/contracts/Exchange.sol @@ -313,7 +313,7 @@ contract Exchange is IExchange, ReentrancyGuarded, EIP712, OwnableUpgradeable, U * @dev Cancel all current orders for a user, preventing them from being matched. Must be called by the trader of the order */ function incrementNonce() external { - nonces[msg.sender] += 1; + nonces[msg.sender] = nonces[msg.sender] + 1; emit NonceIncremented(msg.sender, nonces[msg.sender]); }
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), some gas can be saved by using an unchecked block see resource
File: /contracts/Exchange.sol 607: uint256 receiveAmount = price - totalFee;
diff --git a/contracts/Exchange.sol b/contracts/Exchange.sol index ec27b1d..7f6467e 100644 --- a/contracts/Exchange.sol +++ b/contracts/Exchange.sol @@ -604,7 +604,11 @@ contract Exchange is IExchange, ReentrancyGuarded, EIP712, OwnableUpgradeable, U require(totalFee <= price, "Total amount of fees are more than the price"); /* Amount that will be received by seller. */ - uint256 receiveAmount = price - totalFee; + uint256 receiveAmount; + unchecked { + receiveAmount = price - totalFee; + } + return (receiveAmount); }
The operation price - totalFee
cannot underflow due to a check on Line 604 that ensures that the value of price
is greater than totalFee
prior to perfoming the subtraction
Min | Max | Avg | |
---|---|---|---|
Before | - | - | 40433 |
After | - | - | 40346 |
File: /contracts/Pool.sol 46: _balances[msg.sender] -= amount;
diff --git a/contracts/Pool.sol b/contracts/Pool.sol index 3722082..28a641a 100644 --- a/contracts/Pool.sol +++ b/contracts/Pool.sol @@ -43,7 +43,9 @@ contract Pool is IPool, OwnableUpgradeable, UUPSUpgradeable { */ function withdraw(uint256 amount) public { require(_balances[msg.sender] >= amount); - _balances[msg.sender] -= amount; + unchecked { + _balances[msg.sender] -= amount; + } (bool success,) = payable(msg.sender).call{value: amount}(""); require(success); emit Transfer(address(0), msg.sender, amount);
File: /contracts/Pool.sol 73: _balances[from] -= amount;
diff --git a/contracts/Pool.sol b/contracts/Pool.sol index 3722082..73d2c0f 100644 --- a/contracts/Pool.sol +++ b/contracts/Pool.sol @@ -70,7 +70,9 @@ contract Pool is IPool, OwnableUpgradeable, UUPSUpgradeable { function _transfer(address from, address to, uint256 amount) private { require(_balances[from] >= amount); require(to != address(0)); - _balances[from] -= amount; + unchecked { + _balances[from] -= amount; + } _balances[to] += amount; emit Transfer(from, to, amount);
The operation _balances[from] -= amount
cannot underflow due to the check on Line 71 that ensures that _balances[from]
is greater than amount
before performing the operation
The majority of Solidity for loops increment a uint256 variable that starts at 0. These increment operations never need to be checked for over/underflow because the variable will never reach the max number of uint256 (will run out of gas long before that happens). The default over/underflow check wastes gas in every iteration of virtually every for loop . eg.
e.g Let's work with a sample loop below.
for(uint256 i; i < 10; i++){ //doSomething }
can be written as shown below.
for(uint256 i; i < 10;) { // loop logic unchecked { i++; } }
We can also write it as an inlined function like below.
function inc(i) internal pure returns (uint256) { unchecked { return i + 1; } } for(uint256 i; i < 10; i = inc(i)) { // doSomething }
Affected code
Min | Max | Avg | |
---|---|---|---|
Before | 796257 | 902099 | 852656 |
After | 795872 | 901714 | 852271 |
File: /contracts/Exchange.sol 184: for (uint8 i = 0; i < executionsLength; i++) { 208: }
diff --git a/contracts/Exchange.sol b/contracts/Exchange.sol index ec27b1d..ef4dcdc 100644 --- a/contracts/Exchange.sol +++ b/contracts/Exchange.sol @@ -181,7 +181,7 @@ contract Exchange is IExchange, ReentrancyGuarded, EIP712, OwnableUpgradeable, U _returnDust(remainingETH); */ uint256 executionsLength = executions.length; - for (uint8 i = 0; i < executionsLength; i++) { + for (uint8 i = 0; i < executionsLength;) { assembly { let memPointer := mload(0x40) @@ -205,6 +205,9 @@ contract Exchange is IExchange, ReentrancyGuarded, EIP712, OwnableUpgradeable, U // must be put in delegatecall to maintain the authorization from the caller let result := delegatecall(gas(), address(), memPointer, add(size, 0x04), 0, 0) } + unchecked { + ++i; + } } _returnDust(); }
Min | Max | Avg | |
---|---|---|---|
Before | - | - | 95362 |
After | - | - | 95198 |
File: /contracts/Exchange.sol 307: for (uint8 i = 0; i < orders.length; i++) { 308: cancelOrder(orders[i]); 309: }
diff --git a/contracts/Exchange.sol b/contracts/Exchange.sol index ec27b1d..0d4eadb 100644 --- a/contracts/Exchange.sol +++ b/contracts/Exchange.sol @@ -304,8 +304,11 @@ contract Exchange is IExchange, ReentrancyGuarded, EIP712, OwnableUpgradeable, U * @param orders Orders to cancel */ function cancelOrders(Order[] calldata orders) external { - for (uint8 i = 0; i < orders.length; i++) { + for (uint8 i = 0; i < orders.length;) { cancelOrder(orders[i]); + unchecked { + ++i; + } } }
File: /contracts/Exchange.sol 598: for (uint8 i = 0; i < fees.length; i++) { 599: uint256 fee = (price * fees[i].rate) / INVERSE_BASIS_POINT; 600: _transferTo(paymentToken, from, fees[i].recipient, fee); 601: totalFee += fee; 602: }
diff --git a/contracts/Exchange.sol b/contracts/Exchange.sol index ec27b1d..c098472 100644 --- a/contracts/Exchange.sol +++ b/contracts/Exchange.sol @@ -595,10 +595,13 @@ contract Exchange is IExchange, ReentrancyGuarded, EIP712, OwnableUpgradeable, U uint256 price ) internal returns (uint256) { uint256 totalFee = 0; - for (uint8 i = 0; i < fees.length; i++) { + for (uint8 i = 0; i < fees.length;) { uint256 fee = (price * fees[i].rate) / INVERSE_BASIS_POINT; _transferTo(paymentToken, from, fees[i].recipient, fee); totalFee += fee; + unchecked { + ++i; + } }
File: /contracts/Exchange.sol 48: modifier internalCall() { 49: require(isInternal, "This function should not be called directly"); 50: _; 51: }
Every reason string takes at least 32 bytes so make sure your string fits in 32 bytes or it will become more expensive.Each extra chunk of byetes past the original 32 incurs an MSTORE which costs 3 gas
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.
File:/contracts/Exchange.sol 49: require(isInternal, "This function should not be called directly"); 295: require(!cancelledOrFilled[hash], "Order already cancelled or filled"); 604: require(totalFee <= price, "Total amount of fees are more than the price");
File: /contracts/Pool.sol 63: revert('Caller is not authorized to transfer');
I suggest shortening the revert strings to fit in 32 bytes, or using custom errors.
#0 - c4-judge
2022-11-17T12:55:31Z
berndartmueller marked the issue as grade-b