Platform: Code4rena
Start Date: 05/10/2022
Pot Size: $50,000 USDC
Total HM: 2
Participants: 80
Period: 5 days
Judge: GalloDaSballo
Id: 168
League: ETH
Rank: 9/80
Findings: 3
Award: $2,585.28
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: dipp
Also found by: 0x4non, 0x52, 0xRobocop, 0xc0ffEE, 8olidity, Ch_301, Jeiwan, Junnon, KIntern_NA, Lambda, M4TZ1P, MiloTruck, Nyx, PaludoX0, Ruhum, RustyRabbit, Soosh, TomJ, Trust, arcoun, aviggiano, bardamu, cryptonue, csanuragjain, d3e4, enckrish, exd0tpy, hansfriese, jayphbee, joestakey, ladboy233, minhquanym, minhtrng, nicobevi, obront, polymorphism, rokinot, romand, rotcivegaf, rvierdiiev, saian, serial-coder, trustindistrust, zzykxx
114.8239 USDC - $114.82
https://github.com/code-423n4/2022-10-blur/blob/2fdaa6e13b544c8c11d1c022a575f16c3a72e3bf/contracts/matchingPolicies/StandardPolicyERC1155.sol#L33 https://github.com/code-423n4/2022-10-blur/blob/2fdaa6e13b544c8c11d1c022a575f16c3a72e3bf/contracts/matchingPolicies/StandardPolicyERC1155.sol#L59
When filling ERC1155 token orders with token amount greater than 1, _canMathOrders
returns amount as 1 instead of returning the actual amount of tokens order.amount
. So the buyers will pay order.price
payment tokens for the entire amount of tokens, but gets only 1 token in return
return ( (makerAsk.side != takerBid.side) && (makerAsk.paymentToken == takerBid.paymentToken) && (makerAsk.collection == takerBid.collection) && (makerAsk.tokenId == takerBid.tokenId) && (makerAsk.matchingPolicy == takerBid.matchingPolicy) && (makerAsk.price == takerBid.price), makerAsk.price, makerAsk.tokenId, 1, AssetType.ERC1155 );
return ( (makerBid.side != takerAsk.side) && (makerBid.paymentToken == takerAsk.paymentToken) && (makerBid.collection == takerAsk.collection) && (makerBid.tokenId == takerAsk.tokenId) && (makerBid.matchingPolicy == takerAsk.matchingPolicy) && (makerBid.price == takerAsk.price), makerBid.price, makerBid.tokenId, 1, AssetType.ERC1155 );
(uint256 price, uint256 tokenId, uint256 amount, AssetType assetType) = _canMatchOrders(sell.order, buy.order);
Manual analysis
In matching policy use actual makerBid.amount
and makerAsk.amount
in the return statement
#0 - blur-io-toad
2022-10-16T23:39:43Z
This was an oversight on my part for not putting this contract as out-of-scope. Our marketplace does not handle ERC1155 yet and so we haven't concluded what the matching critieria for those orders will be. This contract was mainly created to test ERC1155 transfers through the rest of the exchange, but shouldn't be deployed initially. When we are prepared to handle ERC1155 orders we will have to develop a new matching policy that determines the amount from the order parameters. Acknowledging that it's incorrect, but won't be making any changes as the contract won't be deployed.
#1 - trust1995
2022-10-27T23:58:26Z
Dup #666
#2 - GalloDaSballo
2022-10-28T00:08:45Z
🌟 Selected for report: Soosh
Also found by: 0x1f8b, 0x4non, 0xc0ffEE, RaymondFam, Ruhum, RustyRabbit, Trust, arcoun, cccz, d3e4, minhtrng, rotcivegaf, saian, sakshamguruji
Users approve ExecutionDelegate
contract for transferring assets when order is filled. A malicious owner can approve arbitrary contract/EOA to transfer assets out of the contract.
Consider removing the function and storing the exchange address in the constructor, or use a multi-sig contract as the owner address
function approveContract(address _contract) onlyOwner external { contracts[_contract] = true; emit ApproveContract(_contract); }
approveContract
can approve EOAsapproveContract
is used to approve contracts to transfer assets out of the contract. But since no isContract
exists, even EOAs can be approved to transfer assets
function approveContract(address _contract) onlyOwner external { contracts[_contract] = true; emit ApproveContract(_contract); }
Add isContract
function to check if the address is a contract
function approveContract(address _contract) onlyOwner external { require(isContract(_contract), "..."); contracts[_contract] = true; emit ApproveContract(_contract); }
_blockrange
is not validatedIn setBlockRange
the _blockRange
value is not validated, so it can be set to 0, preventing oracle authorisation, or set to a very high value, allowing old signatures/ Add input validation to validate the blockrange value
function setBlockRange(uint256 _blockRange) external onlyOwner { blockRange = _blockRange; emit NewBlockRange(blockRange); }
Since execute can receive both ether and WETH, checks should be added to prevent eth transfer when the payment token is WETH to prevent ether from being locked in the contract Add function to retrived ether or accidentally sent ERC20 tokens from the contract
if (paymentToken == address(0)) { require(msg.value == price); }
checks should be added to prevent ether transfer if payment token is WETH Add function to retreive ether or accidentally sent ERC20 tokens
if (paymentToken == address(0)) { require(msg.value == price,"..."); } else { require(msg.value == 0,"...") }
call()
instead of transfer()
Since transfer forwards a fixed amount of gas 2300, the calls may fail if gas costs change in the future, or if the receiver is a smart contract that consumes more than 2300 gas.
Use .call()
to send ether instead of transfer.
refer-https://consensys.net/diligence/blog/2019/09/stop-using-soliditys-transfer-now/
payable(to).transfer(amount);
missing @amount
param in
missing @returns
param in
/** * @dev Compute the merkle root * @param leaf leaf * @param proof proof */ function _computeRoot(
missing @returns
in
/** * @dev Transfer ERC20 token * @param token address of the token * @param from address of the sender * @param to address of the recipient * @param amount amount */ function transferERC20(address token, address from, address to, uint256 amount)
/** * @notice Returns if a policy has been added * @param policy address of the policy to check */ function isPolicyWhitelisted(address policy) external view override returns (bool)
/** * @notice View number of whitelisted policies */ function viewCountWhitelistedPolicies() external view override returns (uint256)
/** * @notice See whitelisted policies * @param cursor cursor * @param size size */ function viewWhitelistedPolicies(uint256 cursor, uint256 size)
/** * @dev Determine if the given address exists * @param what address to check */ function _exists(address what)
/** * @dev Charge a fee in ETH or WETH * @param fees fees to distribute * @param paymentToken address of token to pay in * @param from address to charge fees * @param price price of token */ function _transferFees( Fee[] calldata fees, address paymentToken, address from, uint256 price ) internal returns (uint256)
/** * @dev Call the matching policy to check orders can be matched and get execution parameters * @param sell sell order * @param buy buy order */ function _canMatchOrders(Order calldata sell, Order calldata buy)
/** * @dev Wrapped ecrecover with safety check for v parameter * @param v v * @param r r * @param s s */ function _recover( bytes32 digest, uint8 v, bytes32 r, bytes32 s ) internal pure returns (address)
/** * @dev Verify the validity of oracle signature * @param orderHash hash of the order * @param signatureVersion signature version * @param extraSignature packed oracle signature * @param blockNumber block number used in oracle signature */ function _validateOracleAuthorization
/** * @dev Verify the validity of the user signature * @param orderHash hash of the order * @param trader order trader who should be the signer * @param v v * @param r r * @param s s * @param signatureVersion signature version * @param extraSignature packed merkle path */ function _validateUserAuthorization( bytes32 orderHash, address trader, uint8 v, bytes32 r, bytes32 s, SignatureVersion signatureVersion, bytes calldata extraSignature ) internal view returns (bool)
/** * @dev Verify the validity of the signatures * @param order order * @param orderHash hash of order */ function _validateSignatures(Input calldata order, bytes32 orderHash)
/** * @dev Check if the order can be settled at the current timestamp * @param listingTime order listing time * @param expirationTime order expiration time */ function _canSettleOrder(uint256 listingTime, uint256 expirationTime)
Files imported are not used
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
Events should use indexed fields, indexed fields help off-chain scripts to efficiently filter these events
event OrderCancelled(bytes32 hash); event NonceIncremented(address trader, uint256 newNonce); event NewExecutionDelegate(IExecutionDelegate executionDelegate); event NewPolicyManager(IPolicyManager policyManager); event NewOracle(address oracle); event NewBlockRange(uint256 blockRange);
Require statements does not have revert messages, So if the require condition fails, the transaction will revert without providing informative messages to the user.
require(sell.order.side == Side.Sell);
require(msg.value == price);
abicoder is used by default from solidity version 0.8.0
https://blog.soliditylang.org/2020/12/16/solidity-v0.8.0-release-announcement/
#0 - GalloDaSballo
2022-10-23T23:43:19Z
Disputed
L
R
L
L
NC
NC
Disputed per lack of nuance
NC
NC
#1 - GalloDaSballo
2022-11-07T20:34:42Z
3L 1R 4NC
#2 - GalloDaSballo
2022-11-16T00:21:32Z
I have closed the report as Unsatisfactory, however one finding is Upgraded to Med.
I'm going to set this report to Med and mark it as dup of M-01
#3 - GalloDaSballo
2022-11-16T00:21:41Z
Dup of #235
🌟 Selected for report: IllIllI
Also found by: 0x1f8b, 0xNazgul, 0xSmartContract, Aymen0909, Heuss, Lambda, Pheonix, RaymondFam, ReyAdmirado, Ruhum, Shinchan, Shishigami, __141345__, adriro, ajtra, c3phas, ch0bu, cryptostellar5, d3e4, enckrish, gogo, halden, lucacez, mcwildy, medikko, neko_nyaa, pedr02b2, pfapostol, ret2basic, rvierdiiev, saian, sakman, sakshamguruji
32.6464 USDC - $32.65
By switching from non-zero to non-zero value, expensive SSTORE from 0/false to non-zero can be avoided. and when boolean is used, writing to storage costs more than uint256 as each write operation has to perform extra read operation to correctly place the boolean value in the slot. So switching from 0/false to 1/true can be replaced by 1 and 2
modifier reentrancyGuard { require(!reentrancyLock, "Reentrancy detected"); reentrancyLock = true; _; reentrancyLock = false; }
function open() external onlyOwner { isOpen = 1; emit Opened(); } function close() external onlyOwner { isOpen = 0; emit Closed(); }
Revert strings that are longer than 32 bytes requires at least one additional mstore, along with additional overhead for computing memory offset, etc. Shortening revert strings to fit in 32 bytes will decrease deployment time gas and will decrease runtime gas when the revert condition is met.
require(contracts[msg.sender], "Contract is not approved to make transfers");
Comparison to boolean literal is more expensive than directly checking the value.
Comparison with false
can be replaced by using the !
operator with the value
require(revokedApproval[from] == false, "User has revoked approval");
require(revokedApproval[from] == false, "User has revoked approval");
require(revokedApproval[from] == false, "User has revoked approval");
require(revokedApproval[from] == false, "User has revoked approval");
(cancelledOrFilled[orderHash] == false)
Marking constants as private/internal saves gas on deployment as compiler does not have to create a getter function for these variables, these variables can still be read from the verfied source code or the bytecode.
bytes32 constant public FEE_TYPEHASH = keccak256( "Fee(uint16 rate,address recipient)" ); bytes32 constant public ORDER_TYPEHASH = keccak256( "Order(address trader,uint8 side,address matchingPolicy,address collection,uint256 tokenId,uint256 amount,address paymentToken,uint256 price,uint256 listingTime,uint256 expirationTime,Fee[] fees,uint256 salt,bytes extraParams,uint256 nonce)Fee(uint16 rate,address recipient)" ); bytes32 constant public ORACLE_ORDER_TYPEHASH = keccak256( "OracleOrder(Order order,uint256 blockNumber)Fee(uint16 rate,address recipient)Order(address trader,uint8 side,address matchingPolicy,address collection,uint256 tokenId,uint256 amount,address paymentToken,uint256 price,uint256 listingTime,uint256 expirationTime,Fee[] fees,uint256 salt,bytes extraParams,uint256 nonce)" ); bytes32 constant public ROOT_TYPEHASH = keccak256( "Root(bytes32 root)" ); bytes32 constant EIP712DOMAIN_TYPEHASH = keccak256( "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" );
string public constant name = "Blur Exchange"; string public constant version = "1.0";
Array length can be cached in a variable and used in the loop instead of read from storage in every iteration
for (uint256 i = 0; i < fees.length; i++)
for (uint256 i = 0; i < proof.length; i++)
for (uint8 i = 0; i < orders.length; i++
for (uint8 i = 0; i < fees.length; i++)
Functions used once can be inlined to save gas on performing JUMP between the functions.
function _exists(address what) internal view returns (bool) { uint size; assembly { size := extcodesize(what) } return size > 0; }
If data can fit into 32 bytes, then using bytes32 instead of string is cheaper, as string is a dynamically sized data structures and therefore consumes more gas.
string public constant name = "Blur Exchange"; string public constant version = "1.0";
Custom error from solidity 0.8.4 are cheaper than revert strings with cheaper deployment cost and runtime cost when the revert condition is met.
https://blog.soliditylang.org/2021/04/21/custom-errors/
unchecked
to save gasIf an expression cannnot overflow/underflow it can be placed in a unchecked block to avoid the implicit overflow/underflow checks introduced in 0.8.0 and save gas
https://docs.soliditylang.org/en/v0.8.10/control-structures.html#checked-or-unchecked-arithmetic
require(totalFee <= price, "Total amount of fees are more than the price"); /* Amount that will be received by seller. */ uint256 receiveAmount = price - totalFee;
Due to how the EVM natively works on 256 bit numbers, using a 8 bit number in for-loops introduces additional costs as the EVM has to properly enforce the limits of this smaller type.
for (uint8 i = 0; i < orders.length; i++)
When variables are not set, it is assumed to have it's 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.
bool reentrancyLock = false;
uint256 totalFee = 0;
++x
instead of x += 1
Pre-increment ++x
costs less when compared to x += 1
or x++
for unsigned integers
nonces[msg.sender] += 1;
In event emits using local variables or function arguments instead of storage variable can avoid storage read and save gas
emit NewExecutionDelegate(executionDelegate);
emit NewPolicyManager(policyManager);
emit NewOracle(oracle);
emit NewBlockRange(blockRange);
Variables _v
, _r
and _s
are unnecessary and the values can be directly assigned to the variabesl v
, r
and s
(bytes32[] memory merklePath, uint8 _v, bytes32 _r, bytes32 _s) = abi.decode(extraSignature, (bytes32[], uint8, bytes32, bytes32)); v = _v; r = _r; s = _s;
can be replaced with
(bytes32[] memory merklePath, v, r, s) = abi.decode(extraSignature, (bytes32[], uint8, bytes32, bytes32));
#0 - GalloDaSballo
2022-10-22T23:25:03Z
5000 no reentrancy 400 SLOAD 150 rest
5550