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: 12/62
Findings: 3
Award: $614.97
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: rotcivegaf
Also found by: 0x4non, 0xDecorativePineapple, 9svR6w, Trust, V_B, adriro, ajtra, aviggiano, brgltd, carlitox477, chaduke, codexploder, corerouter, joestakey, ladboy233, s3cunda, saian, wait
66.8068 USDC - $66.81
https://github.com/code-423n4/2022-11-non-fungible/blob/323b7cbf607425dd81da96c0777c8b12e800305d/contracts/Exchange.sol#L206 https://github.com/code-423n4/2022-11-non-fungible/blob/323b7cbf607425dd81da96c0777c8b12e800305d/contracts/Exchange.sol#L212 https://github.com/code-423n4/2022-11-non-fungible/blob/323b7cbf607425dd81da96c0777c8b12e800305d/contracts/Exchange.sol#L216
The dust ETH refund may failed slient.
The delegate call may fail sliently.
The dust ETH refund logic does not handle the return value.
function _returnDust() private { uint256 _remainingETH = remainingETH; assembly { if gt(_remainingETH, 0) { let callStatus := call( gas(), caller(), selfbalance(), 0, 0, 0, 0 ) } } }
as we see, the return value callStatus is not handled. If the underlying sending is a smart contract and does not implement the default receive payable() function, the ETH refund will fail sliently. Then the user lost the dusted ETH and next person trade get the previous user's dusted ETH.
Same case is in the delegatecall when bulk executing.
mstore(memPointer, 0xe04d94ae00000000000000000000000000000000000000000000000000000000) // _execute calldatacopy(add(0x04, memPointer), order_pointer, size) // must be put in separate transaction to bypass failed executions // must be put in delegatecall to maintain the authorization from the caller let result := delegatecall(gas(), address(), memPointer, add(size, 0x04), 0, 0)
the result is not handled, allowing the delegateCall to fail sliently.
User may wrongly assume that the transaction succeed, but the transaction failed sliently.
Manual Review
We recommend the project handle the return value for dusted ETH refund and for delegate call return value properly. When the return value is false, strictly revert the transaction.
#0 - c4-judge
2022-11-16T11:55:49Z
berndartmueller marked the issue as duplicate of #90
#1 - c4-judge
2022-11-16T11:56:03Z
berndartmueller marked the issue as satisfactory
🌟 Selected for report: Trust
Also found by: 0xSmartContract, 0xhacksmithh, 9svR6w, Josiah, deliriusz, ladboy233, zaskoh
505.6113 USDC - $505.61
https://github.com/code-423n4/2022-11-non-fungible/blob/323b7cbf607425dd81da96c0777c8b12e800305d/contracts/Exchange.sol#L633 https://github.com/code-423n4/2022-11-non-fungible/blob/323b7cbf607425dd81da96c0777c8b12e800305d/contracts/Exchange.sol#L635 https://github.com/code-423n4/2022-11-non-fungible/blob/323b7cbf607425dd81da96c0777c8b12e800305d/contracts/Pool.sol#L58 https://github.com/code-423n4/2022-11-non-fungible/blob/323b7cbf607425dd81da96c0777c8b12e800305d/contracts/Pool.sol#L62
Admin can steal all the fund in the Pool.
The Pool contract is meant to be used as a liqudity buffer. so when the trade is setteled, the user can deposit the fund into the Pool beforehand to settle the trade.
else if (paymentToken == POOL) { /* Transfer Pool funds. */ bool success = IPool(POOL).transferFrom(from, to, amount); require(success, "Pool transfer failed"); }
Which calls
function transferFrom(address from, address to, uint256 amount) public returns (bool) { if (msg.sender != EXCHANGE && msg.sender != SWAP) { revert('Caller is not authorized to transfer'); } _transfer(from, to, amount); return true; }
the internal function just substract balance from address(from) and add the balance to address(to)
function _transfer(address from, address to, uint256 amount) private { require(_balances[from] >= amount); require(to != address(0)); _balances[from] -= amount; _balances[to] += amount; emit Transfer(from, to, amount); }
let us look into the transfserFrom again:
function transferFrom(address from, address to, uint256 amount) public returns (bool) { if (msg.sender != EXCHANGE && msg.sender != SWAP) { revert('Caller is not authorized to transfer'); } _transfer(from, to, amount); return true; }
note the access control:
if (msg.sender != EXCHANGE && msg.sender != SWAP) { revert('Caller is not authorized to transfer'); }
address SWAP is out of scope, but the address EXCHANGE is clearly the Exchange.sol set in the Pool.sol constructor.
Basically we can say the contract Exchange has the privilege to control the fund in the Pool because it can call transferFrom to modify the balance before account.
And the Exchange contract is upgradeable and admin can upgradeable.
This means a compromised admin can upgrade the Exchange to a malicious implementation and the malicious implementation can call TransferFrom directly to decrease the user's balance and increase the admin's balance, then admin can call
function withdraw(uint256 amount) public { require(_balances[msg.sender] >= amount); _balances[msg.sender] -= amount; (bool success,) = payable(msg.sender).call{value: amount}(""); require(success); emit Transfer(address(0), msg.sender, amount); }
to withdraw the fund from the Pool.sol at the cost of the user.
Manual Review
We recommend recommend the admin use M of N mulsig to safeguard the admin address. Also the project can make sure that the function transferFrom
function transferFrom(address from, address to, uint256 amount) public returns (bool) { if (msg.sender != EXCHANGE && msg.sender != SWAP) { revert('Caller is not authorized to transfer'); } _transfer(from, to, amount); return true; }
is only callable inside the function _transferTo in Exchange.sol when settling the payment.
function _transferTo( address paymentToken, address from, address to, uint256 amount ) internal {
#0 - c4-judge
2022-11-17T10:28:23Z
berndartmueller marked the issue as duplicate of #179
#1 - c4-judge
2022-11-17T10:28:28Z
berndartmueller marked the issue as satisfactory
🌟 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
Exchange.sol makes use of Open Zeppelin's OwnableUpgradeable.sol && UUPSUpgradeable.sol in the file but does not use an upgradeable version of ReentrancyGuarded.sol && EIP712.sol Contracts.
Consider just using Open Zeppelin's upgradeable equivalent contracts or taking from them what is needed to make your version of the contracts upgradeable.
For upgradeable contracts, inheriting contracts may introduce new variables. In order to be able to add new variables to the upgradeable contract without causing storage collisions, a storage gap should be added to the upgradeable acontract.
If no storage gap is added, when the upgradable contract introduces new variables, it may override the variables in the inheriting contract.
Consider adding a storage gap at the end of the upgradeable abstract contract
uint256[50] private __gap;
whether order is taker order or makter order is determined by the listing time, which is in the control of the user,
function _canMatchOrders(Order calldata sell, Order calldata buy) internal view returns (uint256 price, uint256 tokenId, uint256 amount, AssetType assetType) { bool canMatch; if (sell.listingTime <= buy.listingTime) { /* Seller is maker. */ require(policyManager.isPolicyWhitelisted(sell.matchingPolicy), "Policy is not whitelisted"); (canMatch, price, tokenId, amount, assetType) = IMatchingPolicy(sell.matchingPolicy).canMatchMakerAsk(sell, buy); } else { /* Buyer is maker. */ require(policyManager.isPolicyWhitelisted(buy.matchingPolicy), "Policy is not whitelisted"); (canMatch, price, tokenId, amount, assetType) = IMatchingPolicy(buy.matchingPolicy).canMatchMakerBid(buy, sell); } require(canMatch, "Orders cannot be matched"); return (price, tokenId, amount, assetType); }
listingTime can be spoofed in the order, then maker order can be executed as taker and taker order can be treated as maker.
We recommend the exchange assign listing time strictly offline instead of letting user spoof the listing time.
the current openzeppelin version is oudated.
"@openzeppelin/contracts": "4.4.1", "@openzeppelin/contracts-upgradeable": "^4.6.0",
Please update the openzepplein package to latest version.
#0 - c4-judge
2022-11-15T21:37:52Z
berndartmueller marked the issue as grade-b