Applying Relic Protocol

In this section, we will integrate the Relic Protocol into the previously built contract. We will demonstrate how the Access and Verification can be implemented through an example. The requirements for the example contracts are as follows:

  • Following the requirements of the previously developed contract.
  • However, issue tokens only to accounts that are at least 1 million blocks old.

Modifying Contracts

Installing Dependencies

To use the Relic Protocol's Solidity SDK, you can install the dependency with the following command:

npm install @relicprotocol/contracts

Constructor

import '@relicprotocol/contracts/interfaces/IReliquary.sol';
contract Token is ERC721, Ownable {
IReliquary public immutable reliquary;
constructor(address _reliquary) ERC721('My Birth Relic', 'MBR') Ownable() {
reliquary = IReliquary(_reliquary);
}
}

Reliquary is a contract that stores verified data or proven historical state Facts. In other words, smart contracts can access historical data by querying the Reliquary. To allow the Token contract to access historical data, you can import the Reliquary interface and create a constructor that takes the address of Reliquary as a parameter and stores it in an immutable variable.

Method: Mint

import "@relicprotocol/contracts/lib/FactSigs.sol";
FactSigs.birthCertificateFactSig()

A Fact Signature is a unqiue signature used to categorize a Fact. Each type of Fact (such as Birth Certificate, Account Storage, Storage Slot, Log, Block Header, etc.) has a corresponding Fact Signature. To retrieve Facts from Reliquary, you need to know the Fact signature that corresponds to the type of fact you want to access. In this tutorial, we will be using Birth Certificate facts. You can retrieve the Birth Certificate Fact Signature by importing FactSigs.sol and calling FactSigs.birthCertificateFactSig().

import "@relicprotocol/contracts/lib/FactSigs.sol";
(bool exist, , bytes memory data) = reliquary.verifyFactNoFee(
who, // Target Address
FactSigs.birthCertificateFactSig()
);
require(exist, "birth certificate fact missing");

This code block illustrates how dApp can Access Facts. To retrieve the Birth Certificate Fact, call the Reliquary’s verifyFactNoFee method, passing the fact signature and target address as parameters. This method returns an exist (existence status), version, and data, corresponding to the provided fact signature and target address.

Verifying the existence of a Fact is essential because if the Fact does not exist, your contract cannot proceed with any subsequent operations. Therefore, it is necessary to use the exist value to check if the Fact exists. If exist is false, you must halt the process and return an appropriate error message using the require.

uint48 blockNum = uint48(bytes6(data));
require(blockNum < block.number - 1000000, "account too new");

If the Fact exists, you can retrieve the target address's birth-block number from the data. Note that the Birth Certificate Fact's data only consists of a uint48 value.

In order to ensure that the birth-block is within the desired range, you can utilize the require statement to check if the birth-block number is less than the current block number minus 1 million. If the birth-block is too recent, the process should be aborted and an appropriate error message should be returned to the user.

By combining the two code snippets mentioned earlier with the existing mint function, we can create a new mint function that includes the condition for verifying the target address's birth-block:

function mint(address who) public returns (uint256) {
(bool exist, , bytes memory data) = reliquary.verifyFactNoFee(
who,
FactSigs.birthCertificateFactSig()
);
require(exist, 'birth certificate fact missing');
uint48 blockNum = uint48(bytes6(data));
require(blockNum < block.number - 1000000, 'account too new');
uint256 tokenId = _tokenIds.current();
_mint(who, tokenId);
_tokenIds.increment();
return tokenId;
}

Putting them all together

If you combine all the previously implemented methods, you can implement the contract as follows:

// File: contracts/Token.sol
/// SPDX-License-Identifier: UNLICENSED
pragma solidity >=0.8.12;
import '@openzeppelin/contracts/token/ERC721/ERC721.sol';
import '@openzeppelin/contracts/access/Ownable.sol';
import '@openzeppelin/contracts/utils/Counters.sol';
import '@relicprotocol/contracts/lib/FactSigs.sol';
import '@relicprotocol/contracts/interfaces/IReliquary.sol';
contract Token is ERC721, Ownable {
using Counters for Counters.Counter;
Counters.Counter private _tokenIds;
IReliquary public immutable reliquary;
constructor(address _reliquary) ERC721('My Birth Relic', 'MBR') Ownable() {
reliquary = IReliquary(_reliquary);
}
function mint(address who) public returns (uint256) {
(bool exist, , bytes memory data) = reliquary.verifyFactNoFee(
who,
FactSigs.birthCertificateFactSig()
);
require(exist, 'birth certificate fact missing');
uint48 blockNum = uint48(bytes6(data));
require(blockNum < block.number - 1000000, 'account too new');
uint256 tokenId = _tokenIds.current();
_mint(who, tokenId);
_tokenIds.increment();
return tokenId;
}
}

Modifying Tests

Installing Dependencies

To use the Relic Protocol's Client SDK, you can install the dependency with the following command:

npm install --save-dev @relicprotocol/clients

Helpers

To keep test codes simple, you can make helper/index.ts with some helper functions.

Helper: Deploy

import type { RelicClient } from '@relicprotocol/client'
import { ethers } from 'hardhat'
export const deployToken = async ({ client }: { client: RelicClient }) => {
const Token = await ethers.getContractFactory('Token')
const token = await Token.deploy(client.addresses.reliquary)
await token.deployed()
return { token }
}

This function is much like the earlier deployFixture function. It uses getContractFactory to get the contract factory. Next, you can deploy the Token using that factory with the Reliquary's address from the RelicClient. As a result, this function returns the deployed Token instance, called token.

Helper: Verification

import type { RelicClient } from '@relicprotocol/client'
import { ethers } from 'hardhat'
export const proveBirthFact = async ({
client,
account,
}: {
client: RelicClient
account: string
}) => {
const [signer] = await ethers.getSigners()
return await client.birthCertificateProver
.prove({ account })
.then((tx) => signer.sendTransaction(tx))
.then((res) => res.wait())
}

This function demonstrates the Verification process, which involves sending a transaction to verify and set the Birth Certificate Fact on the Reliquary. As shown, you can create an unsigned transaction for Verification by utilizing the prove method of the prover.

Helper: Reset Proven Fact

import type { RelicClient } from '@relicprotocol/client'
import { ethers } from 'hardhat'
import { setBalance } from '@nomicfoundation/hardhat-network-helpers'
export const resetProvenFact = async ({
client,
factSig,
target,
}: {
client: RelicClient
factSig: string
target: string
}) => {
const prover = await ethers.getImpersonatedSigner(
client.addresses.birthCertificateProver
)
setBalance(prover.address, 100n ** 18n)
const reliquary = await ethers.getContractAt(
'IReliquary',
client.addresses.reliquary
)
await reliquary.connect(prover).resetFact(target, factSig)
}

This function is designed to remove a Fact that was already set in the Reliquary. It is an important step for running tests. The resetFact method in the Reliquary can only be done by the Prover. So, you should create an ImpersonatedSigner using the Birth Certificate Prover's address. Then, use setBalance to give enough balance to the Prover. Finally, by calling resetFact with target address and fact signature, you can remove the earlier set Fact.

Combining Helpers All At Once

By combining all the previously implemented functions, you can create the following test/helpers/index.ts:

// File: test/helpers/index.ts
import type { RelicClient } from '@relicprotocol/client'
import { ethers } from 'hardhat'
import { setBalance } from '@nomicfoundation/hardhat-network-helpers'
export const deployToken = async ({ client }: { client: RelicClient }) => {
const Token = await ethers.getContractFactory('Token')
const token = await Token.deploy(client.addresses.reliquary)
await token.deployed()
return { token }
}
export const proveBirthFact = async ({
client,
account,
}: {
client: RelicClient
account: string
}) => {
const [signer] = await ethers.getSigners()
return await client.birthCertificateProver
.prove({ account })
.then((tx) => signer.sendTransaction(tx))
.then((res) => res.wait())
}
export const resetProvenFact = async ({
client,
factSig,
target,
}: {
client: RelicClient
factSig: string
target: string
}) => {
const prover = await ethers.getImpersonatedSigner(
client.addresses.birthCertificateProver
)
setBalance(prover.address, 100n ** 18n)
const reliquary = await ethers.getContractAt(
'IReliquary',
client.addresses.reliquary
)
await reliquary.connect(prover).resetFact(target, factSig)
}

Fixtures

Fixture: Deployment

This fixture is designed to deploy the Token contract and reset the Birth Certificate Facts corresponding to the target addresses for testing purposes.

First, create a new RelicClient using the ethers.provider with the following code:

import { ethers } from 'hardhat'
import { RelicClient } from '@relicprotocol/client'
const client = await RelicClient.fromProvider(ethers.provider)

Then, you can deploy Token contract via deployToken, already we implemented:

import { deployToken } from './helpers'
const { token } = await deployToken({ client })

To reset the Birth Certificate Facts associated with target addresses, you first need to know the Birth Certificate Fact Signature. Our Client SDK provides a function that makes it easy for you to generate Fact Signatures:

import { utils } from '@relicprotocol/client'
const factSig = utils.toFactSignature(
utils.FeeClass.NoFee,
utils.birthCertificateSigData()
)

Then, you can use resetProvenBirthFact in the following manner:

import { resetProvenBirthFact } from './helpers'
await Promise.all([
resetProvenFact({
client,
factSig,
target: ACCOUNT_TO_BE_SUCCESS,
}),
resetProvenFact({
client,
factSig,
target: ACCOUNT_TO_BE_FAILED,
}),
])

Combining all these steps, you can create a fixture for deploying the Token contract and resetting the Birth Certificate Facts associated with target addresses for testing:

import { deployToken, resetProvenBirthFact } from './helpers'
const deployFixture = async () => {
const client = await RelicClient.fromProvider(ethers.provider)
const { token } = await deployToken({ client })
await Promise.all([
resetProvenBirthFact({
client,
factSig: await token.factSig(),
target: ACCOUNT_TO_BE_SUCCESS,
}),
resetProvenBirthFact({
client,
factSig: await token.factSig(),
target: ACCOUNT_TO_BE_FAILED,
}),
])
return { token }
}

Fixture: Verification

This fixture is designed to verify and set Birth Certificate Facts for target addresses.

First, create a new RelicClient using the ethers.provider with the following code:

import { ethers } from 'hardhat'
import { RelicClient } from '@relicprotocol/client'
const client = await RelicClient.fromProvider(ethers.provider)

Then, to verify Birth Certificate Facts associated with target addresses, we can use proveBirthFact in the following manner:

await Promise.all([ proveBirthFact({ client, account: ACCOUNT_TO_BE_SUCCESS }), proveBirthFact({ client, account: ACCOUNT_TO_BE_FAILED }), ]);

Combining all of these steps, you can create a fixture for verifying Birth Certificate Facts for target addresses as follows:

async function proveFixture() {
const client = await RelicClient.fromProvider(ethers.provider)
await Promise.all([
proveBirthFact({ client, account: ACCOUNT_TO_BE_SUCCESS }),
proveBirthFact({ client, account: ACCOUNT_TO_BE_FAILED }),
])
}

Tests

Test: Should mint with proven fact

import { expect } from 'chai'
import { loadFixture } from '@nomicfoundation/hardhat-network-helpers'
it('Should mint with proven fact', async function () {
const { token } = await loadFixture(deployFixture)
await expect(token.mint(ACCOUNT_TO_BE_SUCCESS)).to.be.revertedWith(
'birth certificate fact missing'
)
})

The purpose of this test code is to ensure that the contract can only mint tokens with a proven fact. This test is designed to check if the contract will reject minting for an account where the Birth Certificate Fact does not exist.

The test loads the fixture using the loadFixture() function and deploys the Token contract. Then, the test attempts to call the mint() function with an account that does not have a corresponding Birth Certificate Fact. The test expects this call to fail and the error message to be "birth certificate fact missing".

Test: Should mint for account to be success

import { expect } from 'chai'
import { loadFixture } from '@nomicfoundation/hardhat-network-helpers'
it('Should mint for account to be success', async function () {
const { token } = await loadFixture(deployFixture)
await loadFixture(proveFixture)
await expect(token.mint(ACCOUNT_TO_BE_SUCCESS)).to.changeTokenBalance(
token,
ACCOUNT_TO_BE_SUCCESS,
1
)
})

The purpose of this test code is to ensure that the contract mints a token for an account only if it has an valid proven fact. The test is designed to check if the balance of the token becomes 1 after a successful minting. Note that ACCOUNT_TO_BE_SUCCESS is the address of an account that meets the required criteria for minting.

The test uses the loadFixture() function to deploy the Token contract and to prove the Birth Certificate Fact for the specified account. Then, the test calls the mint() function with the specified account and expects the token balance of the account to increase by 1. If the test passes, it confirms that the contract successfully minted a token for the account with the associated proven fact.

Test: Shouldn't mint for account to be failed

it("Shouldn't mint for account to be failed", async function () {
const { token } = await loadFixture(deployFixture)
await loadFixture(proveFixture)
await expect(token.mint(ACCOUNT_TO_BE_FAILED)).to.be.revertedWith(
'account too old'
)
})

The purpose of this test code is to ensure that the contract should not mint a token for an account if it is too old based on the Birth Certificate Fact. This test is designed to check if the minting attempt for the specified account will be rejected with the error message "account too old". Note that ACCOUNT_TO_BE_FAILED is the address of an account that doesn’t meet the required criteria for minting.

The test loads the fixture using the loadFixture() function and deploys the Token contract. Then, the test loads the proveFixture to set up the Birth Certificate Fact for the target account. The test attempts to call the mint() function with an account that has a Birth Certificate fact that indicates it is too old. The test expects this call to fail and the error message to be "account too old".

Complete Test Code

By combining the implemented helpers, fixtures, and tests, you can complete the test/Token.ts file as shown:

// File: test/Token.ts
import { expect } from 'chai'
import { ethers } from 'hardhat'
import { RelicClient } from '@relicprotocol/client'
import { loadFixture } from '@nomicfoundation/hardhat-network-helpers'
import { deployToken, resetProvenBirthFact, proveBirthFact } from './helpers'
const ACCOUNT_TO_BE_SUCCESS = 'ANY_ADDRESS_TO_BE_SUCCESS'
const ACCOUNT_TO_BE_FAILED = 'ANY_ADDRESS_TO_BE_FAILED'
describe('Token', function () {
const deployFixture = async () => {
const client = await RelicClient.fromProvider(ethers.provider)
const { token } = await deployToken({ client })
await Promise.all([
resetProvenBirthFact({
client,
factSig: await token.factSig(),
target: ACCOUNT_TO_BE_SUCCESS,
}),
resetProvenBirthFact({
client,
factSig: await token.factSig(),
target: ACCOUNT_TO_BE_FAILED,
}),
])
return { token }
}
async function proveFixture() {
const client = await RelicClient.fromProvider(ethers.provider)
await Promise.all([
proveBirthFact({ client, account: ACCOUNT_TO_BE_SUCCESS }),
proveBirthFact({ client, account: ACCOUNT_TO_BE_FAILED }),
])
}
describe('Mint', function () {
it('Should mint with proven fact', async function () {
const { token } = await loadFixture(deployFixture)
await expect(token.mint(ACCOUNT_TO_BE_SUCCESS)).to.be.revertedWith(
'birth certificate fact missing'
)
})
it('Should mint for account to be success', async function () {
const { token } = await loadFixture(deployFixture)
await loadFixture(proveFixture)
await expect(token.mint(ACCOUNT_TO_BE_SUCCESS)).to.changeTokenBalance(
token,
ACCOUNT_TO_BE_SUCCESS,
1
)
})
it("Shouldn't mint for account to be failed", async function () {
const { token } = await loadFixture(deployFixture)
await loadFixture(proveFixture)
await expect(token.mint(ACCOUNT_TO_BE_FAILED)).to.be.revertedWith(
'account too old'
)
})
})
})

To test the contracts, run the following command:

npx hardhat test

Here is the expected output of the test:

Token Mint ✔ Should mint with proven fact (9096ms) ✔ Should mint for account to be success (5210ms) ✔ Shouldn't mint for account to be failed (1207ms) 3 passing (16s)