DeFiFoundrySolidity
16,653 OP
View results
Submission Details
Severity: medium
Invalid

Using `block.timestamp` for swap deadline offers no protection in StrategyOp.sol

Summary

The claimAndSwap functions in StrategyOp.sol use block.timestamp directly as swap deadline parameter, allowing validators to manipulate transaction execution timing for their advantage.

Vulnerability Details

In PoS chains, validators can hold transactions and execute them at advantageous timestamps since they know their block proposal slots in advance.

Affected code:

IVeloRouter(router).swapExactTokensForTokens(_amount, minOut, _path, address(this), block.timestamp);

Proof of Concept:

// SPDX-License-Identifier: AGPL-3.0
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; // 5 WETH
uint256 minOut = 5.1 ether; // Expecting 2% premium
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); // Longer delay for Optimism
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");
}
}

Test output shows successful manipulation on Optimism:

Ran 1 test for test/TimestampManipulation.t.sol:TimestampManipulationOpTest
[PASS] testVeloRouterTimestampManipulation() (gas: 38977)
Logs:
=== Velodrome Timestamp Manipulation PoC ===
Keeper submits at timestamp: 1000
Validator manipulation in progress...
Time delayed by: 2 hours
=== Optimism Strategy Swap ===
Block timestamp: 8200
Claiming amount from Transmuter: 5000000000000000000
Velo Swap Details:
Timestamp at execution: 8200
Deadline used: 8200
WETH amount in: 5 ETH
Min alETH out: 5 alETH
Depositing to Transmuter: 5100000000000000000
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 367.52µs (108.89µs CPU time)
Ran 1 test suite in 3.44ms (367.52µs CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

Impact

Validators can delay swaps for optimal pricing

Indirect fund risk through unfavorable execution timing

Affects both Arbitrum (Ramses) and Optimism (Velodrome) deployments

Slippage checks provide some mitigation

Tools Used

Foundry test framework

Manual code review

Recommendations

Add deadline parameter:

function claimAndSwap(
uint256 _amountClaim,
uint256 _minOut,
IVeloRouter.route[] calldata _path,
uint256 _deadline
) external onlyKeepers {
require(_deadline > block.timestamp, "Expired");
require(_deadline <= block.timestamp + maxDelay, "Too far");
Updates

Appeal created

inallhonesty Lead Judge 5 months ago
Submission Judgement Published
Invalidated
Reason: Known issue
inallhonesty Lead Judge 5 months ago
Submission Judgement Published
Invalidated
Reason: Known issue

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.