OrderBook

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

Admin Backdoor via Dynamic Token Listing Enables Full Rug Pull Through Emergency Withdrawal

Root + Impact

Description

In normal operation, the contract allows the owner to add new ERC20 tokens that sellers can list via the setAllowedSellToken() function. It also provides an emergencyWithdrawERC20() function that allows the owner to withdraw arbitrary ERC20 tokens from the contract — with the exception of a hardcoded list of "core" tokens.

The issue is that the contract does not protect against owner abuse of dynamically added tokens. This allows a malicious owner to:

  1. List a malicious or deceptive token.

  2. Wait for users to deposit it into the system.

  3. Withdraw all user deposits via emergencyWithdrawERC20.

function setAllowedSellToken(address _token, bool _isAllowed) external onlyOwner {
if (_token == address(0) || _token == address(iUSDC)) revert InvalidToken(); // Cannot allow null or USDC itself
@> allowedSellToken[_token] = _isAllowed;
}
function emergencyWithdrawERC20(address _tokenAddress, uint256 _amount, address _to) external onlyOwner {
if (
@> _tokenAddress == address(iWETH) || _tokenAddress == address(iWBTC) || _tokenAddress == address(iWSOL)
|| _tokenAddress == address(iUSDC)
) {
revert("Cannot withdraw core order book tokens via emergency function");
}
@> // No restriction on dynamically allowed tokens
IERC20 token = IERC20(_tokenAddress);
token.safeTransfer(_to, _amount);
}

Risk

Likelihood:

  • The admin can call setAllowedSellToken() at any time and approve a new token that users can start using.

  • The admin can call emergencyWithdrawERC20() at any time and withdraw user-deposited tokens unless the token is hardcoded as protected.

Impact:

  • Admin can rug user deposits from newly added tokens.

  • Users may be misled into trusting tokens listed via setAllowedSellToken() with no real protection.

Proof of Concept

function testMaliciousTokenRugPull() public {
// Deploy a malicious token
vm.startPrank(owner);
MockUSDT malicious = new MockUSDT(6);
// Mint tokens to victim
malicious.mint(alice, 1e6);
// owner allows malicious token via setAllowedSellToken
book.setAllowedSellToken(address(malicious), true);
// alice approves and creates a sell order
vm.startPrank(alice);
malicious.approve(address(book), 1e6);
book.createSellOrder(address(malicious), 1, 10_000_000, block.timestamp);
vm.stopPrank();
// owner rugs all user funds using emergencyWithdrawERC20
vm.prank(owner);
book.emergencyWithdrawERC20(address(malicious), 1, owner);
// Validate funds were transferred to owner
assertEq(malicious.balanceOf(owner), 1, "owner have user funds");
}

Recommended Mitigation

- function emergencyWithdrawERC20(address _tokenAddress, uint256 _amount, address _to) external onlyOwner {
- if (
- _tokenAddress == address(iWETH) || _tokenAddress == address(iWBTC) || _tokenAddress == address(iWSOL)
- || _tokenAddress == address(iUSDC)
- ) {
- revert("Cannot withdraw core order book tokens via emergency function");
- }
+ function emergencyWithdrawERC20(address _tokenAddress, uint256 _amount, address _to) external onlyOwner {
+ if (allowedSellToken[_tokenAddress]) {
+ revert("Cannot emergency withdraw allowed sell token");
+ }

This change prevents the admin from using emergencyWithdrawERC20() to rug tokens that were previously approved for user deposits via setAllowedSellToken.

Updates

Lead Judging Commences

yeahchibyke Lead Judge about 1 month ago
Submission Judgement Published
Invalidated
Reason: Too generic

Appeal created

sg_milad Submitter
about 1 month ago
yeahchibyke Lead Judge
about 1 month ago
sg_milad Submitter
about 1 month ago
yeahchibyke Lead Judge about 1 month ago
Submission Judgement Published
Invalidated
Reason: Too generic

Support

FAQs

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