Skip to content

Unit Testing Oracle Data-Dependency

Let's see how we can test Oracle Data. First we'll cover the simple test case and then we'll see how we can cover more advanced examples.

Mock Oracle

To test the NFT we first need to setup a mock oracle. Let's create a simple scaffolding for our test. Add a new file in test/SilverNft.t.sol:

//SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

import {Test, console} from  "forge-std/Test.sol";
import {SilverOunce} from "../src/SilverNft.sol";
import {OracleEntrypoint} from "erc4337-oracle/src/OracleEntrypoint.sol";
import {DataDependent} from "erc4337-oracle/src/DataDependent.sol";

contract SilverOunceTest is Test {
    OracleEntrypoint oracle;
    SilverOunce silvernft;
    Account provider;

    function setUp() public {
        Oracle = new OracleEntrypoint();
        provider = makeAccount("provider");
        silvernft = new SilverOunce(address(this), provider.addr, address(oracle));

    }
}

Adding in Data-Cost

Let's pretend that calling the Oracle for the data of POL/USD costs 0.001 gas tokens. Add the following to the setUp phase:

//SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

import {Test, console} from  "forge-std/Test.sol";
import {SilverOunce} from "../src/SilverNft.sol";
import {OracleEntrypoint} from "erc4337-oracle/src/OracleEntrypoint.sol";
import {DataDependent} from "erc4337-oracle/src/DataDependent.sol";

contract SilverOunceTest is Test {
    OracleEntrypoint oracle;
    SilverOunce silvernft;
    Account provider;

    function setUp() public {
        oracle = new OracleEntrypoint();
        provider = makeAccount("provider");
        silvernft = new SilverOunce(address(this), provider.addr, address(oracle));

        //set the price for getting a data point
        bytes memory prefix = "\x19Oracle Signed Price Change:\n148";
        bytes32 prefixedHashMessage = keccak256(
            abi.encodePacked(
                prefix,
                abi.encodePacked(
                    block.chainid,
                    provider.addr,
                    oracle.nonces(provider.addr),
                    silvernft.MARKET_POL(), //or keccak256("CRYPTO_POL")
                    uint256(0.001 ether)
                )
            )
        );

        (uint8 v, bytes32 r, bytes32 s) = vm.sign(
            provider.key,
            prefixedHashMessage
        );

        vm.prank(provider.addr);
        oracle.setPrice(provider.addr, 0, silvernft.MARKET_POL(), uint256(0.001 ether), r, s, v);
    }
}

If we run this with forge test nothing will happen so far, because there are no test cases:

Forge Test without test-cases
Forge Test without test-cases

First Unit Test

Let's write the first test. Add this to the Test contract:

    function test_mintNft() public {
        vm.warp(100000000);

        DataDependent.DataRequirement[] memory dataSources = silvernft
            .requirements(bytes4(keccak256("safeMint(address)")));

        uint[2] memory prices = [uint(35 * 10 ** 18), 4 * 10 ** 17]; //$35 and $0,4

        for (uint256 i = 0; i < dataSources.length; i++) {
            assertEq(dataSources[i].provider, provider.addr); //make sure the provider is correct

            uint256 value = block.timestamp * 1000 * 2 ** (8 * 26); //timestamp first, we do some bitshifting
            value += 18 * 2 ** (8 * 25);
            value += prices[i];

            bytes memory prefix = "\x19Oracle Signed Data Op:\n168";
            bytes32 prefixedHashMessage = keccak256(
                abi.encodePacked(
                    prefix,
                    abi.encodePacked(
                        block.chainid,
                        provider.addr,
                        oracle.nonces(provider.addr),
                        dataSources[i].requester,
                        dataSources[i].dataKey,
                        bytes32(value)
                    )
                )
            );

            (uint8 v, bytes32 r, bytes32 s) = vm.sign(
                provider.key,
                prefixedHashMessage
            );

            vm.prank(provider.addr);

            oracle.storeData(
                provider.addr,
                dataSources[i].requester,
                oracle.nonces(provider.addr),
                dataSources[i].dataKey,
                bytes32(value),
                r,
                s,
                v
            );
        }

        address alice = makeAddr("alice");
        vm.deal(alice, 100000 ether);
        vm.startPrank(alice);
        silvernft.safeMint{value: (87.5 ether + 0.001 ether)}(alice); //$35 XAG / 0,4 POL + 0.001 Data Price
        assertEq(silvernft.balanceOf(alice), 1);
    }

if you run the test now it will pass:

Running the test will pass
Running the test will pass

Reverse Testing the Unit Test

Let's make a small change and send not enough funds so that we also test that the data-dependency works:

...
        address alice = makeAddr("alice");
        vm.deal(alice, 100000 ether);
        vm.startPrank(alice);
        silvernft.safeMint{value: (1 ether + 0.001 ether)}(alice); //$35 XAG / 0,4 POL + 0.001 Data Price
        assertEq(silvernft.balanceOf(alice), 1);
...

If we run the test now, it should fail.

A an expected failing test is passing
A an expected failing test is passing

But it passes. Let's examine the test case further by starting it verbose logging forge test -vvvv

we can see it comes back as an empty data dependency array
we can see it comes back as an empty data dependency array

Let's fix our Smart Contract. There is a typo in our requirements function:

typo in smart contract
typo in smart contract

This is the corrected requirement selector for the SilverOunce.sol NFT:

     function requirements(
        bytes4 _selector
    ) external override view returns (DataRequirement[] memory) {
        if (_selector == bytes4(keccak256("safeMint(address)"))) {
            DataRequirement[] memory requirement = new DataRequirement[](2);
            requirement[0] = DataRequirement(dataProvider, address(this), MARKET_XAG);
            requirement[1] = DataRequirement(dataProvider, address(this), MARKET_POL);
            return requirement;
        }
        return new DataRequirement[](0);
    }

If we run the test now, we see it fails, as expected:

Failing Foundry Test
Failing Foundry Test

If we change the test to supply enough value, we get it to pass again:

...
        address alice = makeAddr("alice");
        vm.deal(alice, 100000 ether);
        vm.startPrank(alice);
        silvernft.safeMint{value: (87.5 ether + 0.001 ether)}(alice); //$35 XAG / 0,4 POL + 0.001 Data Price
        assertEq(silvernft.balanceOf(alice), 1);
...

Test passes
Test passes

The last part is to commit everything to git:

git add . && git commit -a -m "Added test case for Silver NFT"

Now that we have a test, lets see how we can deploy this NFT to Polygon Mainnet.