Schema Hook¶
A schema hook contract is a regular Solidity smart contract that inherits from the ISPHook interface and implements all required functions. Because any contract can implement ISPHook, you can merge Sign Protocol schema callbacks with your application's smart contract logic.
For clarity, this tutorial separates the schema hook contract from the contract containing validation logic.
Validation Logic¶
The schema hook checks that the attestation data contains a number at or above a set threshold. If it does, the attestation is created; if not, the transaction reverts and no attestation is created.
The validator contract needs two functions: setThreshold and _checkThreshold. The threshold set with setThreshold determines which attestations can be created. The schema hook contract enforces it by calling _checkThreshold.
// 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 the schema hook receives an attestation, it reads the value to validate from the attestation data and then checks it.
Reading attestation data¶
To get the attestation data, read the attestation object from Sign Protocol's contract (see all available Sign Protocol smart contract functions here). Because the Sign Protocol smart contract calls your schema hook, _msgSender() returns the contract address 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's data is encoded according to your schema's data format. This schema contains only a uint256, so decode it with abi.decode:
Pass the decoded value to _checkThreshold.
Additional variables in your schema's data field decode the same way. For example, given this schema data format:
[
{
"name": "name",
"type": "string"
},
{
"name": "timestamp",
"type": "uint256"
},
{
"name": "hash",
"type": "bytes32"
}
]
Decode attestation data in this format in Solidity with:
(string memory name, uint256 timestamp, bytes32 hash) = abi.decode(attestation.data, (string, uint256, bytes32));
Bringing it all together¶
This tutorial implements only didReceiveAttestation; the process is the same for the other ISPHook functions. This hook does not check the attester or use schemaId or extraData.
Info
extraData is not your attestation's data. It lets you send additional information to your schema hook when an attestation is created or revoked. extraData is not stored onchain with the attestation; only the schema hook uses it.
// 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();
}
}
After deploying this contract, create your schema with this contract's address as its schema hook. The next page shows how to create an attestation in Solidity.