blockchainsv
Developer Tools & Infrastructure·September 03, 2026·23 min read

Foundry vm.warp and vm.roll: Core Differences Explained

You've just deployed a vesting contract in a test. Tokens release six months after deployment, and you need to verify the cliff, the unlock, and the full payout without actually waiting six months.

Foundry vm.warp and vm.roll: Core Differences Explained

Foundry's vm.warp and vm.roll are the cheatcodes that make this possible.

The important part is that they do not simulate the same thing. vm.warp changes the timestamp exposed as block.timestamp. vm.roll changes the height exposed as block.number. They operate on separate pieces of EVM block context, and neither one updates the other.

That distinction is easy to miss in a small test. It becomes much harder to ignore when a contract combines a time-based condition with a block-based one: a vesting cliff and a validator activation height, a signature deadline and an epoch boundary, or a governance proposal that uses both a timestamp and a snapshot block.

Most of us reach for vm.warp first because time travel feels intuitive. But if the fixture only moves the clock while the contract is checking block height, the test is still at the old height. If it only calls vm.roll, a timestamp-based deadline remains exactly where the setup left it. The result is a test that says 30 days passed while the contract still believes it is at the original block—or a test that has advanced thousands of blocks without moving time by a single second.

Understanding EVM Environment Cheatcodes: Warp vs Roll

Inside a Foundry test, the EVM runs in a controlled local environment rather than on a live network. Several block-related values are available to Solidity through environment opcodes and global variables, including:

  • block.timestamp, backed by the EVM TIMESTAMP value;
  • block.number, backed by the NUMBER value;
  • block.difficulty and, on networks that expose it after the Merge, block.prevrandao;
  • other block-context values such as the coinbase and gas limit.

Foundry cheatcodes let the test harness alter selected parts of that context. vm.warp and vm.roll are the two most commonly used controls.

vm.warp(uint256 timestamp) sets the timestamp that subsequent contract execution will observe. vm.roll(uint256 blockNumber) sets the block number that subsequent contract execution will observe. Their scopes are deliberately narrow:

OperationChangesDoes not changeTypical use
vm.warp(timestamp)block.timestampblock.numberDeadlines, vesting, auctions, timelocks
vm.roll(blockNumber)block.numberblock.timestampEpochs, snapshots, activation heights
skip(seconds)Advances block.timestampblock.numberReadable relative time travel
rewind(seconds)Decreases block.timestampblock.numberTesting earlier-time branches
vm.getBlockTimestamp()Reads the current test-environment timestampNothingTest-side state inspection
vm.getBlockNumber()Reads the current test-environment heightNothingTest-side state inspection

The last two entries need a different mental label. They are read helpers for the Foundry environment, not replacements for Solidity's block.timestamp and block.number in production application code.

vm.warp and vm.roll move different dials on the same console. Twisting one does not budge the other.

Call vm.warp(1_700_000_000), and the block number stays at whatever value the fixture or previous call established. Call vm.roll(20_000_000), and the timestamp remains unchanged. If a test needs both values to represent a coherent later point in a simulated chain, it has to set both explicitly.

That independence is not an accidental quirk. A block number and a block timestamp are different pieces of chain state. Real block production advances them together as new blocks are mined, but they still have separate semantics. Foundry exposes separate cheatcodes because tests often need to isolate one dimension.

The difference between a real block and a test context

On a live network, a new block normally brings a new height and a timestamp selected according to that network's consensus rules. The relationship is not a universal fixed conversion, though. There is no general rule that lets a test safely infer a timestamp from a block number, or a block number from elapsed seconds.

A local test can intentionally create combinations that would not appear naturally on a target network. For example, it can set a much later block number while retaining the original timestamp, or move the timestamp backward while leaving the height unchanged. That is useful for testing boundaries and failure handling, but it means the fixture must state which invariant it is trying to model.

If the contract cares only about elapsed time, move the timestamp. If it cares only about a checkpoint height, move the block number. If it cares about both, make the relationship explicit in the test instead of assuming that one cheatcode implies the other.

This is the core of the foundry vm warp vs vm roll difference: vm.warp changes the value returned by the timestamp-related environment read, while vm.roll changes the value returned by the block-number-related read. Neither command is a general-purpose “advance the chain” operation.

Manipulating Block Timestamps with vm.warp and Forge Std

When contract logic depends on block.timestamp, vm.warp is the direct Foundry tool. It accepts a Unix timestamp and changes the value returned to subsequently executed code.

This is useful for:

  • vesting schedules and cliffs;
  • timelock delays;
  • auction start and end times;
  • signature and permit deadlines;
  • rate-limit windows;
  • cooldown periods;
  • staking rewards that accrue over time;
  • claim periods and expiration branches.

The cheatcode does not wait, mine a realistic sequence of blocks, or recalculate application state. It simply changes the timestamp in the test environment. Storage variables such as startTime, lastUpdate, or deadline remain whatever the contract previously wrote them as. A test must still call the relevant function or trigger the relevant state transition before asserting the result.

Suppose a constructor stores startTime = block.timestamp, and the release condition is:

block.timestamp >= startTime + cliff

Warping forward changes the left side of that comparison. It does not rewrite startTime, and it does not automatically execute the release function. The test still has to call the function that performs the calculation.

Absolute and relative time travel

There are two useful styles of timestamp manipulation.

With an absolute timestamp, the test specifies the exact environment value:

vm.warp(targetTimestamp)

This is useful when the test is built around a known deadline or when a fixture must reproduce a particular boundary exactly. It also makes the relationship between the setup and the assertion obvious: the test is intentionally executing at targetTimestamp.

With a relative adjustment, the test moves from the current environment value:

skip(seconds)

Forge Std's skip helper is generally easier to read for ordinary progression. A vesting test can express the intended event as moving past a cliff by a chosen duration rather than manually performing timestamp arithmetic. The helper is still test-side time travel; it does not alter how the deployed contract stores or computes its own schedule.

rewind(seconds) performs the opposite operation by decreasing the environment timestamp. It can be useful for testing branches that should reject an action before a deadline or before a release point. It also creates states that require careful interpretation.

On a real network, a transaction executed in a block cannot observe a timestamp earlier than the timestamp of the block in which the contract was constructed, assuming the contract already exists and the network follows its normal chain history. A deployed contract does not travel backward through the canonical chain.

Foundry tests are different. vm.warp and rewind are deliberate controls over a local test environment, so they can create a timestamp earlier than the deployment timestamp. That state is impossible as a normal historical progression on a real network, but it is possible in the harness. It can be useful for testing defensive assumptions, yet it should not be mistaken for an ordinary production scenario.

A backward timestamp can also expose assumptions that were never meant to be tested in isolation. Consider a contract that stores lastAccrual during construction and later calculates elapsed time as block.timestamp - lastAccrual. Rewinding below that stored value may cause a revert under Solidity's checked arithmetic, or may exercise an explicit precondition. Neither outcome means rewind is broken. It means the test has intentionally supplied a block context outside the contract's normal chain-history assumptions.

For most positive-path tests, forward movement is clearer:

1. Deploy the contract and record the relevant starting state.

2. Assert the behavior before the cliff or deadline.

3. Call skip or vm.warp to the exact boundary.

4. Assert the behavior at the boundary.

5. Move just beyond it and assert the post-boundary behavior.

The boundary itself deserves its own assertion. A contract using >= and one using > will behave differently at the exact unlock timestamp. Testing only a point well after the deadline can hide that distinction.

What vm.warp does not do

vm.warp does not:

  • increment block.number;
  • mine a sequence of intermediate blocks;
  • update a contract's stored timestamp fields;
  • execute scheduled callbacks;
  • make an external oracle report a new value;
  • prove that a production network can reach the same timestamp-height combination.

The behavior of other block-context fields is outside the narrow, verified scope of vm.warp itself. In particular, do not use a timestamp warp as evidence that every other environment value has been recalculated in a way that matches a newly mined production block. If a test depends on another field, establish and verify that field through the appropriate mechanism rather than inferring it from vm.warp.

This is why a time-based test may still need vm.roll. If the contract calculates an epoch from both a timestamp and a block checkpoint, advancing only the timestamp tests one input while leaving the other stale.

Controlling Block Height Progression via vm.roll

vm.roll(uint256 blockNumber) changes the block height exposed to subsequent execution. It is the right tool when the contract's condition is explicitly tied to block.number.

Typical examples include:

  • governance snapshots keyed to a block;
  • reward epochs that begin at known heights;
  • validator or sequencer activation checkpoints;
  • delayed execution based on a minimum block;
  • tests for block.number-based replay protection;
  • logic that distinguishes one checkpoint from another.

If a proposal becomes executable after activationBlock, the relevant test operation is a roll to or beyond that height. Moving the timestamp does not satisfy a block-number comparison.

The reverse is also true. Rolling forward does not imply that time has passed. Foundry does not assume a twelve-second, two-second, or any other block interval. The timestamp stays at its previous value until the test changes it.

That makes this test setup incomplete for a contract that checks both conditions:

  • roll to the later block;
  • call the contract;
  • discover that its timestamp-based guard still sees the original time.

The correct fixture sets both values deliberately, for example by rolling to the intended checkpoint and warping to a timestamp that is valid for the scenario. The order is usually less important than the final environment, but it can matter when setup calls themselves read block state or when an external call is made between the two operations.

Do not confuse height with elapsed time

A block number is an index, not a duration. Even if a particular network usually produces blocks at a predictable cadence, that cadence can change, and Layer-2 systems add another layer of complexity. L2 block numbers and timestamps may be derived or sequenced under rules that differ from the L1 environment the developer has in mind.

A test that says “advance 100 blocks” is testing block-height behavior. A test that says “advance one day” is testing timestamp behavior. If the application translates one into the other, that translation belongs in the contract's documented assumptions and in the test's setup—not in an unstated expectation about what vm.roll should do.

vm.roll also does not create a realistic history of every intervening block. It changes the current value. If the contract needs to inspect historical checkpoints, relies on an oracle's update cadence, or expects intermediate state transitions, a single roll may not be enough. The test may need to execute those transitions explicitly or use a more representative fixture.

Choosing the right boundary

Block-based conditions deserve the same boundary discipline as timestamp conditions. For a requirement such as block.number >= activationBlock, test the block immediately before the activation point, the activation point itself, and a later height. For a strict comparison such as block.number > activationBlock, the exact activation height should still fail.

It is also worth checking which block a contract actually observes. A call made after a cheatcode may execute with the newly configured context, but a helper, mock, or preceding state transition may have captured the old value. Assertions should be placed around the operation that matters rather than relying only on a setup variable in the test.

This is particularly important for governance systems. A snapshot may store a block number during proposal creation, while execution later compares the current block against that stored value. Moving the current height does not rewrite the snapshot. The test must distinguish between the historical value saved in contract storage and the current block.number used by the execution path.

Avoiding State Conflicts and Compiler Optimization Pitfalls

The most confusing failures often appear when three separate concerns are mixed together:

1. the test changes the block environment;

2. the contract reads that environment through Solidity globals;

3. the compiler optimizes those reads under assumptions about a transaction's execution context.

A test can therefore fail for reasons that look like an error in the vesting or governance logic but actually come from the way the environment value was accessed.

The --via-ir concern

With the IR-based compilation pipeline enabled through --via-ir, the compiler can apply more aggressive transformations. In particular, code that reads environment values may be optimized under the assumption that values such as block.number and block.timestamp are stable within a transaction context. On a real chain, those values do not ordinarily change halfway through one transaction.

Foundry cheatcodes complicate that picture because the test harness can rewrite the local environment between calls and, depending on where the read occurs, can expose behavior that is surprising under optimization. This is especially relevant when tests combine cheatcode calls, internal helpers, and contracts compiled with different settings.

The practical response is not to replace production globals with test cheatcodes everywhere. Instead, separate the application interface from the test environment.

For production code, block.timestamp and block.number remain the normal Solidity mechanisms for reading the current block context. A deployed application contract cannot call the Foundry cheatcode interface as a general substitute, and a test helper is not a network-compatible implementation of those globals.

For the test harness, vm.getBlockTimestamp() and vm.getBlockNumber() provide explicit reads of the values currently maintained by Foundry. They are useful when the test itself needs to confirm what environment it has established, or when a deliberately test-only abstraction must read the harness state in a way that avoids a compiler-related problem.

A test can use them to make its setup and assertions clearer:

  • read the current harness timestamp before calculating a relative warp;
  • read the current harness block number before selecting the next checkpoint;
  • verify that a fixture changed the intended axis and left the other one untouched;
  • feed those values into a test-only adapter or mock whose purpose is to model environment access.

That is a narrower and safer use than routing every application read through vm.getBlockTimestamp() or vm.getBlockNumber().

vm.getBlockTimestamp() and vm.getBlockNumber() are test-environment readers. They are not drop-in replacements for block.timestamp and block.number in a deployed contract.

A test-only abstraction is different from changing the contract

Sometimes an application is designed with an environment abstraction: a production implementation reads the Solidity globals, while a test implementation supplies controlled values. That can be a reasonable architecture when the contract genuinely benefits from dependency injection or when deterministic simulation is a central requirement.

The important boundary is explicitness. A test-only clock or block-number provider should be identifiable as such, and the production path should preserve the semantics the deployed contract will actually use. Calling a Foundry cheatcode from an application contract would couple that contract to the test VM and would not reproduce ordinary network execution.

In other words, vm.getBlockTimestamp() can help the harness observe or supply a value to a test abstraction. It should not be presented as a universal workaround for application code that reads block.timestamp.

The same caution applies to vm.getBlockNumber(). If a test abstraction uses it, the test should make clear whether it is checking the harness state, injecting a value into a mock, or asserting what the deployed contract observed. Those are different claims.

Keep environment changes between meaningful calls

A clean test usually changes one variable, performs the operation that consumes it, and asserts the result. This structure makes failures easier to diagnose:

  • establish the initial timestamp and block number;
  • execute the setup transaction;
  • warp or roll to the intended boundary;
  • execute the application call;
  • inspect the resulting state and events.

Avoid hiding several environment changes inside a helper whose name suggests only one action. A helper called advanceToUnlock that silently rolls the block as well as warping the timestamp can be convenient, but it may obscure why a block-based branch passed. If the application genuinely requires both values, name the helper accordingly and document the relationship it establishes.

The same principle applies to fixtures shared across tests. A fixture that leaves the environment at an arbitrary timestamp or block height can make a test depend on execution order or on an unrelated setup detail. Prefer a visible starting point, then apply the smallest change needed for the scenario.

Reliable State Querying with vm.getBlockTimestamp and vm.getBlockNumber

The read cheatcodes are most useful when the test needs to inspect the environment it has created. They can turn hidden setup assumptions into explicit assertions.

For example, a test that advances time relatively can capture the starting value, call skip, and then verify that the harness timestamp moved by the expected amount. A block-based test can capture the starting height, call vm.roll, and confirm that the new height is the one intended for the checkpoint.

These checks do not replace assertions against contract behavior. They complement them. A passing environment assertion tells you that the fixture changed the expected test value; it does not prove that the contract read that value in the relevant branch.

Reading the timestamp without confusing it with contract storage

There are usually at least three timestamp-like values in a time-dependent test:

1. the current environment value exposed through block.timestamp;

2. a value stored by the contract, such as startTime;

3. a test variable representing the intended scenario.

vm.getBlockTimestamp() reads the first category from the Foundry environment. It does not read a contract's storage and does not tell you when a particular state variable was written.

That distinction matters in vesting tests. If the constructor stores the deployment timestamp, then warping later changes the current environment value but not the stored start time. A reliable test can compare both values conceptually: the current timestamp should be at the intended point relative to the contract's recorded schedule.

The same separation helps with cooldown logic. A contract may store lastClaim, while the test uses vm.getBlockTimestamp() to calculate how far to move forward. If the test instead assumes that the helper reads lastClaim, it can accidentally create a valid-looking but incorrect fixture.

Reading block height for checkpoint tests

vm.getBlockNumber() serves the analogous role for height-based logic. It reads the current block number maintained by the test environment. It does not retrieve a historical snapshot stored in a governance contract, and it does not reconstruct the blocks between the previous height and the new one.

A useful pattern is to derive a test checkpoint from the current harness value rather than hard-coding an unrelated number. That can make a test less sensitive to the initial environment, provided the resulting condition remains clear. For example, the test can read the current height, choose a later activation point, roll to it, and then assert that execution crosses the intended threshold.

Hard-coded heights still have a place when the scenario itself is about a known checkpoint. The choice should reflect the behavior under test, not a preference for one style. What matters is that the expected relationship is visible.

When to assert the environment directly

Direct environment assertions are especially helpful in four situations:

  • a shared setup function performs several cheatcode operations;
  • a test uses both vm.warp and vm.roll;
  • compilation through --via-ir makes environment reads difficult to reason about;
  • a failure could come from either the fixture or the contract.

They are less valuable when they merely repeat the cheatcode call without testing application behavior. A test that asserts only that vm.warp changed the timestamp proves the cheatcode works, not that the vesting condition is correct.

A stronger test connects the layers:

1. confirm the initial environment and stored contract state;

2. change the intended environment value;

3. confirm the new environment value;

4. call the contract function;

5. assert the state transition, return value, revert, or emitted event.

That sequence is more verbose than a one-line warp, but it leaves much less ambiguity when a boundary test fails.

Building a Coherent Warp-and-Roll Fixture

When a contract uses both time and height, the fixture should represent the relationship the application expects rather than an arbitrary pair of values.

Consider a governance action that becomes executable only after both a delay and an activation block. The test needs to distinguish at least four states:

  • before both conditions are satisfied;
  • after the timestamp condition but before the block condition;
  • after the block condition but before the timestamp condition;
  • after both conditions are satisfied.

Testing only the final state is not enough. It can hide a bug where the contract checks only one side of the intended conjunction.

A compact comparison helps keep the setup honest:

ScenarioTimestampBlock numberExpected purpose
Initial stateBefore deadlineBefore activationConfirm both guards reject
Time onlyAfter deadlineBefore activationIsolate the height guard
Height onlyBefore deadlineAfter activationIsolate the timestamp guard
Complete stateAfter deadlineAfter activationConfirm the action can proceed

The values themselves do not need to imitate a specific live chain unless that is part of the test. They need to satisfy the invariant being tested. If the application requires a timestamp and height that would be impossible under the target network's rules, the fixture should not quietly present that pair as a realistic production state. Use it as an isolated logic test, or build a more representative progression.

Order, transactions, and observed context

The final environment is usually what matters, but execution order can still affect a test. A setup call may read block.timestamp and store it. A mock may capture block.number. An external call may execute before the second cheatcode is applied. In those cases, warping and rolling after the fact cannot retroactively change what the earlier call observed.

A dependable sequence is:

1. establish the deployment context;

2. deploy or initialize contracts;

3. record values that are meant to remain historical;

4. apply the warp and/or roll;

5. execute the function under test;

6. inspect current and stored values separately.

This prevents a common mistake: moving the environment before deployment and then assuming the contract's stored start time represents the original fixture rather than the already-modified context.

Avoiding accidental coupling

A test may pass because two unrelated conditions happen to be satisfied at the same time. For example, a helper might both call skip and vm.roll, allowing a proposal to execute even though the test was intended to verify only its timestamp delay.

Isolation is not pedantry here. It tells you which input caused the observed result. Use separate tests for separate guards, then add an integration-style test for the combined condition. That gives the contract room to evolve: if a future change replaces a block-based guard with a timestamp-based one, the focused tests will reveal the behavioral change immediately.

Practical Patterns for Solidity Time Manipulation in Foundry

The most reliable solidity time manipulation foundry tests are small in what they manipulate and precise in what they assert.

For a vesting schedule, test the stored start time, the exact cliff, the first valid timestamp, and a later claim. If the contract calculates a linear amount over time, test more than the final payout: check the amount before the cliff, at the beginning of release, and after additional elapsed time. The goal is to verify both the boundary and the arithmetic path.

For a timelock, distinguish creation time from execution time. Warping after queuing an operation should affect the current deadline comparison, but it should not modify the queued operation's stored timestamp. If the operation is still rejected, inspect which side of the comparison is stale.

For signatures and permits, use an absolute timestamp when reproducing a signed deadline. A signature commits to a specific value. Relative time travel may be readable, but an absolute warp makes it clear that the test is checking expiry against the signed deadline rather than against an incidental fixture state.

For block-based governance, record the snapshot height and roll the current block independently. A snapshot is historical data; vm.getBlockNumber() is the current environment. Confusing the two can produce a test that appears to validate voting power at a snapshot while actually checking only the current block guard.

For Layer-2 applications, be especially cautious about assumptions imported from L1. A block number may not represent elapsed wall-clock time in the way the test author expects. Timestamp behavior, sequencing, and finality assumptions should be modeled explicitly if they affect the contract's security or accounting logic.

Final Perspective

The foundry vm warp vs vm roll difference is simple at the opcode level and consequential at the test-design level:

  • use vm.warp when the contract reads block.timestamp;
  • use vm.roll when it reads block.number;
  • use both when the contract requires both conditions;
  • use skip and rewind when readable relative timestamp changes are more useful than absolute values;
  • use vm.getBlockTimestamp() and vm.getBlockNumber() to inspect the harness, not to replace production Solidity globals.

Neither cheatcode should be treated as a complete simulation of block production. They change selected pieces of the environment. They do not create every intermediate block, update stored application state, refresh external data, or establish a universal relationship between time and height.

That narrowness is a strength when the test uses it deliberately. Isolate the timestamp guard, isolate the block-height guard, then test the combined scenario with both values set to an explicit and defensible state. Once the fixture says exactly which part of the EVM context it is changing, failures become much easier to read—and the test is far less likely to pass for the wrong reason.

FAQ

What is the difference between vm.warp and vm.roll in Foundry?
vm.warp sets the timestamp exposed as block.timestamp. vm.roll sets the block height exposed as block.number, and neither command changes the other value.
Does vm.warp increase the block number?
No. vm.warp changes only the test-environment timestamp; block.number remains unchanged unless the test also calls vm.roll.
Does vm.roll advance time in Foundry?
No. vm.roll changes only block.number. The timestamp stays at its previous value until the test changes it with vm.warp, skip, or another timestamp control.
How do I test a contract that requires both a timestamp and a block height?
Set both values explicitly: use vm.warp or skip for the required timestamp and vm.roll for the required block number. Test the states where only one condition is satisfied as well as the state where both are satisfied.
What is the difference between vm.getBlockTimestamp and block.timestamp?
vm.getBlockTimestamp reads the current timestamp maintained by Foundry's test environment. It does not read a timestamp stored in contract storage and is not a drop-in replacement for block.timestamp in deployed application code.

By Chloe Redfern