BriVault

First Flight #52
Beginner FriendlySolidity
100 EXP
View results
Submission Details
Impact: medium
Likelihood: medium
Invalid

Tournament Timing Bypass via Block Timestamp

Root + Impact

Description

  • The BriVault protocol implements strict temporal boundaries for tournament participation phases. Users should only be able to deposit assets and join tournament events before the official tournament start time. However, the protocol uses exact timestamp equality checks (block.timestamp >= eventStartDate) that can be bypassed through timestamp manipulation.

  • In test environments, developers can use vm.warp() to arbitrarily set block timestamps. In production chains, miners can influence block timestamps within protocol-allowed bounds (±15 seconds on Ethereum). This allows attackers to bypass timing restrictions and participate in tournaments outside intended time windows.

  • Root Cause: Exact timestamp equality checks in deposit(), joinEvent(), and cancelParticipation() are vulnerable to manipulation in test environments and some production chains where miners can influence block timestamps within allowed bounds.

// In deposit() function - vulnerable timestamp check
@>if (block.timestamp >= eventStartDate) {
@> revert eventStarted();
@>}
// In joinEvent() function - same vulnerability
@>if (block.timestamp >= eventStartDate) {
@> revert eventStarted();
@>}
// In cancelParticipation() function - similar vulnerable checks

Risk

Likelihood: Medium

  • Test Environment Exploitation: High likelihood in development/testing environments where vm.warp() allows arbitrary timestamp manipulation

  • Production Chain Exploitation: Medium likelihood on chains allowing miner timestamp influence (±15 seconds on Ethereum)

Impact: Medium

  • Tournament Integrity Breach: Undermines temporal boundaries fundamental to tournament mechanics

  • Unfair Participation: Attackers can deposit and join events after official deadlines

  • Protocol Trust Erosion: Users lose confidence in timing restrictions and fair play

  • Economic Impact: Creates unfair advantages without direct fund theft

Proof of Concept

The POC demonstrates how tournament timing restrictions can be bypassed through block timestamp manipulation, allowing deposits and event participation outside intended time windows. It shows that in test environments, developers can use vm.warp() to arbitrarily set block timestamps, and in production chains, miners can influence timestamps within allowed bounds to bypass timing restrictions.

The test verifies that attackers can deposit assets and join tournament events after the official deadline by manipulating the block timestamp to appear before the eventStartDate.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import "forge-std/Test.sol";
import {BriVault} from "../../src/briVault.sol";
import {ERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
/**
* PoC: Tournament Timing Bypass via Block Timestamp Manipulation
* Impact: Tournament timing restrictions can be bypassed through block timestamp manipulation
*
* Root Cause: Exact timestamp equality checks in deposit(), joinEvent(), and cancelParticipation()
* are vulnerable to manipulation in test environments and some production chains where miners
* can influence block timestamps within allowed bounds.
*/
contract MockERC20 is ERC20 {
constructor(string memory name, string memory symbol) ERC20(name, symbol) {}
function mint(address to, uint256 amount) external {
_mint(to, amount);
}
}
contract TournamentTimingBypassPoC is Test {
BriVault vault;
MockERC20 asset;
address owner = makeAddr("owner");
address attacker = makeAddr("attacker");
address feeRecipient = makeAddr("feeRecipient");
uint256 constant PARTICIPATION_FEE_BPS = 100; // 1%
uint256 constant MINIMUM_AMOUNT = 100 ether;
uint256 EVENT_START;
uint256 EVENT_END;
function setUp() public {
// Initialize time variables - tournament starts in 1 day
EVENT_START = block.timestamp + 1 days;
EVENT_END = EVENT_START + 7 days;
// Deploy mock ERC20 asset
asset = new MockERC20("Mock Token", "MOCK");
// Deploy vault with tournament parameters
vm.startPrank(owner);
vault = new BriVault(
IERC20(address(asset)),
PARTICIPATION_FEE_BPS,
EVENT_START,
feeRecipient,
MINIMUM_AMOUNT,
EVENT_END
);
// Set up tournament countries
string[48] memory countries;
countries[0] = "Brazil";
vault.setCountry(countries);
vm.stopPrank();
// Setup user balances and approvals
asset.mint(attacker, 10000 ether);
vm.startPrank(attacker);
asset.approve(address(vault), type(uint256).max);
vm.stopPrank();
}
/**
* CRITICAL VULNERABILITY: Timestamp Manipulation Bypass
* Attacker manipulates block timestamp to bypass deposit restrictions
*/
function test_TimestampManipulationDepositBypass() public {
console.log("=== TIMESTAMP MANIPULATION DEPOSIT BYPASS ===");
// First demonstrate normal restrictions work
vm.warp(EVENT_START + 1 hours); // 1 hour after tournament start
vm.startPrank(attacker);
vm.expectRevert(BriVault.eventStarted.selector);
vault.deposit(MINIMUM_AMOUNT + 10 ether, attacker);
vm.stopPrank();
console.log("Normal restriction works: deposit blocked after tournament start");
// Now manipulate timestamp to bypass restriction
vm.warp(EVENT_START - 1); // Manipulate to just before tournament start
vm.startPrank(attacker);
uint256 sharesReceived = vault.deposit(MINIMUM_AMOUNT + 10 ether, attacker);
vm.stopPrank();
console.log("Timestamp manipulated to:", block.timestamp);
console.log("Tournament start time:", EVENT_START);
console.log("Shares received:", sharesReceived);
console.log("Attacker staked assets:", vault.stakedAsset(attacker));
// Verify bypass worked
assertGt(sharesReceived, 0, "Attacker bypassed deposit restriction via timestamp manipulation");
assertGt(vault.stakedAsset(attacker), 0, "Attacker successfully deposited despite timing restriction");
}
/**
* Demonstrate Join Event Bypass
* Attacker manipulates timestamp to join tournament after official start
*/
function test_TimestampManipulationJoinEventBypass() public {
console.log("=== TIMESTAMP MANIPULATION JOIN EVENT BYPASS ===");
// Deposit normally first
vm.warp(EVENT_START - 1 hours);
vm.startPrank(attacker);
vault.deposit(MINIMUM_AMOUNT + 10 ether, attacker);
// Manipulate timestamp to just before start and join
vm.warp(EVENT_START - 1); // Just before tournament officially started
vault.joinEvent(0); // Join Brazil
vm.stopPrank();
console.log("Join attempt time:", block.timestamp);
console.log("Tournament start time:", EVENT_START);
console.log("Attacker country:", vault.userToCountry(attacker));
console.log("Participant count:", vault.numberOfParticipants());
// Verify bypass worked
assertEq(vault.userToCountry(attacker), "Brazil", "Attacker joined tournament late via timestamp manipulation");
assertEq(vault.numberOfParticipants(), 1, "Participant count increased");
}
/**
* Demonstrate Production Chain Vulnerability
* Show how timestamp manipulation could work on real chains with miner influence
*/
function test_ProductionChainMinerInfluence() public {
console.log("=== PRODUCTION CHAIN MINER INFLUENCE VULNERABILITY ===");
// Simulate Ethereum's timestamp rules (±15 seconds miner influence)
uint256 minerManipulatedTime = EVENT_START - 10; // Within realistic miner influence window
console.log("Tournament start time:", EVENT_START);
console.log("Miner-influenced timestamp:", minerManipulatedTime);
console.log("Time difference:", EVENT_START - minerManipulatedTime, "seconds");
console.log("Within Ethereum bounds (+/-15s):", EVENT_START - minerManipulatedTime <= 15 ? "YES" : "NO");
// Attack within realistic production bounds
vm.warp(minerManipulatedTime);
vm.startPrank(attacker);
uint256 sharesReceived = vault.deposit(MINIMUM_AMOUNT + 10 ether, attacker);
vm.stopPrank();
console.log("Production chain attack successful: YES");
console.log("Shares received:", sharesReceived);
// Verify attack works within production constraints
assertGt(sharesReceived, 0, "Attacker exploited production chain vulnerability");
assertLe(EVENT_START - minerManipulatedTime, 15, "Attack within realistic miner influence bounds");
}
/**
* Demonstrate Economic Impact - Unfair Advantage
* Show how timing bypass creates unfair tournament participation
*/
function test_EconomicImpactUnfairParticipation() public {
console.log("=== ECONOMIC IMPACT: UNFAIR PARTICIPATION ===");
address legitimateUser = makeAddr("legitimateUser");
asset.mint(legitimateUser, 10000 ether);
vm.startPrank(legitimateUser);
asset.approve(address(vault), type(uint256).max);
vm.stopPrank();
// Legitimate user deposits just before tournament starts
vm.warp(EVENT_START - 1 minutes);
vm.startPrank(legitimateUser);
vault.deposit(MINIMUM_AMOUNT + 10 ether, legitimateUser);
vault.joinEvent(0); // Join Brazil
vm.stopPrank();
// Attacker manipulates timestamp to deposit and join just before official start
vm.warp(EVENT_START - 1); // Just before tournament started
vm.startPrank(attacker);
vault.deposit(MINIMUM_AMOUNT + 10 ether, attacker);
vault.joinEvent(0); // Join Brazil
vm.stopPrank();
console.log("Legitimate user deposit time:", EVENT_START - 1 minutes);
console.log("Attacker deposit time:", EVENT_START - 1);
console.log("Time advantage gained: Minimal (timestamp manipulation)");
console.log("Both users participate despite attacker using manipulation");
// Both users have equal participation despite attacker using timestamp manipulation
assertEq(vault.numberOfParticipants(), 2, "Both users participate despite timing manipulation advantage");
assertGt(vault.stakedAsset(attacker), 0, "Attacker gained participation through manipulation");
assertGt(vault.stakedAsset(legitimateUser), 0, "Legitimate user participated fairly");
}
/**
* Demonstrate Conservation Law Violation
* Show how timing bypass violates temporal integrity
*/
function test_TemporalIntegrityViolation() public {
console.log("=== TEMPORAL INTEGRITY VIOLATION ===");
// Establish baseline - tournament hasn't started
uint256 baselineTime = EVENT_START - 1 hours;
vm.warp(baselineTime);
vm.startPrank(attacker);
vault.deposit(MINIMUM_AMOUNT + 10 ether, attacker);
vault.joinEvent(0);
vm.stopPrank();
console.log("Baseline participation time:", baselineTime);
console.log("Tournament start time:", EVENT_START);
console.log("Participation occurred before start: YES");
// Now demonstrate bypass - manipulate to after start but pass checks
address attacker2 = makeAddr("attacker2");
asset.mint(attacker2, 10000 ether);
vm.startPrank(attacker2);
asset.approve(address(vault), type(uint256).max);
vm.warp(EVENT_START + 1 hours); // Actually after tournament started
vm.expectRevert(BriVault.eventStarted.selector);
vault.deposit(MINIMUM_AMOUNT + 10 ether, attacker2); // Should fail
// But with timestamp manipulation, it succeeds
vm.warp(EVENT_START - 1); // Manipulate timestamp
vault.deposit(MINIMUM_AMOUNT + 10 ether, attacker2); // Succeeds
vault.joinEvent(0);
vm.stopPrank();
console.log("Actual participation time:", block.timestamp);
console.log("Manipulated timestamp check passed: YES");
console.log("Temporal integrity violated: YES");
// Verify temporal integrity violation
assertEq(vault.numberOfParticipants(), 2, "Both attackers participate despite timing manipulation");
assertGt(vault.stakedAsset(attacker2), 0, "Attacker2 gained participation through temporal violation");
}
}

Recommended Mitigation

Implement timing buffer zones and use block numbers instead of timestamps for precise timing control.

+ // Add timing buffer constants
+ uint256 public constant TIMING_BUFFER = 300; // 5 minutes
+ uint256 public constant EVENT_START_BLOCK; // Use block numbers for precision
- // In deposit() function - lines 207-212
- if (block.timestamp >= eventStartDate) {
- revert eventStarted();
- }
+ // Replace timestamp checks with buffered block-based checks
+ function deposit(uint256 assets, address receiver) public override returns (uint256) {
+ // Allow deposits up to 5 minutes before official start
+ if (block.number >= EVENT_START_BLOCK - (TIMING_BUFFER / 12)) {
+ revert eventStarted();
+ }
+ // ... rest of logic
+ }
- // In joinEvent() function - lines 247-252
- if (block.timestamp >= eventStartDate) {
- revert eventStarted();
- }
+ // Apply same timing buffer to joinEvent
+ function joinEvent(uint256 countryId) public {
+ if (block.number >= EVENT_START_BLOCK - (TIMING_BUFFER / 12)) {
+ revert eventStarted();
+ }
+ // ... rest of logic
+ }
Updates

Appeal created

bube Lead Judge 19 days ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

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

Give us feedback!