Root + Impact
Description
-
Users are able to place orders for their tokens on their selected price in USDC token only, and only USDC token can be used to fill orders.
-
USDC is an ERC-20 token contract which is controlled by Circle. Custodial wallets owned by Circle have the ability to restrict certain addresses of their choosing to be blacklisted from all USDC transactions.
-
If the OrderBook
contract is blacklisted from USDC transactions, then no orders will be filled due to buyOrder()
function getting reverted on USDC transactions for protocol fees.
function buyOrder(uint256 _orderId) public {
Order storage order = orders[_orderId];
if (order.seller == address(0)) revert OrderNotFound();
if (!order.isActive) revert OrderNotActive();
if (block.timestamp >= order.deadlineTimestamp) revert OrderExpired();
order.isActive = false;
uint256 protocolFee = (order.priceInUSDC * FEE) / PRECISION;
uint256 sellerReceives = order.priceInUSDC - protocolFee;
@> iUSDC.safeTransferFrom(msg.sender, address(this), protocolFee);
iUSDC.safeTransferFrom(msg.sender, order.seller, sellerReceives);
IERC20(order.tokenToSell).safeTransfer(msg.sender, order.amountToSell);
totalFees += protocolFee;
emit OrderFilled(_orderId, msg.sender, order.seller);
}
Risk
Likelihood:
Impact:
Proof of Concept
Add the following ERC-20 contract code for a mock USDC with blacklisting feature in test/mocks/BlacklistingUSDC.sol
:
pragma solidity 0.8.26;
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
contract BlacklistingUSDC is ERC20, Ownable {
uint8 constant tokenDecimals = 6;
mapping(address => bool) public isBlacklisted;
modifier notBlacklisted(address _from, address _to) {
require(!isBlacklisted[_from], "Sender is blacklisted");
require(!isBlacklisted[_to], "Recipient is blacklisted");
_;
}
constructor() ERC20("BlacklistingUSDC", "bUSDC") Ownable(msg.sender) {}
function decimals() public pure override returns (uint8) {
return tokenDecimals;
}
function mint(address to, uint256 value) public {
uint256 updateDecimals = uint256(tokenDecimals);
_mint(to, (value * 10 ** updateDecimals));
}
function blacklist(address _account) external onlyOwner {
isBlacklisted[_account] = true;
}
function unBlacklist(address _account) external onlyOwner {
isBlacklisted[_account] = false;
}
function transfer(address to, uint256 value) public override notBlacklisted(msg.sender, to) returns (bool) {
super.transfer(to, value);
return true;
}
function transferFrom(address from, address to, uint256 value) public override notBlacklisted(from, to) returns (bool) {
super.transferFrom(from, to, value);
return true;
}
}
Then place the following function in test/TestOrderBook.t.sol
and run with forge test --mt test_USDCBlacklistedContract
:
import {BlacklistingUSDC} from "./mocks/BlacklistingUSDC.sol";
.
.
.
function test_USDCBlacklistedContract() public {
vm.startPrank(owner);
BlacklistingUSDC busdc = new BlacklistingUSDC();
book = new OrderBook(address(weth), address(wbtc), address(wsol), address(busdc), owner);
busdc.blacklist(address(book));
vm.stopPrank();
vm.startPrank(alice);
wbtc.approve(address(book), 2e8);
uint256 aliceId = book.createSellOrder(address(wbtc), 2e8, 180_000e6, 2 days);
vm.stopPrank();
vm.startPrank(dan);
busdc.approve(address(book), 200_000e6);
busdc.mint(dan, 180_000e6);
vm.expectRevert("Recipient is blacklisted");
book.buyOrder(aliceId);
}
This test case passes when the buyOrder()
function reverts due to contract being blacklisted.
Recommended Mitigation
-constructor(address _weth, address _wbtc, address _wsol, address _usdc, address _owner) Ownable(_owner) {
+constructor(address _weth, address _wbtc, address _wsol, address _wusdc, address _owner) Ownable(_owner) {