Imagine: a multi-sig wallet with 5 signers, daily limit $2M. An employee initiates a withdrawal of $1.8M to an address that was added to the OFAC sanctions list an hour ago. Signatures are collected, but the policy engine at the pre-check stage calls Chainalysis KYT, gets a risk score of 85—the transaction is blocked. Without such an engine, the funds would be frozen for weeks, and the company could face a fine of up to $500,000. We design and implement a rule system for custodial wallets and corporate multisigs. The engine applies a set of rules before signing—this is a key difference from post-processing. Our team has 8 years of experience in blockchain development and over 40 implementations for major custodial solutions.
How does the transaction management system work?
Policy engine—a layer between the initiator and execution. It evaluates rules before the transaction goes to signature and into the mempool. Rules check parameters, context (sender role, time), external data (compliance API), and on-chain state. Result: allow, deny, request additional signatures, or delay.
According to the Safe{Core} Protocol specification, Hooks' preCheck is called before transaction execution.
Architecture of the rule system
Levels of policy application — policy system design
The policy engine can exist on several levels—they are often mixed, causing problems:
- Off-chain pre-execution — the most common. Rules are checked in the service before signing. Flexible, cheap, supports any logic. Drawback: requires trust in this service.
- On-chain enforcement — a smart contract, the entry point for all transactions. Safe{Core} Protocol Hooks is an example. Stronger guarantees, but logic is limited on-chain: no access to external data without oracles, each check costs gas.
- Hybrid — policies are verified off-chain, the contract accepts a proof (commitment scheme or trusted signer attestation).
Why combine off-chain and on-chain?
Off-chain evaluation is 10 times faster and does not consume gas, but on-chain provides immutable guarantees. The optimal solution is a hybrid architecture: fast off-chain rules as the first filter, critical policies (protocol limits) in smart contracts. In one project, we process 500+ rules with latency under 5 ms, saving up to 70% time on manual moderation. Development cost is estimated individually but pays off through reduced compliance incidents. One unblocked transaction can save over $1 million, and annual savings can reach $5 million.
Rule model
A rule consists of a condition and an action. Conditions can be:
- Parametric:
amount > threshold,recipient in whitelist,token == USDC - Contextual:
sender.role == OPERATOR,time_of_day in 09:00-18:00,daily_volume + amount <= limit - External:
chainalysis_risk_score(recipient) < 70,ofac_check(recipient) == CLEAR - On-chain:
recipient.is_contract == false,token.paused == false
Actions: ALLOW, DENY, REQUIRE_APPROVAL(n_signers), DELAY(duration), NOTIFY(channels).
Rules have priorities, conflicts are possible. Need clear semantics: first-match, all-must-pass, whitelist-overrides-blacklist. This is a design decision.
Example rule interface
interface PolicyRule {
id: string;
priority: number;
conditions: Condition[];
conditionLogic: 'AND' | 'OR';
action: PolicyAction;
metadata: { name: string; owner: string; updatedAt: number };
}
interface PolicyAction {
type: 'ALLOW' | 'DENY' | 'REQUIRE_APPROVAL' | 'DELAY';
params?: {
requiredApprovers?: string[];
minApprovals?: number;
delaySeconds?: number;
notifyChannels?: string[];
};
}
Evaluator: evaluation order
The engine must process rules efficiently—especially when there are hundreds of rules and some require external calls (compliance provider API).
The optimal strategy: short-circuit evaluation with caching. First, cheap local conditions (transaction parameters, roles) are checked, then cached external data, and finally fresh API calls with timeout.
class PolicyEvaluator:
def evaluate(self, tx: Transaction, context: EvalContext) -> PolicyDecision:
sorted_rules = sorted(self.rules, key=lambda r: r.priority, reverse=True)
for rule in sorted_rules:
cheap_conditions = [c for c in rule.conditions if c.type == 'PARAMETRIC']
if not self._eval_conditions(cheap_conditions, tx, context):
continue
expensive_conditions = [c for c in rule.conditions if c.type == 'EXTERNAL']
cache_key = self._cache_key(expensive_conditions, tx)
cached = self.cache.get(cache_key)
results = cached if cached else self._eval_external(expensive_conditions, tx)
self.cache.set(cache_key, results, ttl=300)
if self._eval_conditions_with_results(rule.conditions, results, rule.conditionLogic):
return PolicyDecision(action=rule.action, rule_id=rule.id)
return PolicyDecision(action=DEFAULT_ACTION)
On-chain implementation: Safe Hooks
The Safe{Core} Protocol (EIP-7579 compatible) provides a hook mechanism:
interface ISafeProtocolHooks {
function preCheck(
Safe safe,
SafeTransaction calldata tx,
uint256 executionType,
bytes calldata executionMeta
) external returns (bytes memory preCheckData);
function postCheck(
Safe safe,
bool success,
bytes calldata preCheckData
) external;
}
preCheck is called before execution. If it reverts—the transaction fails. Here you can: check whitelist/blacklist (stored in hook storage), check limits (via accumulators by addresses/tokens), require additional approval via timelock.
Example limit hook:
contract DailyLimitHook is ISafeProtocolHooks {
mapping(address => mapping(address => uint256)) public dailyVolume;
mapping(address => mapping(address => uint256)) public lastResetDay;
mapping(address => mapping(address => uint256)) public dailyLimit;
function preCheck(Safe safe, SafeTransaction calldata tx, uint256, bytes calldata)
external returns (bytes memory)
{
address token = _extractToken(tx.data);
uint256 amount = _extractAmount(tx.data);
uint256 today = block.timestamp / 1 days;
address safeAddr = address(safe);
if (lastResetDay[safeAddr][token] < today) {
dailyVolume[safeAddr][token] = 0;
lastResetDay[safeAddr][token] = today;
}
require(
dailyVolume[safeAddr][token] + amount <= dailyLimit[safeAddr][token],
"DailyLimitExceeded"
);
return abi.encode(token, amount);
}
function postCheck(Safe safe, bool success, bytes calldata preCheckData) external {
if (success) {
(address token, uint256 amount) = abi.decode(preCheckData, (address, uint256));
dailyVolume[address(safe)][token] += amount;
}
}
}
How to integrate compliance API?
For financial products, the policy engine inevitably includes integration with compliance providers. Key ones:
- Chainalysis — KYT API checks addresses (risk score) and transactions (exposure to known clusters). Latency: 200–800ms, need cache and graceful degradation.
- Elliptic — similar functionality, different risk assessment model. Used in Fireblocks.
- TRM Labs — specializes in cross-chain analysis, good coverage of Solana and Tron.
- OFAC screening — can be done through the same providers or via a self-maintained snapshot of the SDN list (updated infrequently, can be stored locally and updated via webhook).
Important: compliance APIs have SLAs and may be unavailable. The policy engine must have a clear policy for the EXTERNAL_CHECK_TIMEOUT case: fail-open (allow with log) vs. fail-closed (block). This is a business decision, but it must be documented.
| Provider | Features | Latency | Network Coverage |
|---|---|---|---|
| Chainalysis | KYT, risk scores | 200-800ms | Ethereum, Bitcoin, 10+ networks |
| Elliptic | Focus on sanctions | 300-600ms | Major L1s |
| TRM Labs | Cross-chain, Solana | 150-500ms | 30+ networks |
How we implement the policy engine?
- Requirements analysis — determine business rules, compliance obligations, technical constraints (latency, throughput).
- Rule model design — design rule hierarchy, conditions, actions, conflict resolution policies.
- Evaluator development — write the engine core with short-circuit evaluation, caching, and external API integration.
- Integration and testing — connect the engine to the wallet or multisig, perform load and regression testing.
Order policy engine development to protect your assets.
Monitoring and audit
A rule system without a full audit log is useless for compliance. Each decision must contain:
- Transaction hash or pre-tx ID
- List of applied rules and their results
- Values of all conditions at the time of evaluation
- Final decision and executor
- Timestamp with millisecond accuracy
This is an immutable log. Storage: PostgreSQL with an append-only table + replication to S3/Arweave for long-term retention. For regulatory requirements — minimum 5 years.
| Component | Technology |
|---|---|
| Rule storage | PostgreSQL + Redis cache |
| Evaluator | Go / Python service |
| On-chain hooks | Solidity (Safe Protocol) |
| Compliance API | Chainalysis / Elliptic / TRM |
| Audit log | PostgreSQL → S3 |
| Admin UI | React + role-based access |
What's included in development
- Project documentation (architecture, rule model)
- Source code of the engine (off-chain evaluator and on-chain hooks)
- Integration with compliance providers (Chainalysis, Elliptic, etc.)
- Caching and graceful degradation setup
- Audit log deployment and configuration
- Team training and administrator documentation
- 3 months post-release support
Get a consultation on policy engine implementation—we'll help design and deploy a turnkey solution.







