A calling contract is any smart contract that invokes the Forte Rules Engine to evaluate a policy. This is the on-chain portion of your application and is fully under your control.
Overview
Calling contracts serve as the entry point for policy evaluation. When a function in your calling contract is executed, it can trigger the Rules Engine to evaluate one or more rules defined in a subscribed policy.
How It Works
- Your contract extends the
RulesEngineClient contract
- Your functions include modifiers that call the Rules Engine
- The Rules Engine evaluates the policy rules
- The transaction either continues or reverts based on the rule evaluation
Integration Requirements
To subscribe a smart contract to a policy, it must extend the RulesEngineClient contract included in the Forte Rules Engine.
import "@forte-rules-engine/client/RulesEngineClient.sol";
contract MyContract is RulesEngineClient {
// Your contract implementation
}
Defining Calling Functions in Policies
When you create a policy, you must specify which smart contract functions can trigger it. These are defined in the CallingFunctions array of your policy (see Policy Templates for examples).
CallingFunction Properties
Each calling function in your policy must specify:
{
"Name": "transfer(address to, uint256 value)",
"FunctionSignature": "transfer(address to, uint256 value)",
"EncodedValues": "address to, uint256 value, uint256 fromBalance"
}
- Name: A unique identifier for the function within the policy
- FunctionSignature: Must exactly match your smart contract’s function signature (including parameter types and order)
- EncodedValues: Parameters that will be passed to the Rules Engine for evaluation
EncodedValues: The Data Bridge
EncodedValues is one of the most important concepts in calling functions - it defines what data is sent from your contract to the Rules Engine for rule evaluation.
What Are EncodedValues?
EncodedValues specify:
- Which parameters from your function signature to include
- Additional data beyond the function parameters (like balances, timestamps, etc.)
- The data types that rules can reference
Key Characteristics
Can Include More Than Function Parameters:
{
"FunctionSignature": "transfer(address to, uint256 amount)",
"EncodedValues": "address to, uint256 amount, uint256 senderBalance, uint256 receiverBalance"
}
In this example, senderBalance and receiverBalance are not in the function signature but are calculated and passed by your contract.
Names Don’t Need to Match Contract Parameters:
// Your contract function
function transfer(address to, uint256 value) public { ... }
// Your policy can use different names
{
"EncodedValues": "address recipient, uint256 amount"
}
The parameter names in EncodedValues are just identifiers used within your policy - they don’t need to match your contract’s parameter names.
Referenced Throughout Your Policy:
Once defined in EncodedValues, these parameters can be used in:
- Rule conditions:
amount > 1000
- Foreign call parameters:
"ValuesToPass": "to"
- Tracker keys:
TR:balances(to)
Supported Parameter Types
EncodedValues supports the following Solidity types:
Primitive Types:
uint256 - Unsigned integers
address - Ethereum addresses
bool - Boolean values
bytes - Byte arrays
string - String values
Array Types:
uint256[]
address[]
bool[]
bytes[]
string[]
Function Signature Matching
The FunctionSignature in your policy must exactly match your contract’s function signature:
Correct:
// Contract
function mint(address to, uint256 quantity) public { ... }
// Policy
"FunctionSignature": "mint(address to, uint256 quantity)"
Incorrect (will not match):
// Wrong parameter types
"FunctionSignature": "mint(address, uint256)"
// Wrong parameter names don't matter, but types must be exact
"FunctionSignature": "mint(address recipient, uint256 amount)" // This is OK
The function signature must be ABI-compatible and match exactly, including parameter types and
order. Parameter names in the signature don’t affect matching, but types must be precise.
Passing Additional Data to Rules Engine
One of the most powerful features of calling contracts is the ability to pass additional context beyond the function parameters.
Common Additional Data Examples
Balance Information:
function beforeTransfer(address from, address to, uint256 amount) internal {
uint256 senderBalance = balanceOf(from);
uint256 receiverBalance = balanceOf(to);
bytes memory encodedData = abi.encode(
from,
to,
amount,
senderBalance, // Additional data
receiverBalance // Additional data
);
checkRules(encodedData, "transfer(address,uint256)");
}
Timestamp or Block Information:
function beforeUnstake(uint256 amount) internal {
bytes memory encodedData = abi.encode(
amount,
block.timestamp // Additional data
);
checkRules(encodedData, "unstake(uint256)");
}
Ownership or Role Information:
function beforeMint(address to, uint256 quantity) internal {
address owner = owner();
bytes memory encodedData = abi.encode(
to,
quantity,
owner // Additional data
);
checkRules(encodedData, "mint(address,uint256)");
}
Why Pass Additional Data?
Rules often need context that isn’t explicitly passed as function parameters:
- Current State: Balances, allowances, ownership
- Calculated Values: Percentages, ratios, derived amounts
- Environmental Data: Block timestamps, block numbers (though Global Variables are often better for this)
- Cross-Reference Data: Data from related accounts or contracts
Function Resolution
The Rules Engine uses flexible function resolution when matching calling functions:
Primary Matching
The engine first attempts to match the Name field exactly as specified in the policy.
Fallback Matching
If an exact match fails, the engine performs case-insensitive matching:
// Policy defines
"Name": "transfer(address,uint256)"
// Contract calls with
"Transfer(address,uint256)" // Will match via case-insensitive fallback
This provides flexibility but it’s recommended to use exact matches for clarity.
Best Practices
1. Keep EncodedValues Focused
Only include data that your rules actually need. Encoding and passing unnecessary data costs gas.
2. Use Descriptive Names
Even though parameter names don’t affect matching, use clear names in your policy for maintainability:
// Good
"EncodedValues": "address to, uint256 amount, uint256 toBalance"
// Less clear
"EncodedValues": "address a, uint256 b, uint256 c"
3. Document Additional Parameters
When passing data beyond function parameters, document what each additional value represents.
4. Validate Data Before Encoding
Ensure the data you’re passing is in the correct format and type before encoding:
// Bad - could pass wrong type
bytes memory encodedData = abi.encode(msg.sender, "100"); // string instead of uint256
// Good - explicitly typed
uint256 amount = 100;
bytes memory encodedData = abi.encode(msg.sender, amount);
5. Consistent Ordering
Maintain consistent parameter ordering between your policy’s EncodedValues and your contract’s encoding:
// Policy
"EncodedValues": "address to, uint256 amount, uint256 balance"
// Contract - same order
abi.encode(to, amount, balance) // Correct
abi.encode(balance, to, amount) // Wrong!
Integration Workflow
For full integration of your contract with the Forte Rules Engine:
- Define your policy including CallingFunctions
- Add modifiers to your calling contract
- Register the policy with rules engine
- Deploy your contract
- Apply the policy
- Test the configuration