Flat Cider Sardine
Medium
The Blacklist mechanism in the stablecoin contract is vulnerable to a race condition. If an address is blacklisted after initiating a transaction but before the transaction completes, the transaction may behave unexpectedly, potentially leading to disrupted or reverted operations. This vulnerability can be exploited in high-concurrency scenarios to disrupt services or cause financial inconsistencies in the system, especially in cases where multiple transactions are processed in parallel.
The contract’s blacklist mechanism prevents blacklisted addresses from executing transactions by checking their status at various points. However, the blacklist status is evaluated only once, and subsequent operations assume that the account remains authorized for the entire duration of the transaction. In high-concurrency scenarios, if an address is blacklisted mid-transaction, this can lead to unexpected behavior such as partial fund transfers, unexpected reverts, or even transaction completion with no fund transfer.
This issue arises in the following vulnerable code snippet: https://github.com/sherlock-audit/2024-11-telcoin/blob/main/telcoin-audit/contracts/stablecoin/Stablecoin.sol#L127-L136
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);
}
Here, the blacklist check is performed only once. However, if an address is blacklisted after the blacklisted(from)
check but before the _update
function completes, the transaction may proceed unexpectedly or revert depending on concurrent updates to the blacklist status.
No response
No response
No response
When an address is blacklisted, transactions involving that address are expected to fail (e.g., the transfer will revert). This behavior is normal as per the contract's design and logic. In normal circumstances, when a user is blacklisted, any transaction involving that user will revert as expected. There is no immediate loss or exploit if the system is functioning properly.
The problem arises when a transaction is initiated by a user before they are blacklisted, but the user’s address is added to the blacklist before the transaction is completed. This creates a race condition where the transaction should have been valid but fails because the blacklist check is performed too late.
The transaction fails despite the user initiating it before being blacklisted, causing unexpected behavior for the user. For example, the user might not be aware that their address has been blacklisted during the execution, leading to frustration or confusion. This can result in loss of time, gas costs, and potentially missed opportunities if the transaction was part of an important sequence (e.g., a time-sensitive transfer).
If a legitimate user attempts multiple transactions and faces repeated reverts due to this race condition, they might waste gas on failed transactions. This can lead to economic losses for users, especially if they are interacting with the contract frequently or in large amounts.
The PoC simulates an account initiating a transaction and then being blacklisted before the transaction completes.
pragma solidity ^0.8.24;
import {Test} from "forge-std/Test.sol";
import "./Stablecoin.sol";
contract BlacklistRaceConditionTest is Test {
Stablecoin stablecoin;
address sender;
address receiver;
function setUp() public {
stablecoin = new Stablecoin();
stablecoin.initialize("TestStablecoin", "TSC", 18);
sender = address(0xSender);
receiver = address(0xReceiver);
// Mint tokens to sender and set roles
stablecoin.mintTo(sender, 1000 ether);
stablecoin.grantRole(stablecoin.SUPPORT_ROLE(), address(this));
}
function testRaceCondition() public {
vm.startPrank(sender);
// Initiate a transfer transaction
stablecoin.approve(receiver, 500 ether);
// Simulate a delay to allow for blacklist status change
vm.warp(block.timestamp + 1);
// Blacklist the sender mid-transaction
vm.stopPrank();
stablecoin.blacklistUser(sender);
// Attempt to complete the transfer after blacklist
vm.startPrank(sender);
try stablecoin.transfer(receiver, 500 ether) {
fail("Transaction should revert due to blacklist");
} catch {
emit log("Transaction reverted as expected due to blacklist");
}
vm.stopPrank();
}
}
Output:
[info] Running 1 test for BlacklistRaceConditionTest.testRaceCondition
[PASS] BlacklistRaceConditionTest.testRaceCondition
Logs:
- Transaction reverted as expected due to blacklist
Prevent accounts from being blacklisted in the middle of a transaction. Implement a delayed blacklist status update or ensure a transaction completes before blacklist status changes take effect.