OrderBook

First Flight #43
Beginner FriendlySolidity
100 EXP
View results
Submission Details
Impact: high
Likelihood: high
Invalid

Owner Has Unrestricted Access to All Protocol Fees via `OrderBook::withdrawFees` function

Root + Impact

Description

The `withdrawFees()` function allows the owner to withdraw all accumulated protocol fees to any address without restrictions, governance, or transparency mechanisms. This creates a centralized control over protocol revenue.
@> function withdrawFees(address _to) external onlyOwner {
if (totalFees == 0) {
revert InvalidAmount();
}
if (_to == address(0)) {
revert InvalidAddress();
}
iUSDC.safeTransfer(_to, totalFees);
totalFees = 0;
emit FeesWithdrawn(_to);
}

Risk

Likelihood:

  • Onwer can can immediately withdraw protocol revenue anytime at will

Impact:

- Owner can:
- Drain all protocol fees at any time
- Redirect fees to personal addresses
- Prevent protocol development funding
- Create exit scams

Proof of Concept


1. Owner can call: `withdrawFees(personalWallet1)`

2. Result: All fees go to owner's personal address


run forge test --mt test_ownerCanWithdrawFeesAnytime -vvvvv

with the below PoC in TestOrderBook.t.sol

function test_ownerCanWithdrawFeesAnytime() public {
vm.startPrank(alice);
wbtc.approve(address(book), 2e8);
uint256 orderId1 = book.createSellOrder(
address(wbtc),
1e8,
100_000e6,
1 days
);
vm.stopPrank();
vm.startPrank(bob);
weth.approve(address(book), 2e18);
uint256 orderId2 = book.createSellOrder(
address(weth),
1e18,
5_000e6,
1 days
);
vm.stopPrank();
vm.startPrank(dan);
usdc.approve(address(book), 200_000e6);
book.buyOrder(orderId1);
vm.stopPrank();
uint256 feesAfterFirstBuy = book.totalFees();
assertGt(feesAfterFirstBuy, 0, "Should have fees after first buy");
address personalWallet1 = makeAddr("personal_wallet_1");
vm.prank(owner);
book.withdrawFees(personalWallet1);
assertEq(usdc.balanceOf(personalWallet1), feesAfterFirstBuy);
assertEq(book.totalFees(), 0, "Fees should be reset");
vm.startPrank(dan);
book.buyOrder(orderId2);
vm.stopPrank();
uint256 feesAfterSecondBuy = book.totalFees();
assertGt(feesAfterSecondBuy, 0, "Should have fees after second buy");
// Owner withdraws fees again to a different address
address personalWallet2 = makeAddr("personal_wallet_2");
vm.prank(owner);
book.withdrawFees(personalWallet2);
assertEq(usdc.balanceOf(personalWallet2), feesAfterSecondBuy);
assertEq(book.totalFees(), 0, "Fees should be reset again");
assertEq(
usdc.balanceOf(personalWallet1) + usdc.balanceOf(personalWallet2),
feesAfterFirstBuy + feesAfterSecondBuy,
"Total fees should match sum of individual withdrawals"
);
}

Recommended Mitigation

Implement a multi-signature wallet for fee withdrawals
Add a governance mechanism for fee distribution decisions
Implement automatic fee distribution to multiple stakeholders Add a timelock for large fee withdrawals Consider implementing a treasury contract with defined allocation rules
Updates

Lead Judging Commences

yeahchibyke Lead Judge 4 months ago
Submission Judgement Published
Invalidated
Reason: Design choice

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.