Smart Contracts
Integrating Predicate on the smart contract level ensures no transaction is processed without a direct compliance attestation.
Select integration pattern
Based off your application, choose between two smart contract integration patterns
Make the code changes
While every application is different, you will find an example contract integration below
Deploy your contract
Optionally, modify existing deployment scripts to include the addition of new state variables (PolicyID string and ServiceManager address)
Mental model
- Predicated functions on your contract require a
PredicateMessage, an attestation produced by the PredicateAPI.
- The contract, via
PredicateClient, verifies the attestation onchain before executing business logic.
- If the message is valid and unexpired for the exact call, execution proceeds; otherwise the transaction reverts.
Integration Pattern
There are two recommended integration patterns.
Both integration patterns leverage PredicateClient.sol, an audited, OpenZeppelin-namespaced smart contract we provide to simplify onchain integration.
To install the Predicate Contracts, run one of the following commands:
npm i @predicate/contracts
Direct Inheritance
Proxy Pattern
Recommended when users don’t originally directly interact with your smart contract (e.g., Uniswap V4 hooks, batch transactions).
Your application contract inherits the PredicateClient contract directly allowing you to enforce attestations directly.
This is the simplest integration as no additional smart contract deployment is required.Application Contract ExampleYou will need to implement the setPolicy and setPredicateManager functions, which we recommend you make ownable.Example Contract Direct Inheritance Pattern
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import {PredicateClient} from "../lib/predicate-contracts/src/mixins/PredicateClient.sol";
import {PredicateMessage} from "../lib/predicate-contracts/src/interfaces/IPredicateClient.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
contract MetaCoin is PredicateClient, Ownable {
mapping(address => uint256) public balances;
event Transfer(address indexed _from, address indexed _to, uint256 _value);
constructor(address _owner, address _predicateManager, string memory _policyID) Ownable(_owner) {
balances[_owner] = 10_000_000_000_000;
_initPredicateClient(_predicateManager, _policyID);
}
// Predicated function that requires predicate validation
function sendCoin(address _receiver, uint256 _amount, PredicateMessage calldata _message) external payable {
bytes memory encodedSigAndArgs = abi.encodeWithSignature("_sendCoin(address,uint256)", _receiver, _amount);
require(
_authorizeTransaction(_message, encodedSigAndArgs, msg.sender, msg.value),
"MetaCoin: unauthorized transaction"
);
_sendCoin(_receiver, _amount);
}
function _sendCoin(address _receiver, uint256 _amount) internal {
require(balances[msg.sender] >= _amount, "MetaCoin: insufficient balance");
balances[msg.sender] -= _amount;
balances[_receiver] += _amount;
emit Transfer(msg.sender, _receiver, _amount);
}
// Admin functions - implement these with proper access control
function setPolicy(string memory _policyID) external onlyOwner {
_setPolicy(_policyID);
}
function setPredicateManager(address _predicateManager) external onlyOwner {
_setPredicateManager(_predicateManager);
}
}
Deploying the contracts
Update your deployment script to include the following constructor arguments:
_predicateManager: The ServiceManager address for your target chain. See Supported Chains.
_policyID: The policy identifier. For initial testing, you may use x-test-random, which returns a compliant response approximately 50% of the time. You will update this value in step two.
Recommended when users directly interact with your application smart contract. Transactions are routed through a proxy contract which inherits the PredicateClient contract.
All users are authorized before the call is delegated to your application contract. An additional contract deployment (the proxy) is required for this pattern.Application Contract ExampleEnsure only the proxy contract can call the predicated function (for example, sendCoin).Example Contract with Proxy Pattern
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.12;
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {PredicateProtected} from "./PredicateProtected.sol";
contract MetaCoin is Ownable, PredicateProtected {
mapping(address => uint256) public balances;
event Transfer(address indexed _from, address indexed _to, uint256 _value);
constructor(address _owner, address _predicateProxyAddress) Ownable(_owner) {
balances[_owner] = 10_000_000_000_000;
_setPredicateProxy(_predicateProxyAddress);
}
function sendCoin(address _sender, address _receiver, uint256 _amount) external payable onlyPredicateProxy {
_sendCoin(_sender, _receiver, _amount);
}
function _sendCoin(address _sender, address _receiver, uint256 _amount) internal {
require(balances[_sender] >= _amount, "MetaCoin: insufficient balance");
balances[_sender] -= _amount;
balances[_receiver] += _amount;
emit Transfer(_sender, _receiver, _amount);
}
}
Proxy Contract:You will need to implement the setPolicy and setPredicateManager functions, which we recommend you make ownable.// SPDX-License-Identifier: MIT
pragma solidity ^0.8.12;
import {PredicateClient} from "../../mixins/PredicateClient.sol";
import {PredicateMessage} from "../../interfaces/IPredicateClient.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {MetaCoin} from "./MetaCoin.sol";
contract PredicateClientProxy is PredicateClient, Ownable {
MetaCoin private _metaCoin;
constructor(address _owner, address _metaCoinContract, address _predicateManager, string memory _policyID) Ownable(_owner) {
_initPredicateClient(_predicateManager, _policyID);
_metaCoin = MetaCoin(_metaCoinContract);
}
function proxySendCoin(address _receiver, uint256 _amount, PredicateMessage calldata _message) external payable {
bytes memory encodedSigAndArgs = abi.encodeWithSignature("_sendCoin(address,uint256)", _receiver, _amount);
require(
_authorizeTransaction(_message, encodedSigAndArgs, msg.sender, msg.value),
"MetaCoin: unauthorized transaction"
);
_metaCoin.sendCoin{value: msg.value}(msg.sender, _receiver, _amount);
}
// Admin functions - implement these with proper access control
function setPolicy(string memory _policyID) external onlyOwner {
_setPolicy(_policyID);
}
function setPredicateManager(address _predicateManager) external onlyOwner {
_setPredicateManager(_predicateManager);
}
}
Deploying the Contracts
- Deploy the proxy contract with the required constructor arguments:
_predicateManager: The ServiceManager address for your target chain. See Supported Chains.
_policyID: The policy identifier. For initial testing, you may use x-test-random, which returns a compliant response approximately 50% of the time. You will update this value in step two.
- Update your application contract deployment to restrict access to predicated functions to the proxy contract only.
- Verify access control so that only the proxy contract can invoke the predicated function.
Next Step: After deploying your contracts, proceed to Application Onboarding to configure your organization and policies.