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
# Full suiteforge test
# Verbose, shows test names and pass/failforge test -v
# L2 tests onlyforge test --match-contract PriceFeed_L2_Tests
# Deployment integration onlyforge test --match-path test/Deploy.t.sol
# Gas reportforge test --gas-report