@chainlink/ace 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.foundry-version +1 -0
- package/.github/CODEOWNERS +1 -0
- package/.github/workflows/auto-release-version.yml +107 -0
- package/.github/workflows/create-version-pr.yml +95 -0
- package/.github/workflows/forge-docs.yml +90 -0
- package/.github/workflows/forge-test.yml +59 -0
- package/.solhint-test.json +18 -0
- package/.solhint.json +16 -0
- package/.solhintignore +3 -0
- package/.solhintignore-test +2 -0
- package/Glossary.md +141 -0
- package/LICENSE +59 -0
- package/README.md +218 -0
- package/assets/chainlink-logo.svg +21 -0
- package/chainlink-ace-License-grants +2 -0
- package/foundry.toml +33 -0
- package/getting_started/GETTING_STARTED.md +477 -0
- package/getting_started/MyVault.sol +48 -0
- package/getting_started/advanced/.env.example +36 -0
- package/getting_started/advanced/GETTING_STARTED_ADVANCED.md +431 -0
- package/getting_started/advanced/SanctionsList.sol +25 -0
- package/getting_started/advanced/SanctionsPolicy.sol +58 -0
- package/package.json +41 -0
- package/packages/cross-chain-identity/README.md +148 -0
- package/packages/cross-chain-identity/docs/API_GUIDE.md +120 -0
- package/packages/cross-chain-identity/docs/API_REFERENCE.md +271 -0
- package/packages/cross-chain-identity/docs/CONCEPTS.md +253 -0
- package/packages/cross-chain-identity/docs/CREDENTIAL_FLOW.md +195 -0
- package/packages/cross-chain-identity/docs/SECURITY.md +70 -0
- package/packages/cross-chain-identity/src/CredentialRegistry.sol +245 -0
- package/packages/cross-chain-identity/src/CredentialRegistryIdentityValidator.sol +339 -0
- package/packages/cross-chain-identity/src/CredentialRegistryIdentityValidatorPolicy.sol +71 -0
- package/packages/cross-chain-identity/src/IdentityRegistry.sol +123 -0
- package/packages/cross-chain-identity/src/TrustedIssuerRegistry.sol +140 -0
- package/packages/cross-chain-identity/src/interfaces/ICredentialDataValidator.sol +30 -0
- package/packages/cross-chain-identity/src/interfaces/ICredentialRegistry.sol +170 -0
- package/packages/cross-chain-identity/src/interfaces/ICredentialRequirements.sol +192 -0
- package/packages/cross-chain-identity/src/interfaces/ICredentialValidator.sol +37 -0
- package/packages/cross-chain-identity/src/interfaces/IIdentityRegistry.sol +85 -0
- package/packages/cross-chain-identity/src/interfaces/IIdentityValidator.sol +18 -0
- package/packages/cross-chain-identity/src/interfaces/ITrustedIssuerRegistry.sol +61 -0
- package/packages/cross-chain-identity/test/CredentialRegistry.t.sol +220 -0
- package/packages/cross-chain-identity/test/CredentialRegistryIdentityValidator.t.sol +554 -0
- package/packages/cross-chain-identity/test/CredentialRegistryIdentityValidatorPolicy.t.sol +114 -0
- package/packages/cross-chain-identity/test/IdentityRegistry.t.sol +106 -0
- package/packages/cross-chain-identity/test/IdentityValidator.t.sol +969 -0
- package/packages/cross-chain-identity/test/TrustedIssuerRegistry.t.sol +123 -0
- package/packages/cross-chain-identity/test/helpers/BaseProxyTest.sol +112 -0
- package/packages/cross-chain-identity/test/helpers/MockCredentialDataValidator.sol +26 -0
- package/packages/cross-chain-identity/test/helpers/MockCredentialRegistryReverting.sol +131 -0
- package/packages/policy-management/README.md +197 -0
- package/packages/policy-management/docs/API_GUIDE.md +290 -0
- package/packages/policy-management/docs/API_REFERENCE.md +173 -0
- package/packages/policy-management/docs/CONCEPTS.md +156 -0
- package/packages/policy-management/docs/CUSTOM_POLICIES_TUTORIAL.md +195 -0
- package/packages/policy-management/docs/POLICY_ORDERING_GUIDE.md +91 -0
- package/packages/policy-management/docs/SECURITY.md +57 -0
- package/packages/policy-management/src/core/Policy.sol +124 -0
- package/packages/policy-management/src/core/PolicyEngine.sol +382 -0
- package/packages/policy-management/src/core/PolicyFactory.sol +92 -0
- package/packages/policy-management/src/core/PolicyProtected.sol +126 -0
- package/packages/policy-management/src/extractors/ComplianceTokenForceTransferExtractor.sol +57 -0
- package/packages/policy-management/src/extractors/ComplianceTokenFreezeUnfreezeExtractor.sol +54 -0
- package/packages/policy-management/src/extractors/ComplianceTokenMintBurnExtractor.sol +61 -0
- package/packages/policy-management/src/extractors/ERC20ApproveExtractor.sol +57 -0
- package/packages/policy-management/src/extractors/ERC20TransferExtractor.sol +62 -0
- package/packages/policy-management/src/extractors/ERC3643ForcedTransferExtractor.sol +56 -0
- package/packages/policy-management/src/extractors/ERC3643FreezeUnfreezeExtractor.sol +55 -0
- package/packages/policy-management/src/extractors/ERC3643MintBurnExtractor.sol +51 -0
- package/packages/policy-management/src/extractors/ERC3643SetAddressFrozenExtractor.sol +51 -0
- package/packages/policy-management/src/interfaces/IExtractor.sol +17 -0
- package/packages/policy-management/src/interfaces/IMapper.sol +17 -0
- package/packages/policy-management/src/interfaces/IPolicy.sol +61 -0
- package/packages/policy-management/src/interfaces/IPolicyEngine.sol +264 -0
- package/packages/policy-management/src/interfaces/IPolicyProtected.sol +48 -0
- package/packages/policy-management/src/policies/AllowPolicy.sol +104 -0
- package/packages/policy-management/src/policies/BypassPolicy.sol +90 -0
- package/packages/policy-management/src/policies/IntervalPolicy.sol +223 -0
- package/packages/policy-management/src/policies/MaxPolicy.sol +73 -0
- package/packages/policy-management/src/policies/OnlyAuthorizedSenderPolicy.sol +84 -0
- package/packages/policy-management/src/policies/OnlyOwnerPolicy.sol +35 -0
- package/packages/policy-management/src/policies/PausePolicy.sol +82 -0
- package/packages/policy-management/src/policies/README.md +632 -0
- package/packages/policy-management/src/policies/RejectPolicy.sol +89 -0
- package/packages/policy-management/src/policies/RoleBasedAccessControlPolicy.sol +162 -0
- package/packages/policy-management/src/policies/SecureMintPolicy.sol +271 -0
- package/packages/policy-management/src/policies/VolumePolicy.sol +133 -0
- package/packages/policy-management/src/policies/VolumeRatePolicy.sol +192 -0
- package/packages/policy-management/test/PolicyEngine.t.sol +368 -0
- package/packages/policy-management/test/PolicyFactory.t.sol +114 -0
- package/packages/policy-management/test/PolicyProtectedToken.t.sol +75 -0
- package/packages/policy-management/test/extractors/ComplianceTokenForceTransferExtractor.t.sol +59 -0
- package/packages/policy-management/test/extractors/ComplianceTokenFreezeUnfreezeExtractor.t.sol +74 -0
- package/packages/policy-management/test/extractors/ComplianceTokenMintBurnExtractor.t.sol +92 -0
- package/packages/policy-management/test/extractors/ERC20ApproveExtractor.t.sol +58 -0
- package/packages/policy-management/test/extractors/ERC3643ForcedTransferExtractor.t.sol +59 -0
- package/packages/policy-management/test/extractors/ERC3643FreezeUnfreezeExtractor.t.sol +74 -0
- package/packages/policy-management/test/extractors/ERC3643MintBurnExtractor.t.sol +73 -0
- package/packages/policy-management/test/extractors/ERC3643SetAddressFrozenExtractor.t.sol +56 -0
- package/packages/policy-management/test/helpers/BaseProxyTest.sol +75 -0
- package/packages/policy-management/test/helpers/CustomMapper.sol +26 -0
- package/packages/policy-management/test/helpers/DummyExtractor.sol +11 -0
- package/packages/policy-management/test/helpers/ExpectedParameterPolicy.sol +39 -0
- package/packages/policy-management/test/helpers/MockAggregatorV3.sol +51 -0
- package/packages/policy-management/test/helpers/MockToken.sol +66 -0
- package/packages/policy-management/test/helpers/MockTokenExtractor.sol +34 -0
- package/packages/policy-management/test/helpers/PolicyAlwaysAllowed.sol +45 -0
- package/packages/policy-management/test/helpers/PolicyAlwaysContinue.sol +23 -0
- package/packages/policy-management/test/helpers/PolicyAlwaysRejected.sol +23 -0
- package/packages/policy-management/test/helpers/PolicyFailingRun.sol +22 -0
- package/packages/policy-management/test/policies/AllowPolicy.t.sol +174 -0
- package/packages/policy-management/test/policies/BypassPolicy.t.sol +159 -0
- package/packages/policy-management/test/policies/IntervalPolicy.t.sol +307 -0
- package/packages/policy-management/test/policies/MaxPolicy.t.sol +54 -0
- package/packages/policy-management/test/policies/OnlyAuthorizedSenderPolicy.t.sol +95 -0
- package/packages/policy-management/test/policies/OnlyOwnerPolicy.t.sol +47 -0
- package/packages/policy-management/test/policies/PausePolicy.t.sol +75 -0
- package/packages/policy-management/test/policies/RejectPolicy.t.sol +182 -0
- package/packages/policy-management/test/policies/RoleBasedAccessControlPolicy.t.sol +223 -0
- package/packages/policy-management/test/policies/SecureMintPolicy.t.sol +442 -0
- package/packages/policy-management/test/policies/VolumePolicy.t.sol +158 -0
- package/packages/policy-management/test/policies/VolumeRatePolicy.t.sol +165 -0
- package/packages/tokens/erc-20/src/ComplianceTokenERC20.sol +345 -0
- package/packages/tokens/erc-20/src/ComplianceTokenStoreERC20.sol +29 -0
- package/packages/tokens/erc-20/test/ComplianceTokenERC20.t.sol +556 -0
- package/packages/tokens/erc-20/test/helpers/BaseProxyTest.sol +75 -0
- package/packages/tokens/erc-3643/README.md +24 -0
- package/packages/tokens/erc-3643/src/ComplianceTokenERC3643.sol +564 -0
- package/packages/tokens/erc-3643/src/ComplianceTokenStoreERC3643.sol +30 -0
- package/packages/tokens/erc-3643/test/ComplianceTokenERC3643.t.sol +815 -0
- package/packages/tokens/erc-3643/test/helpers/BaseProxyTest.sol +76 -0
- package/packages/tokens/erc-3643/test/helpers/ExpectedContextPolicy.sol +32 -0
- package/packages/vendor/erc-3643/compliance/modular/IModularCompliance.sol +220 -0
- package/packages/vendor/erc-3643/registry/interface/IClaimTopicsRegistry.sol +101 -0
- package/packages/vendor/erc-3643/registry/interface/IIdentityRegistry.sol +251 -0
- package/packages/vendor/erc-3643/registry/interface/IIdentityRegistryStorage.sol +191 -0
- package/packages/vendor/erc-3643/registry/interface/ITrustedIssuersRegistry.sol +161 -0
- package/packages/vendor/erc-3643/token/IToken.sol +457 -0
- package/packages/vendor/onchain-id/interface/IClaimIssuer.sol +53 -0
- package/packages/vendor/onchain-id/interface/IERC734.sol +110 -0
- package/packages/vendor/onchain-id/interface/IERC735.sol +105 -0
- package/packages/vendor/onchain-id/interface/IIdentity.sol +26 -0
- package/packages/vendor/onchain-id/interface/IImplementationAuthority.sol +21 -0
- package/remappings.txt +6 -0
- package/script/DeployComplianceTokenERC20.s.sol +191 -0
- package/script/DeployComplianceTokenERC3643.s.sol +208 -0
- package/script/DeploySimpleComplianceToken.s.sol +38 -0
- package/script/getting_started/DeployGettingStarted.s.sol +74 -0
- package/script/getting_started/advanced/DeployAdvancedGettingStarted.s.sol +332 -0
- package/script/getting_started/advanced/DeploySanctionsList.s.sol +26 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# Tutorial: Creating a Custom Policy
|
|
2
|
+
|
|
3
|
+
This tutorial will guide you through the process of creating your own custom policy from scratch. We will build a simple `LockoutPolicy` that blocks transactions from an address for a specified period of time.
|
|
4
|
+
|
|
5
|
+
This will teach you the core principles of policy development and provide a template for your own, more complex policies.
|
|
6
|
+
|
|
7
|
+
> **Note:** A clean, copy-pasteable boilerplate template is available at the end of this tutorial for you to use as a starting point.
|
|
8
|
+
|
|
9
|
+
## Step 1: The Policy Contract Boilerplate
|
|
10
|
+
|
|
11
|
+
Every policy must inherit from the base `Policy` contract and implement the `run` function. Let's create our file `LockoutPolicy.sol` with the basic structure.
|
|
12
|
+
|
|
13
|
+
**`LockoutPolicy.sol`**
|
|
14
|
+
|
|
15
|
+
```solidity
|
|
16
|
+
// SPDX-License-Identifier: MIT
|
|
17
|
+
pragma solidity 0.8.26;
|
|
18
|
+
|
|
19
|
+
import { Policy } from "@chainlink/policy-management/core/Policy.sol";
|
|
20
|
+
import { IPolicyEngine } from "@chainlink/policy-management/interfaces/IPolicyEngine.sol";
|
|
21
|
+
|
|
22
|
+
contract LockoutPolicy is Policy {
|
|
23
|
+
// Our policy's logic will go here.
|
|
24
|
+
|
|
25
|
+
function run(
|
|
26
|
+
address caller,
|
|
27
|
+
address subject,
|
|
28
|
+
bytes4 selector,
|
|
29
|
+
bytes[] calldata parameters,
|
|
30
|
+
bytes calldata context
|
|
31
|
+
) public view virtual override returns (IPolicyEngine.PolicyResult) {
|
|
32
|
+
// Our enforcement logic will go here.
|
|
33
|
+
return IPolicyEngine.PolicyResult.Continue; // Default to Continue
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
This is the simplest possible policy. It doesn't do anything yet, but it's a valid, compilable policy contract.
|
|
39
|
+
|
|
40
|
+
## Step 2: Adding State and Logic
|
|
41
|
+
|
|
42
|
+
Our `LockoutPolicy` needs to store two pieces of information for each locked-out address: the address itself, and the Unix timestamp when their lockout period expires. We'll use a `mapping` for this.
|
|
43
|
+
|
|
44
|
+
We also need a public function that the policy owner can call to add or update a lockout.
|
|
45
|
+
|
|
46
|
+
```solidity
|
|
47
|
+
// ... imports ...
|
|
48
|
+
|
|
49
|
+
contract LockoutPolicy is Policy {
|
|
50
|
+
mapping(address => uint256) public lockoutExpiresAt;
|
|
51
|
+
|
|
52
|
+
/// @notice The policy owner can call this to lock an address.
|
|
53
|
+
/// @param account The address to lock out.
|
|
54
|
+
/// @param duration The duration of the lockout, in seconds.
|
|
55
|
+
function setLockout(address account, uint256 duration) public onlyOwner {
|
|
56
|
+
lockoutExpiresAt[account] = block.timestamp + duration;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function run(
|
|
60
|
+
address caller,
|
|
61
|
+
address, // subject
|
|
62
|
+
bytes4, // selector
|
|
63
|
+
bytes[] calldata, // parameters
|
|
64
|
+
bytes calldata // context
|
|
65
|
+
) public view virtual override returns (IPolicyEngine.PolicyResult) {
|
|
66
|
+
// We only care about the original sender of the transaction.
|
|
67
|
+
if (lockoutExpiresAt[caller] > block.timestamp) {
|
|
68
|
+
// If the sender's lockout period is still active, reject.
|
|
69
|
+
revert IPolicyEngine.PolicyRejected("LockoutPolicy: Address is locked out");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Otherwise, continue to the next policy.
|
|
73
|
+
return IPolicyEngine.PolicyResult.Continue;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Our policy now has logic! It checks if the `sender` of the transaction is trying to transact before their `lockoutExpiresAt` timestamp has been reached. If they are, it rejects the transaction. Otherwise, it takes no action and lets the `PolicyEngine` continue to the next policy.
|
|
79
|
+
|
|
80
|
+
## Step 3: Input Validation (A Critical Best Practice)
|
|
81
|
+
|
|
82
|
+
What if our policy needed to check a parameter from the transaction data? For example, what if we wanted to lock out the `recipient` of a transfer instead of the `sender`?
|
|
83
|
+
|
|
84
|
+
The policy would receive the recipient's address in the `parameters` array. It is a **critical best practice to always validate the inputs** your policy receives.
|
|
85
|
+
|
|
86
|
+
Let's modify our `run` function to expect the `recipient` as a parameter and add the necessary `require` check.
|
|
87
|
+
|
|
88
|
+
```solidity
|
|
89
|
+
// ...
|
|
90
|
+
function run(
|
|
91
|
+
address, // caller
|
|
92
|
+
address, // subject
|
|
93
|
+
bytes4, // selector
|
|
94
|
+
bytes[] calldata parameters,
|
|
95
|
+
bytes calldata // context
|
|
96
|
+
) public view virtual override returns (IPolicyEngine.PolicyResult) {
|
|
97
|
+
// BEST PRACTICE: Always validate your expected inputs.
|
|
98
|
+
// This prevents reverts and ensures your policy is used correctly.
|
|
99
|
+
require(parameters.length == 1, "LockoutPolicy: Expected 1 parameter");
|
|
100
|
+
|
|
101
|
+
// Decode the address from the parameters array.
|
|
102
|
+
address recipient = abi.decode(parameters[0], (address));
|
|
103
|
+
|
|
104
|
+
if (lockoutExpiresAt[recipient] > block.timestamp) {
|
|
105
|
+
revert IPolicyEngine.PolicyRejected("LockoutPolicy: Address is locked out");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return IPolicyEngine.PolicyResult.Continue;
|
|
109
|
+
}
|
|
110
|
+
// ...
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
This `require` statement makes our policy more robust. It provides a clear error message if the policy is ever misconfigured in the `PolicyEngine` without the required parameter, which makes debugging much easier.
|
|
114
|
+
|
|
115
|
+
## Step 4: Using Your Custom Policy
|
|
116
|
+
|
|
117
|
+
Now your `LockoutPolicy` is complete. Using it follows the same pattern as any pre-built policy.
|
|
118
|
+
|
|
119
|
+
In your deployment script:
|
|
120
|
+
|
|
121
|
+
1. **Deploy it**: `LockoutPolicy lockoutPolicy = new LockoutPolicy();`
|
|
122
|
+
2. **Initialize it**: `lockoutPolicy.initialize(address(policyEngine), OWNER_ADDRESS, new bytes(0));`
|
|
123
|
+
> **What is the `new bytes(0)` parameter?**
|
|
124
|
+
>
|
|
125
|
+
> The third argument of the `initialize` function is a `bytes` array called `configParams`. It's a the feature for passing initial setup data to more complex policies (e.g., setting a volume limit for a [`VolumePolicy`](../src/policies/VolumePolicy.sol)). Since our `LockoutPolicy` doesn't need any initial configuration, we pass an empty byte array to explicitly provide no parameters. The `initialize` function passes these bytes to an internal `configure` function, which you can override to handle this data in your own policies.
|
|
126
|
+
3. **Add it to the engine**: To use the version of our policy that checks the `recipient`, you must specify the parameter name. Assume we have an [`ERC20TransferExtractor`](../src/extractors/ERC20TransferExtractor.sol) registered for our target function.
|
|
127
|
+
|
|
128
|
+
```solidity
|
|
129
|
+
// Define the parameter name your policy needs
|
|
130
|
+
bytes32[] memory policyParams = new bytes32[](1);
|
|
131
|
+
policyParams[0] = keccak256("to"); // This must match the name from the Extractor
|
|
132
|
+
|
|
133
|
+
// Add the policy to the engine and specify its required parameter
|
|
134
|
+
policyEngine.addPolicy(
|
|
135
|
+
address(myContract),
|
|
136
|
+
myContract.transfer.selector,
|
|
137
|
+
address(lockoutPolicy),
|
|
138
|
+
policyParams
|
|
139
|
+
);
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
You can then call `lockoutPolicy.setLockout(LOCKED_ADDRESS, 3600)` to block a recipient for one hour. Any transfer to that address will be rejected.
|
|
143
|
+
|
|
144
|
+
You now have a complete, secure, and reusable custom policy and the knowledge needed to create many more!
|
|
145
|
+
|
|
146
|
+
## Boilerplate Template for Custom Policies
|
|
147
|
+
|
|
148
|
+
Here is a clean, commented template that you can use as a starting point for your own custom policies.
|
|
149
|
+
|
|
150
|
+
```solidity
|
|
151
|
+
// SPDX-License-Identifier: MIT
|
|
152
|
+
pragma solidity 0.8.26;
|
|
153
|
+
|
|
154
|
+
import { Policy } from "@chainlink/policy-management/core/Policy.sol";
|
|
155
|
+
import { IPolicyEngine } from "@chainlink/policy-management/interfaces/IPolicyEngine.sol";
|
|
156
|
+
|
|
157
|
+
contract MyCustomPolicy is Policy {
|
|
158
|
+
/**
|
|
159
|
+
* @notice Use the `configure` function to decode and store any initial
|
|
160
|
+
* setup data passed into the `configParams` of the `initialize` function.
|
|
161
|
+
*/
|
|
162
|
+
// function configure(bytes calldata parameters) internal override onlyInitializing {
|
|
163
|
+
// // Your custom initialization logic here...
|
|
164
|
+
// }
|
|
165
|
+
|
|
166
|
+
function run(
|
|
167
|
+
address caller,
|
|
168
|
+
address subject,
|
|
169
|
+
bytes4 selector,
|
|
170
|
+
bytes[] calldata parameters,
|
|
171
|
+
bytes calldata context
|
|
172
|
+
) public view virtual override returns (IPolicyEngine.PolicyResult) {
|
|
173
|
+
// Your custom validation logic here...
|
|
174
|
+
//
|
|
175
|
+
// Based on your logic, return one of the two possible results or revert.
|
|
176
|
+
|
|
177
|
+
// Revert with PolicyRejected to definitively block the transaction.
|
|
178
|
+
// This HALTS execution and bypasses all subsequent policies.
|
|
179
|
+
// if (condition_for_rejection) {
|
|
180
|
+
// revert IPolicyEngine.PolicyRejected("reason for rejection");
|
|
181
|
+
// }
|
|
182
|
+
|
|
183
|
+
// Return ALLOWED to definitively approve the transaction.
|
|
184
|
+
// This ALSO HALTS execution and bypasses all subsequent policies.
|
|
185
|
+
// This is powerful and should be used with care (e.g., for an admin bypass).
|
|
186
|
+
// if (condition_for_allowance) {
|
|
187
|
+
// return IPolicyEngine.PolicyResult.Allowed;
|
|
188
|
+
// }
|
|
189
|
+
|
|
190
|
+
// Return CONTINUE if your check passes but other policies in the chain should still be executed.
|
|
191
|
+
// This is the most common return value for a passing check.
|
|
192
|
+
return IPolicyEngine.PolicyResult.Continue;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
```
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# Policy Ordering Guide
|
|
2
|
+
|
|
3
|
+
## Why Order Matters
|
|
4
|
+
|
|
5
|
+
Policies execute sequentially in the exact order they were added to the `PolicyEngine`. The execution order is critical because:
|
|
6
|
+
|
|
7
|
+
- **reverts `PolicyRejected`** - Immediately stops execution and reverts the transaction
|
|
8
|
+
- **`Allowed`** - Immediately stops execution and allows the transaction (skips all remaining policies)
|
|
9
|
+
- **`Continue`** - Proceeds to the next policy in the chain, or applies the default engine behavior if no more policies remain
|
|
10
|
+
|
|
11
|
+
This means the position of each policy in the chain directly affects which policies get executed.
|
|
12
|
+
|
|
13
|
+
## Adding Policies
|
|
14
|
+
|
|
15
|
+
### `addPolicy()` - Append to End
|
|
16
|
+
|
|
17
|
+
Adds a policy to the end of the existing policy chain.
|
|
18
|
+
|
|
19
|
+
```solidity
|
|
20
|
+
// Current chain: []
|
|
21
|
+
policyEngine.addPolicy(target, selector, address(policyA), params);
|
|
22
|
+
// Result: [PolicyA]
|
|
23
|
+
|
|
24
|
+
policyEngine.addPolicy(target, selector, address(policyB), params);
|
|
25
|
+
// Result: [PolicyA, PolicyB]
|
|
26
|
+
|
|
27
|
+
policyEngine.addPolicy(target, selector, address(policyC), params);
|
|
28
|
+
// Result: [PolicyA, PolicyB, PolicyC]
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### `addPolicyAt()` - Insert at Specific Position
|
|
32
|
+
|
|
33
|
+
Inserts a policy at a specific position, shifting existing policies to the right.
|
|
34
|
+
|
|
35
|
+
```solidity
|
|
36
|
+
// Current chain: [PolicyA, PolicyB, PolicyC]
|
|
37
|
+
|
|
38
|
+
// Insert PolicyD at position 1
|
|
39
|
+
policyEngine.addPolicyAt(target, selector, address(policyD), params, 1);
|
|
40
|
+
// Result: [PolicyA, PolicyD, PolicyB, PolicyC]
|
|
41
|
+
|
|
42
|
+
// Insert PolicyE at position 0 (first position)
|
|
43
|
+
policyEngine.addPolicyAt(target, selector, address(policyE), params, 0);
|
|
44
|
+
// Result: [PolicyE, PolicyA, PolicyD, PolicyB, PolicyC]
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Removing Policies
|
|
48
|
+
|
|
49
|
+
### `removePolicy()` - Remove by Address
|
|
50
|
+
|
|
51
|
+
Removes a specific policy from the chain, shifting remaining policies to maintain order.
|
|
52
|
+
|
|
53
|
+
```solidity
|
|
54
|
+
// Current chain: [PolicyA, PolicyB, PolicyC, PolicyD]
|
|
55
|
+
|
|
56
|
+
policyEngine.removePolicy(target, selector, address(policyB));
|
|
57
|
+
// Result: [PolicyA, PolicyC, PolicyD]
|
|
58
|
+
|
|
59
|
+
policyEngine.removePolicy(target, selector, address(policyA));
|
|
60
|
+
// Result: [PolicyC, PolicyD]
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Checking Current Order
|
|
64
|
+
|
|
65
|
+
Use `getPolicies()` to view the current policy chain in execution order:
|
|
66
|
+
|
|
67
|
+
```solidity
|
|
68
|
+
address[] memory policies = policyEngine.getPolicies(target, selector);
|
|
69
|
+
// Returns: [policy1Address, policy2Address, policy3Address, ...]
|
|
70
|
+
// Array index 0 executes first, then index 1, then index 2, etc.
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Reordering Existing Policies
|
|
74
|
+
|
|
75
|
+
To change the position of an existing policy:
|
|
76
|
+
|
|
77
|
+
1. Remove the policy from its current position
|
|
78
|
+
2. Add it back at the desired position
|
|
79
|
+
|
|
80
|
+
```solidity
|
|
81
|
+
// Current: [PolicyA, PolicyB, PolicyC]
|
|
82
|
+
// Goal: Move PolicyC to first position
|
|
83
|
+
|
|
84
|
+
// Step 1: Remove PolicyC
|
|
85
|
+
policyEngine.removePolicy(target, selector, address(policyC));
|
|
86
|
+
// Result: [PolicyA, PolicyB]
|
|
87
|
+
|
|
88
|
+
// Step 2: Add PolicyC at position 0
|
|
89
|
+
policyEngine.addPolicyAt(target, selector, address(policyC), params, 0);
|
|
90
|
+
// Result: [PolicyC, PolicyA, PolicyB]
|
|
91
|
+
```
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Security Considerations: Policy Management
|
|
2
|
+
|
|
3
|
+
The Policy Management component provides a powerful and flexible way to enforce on-chain rules, but this flexibility requires careful attention to security during implementation and administration.
|
|
4
|
+
|
|
5
|
+
## 1. Policy Administration is a Critical Control
|
|
6
|
+
|
|
7
|
+
The ability to add, remove, or reorder policies in the `PolicyEngine` is the most sensitive administrative power in the system. An attacker who gains control over these functions can effectively disable or bypass all compliance rules.
|
|
8
|
+
|
|
9
|
+
- **Access Control:** Only highly trusted roles, such as a DAO, a multi-sig wallet, or a designated `owner`, MUST be able to call `addPolicy`, `removePolicy`, `setExtractor`, `setPolicyMapper`, and `setDefaultAllow`.
|
|
10
|
+
- **Timelocks:** It is RECOMMENDED that all administrative changes to the `PolicyEngine` are passed through a timelock contract. This creates a delay between the proposal of a change and its execution, giving users time to review the change and exit the system if they do not agree with it.
|
|
11
|
+
|
|
12
|
+
## 2. Policy Order Matters
|
|
13
|
+
|
|
14
|
+
The `PolicyEngine` executes policies for a given function in a specific, ordered sequence. The outcome of the entire chain can change dramatically based on this order.
|
|
15
|
+
|
|
16
|
+
- **`Allowed` as a Bypass:** A policy that returns `Allowed` will immediately halt execution and bypass all subsequent policies in the chain. For this reason, permissive policies (like a `BypassPolicy` for admins) should be placed with extreme care, typically at the beginning of the policy chain.
|
|
17
|
+
- **Ordering for Restriction:** Restrictive policies should generally be placed before more lenient ones. For example, a `RejectPolicy` for a hard-coded denylist should come before a `VolumePolicy` to ensure the denied user cannot transact at all.
|
|
18
|
+
|
|
19
|
+
## 3. Trust in Policy, Extractor, and Mapper Contracts
|
|
20
|
+
|
|
21
|
+
The `PolicyEngine` delegates trust to the individual `Policy`, `Extractor`, and `Mapper` contracts it is configured to use. A vulnerability in any one of these components can compromise the entire system.
|
|
22
|
+
|
|
23
|
+
- **Policy Trust:** Malicious or poorly written policies can introduce vulnerabilities. Only install trusted, audited policies.
|
|
24
|
+
- **`postRun` State Changes:** The `postRun` function on a policy can modify state. This is a powerful feature that could be used for malicious purposes if the policy is not trustworthy (e.g., draining funds, changing ownership).
|
|
25
|
+
- **Extractor/Mapper Trust:** The `PolicyEngine` relies on `Extractors` to correctly and honestly parse transaction data. If an extractor is compromised or misrepresents data, policies may make decisions based on false information, potentially leading to a bypass. For example, an extractor could lie about the `value` of a transfer to circumvent a `VolumePolicy`.
|
|
26
|
+
|
|
27
|
+
## 4. External Call Risks in Policy Logic
|
|
28
|
+
|
|
29
|
+
Policies that make external calls during execution can introduce certain risks, though the specific risks depend on whether the policy functions are `view` or state-changing.
|
|
30
|
+
|
|
31
|
+
- **External Calls in Policies:** Many policies make external calls during their `run()` function execution. Examples include:
|
|
32
|
+
- Data source queries (e.g., `SanctionsPolicy` calling `dataRegistry.getDataSource()` and `sanctionsList.isSanctioned()`)
|
|
33
|
+
- Identity verification (e.g., `CredentialRegistryIdentityValidatorPolicy` calling multiple registry contracts)
|
|
34
|
+
- Price feeds, oracles, or other external data sources
|
|
35
|
+
- **Risks for `view` Policy Functions:** Since most policy `run()` functions are `view` (read-only), traditional reentrancy attacks are **not possible** because no state can be modified. However, other risks exist:
|
|
36
|
+
- **Denial of Service** - malicious external contracts could revert or consume excessive gas
|
|
37
|
+
- **Inconsistent reads** - external contract state could change between multiple calls within the same policy
|
|
38
|
+
- **Gas exhaustion** - deep call chains could cause transaction failures
|
|
39
|
+
- **Risks for State-Changing Policy Functions:** Policies with non-view functions (like `postRun()`) could be vulnerable to traditional reentrancy if they make external calls and modify state
|
|
40
|
+
- **Architectural Considerations:** Since protected contracts are **decoupled** from policy logic and policies can be changed dynamically:
|
|
41
|
+
- **Protected contract developers** cannot predict which policies will be applied to their functions
|
|
42
|
+
- **Policy administrators** bear the responsibility for ensuring safe policy combinations
|
|
43
|
+
- **External call risks** should be considered at the policy composition level, not forced onto every protected function
|
|
44
|
+
- **Mitigation Strategies:**
|
|
45
|
+
- **Use trusted external contracts** - only interact with well-established, audited external contracts in policy logic to minimize DoS and manipulation risks
|
|
46
|
+
- **Implement proper error handling** - ensure policies gracefully handle external contract failures rather than causing transaction reverts
|
|
47
|
+
- **Consider gas limits** - external calls in policies consume gas and could cause transaction failures if gas limits are exceeded
|
|
48
|
+
- **For non-view policies** - if a policy's `postRun()` or other functions modify state and make external calls, consider reentrancy protection
|
|
49
|
+
- **Policy administrators should assess risk** - when adding policies with external calls, evaluate the trustworthiness of the external contracts being called
|
|
50
|
+
|
|
51
|
+
## 5. `context` Handling and Race Conditions
|
|
52
|
+
|
|
53
|
+
The `context` field in the `PolicyEngine.Payload` is a powerful feature for passing arbitrary data, but it must be managed carefully to avoid race conditions and incorrect usage.
|
|
54
|
+
|
|
55
|
+
- **Context is Per-Sender, Not Per-Transaction:** The standard implementation of `PolicyProtected` stores context in a mapping (`sender` => `context`). It is **not** automatically cleared after every transaction. If context is set but not consumed in the same transaction by a protected method, the stale context may be incorrectly reused by a subsequent call from the same sender.
|
|
56
|
+
- **Atomic Operations Recommended:** To mitigate this, it is **strongly recommended** to set and consume `context` within the same atomic transaction. The caller should first call `setContext(bytes)` and then immediately call the protected function. The `runPolicy` modifier will then consume the context and clear it.
|
|
57
|
+
- **Re-entrancy & Multi-User Scenarios:** In contracts that can be used by multiple users (like relayers or governance contracts), context from one user could potentially be overwritten by another before it is consumed. Implementations must ensure that context cannot be mismatched, especially in scenarios involving re-entrancy.
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// SPDX-License-Identifier: BUSL-1.1
|
|
2
|
+
pragma solidity 0.8.26;
|
|
3
|
+
|
|
4
|
+
import {IPolicy} from "../interfaces/IPolicy.sol";
|
|
5
|
+
import {IPolicyEngine} from "../interfaces/IPolicyEngine.sol";
|
|
6
|
+
import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
|
|
7
|
+
import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
|
|
8
|
+
import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
|
|
9
|
+
import {ERC165Upgradeable} from "@openzeppelin/contracts-upgradeable/utils/introspection/ERC165Upgradeable.sol";
|
|
10
|
+
|
|
11
|
+
abstract contract Policy is Initializable, OwnableUpgradeable, ERC165Upgradeable, IPolicy {
|
|
12
|
+
error Unauthorized();
|
|
13
|
+
|
|
14
|
+
/// @custom:storage-location erc7201:policy-management.Policy
|
|
15
|
+
struct PolicyStorage {
|
|
16
|
+
address policyEngine;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// keccak256(abi.encode(uint256(keccak256("policy-management.Policy")) - 1)) &
|
|
20
|
+
// ~bytes32(uint256(0xff))
|
|
21
|
+
// solhint-disable-next-line const-name-snakecase
|
|
22
|
+
bytes32 private constant PolicyStorageLocation = 0xe4b9805cdebef99d9d38a3fed61079cbd4f3c0d610c4396bdc28a2ed8ad07100;
|
|
23
|
+
|
|
24
|
+
function _getPolicyStorage() private pure returns (PolicyStorage storage $) {
|
|
25
|
+
// solhint-disable-next-line no-inline-assembly
|
|
26
|
+
assembly {
|
|
27
|
+
$.slot := PolicyStorageLocation
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
constructor() {
|
|
32
|
+
_disableInitializers();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
modifier onlyPolicyEngine() {
|
|
36
|
+
if (msg.sender != _getPolicyStorage().policyEngine) {
|
|
37
|
+
revert Unauthorized();
|
|
38
|
+
}
|
|
39
|
+
_;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @notice Initializes the policy contract.
|
|
44
|
+
* @dev This function MUST be called immediately after deployment (or via proxy initializer),
|
|
45
|
+
* setting up the policy with the address of the policy engine, initial ownership, and
|
|
46
|
+
* any configuration parameters required by the policy.
|
|
47
|
+
* @param policyEngine The address of the policy engine that will invoke this policy.
|
|
48
|
+
* @param initialOwner The address that will be assigned as the initial owner of the policy contract.
|
|
49
|
+
* @param configParams Arbitrary encoded configuration parameters to initialize the policy logic.
|
|
50
|
+
*/
|
|
51
|
+
function initialize(
|
|
52
|
+
address policyEngine,
|
|
53
|
+
address initialOwner,
|
|
54
|
+
bytes calldata configParams
|
|
55
|
+
)
|
|
56
|
+
public
|
|
57
|
+
virtual
|
|
58
|
+
initializer
|
|
59
|
+
{
|
|
60
|
+
__Policy_init(policyEngine, initialOwner);
|
|
61
|
+
configure(configParams);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// solhint-disable-next-line no-empty-blocks
|
|
65
|
+
function configure(bytes calldata parameters) internal virtual onlyInitializing {}
|
|
66
|
+
|
|
67
|
+
function __Policy_init(address policyEngine, address initialOwner) internal onlyInitializing {
|
|
68
|
+
__Policy_init_unchained(policyEngine);
|
|
69
|
+
__Ownable_init(initialOwner);
|
|
70
|
+
__ERC165_init();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function __Policy_init_unchained(address policyEngine) internal onlyInitializing {
|
|
74
|
+
_getPolicyStorage().policyEngine = policyEngine;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/// @inheritdoc IPolicy
|
|
78
|
+
// solhint-disable-next-line no-empty-blocks
|
|
79
|
+
function onInstall(bytes4 /*selector*/ ) public virtual override onlyPolicyEngine {}
|
|
80
|
+
|
|
81
|
+
/// @inheritdoc IPolicy
|
|
82
|
+
// solhint-disable-next-line no-empty-blocks
|
|
83
|
+
function onUninstall(bytes4 /*selector*/ ) public virtual override onlyPolicyEngine {}
|
|
84
|
+
|
|
85
|
+
function run(
|
|
86
|
+
address caller,
|
|
87
|
+
address subject,
|
|
88
|
+
bytes4 selector,
|
|
89
|
+
bytes[] calldata parameters,
|
|
90
|
+
bytes calldata context
|
|
91
|
+
)
|
|
92
|
+
public
|
|
93
|
+
view
|
|
94
|
+
virtual
|
|
95
|
+
override
|
|
96
|
+
returns (IPolicyEngine.PolicyResult);
|
|
97
|
+
|
|
98
|
+
function postRun(
|
|
99
|
+
address, /*caller*/
|
|
100
|
+
address, /*subject*/
|
|
101
|
+
bytes4, /*selector*/
|
|
102
|
+
bytes[] calldata, /*parameters*/
|
|
103
|
+
bytes calldata /*context*/
|
|
104
|
+
)
|
|
105
|
+
public
|
|
106
|
+
virtual
|
|
107
|
+
override
|
|
108
|
+
onlyPolicyEngine
|
|
109
|
+
// solhint-disable-next-line no-empty-blocks
|
|
110
|
+
{}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* @dev See {ERC165-supportsInterface}.
|
|
114
|
+
*/
|
|
115
|
+
function supportsInterface(bytes4 interfaceId)
|
|
116
|
+
public
|
|
117
|
+
view
|
|
118
|
+
virtual
|
|
119
|
+
override(ERC165Upgradeable, IERC165)
|
|
120
|
+
returns (bool)
|
|
121
|
+
{
|
|
122
|
+
return interfaceId == type(IPolicy).interfaceId || super.supportsInterface(interfaceId);
|
|
123
|
+
}
|
|
124
|
+
}
|