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:
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:
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.
But it passes. Letās examine the test case further by starting it verbose logging forge test -vvvv
Letās fix our Smart Contract. There is a typo in our requirements function:
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:
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);
...
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.