Skip to content

Test Suite

Three contracts cover the full surface: unit tests for core validation, a separate L2 contract for sequencer-specific behavior, and deployment integration tests that run the script itself.

Structure

abstract contract Base_Test is Test {
PriceFeedConsumer consumer;
MockV3Aggregator feed;
MockV3Aggregator uptimeFeed;
address admin = makeAddr("admin");
uint256 constant HEARTBEAT = 3600;
uint256 constant DEVIATION = 500;
int256 constant INITIAL_PRICE = 30_000e8;
function setUp() public virtual {
vm.warp(1_700_000_000);
feed = new MockV3Aggregator(8, INITIAL_PRICE);
uptimeFeed = new MockV3Aggregator(0, 0);
vm.prank(admin);
consumer = new PriceFeedConsumer(address(feed), HEARTBEAT, DEVIATION);
}
}

Base_Test is abstract and holds no tests. Its only job is to set up the shared environment that the concrete test contracts inherit.

vm.warp(1_700_000_000) deserves attention. Foundry starts with a block timestamp of zero. Any test that calls block.timestamp - updatedAt with a non-zero updatedAt will underflow and wrap around to a huge number, causing staleness checks to pass when they should fail. Warping to a realistic timestamp (November 2023 in this case) eliminates that class of false passing test without affecting any other test logic.

Note on vm.warp(1_700_000_000): By default, Foundry initializes the block timestamp at zero. This creates a subtle bug in price feed testing: any comparison like block.timestamp - updatedAt will underflow if updatedAt is non-zero, causing an immediate revert that might be mistaken for a passing staleness test. We warp to a realistic Unix timestamp, roughly November 2023, to ensure our math works as it would on a live mainnet node.

Unit tests: PriceFeed_Unit_Tests

Covers the core validation path on a standard L1 configuration.

contract PriceFeed_Unit_Tests is Base_Test {
function test_GetPrice_ReturnsCorrectValue() public view { ... }
function test_GetPrice_RevertsWhen_PriceIsStale() public { ... }
function test_GetPrice_RevertsWhen_DeviationExceeded() public { ... }
}

Staleness test

function test_GetPrice_RevertsWhen_PriceIsStale() public {
vm.warp(block.timestamp + HEARTBEAT + 1);
uint256 expectedTime = block.timestamp - HEARTBEAT - 1;
vm.expectRevert(abi.encodeWithSelector(
PriceFeedConsumer.StalePrice.selector, expectedTime
));
consumer.getPrice();
}

vm.warp moves the block timestamp past the threshold. vm.expectRevert with the full encoded selector and parameter goes further than just checking that something reverted. It verifies the exact updatedAt value carried in the error, which means the test breaks if the staleness logic changes silently.

Deviation test

function test_GetPrice_RevertsWhen_DeviationExceeded() public {
int256 spikePrice = INITIAL_PRICE * 110 / 100; // 10% spike
feed.updateAnswer(spikePrice);
vm.expectRevert(abi.encodeWithSelector(
PriceFeedConsumer.DeviationExceeded.selector, 1000
));
consumer.getPrice();
}

We intentionally hardcode the 1000 bps value in this test. It makes the suite ‘brittle’ in the best way possible: if a future refactor of _calcDeviation shifts the math by even a single basis point, the test fails immediately. We’d rather have a loud break during development than a silent calculation error in production.

L2 tests: PriceFeed_L2_Tests

Inherits Base_Test and adds the sequencer uptime feed in its own setUp:

contract PriceFeed_L2_Tests is Base_Test {
function setUp() public override {
super.setUp();
vm.prank(admin);
consumer.setSequencerUptimeFeed(address(uptimeFeed));
}
function test_GetPrice_RevertsWhen_SequencerDown() public { ... }
function test_GetPrice_RevertsDuring_GracePeriod() public { ... }
}

Sequencer down test

function test_GetPrice_RevertsWhen_SequencerDown() public {
uptimeFeed.updateAnswer(1);
vm.expectRevert(PriceFeedConsumer.SequencerDown.selector);
consumer.getPrice();
}

Setting answer to 1 on the mock uptime feed simulates a downed sequencer.

Grace period test

function test_GetPrice_RevertsDuring_GracePeriod() public {
uptimeFeed.updateRoundData(1, 0, block.timestamp - 10, block.timestamp - 10);
vm.expectRevert(PriceFeedConsumer.GracePeriodNotOver.selector);
consumer.getPrice();
}

updateRoundData sets startedAt to 10 seconds ago. The sequencer “restarted” 10 seconds ago, well within the 1-hour grace period. This verifies the grace period check independently from the downtime check.

Deployment integration tests: Deploy_Integration_Test

Tests the deployment script itself rather than the contract in isolation.

function test_Run_ConfiguresL1Correctly() public {
vm.setEnv("UPTIME_FEED", "");
PriceFeedConsumer consumer = deployer.run();
assertEq(consumer.sequencerUptimeFeed(), address(0));
}
function test_Run_ConfiguresL2Correctly() public {
address mockUptime = address(0x123);
deployer.setUptimeFeed(mockUptime);
PriceFeedConsumer consumer = deployer.run();
assertEq(consumer.sequencerUptimeFeed(), mockUptime);
}

vm.etch plants mock aggregator bytecode at the Sepolia feed address the script expects. The script runs against a deterministic address without needing a live network. L1 and L2 paths are tested separately: one clears UPTIME_FEED to confirm the sequencer feed stays at address(0), the other injects a mock uptime address via deployer.setUptimeFeed() and confirms it lands on the deployed contract.

Running the suite

Terminal window
# Full suite
forge test
# Verbose, shows test names and pass/fail
forge test -v
# L2 tests only
forge test --match-contract PriceFeed_L2_Tests
# Deployment integration only
forge test --match-path test/Deploy.t.sol
# Gas report
forge test --gas-report