Skip to content

Latest commit

 

History

History
179 lines (133 loc) · 6.47 KB

023.md

File metadata and controls

179 lines (133 loc) · 6.47 KB

Magic Laurel Gorilla

Medium

Fund Seizure Mechanism Bypass in Blacklist Through MEV/Frontrunning Leading to Failed Asset Recovery

Summary

Lack of atomicity in blacklisting operation can be exploited to drain funds, as users can detect the blacklist transaction in mempool and move their funds before their balance is seized.

Root Cause

In Blacklist.sol, the blacklist operation is non-atomic and easily identifiable in the mempool through the addBlackList function call, giving users time to react before the _onceBlacklisted seizes their funds. https://github.com/sherlock-audit/2024-11-telcoin/blob/main/telcoin-audit/contracts/util/abstract/Blacklist.sol#L77-L85

https://github.com/sherlock-audit/2024-11-telcoin/blob/main/telcoin-audit/contracts/stablecoin/Stablecoin.sol#L118-L125

Internal pre-conditions

  1. User needs to have a balance in the Stablecoin contract

External pre-conditions

No response

Attack Path

  1. ADMIN submits transaction to addBlackList(targetUser).
  2. targetUser(blacklisted address) sets a MEV bot sees pending blacklist transaction in mempool.
  3. targetUser frontruns with transfer of all their tokens to different address.
  4. When blacklist transaction executes, _onceBlacklisted tries to seize 0 balance.

Impact

INVARIANT BROKEN! The protocol fails to seize funds from malicious users, defeating the purpose of the blacklist system's fund seizure mechanism.

PoC

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.24;

import "forge-std/Test.sol";
import "../src/stablecoin/Stablecoin.sol";
import "../src/util/abstract/Blacklist.sol";

contract BlacklistFrontrunTest is Test {
    Stablecoin public stablecoin;
    address public admin = address(0x1);
    address public blacklister = address(0x2);
    address public alice = address(0x3);
    address public bob = address(0x4);
    
    // Initial balance for Alice
    uint256 constant INITIAL_BALANCE = 1000 ether;
    
    function setUp() public {
        // Deploy Stablecoin contract
        stablecoin = new Stablecoin();
        
        // Initialize with admin
        stablecoin.initialize("Test Stablecoin", "TST");
        
        // Setup roles
        vm.startPrank(admin);
        stablecoin.grantRole(stablecoin.DEFAULT_ADMIN_ROLE(), admin);
        stablecoin.grantRole(stablecoin.BLACKLISTER_ROLE(), blacklister);
        stablecoin.grantRole(stablecoin.MINTER_ROLE(), admin);
        vm.stopPrank();
        
        // Mint initial tokens to Alice
        vm.prank(admin);
        stablecoin.mint(alice, INITIAL_BALANCE);
    }
    
    function test_normalBlacklistSeizure() public {
        // Normal case where blacklist succeeds in seizing funds
        vm.prank(blacklister);
        stablecoin.addBlackList(alice);
        
        // Verify funds were seized
        assertEq(stablecoin.balanceOf(alice), 0, "Alice balance should be 0");
        assertEq(stablecoin.balanceOf(blacklister), INITIAL_BALANCE, "Blacklister should have seized funds");
    }
    
    function test_frontrunBlacklistAttack() public {
        // Store Alice's initial balance for verification
        uint256 aliceInitialBalance = stablecoin.balanceOf(alice);
        
        // Simulate mempool monitoring
        // We get the calldata that would be used to blacklist Alice
        bytes memory blacklistCalldata = abi.encodeWithSelector(
            stablecoin.addBlackList.selector,
            alice
        );
        
        // FRONTRUN: Alice sees the blacklist transaction and quickly moves funds
        vm.prank(alice);
        stablecoin.transfer(bob, aliceInitialBalance);
        
        // Original blacklist transaction executes
        vm.prank(blacklister);
        stablecoin.addBlackList(alice);
        
        // Verify the attack results
        assertEq(stablecoin.blacklisted(alice), true, "Alice should be blacklisted");
        assertEq(stablecoin.balanceOf(alice), 0, "Alice balance should be 0");
        assertEq(stablecoin.balanceOf(bob), INITIAL_BALANCE, "Bob should have all the funds");
        assertEq(stablecoin.balanceOf(blacklister), 0, "Blacklister should have seized nothing");
        
        // Verify Alice can't receive new funds after blacklist
        vm.expectRevert(abi.encodeWithSelector(Blacklist.Blacklisted.selector, alice));
        vm.prank(bob);
        stablecoin.transfer(alice, 100);
    }
    
    function test_frontrunWithMultipleTransfers() public {
        // Split the balance transfer to multiple addresses to make recovery harder
        address[] memory receivers = new address[](3);
        receivers[0] = address(0x5);
        receivers[1] = address(0x6);
        receivers[2] = address(0x7);
        
        uint256 splitAmount = INITIAL_BALANCE / 3;
        
        // FRONTRUN: Alice splits and transfers funds
        vm.startPrank(alice);
        for(uint i = 0; i < receivers.length; i++) {
            stablecoin.transfer(receivers[i], splitAmount);
        }
        vm.stopPrank();
        
        // Blacklist transaction executes
        vm.prank(blacklister);
        stablecoin.addBlackList(alice);
        
        // Verify the attack results
        assertEq(stablecoin.balanceOf(alice), 0, "Alice balance should be 0");
        assertEq(stablecoin.balanceOf(blacklister), 0, "Blacklister should have seized nothing");
        
        // Verify funds are distributed among receivers
        for(uint i = 0; i < receivers.length; i++) {
            assertEq(stablecoin.balanceOf(receivers[i]), splitAmount, 
                "Each receiver should have their split amount");
        }
    }
    
    function test_blacklistWithNoBalance() public {
        // Transfer all funds before blacklist
        vm.prank(alice);
        stablecoin.transfer(bob, INITIAL_BALANCE);
        
        // Try to blacklist empty account
        vm.prank(blacklister);
        stablecoin.addBlackList(alice);
        
        assertEq(stablecoin.balanceOf(alice), 0, "Alice balance should be 0");
        assertEq(stablecoin.balanceOf(blacklister), 0, "Blacklister should have seized nothing");
        assertEq(stablecoin.balanceOf(bob), INITIAL_BALANCE, "Bob should have all funds");
    }
}

Mitigation

  1. Use a two-step process with time-lock:
function proposeBlacklist(address user) external onlyRole(BLACKLISTER_ROLE) {
    proposedBlacklist[user] = block.timestamp + DELAY;
}

function executeBlacklist(address user) external onlyRole(BLACKLISTER_ROLE) {
    require(block.timestamp >= proposedBlacklist[user], "Too early");
    // Execute blacklist
}