Schema Hook

A schema hook contract is a regular solidity smart contract that inherits from the ISPHook interface and implements all required functions. You can implement the ISPHook interface on any contract, allowing you to merge Sign Protocol schema callbacks with your application's smart contract logic.

For clarity in this tutorial, we will separate our schema hook contract from the contract containing validation logic.

Validation Logic

Our schema hook is responsible for ensuring attestation data contains a number above a certain threshold. If this condition is met (the threshold value in the attestation data is large enough), the attestation will be successfully created. Otherwise, we will revert the transaction and the attestation will fail to be created.

For our validator contract, we need to create two functions: setThreshold and _checkThreshold. The threshold specified in setThreshold will determine which attestations are allowed to be created. We will enforce this by calling _checkThreshold in our schema hook contract.

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

import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";

// @dev This contract manages attestation data validation logic.
contract DataValidator is Ownable {
    uint256 public threshold;

    error NumberBelowThreshold();

    constructor() Ownable(_msgSender()) { }

    function setThreshold(uint256 threshold_) external onlyOwner {
        threshold = threshold_;
    }

    function _checkThreshold(uint256 number) internal view {
        // solhint-disable-next-line custom-errors
        require(number >= threshold, NumberBelowThreshold());
    }
}

Schema Hook Implementation

When an attestation is received, the schema hook's logic is pretty simple: we need to read the attestation data to extract the threshold value we are trying to validate, and then we need to validate it.

Reading Attestation Data

To get the attestation data, we need to read the attestation object from Sign Protocol's contract. You can find all available Sign Protocol smart contract functions here. Remember that your schema hook is being called from the Sign Protocol smart contract, so you can use _msgSender() to get the appropriate smart contract address that you need to call.

import { Attestation } from "@ethsign/sign-protocol-evm/src/models/Attestation.sol";
...
Attestation memory attestation = ISP(_msgSender()).getAttestation(attestationId);

Decoding Attestation Data

The attestation object you retrieved in the last step will have data encoded according to your schema's data format. In our case, the schema we created contains only a uint256. We can use abi.decode to decode the attestation data into the correct format.

abi.decode(attestation.data, (uint256));

You can now use this decoded data to call _checkThreshold.

If your schema has additional variables in its data field, these will be decoded similarly. For example, take the following schema data format:

[
    {
        "name": "name",
        "type": "string"
    },
    {
        "name": "timestamp",
        "type": "uint256"
    },
    {
        "name": "hash",
        "type": "bytes32"
    }
]

An attestation's data following the above data format can be decoded in Solidity using the following:

(string memory name, uint256 timestamp, bytes32 hash) = abi.decode(attestation.data, (string, uint256, bytes32));

Bringing It All Together

For this tutorial, we will only implement didReceiveAttestation, but the process is the same for the other functions in ISPHook. In our case, we did not check the attester, nor did we use the schemaId or extraData.

extraData is not the same as your attestation's data - this parameter provides a way for you to send additional information to your schema hook when an attestation is being created or revoked. extraData is not stored onchain with the attestation itself. It is only used in the schema hook.

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

import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";
import { IERC20 } from "@openzeppelin/contracts/interfaces/IERC20.sol";
import { ISP } from "@ethsign/sign-protocol-evm/src/interfaces/ISP.sol";
import { ISPHook } from "@ethsign/sign-protocol-evm/src/interfaces/ISPHook.sol";
import { Attestation } from "@ethsign/sign-protocol-evm/src/models/Attestation.sol";

// @dev This contract manages attestation data validation logic.
contract DataValidator is Ownable {
    uint256 public threshold;

    error NumberBelowThreshold();

    constructor() Ownable(_msgSender()) { }

    function setThreshold(uint256 threshold_) external onlyOwner {
        threshold = threshold_;
    }

    function _checkThreshold(uint256 number) internal view {
        // solhint-disable-next-line custom-errors
        require(number >= threshold, NumberBelowThreshold());
    }
}

// @dev This contract implements the actual schema hook.
contract DataValidatorHook is ISPHook, DataValidator {
    error UnsupportedOperation();

    function didReceiveAttestation(
        address, // attester
        uint64, // schemaId
        uint64 attestationId,
        bytes calldata // extraData
    )
        external
        payable
    {
        Attestation memory attestation = ISP(_msgSender()).getAttestation(attestationId);
        _checkThreshold(abi.decode(attestation.data, (uint256)));
    }

    function didReceiveAttestation(
        address, // attester
        uint64, // schemaId
        uint64, // attestationId
        IERC20, // resolverFeeERC20Token
        uint256, // resolverFeeERC20Amount
        bytes calldata // extraData
    )
        external
        pure
    {
        revert UnsupportedOperation();
    }

    function didReceiveRevocation(
        address, // attester
        uint64, // schemaId
        uint64, // attestationId
        bytes calldata // extraData
    )
        external
        payable
    {
        revert UnsupportedOperation();
    }

    function didReceiveRevocation(
        address, // attester
        uint64, // schemaId
        uint64, // attestationId
        IERC20, // resolverFeeERC20Token
        uint256, // resolverFeeERC20Amount
        bytes calldata // extraData
    )
        external
        pure
    {
        revert UnsupportedOperation();
    }
}

Once this contract has been deployed, you can create your schema and set the appropriate contract address as your schema hook. Once you have created your schema with a schema hook, move to the next page where we will go over how to create an attestation in solidity.

Last updated

Logo

Copyright Sign 2021-2024