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

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

Summary

The claimAndSwap functions in StrategyArb.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:

// StrategyArb.sol
IRamsesRouter(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 IRamsesRouter {
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:", amount);
}
function deposit(uint256 amount, address) external {
console.log("Depositing amount:", amount);
}
function getUnexchangedBalance(address) external pure returns (uint256) { return 0; }
}
contract MockRamsesRouter is IRamsesRouter {
function swapExactTokensForTokens(
uint amountIn,
uint amountOutMin,
route[] calldata,
address,
uint deadline
) external returns (uint[] memory) {
console.log("Swap executed at timestamp:", block.timestamp);
console.log("With deadline:", deadline);
console.log("Amount in:", amountIn);
console.log("Min amount out:", amountOutMin);
uint[] memory amounts = new uint[]();
amounts[0] = amountOutMin;
return amounts;
}
}
contract StrategyArb is BaseStrategy {
address public keeper;
MockRamsesRouter public router;
MockTransmuter public transmuter;
constructor() {
router = new MockRamsesRouter();
transmuter = new MockTransmuter();
}
function setKeeper(address _keeper) external override { keeper = _keeper; }
function claimAndSwap(uint256 amount, uint256 minOut, IRamsesRouter.route[] calldata path) external {
console.log("\n=== Transaction Details ===");
console.log("Block timestamp at execution:", block.timestamp);
console.log("Using timestamp as deadline:", block.timestamp);
transmuter.claim(amount, address(this));
router.swapExactTokensForTokens(amount, minOut, path, address(this), block.timestamp);
transmuter.deposit(minOut, address(this));
}
}
contract TimestampManipulationTest is Test {
StrategyArb strategy;
address keeper = address(0x1);
function setUp() public {
strategy = new StrategyArb();
vm.prank(strategy.management());
strategy.setKeeper(keeper);
}
function testTimestampManipulation() public {
uint256 amountClaim = 1 ether;
uint256 minOut = 1.01 ether;
IRamsesRouter.route[] memory path = new IRamsesRouter.route[]();
console.log("\n=== Timestamp Manipulation PoC ===");
vm.warp(1000);
uint256 intendedTimestamp = block.timestamp;
console.log("Intended execution timestamp:", intendedTimestamp);
console.log("\nValidator delays transaction...");
vm.warp(block.timestamp + 1 hours);
console.log("Actual execution timestamp:", block.timestamp);
console.log("Time delayed by:", block.timestamp - intendedTimestamp, "seconds");
vm.prank(keeper);
strategy.claimAndSwap(amountClaim, minOut, path);
assertTrue(block.timestamp > intendedTimestamp + 30 minutes, "Transaction executed too early");
}
}

Test output shows successful manipulation:

Ran 1 test for test/TimestampManipulation.t.sol:TimestampManipulationTest
[PASS] testTimestampManipulation() (gas: 38649)
Logs:
=== Timestamp Manipulation PoC ===
Intended execution timestamp: 1000
Validator delays transaction...
Actual execution timestamp: 4600
Time delayed by: 3600 seconds
=== Transaction Details ===
Block timestamp at execution: 4600
Using timestamp as deadline: 4600
Claiming amount: 1000000000000000000
Swap executed at timestamp: 4600
With deadline: 4600
Amount in: 1000000000000000000
Min amount out: 1010000000000000000
Depositing amount: 1010000000000000000
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 408.13µs (145.28µs CPU time)
Ran 1 test suite in 3.70ms (408.13µs CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

Impact

Validators can:

Hold transactions until prices are favorable

Execute swaps at manipulated timestamps

Extract value through strategic execution timing

Undermine intended swap timing protections

Tools Used

Foundry testing framework

Manual code review

Recommendations

Allow function caller to specify deadline parameter:

function claimAndSwap(
uint256 _amountClaim,
uint256 _minOut,
IRamsesRouter.route[] calldata _path,
uint256 _deadline
) external onlyKeepers {
require(_deadline > block.timestamp, "Expired deadline");
...
}

Implement maximum deadline duration

Add slippage control based on oracle prices

Consider implementing commit-reveal scheme for critical operations

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.