@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.
Files changed (150) hide show
  1. package/.foundry-version +1 -0
  2. package/.github/CODEOWNERS +1 -0
  3. package/.github/workflows/auto-release-version.yml +107 -0
  4. package/.github/workflows/create-version-pr.yml +95 -0
  5. package/.github/workflows/forge-docs.yml +90 -0
  6. package/.github/workflows/forge-test.yml +59 -0
  7. package/.solhint-test.json +18 -0
  8. package/.solhint.json +16 -0
  9. package/.solhintignore +3 -0
  10. package/.solhintignore-test +2 -0
  11. package/Glossary.md +141 -0
  12. package/LICENSE +59 -0
  13. package/README.md +218 -0
  14. package/assets/chainlink-logo.svg +21 -0
  15. package/chainlink-ace-License-grants +2 -0
  16. package/foundry.toml +33 -0
  17. package/getting_started/GETTING_STARTED.md +477 -0
  18. package/getting_started/MyVault.sol +48 -0
  19. package/getting_started/advanced/.env.example +36 -0
  20. package/getting_started/advanced/GETTING_STARTED_ADVANCED.md +431 -0
  21. package/getting_started/advanced/SanctionsList.sol +25 -0
  22. package/getting_started/advanced/SanctionsPolicy.sol +58 -0
  23. package/package.json +41 -0
  24. package/packages/cross-chain-identity/README.md +148 -0
  25. package/packages/cross-chain-identity/docs/API_GUIDE.md +120 -0
  26. package/packages/cross-chain-identity/docs/API_REFERENCE.md +271 -0
  27. package/packages/cross-chain-identity/docs/CONCEPTS.md +253 -0
  28. package/packages/cross-chain-identity/docs/CREDENTIAL_FLOW.md +195 -0
  29. package/packages/cross-chain-identity/docs/SECURITY.md +70 -0
  30. package/packages/cross-chain-identity/src/CredentialRegistry.sol +245 -0
  31. package/packages/cross-chain-identity/src/CredentialRegistryIdentityValidator.sol +339 -0
  32. package/packages/cross-chain-identity/src/CredentialRegistryIdentityValidatorPolicy.sol +71 -0
  33. package/packages/cross-chain-identity/src/IdentityRegistry.sol +123 -0
  34. package/packages/cross-chain-identity/src/TrustedIssuerRegistry.sol +140 -0
  35. package/packages/cross-chain-identity/src/interfaces/ICredentialDataValidator.sol +30 -0
  36. package/packages/cross-chain-identity/src/interfaces/ICredentialRegistry.sol +170 -0
  37. package/packages/cross-chain-identity/src/interfaces/ICredentialRequirements.sol +192 -0
  38. package/packages/cross-chain-identity/src/interfaces/ICredentialValidator.sol +37 -0
  39. package/packages/cross-chain-identity/src/interfaces/IIdentityRegistry.sol +85 -0
  40. package/packages/cross-chain-identity/src/interfaces/IIdentityValidator.sol +18 -0
  41. package/packages/cross-chain-identity/src/interfaces/ITrustedIssuerRegistry.sol +61 -0
  42. package/packages/cross-chain-identity/test/CredentialRegistry.t.sol +220 -0
  43. package/packages/cross-chain-identity/test/CredentialRegistryIdentityValidator.t.sol +554 -0
  44. package/packages/cross-chain-identity/test/CredentialRegistryIdentityValidatorPolicy.t.sol +114 -0
  45. package/packages/cross-chain-identity/test/IdentityRegistry.t.sol +106 -0
  46. package/packages/cross-chain-identity/test/IdentityValidator.t.sol +969 -0
  47. package/packages/cross-chain-identity/test/TrustedIssuerRegistry.t.sol +123 -0
  48. package/packages/cross-chain-identity/test/helpers/BaseProxyTest.sol +112 -0
  49. package/packages/cross-chain-identity/test/helpers/MockCredentialDataValidator.sol +26 -0
  50. package/packages/cross-chain-identity/test/helpers/MockCredentialRegistryReverting.sol +131 -0
  51. package/packages/policy-management/README.md +197 -0
  52. package/packages/policy-management/docs/API_GUIDE.md +290 -0
  53. package/packages/policy-management/docs/API_REFERENCE.md +173 -0
  54. package/packages/policy-management/docs/CONCEPTS.md +156 -0
  55. package/packages/policy-management/docs/CUSTOM_POLICIES_TUTORIAL.md +195 -0
  56. package/packages/policy-management/docs/POLICY_ORDERING_GUIDE.md +91 -0
  57. package/packages/policy-management/docs/SECURITY.md +57 -0
  58. package/packages/policy-management/src/core/Policy.sol +124 -0
  59. package/packages/policy-management/src/core/PolicyEngine.sol +382 -0
  60. package/packages/policy-management/src/core/PolicyFactory.sol +92 -0
  61. package/packages/policy-management/src/core/PolicyProtected.sol +126 -0
  62. package/packages/policy-management/src/extractors/ComplianceTokenForceTransferExtractor.sol +57 -0
  63. package/packages/policy-management/src/extractors/ComplianceTokenFreezeUnfreezeExtractor.sol +54 -0
  64. package/packages/policy-management/src/extractors/ComplianceTokenMintBurnExtractor.sol +61 -0
  65. package/packages/policy-management/src/extractors/ERC20ApproveExtractor.sol +57 -0
  66. package/packages/policy-management/src/extractors/ERC20TransferExtractor.sol +62 -0
  67. package/packages/policy-management/src/extractors/ERC3643ForcedTransferExtractor.sol +56 -0
  68. package/packages/policy-management/src/extractors/ERC3643FreezeUnfreezeExtractor.sol +55 -0
  69. package/packages/policy-management/src/extractors/ERC3643MintBurnExtractor.sol +51 -0
  70. package/packages/policy-management/src/extractors/ERC3643SetAddressFrozenExtractor.sol +51 -0
  71. package/packages/policy-management/src/interfaces/IExtractor.sol +17 -0
  72. package/packages/policy-management/src/interfaces/IMapper.sol +17 -0
  73. package/packages/policy-management/src/interfaces/IPolicy.sol +61 -0
  74. package/packages/policy-management/src/interfaces/IPolicyEngine.sol +264 -0
  75. package/packages/policy-management/src/interfaces/IPolicyProtected.sol +48 -0
  76. package/packages/policy-management/src/policies/AllowPolicy.sol +104 -0
  77. package/packages/policy-management/src/policies/BypassPolicy.sol +90 -0
  78. package/packages/policy-management/src/policies/IntervalPolicy.sol +223 -0
  79. package/packages/policy-management/src/policies/MaxPolicy.sol +73 -0
  80. package/packages/policy-management/src/policies/OnlyAuthorizedSenderPolicy.sol +84 -0
  81. package/packages/policy-management/src/policies/OnlyOwnerPolicy.sol +35 -0
  82. package/packages/policy-management/src/policies/PausePolicy.sol +82 -0
  83. package/packages/policy-management/src/policies/README.md +632 -0
  84. package/packages/policy-management/src/policies/RejectPolicy.sol +89 -0
  85. package/packages/policy-management/src/policies/RoleBasedAccessControlPolicy.sol +162 -0
  86. package/packages/policy-management/src/policies/SecureMintPolicy.sol +271 -0
  87. package/packages/policy-management/src/policies/VolumePolicy.sol +133 -0
  88. package/packages/policy-management/src/policies/VolumeRatePolicy.sol +192 -0
  89. package/packages/policy-management/test/PolicyEngine.t.sol +368 -0
  90. package/packages/policy-management/test/PolicyFactory.t.sol +114 -0
  91. package/packages/policy-management/test/PolicyProtectedToken.t.sol +75 -0
  92. package/packages/policy-management/test/extractors/ComplianceTokenForceTransferExtractor.t.sol +59 -0
  93. package/packages/policy-management/test/extractors/ComplianceTokenFreezeUnfreezeExtractor.t.sol +74 -0
  94. package/packages/policy-management/test/extractors/ComplianceTokenMintBurnExtractor.t.sol +92 -0
  95. package/packages/policy-management/test/extractors/ERC20ApproveExtractor.t.sol +58 -0
  96. package/packages/policy-management/test/extractors/ERC3643ForcedTransferExtractor.t.sol +59 -0
  97. package/packages/policy-management/test/extractors/ERC3643FreezeUnfreezeExtractor.t.sol +74 -0
  98. package/packages/policy-management/test/extractors/ERC3643MintBurnExtractor.t.sol +73 -0
  99. package/packages/policy-management/test/extractors/ERC3643SetAddressFrozenExtractor.t.sol +56 -0
  100. package/packages/policy-management/test/helpers/BaseProxyTest.sol +75 -0
  101. package/packages/policy-management/test/helpers/CustomMapper.sol +26 -0
  102. package/packages/policy-management/test/helpers/DummyExtractor.sol +11 -0
  103. package/packages/policy-management/test/helpers/ExpectedParameterPolicy.sol +39 -0
  104. package/packages/policy-management/test/helpers/MockAggregatorV3.sol +51 -0
  105. package/packages/policy-management/test/helpers/MockToken.sol +66 -0
  106. package/packages/policy-management/test/helpers/MockTokenExtractor.sol +34 -0
  107. package/packages/policy-management/test/helpers/PolicyAlwaysAllowed.sol +45 -0
  108. package/packages/policy-management/test/helpers/PolicyAlwaysContinue.sol +23 -0
  109. package/packages/policy-management/test/helpers/PolicyAlwaysRejected.sol +23 -0
  110. package/packages/policy-management/test/helpers/PolicyFailingRun.sol +22 -0
  111. package/packages/policy-management/test/policies/AllowPolicy.t.sol +174 -0
  112. package/packages/policy-management/test/policies/BypassPolicy.t.sol +159 -0
  113. package/packages/policy-management/test/policies/IntervalPolicy.t.sol +307 -0
  114. package/packages/policy-management/test/policies/MaxPolicy.t.sol +54 -0
  115. package/packages/policy-management/test/policies/OnlyAuthorizedSenderPolicy.t.sol +95 -0
  116. package/packages/policy-management/test/policies/OnlyOwnerPolicy.t.sol +47 -0
  117. package/packages/policy-management/test/policies/PausePolicy.t.sol +75 -0
  118. package/packages/policy-management/test/policies/RejectPolicy.t.sol +182 -0
  119. package/packages/policy-management/test/policies/RoleBasedAccessControlPolicy.t.sol +223 -0
  120. package/packages/policy-management/test/policies/SecureMintPolicy.t.sol +442 -0
  121. package/packages/policy-management/test/policies/VolumePolicy.t.sol +158 -0
  122. package/packages/policy-management/test/policies/VolumeRatePolicy.t.sol +165 -0
  123. package/packages/tokens/erc-20/src/ComplianceTokenERC20.sol +345 -0
  124. package/packages/tokens/erc-20/src/ComplianceTokenStoreERC20.sol +29 -0
  125. package/packages/tokens/erc-20/test/ComplianceTokenERC20.t.sol +556 -0
  126. package/packages/tokens/erc-20/test/helpers/BaseProxyTest.sol +75 -0
  127. package/packages/tokens/erc-3643/README.md +24 -0
  128. package/packages/tokens/erc-3643/src/ComplianceTokenERC3643.sol +564 -0
  129. package/packages/tokens/erc-3643/src/ComplianceTokenStoreERC3643.sol +30 -0
  130. package/packages/tokens/erc-3643/test/ComplianceTokenERC3643.t.sol +815 -0
  131. package/packages/tokens/erc-3643/test/helpers/BaseProxyTest.sol +76 -0
  132. package/packages/tokens/erc-3643/test/helpers/ExpectedContextPolicy.sol +32 -0
  133. package/packages/vendor/erc-3643/compliance/modular/IModularCompliance.sol +220 -0
  134. package/packages/vendor/erc-3643/registry/interface/IClaimTopicsRegistry.sol +101 -0
  135. package/packages/vendor/erc-3643/registry/interface/IIdentityRegistry.sol +251 -0
  136. package/packages/vendor/erc-3643/registry/interface/IIdentityRegistryStorage.sol +191 -0
  137. package/packages/vendor/erc-3643/registry/interface/ITrustedIssuersRegistry.sol +161 -0
  138. package/packages/vendor/erc-3643/token/IToken.sol +457 -0
  139. package/packages/vendor/onchain-id/interface/IClaimIssuer.sol +53 -0
  140. package/packages/vendor/onchain-id/interface/IERC734.sol +110 -0
  141. package/packages/vendor/onchain-id/interface/IERC735.sol +105 -0
  142. package/packages/vendor/onchain-id/interface/IIdentity.sol +26 -0
  143. package/packages/vendor/onchain-id/interface/IImplementationAuthority.sol +21 -0
  144. package/remappings.txt +6 -0
  145. package/script/DeployComplianceTokenERC20.s.sol +191 -0
  146. package/script/DeployComplianceTokenERC3643.s.sol +208 -0
  147. package/script/DeploySimpleComplianceToken.s.sol +38 -0
  148. package/script/getting_started/DeployGettingStarted.s.sol +74 -0
  149. package/script/getting_started/advanced/DeployAdvancedGettingStarted.s.sol +332 -0
  150. package/script/getting_started/advanced/DeploySanctionsList.s.sol +26 -0
@@ -0,0 +1,431 @@
1
+ # Advanced Getting Started Guide
2
+
3
+ > **Prerequisites:** This guide builds on the [Getting Started Guide](../GETTING_STARTED.md), which teaches the core ACE integration pattern using Policy Management. **If you're new to ACE, start there first** to understand how `PolicyProtected`, `PolicyEngine`, and policies work together. This advanced guide adds Cross-Chain Identity for credential verification (KYC, sanctions, etc.).
4
+
5
+ ## The Scenario: Lifecycle of a Tokenized Money Market Fund
6
+
7
+ In this guide, you'll take on distinct roles to deploy and interact with a complete, realistic compliance system for a **Tokenized Money Market Fund (MMF)**. This is a common real-world asset that requires robust compliance at every step.
8
+
9
+ By the end, you'll have learned how to use the core components of this toolkit to solve real-world problems.
10
+
11
+ **The Roles You'll Play:**
12
+
13
+ - **The Fund Manager:** The owner and administrator of the MMF contract. Your job is to mint the initial supply of shares, configure the compliance system, and distribute shares to verified investors.
14
+ - **The Sanctions Provider:** An independent entity that maintains their own sanctions list. You'll deploy and manage the list of sanctioned addresses.
15
+ - **The Credential Issuer:** A trusted, third-party entity responsible for performing offchain KYC checks and registering investors' identities and credentials onchain.
16
+ - **The Investors:** Alice and Charlie, who want to receive shares in the MMF.
17
+
18
+ **The Compliance Rules We'll Enforce:**
19
+
20
+ 1. Only the **Fund Manager** can mint new MMF shares.
21
+ 2. Shares can **only be transferred to investors who have passed KYC**.
22
+ 3. Shares **cannot be transferred to anyone on a sanctions list**.
23
+
24
+ This tutorial will show you how to enforce these rules dynamically using Cross-Chain Identity and Policy Management, without hard-coding them into the MMF contract itself.
25
+
26
+ ## How the Components Work Together
27
+
28
+ Let's take 5 minutes to understand some fundamental concepts of this modular toolkit.
29
+
30
+ ### 🔗 Cross-Chain Identity Infrastructure
31
+
32
+ A **Cross-Chain Identifier (CCID)** sits at the centre of the identity model.
33
+
34
+ - **CCID (`bytes32`)** – One identifier per user that is recognised across _all_ EVM chains.
35
+ - **`IdentityRegistry`** – Maps each onchain address to its CCID (`address → CCID`). A single CCID can have many addresses (e.g., the same user on Ethereum, Polygon, and a private chain).
36
+ - **`CredentialRegistry`** – Stores credentials keyed by CCID. Any address that resolves to that CCID automatically "inherits" the credential (e.g., KYC, AML).
37
+
38
+ ```mermaid
39
+ graph LR
40
+ subgraph "Identity"
41
+ Address[Onchain Address] --> IR[IdentityRegistry]
42
+ IR --> CCID
43
+ end
44
+ subgraph "Credentials"
45
+ CCID --> CredReg[CredentialRegistry]
46
+ end
47
+ ```
48
+
49
+ **Learn more** → [CCID deep dive](../../packages/cross-chain-identity/docs/CONCEPTS.md#cross-chain-identifier-ccid)
50
+
51
+ _**Contracts Deployed in this Example:** `IdentityRegistry.sol`, `CredentialRegistry.sol`_
52
+
53
+ ### 🛡️ Policy Management Infrastructure
54
+
55
+ A **Policy Engine** lets you compose multiple rules without touching the core contract code. The engine is connected to your application via a `PolicyProtected` contract.
56
+
57
+ - **`PolicyProtected` Contract**: Your application contract inherits from `PolicyProtected`. You then add the `runPolicy` modifier to any function you want to secure. This modifier is the "hook" that tells your contract to ask the `PolicyEngine` for permission before proceeding.
58
+ - **`PolicyEngine`**: Executes a chain of policies for any protected function. An administrator registers policies against a specific function signature (selector), creating a flexible ruleset.
59
+ - **Policies**: Self-contained contracts that hold a single rule (e.g., `OnlyOwnerPolicy`, `VolumePolicy`). Each returns `Allowed`, `Continue`, or reverts with a `PolicyRejected` error.
60
+ - **Extractors**: Helper contracts that parse transaction data for the policies.
61
+
62
+ The `PolicyEngine` executes policies **in the order they are added**. The first definitive result (`Allowed` return or `PolicyRejected` revert) decides the outcome. If all policies return `Continue`, the engine's pre-configured default behavior (allow or reject based on the boolean `defaultAllow` parameter) is applied. Therefore, policy ordering is critical.
63
+
64
+ ```mermaid
65
+ sequenceDiagram
66
+ participant User
67
+ participant PolicyProtectedContract as Policy-Protected Contract
68
+ participant PolicyEngine as Policy Engine
69
+ participant Policy1 as Policy 1
70
+ participant Policy2 as Policy 2
71
+
72
+ User->>PolicyProtectedContract: 1. Calls a protected function()
73
+ PolicyProtectedContract->>PolicyEngine: 2. Asks "Is this call allowed?"
74
+ Note over PolicyProtectedContract: The `runPolicy` modifier triggers this.
75
+
76
+ PolicyEngine->>Policy1: 3. run()
77
+ Policy1-->>PolicyEngine: 4. Continue
78
+ PolicyEngine->>Policy2: 5. run()
79
+
80
+ alt Policy 2 Rejects
81
+ Policy2-->>PolicyEngine: 6a. REVERT
82
+ PolicyEngine-->>PolicyProtectedContract: 7a. REVERT
83
+ else Policy 2 Allows
84
+ Policy2-->>PolicyEngine: 6b. Allow
85
+ PolicyEngine-->>PolicyProtectedContract: 7b. SUCCESS
86
+ else Policy 2 Continues (and no more policies)
87
+ Policy2-->>PolicyEngine: 6c. Continue
88
+ PolicyEngine-->>PolicyProtectedContract: 7c. Engine's Default (allow or reject based on config)
89
+ end
90
+
91
+ Note over PolicyProtectedContract: User's transaction either succeeds or fails based on the final outcome.
92
+ ```
93
+
94
+ **Learn more** → [Policy flow](../../packages/policy-management/docs/CONCEPTS.md#the-policy-flow-a-step-by-step-breakdown)
95
+
96
+ _**Contracts Deployed in this Example:** `PolicyEngine.sol`, `OnlyOwnerPolicy.sol`, `OnlyAuthorizedSenderPolicy.sol`, `CredentialRegistryIdentityValidatorPolicy.sol`, `SanctionsPolicy.sol`_
97
+
98
+ ### Architecture of this Example
99
+
100
+ This diagram shows how the deployed contracts are wired together for our Tokenized Money Market Fund.
101
+
102
+ ```mermaid
103
+ graph TD
104
+ subgraph "Application"
105
+ Token["MMF Token"]
106
+ end
107
+
108
+ subgraph "Policy Management"
109
+ Engine["PolicyEngine"]
110
+ IVP["CredentialRegistryIdentityValidatorPolicy"]
111
+ OAP["OnlyAuthorizedSenderPolicy"]
112
+ SP["SanctionsPolicy"]
113
+ OOP["OnlyOwnerPolicy"]
114
+ end
115
+
116
+ subgraph "Cross-Chain Identity"
117
+ IR["IdentityRegistry"]
118
+ CR["CredentialRegistry"]
119
+ end
120
+
121
+ subgraph "External Data"
122
+ SL["SanctionsList"]
123
+ end
124
+
125
+ Token -->|uses| Engine
126
+
127
+ Engine -->|"calls"| IVP
128
+ Engine -->|"calls"| OAP
129
+ Engine -->|"calls"| SP
130
+ Engine -->|"calls"| OOP
131
+
132
+ IVP -->|reads from| IR
133
+ IVP -->|reads from| CR
134
+ SP -.->|reads from| SL
135
+ ```
136
+
137
+ **The Magic**: These components work together automatically. When someone tries to transfer tokens, the system:
138
+
139
+ 1. **Identifies** who they are and checks their KYC status (**Cross-Chain Identity**).
140
+ 2. **Enforces** the rules and allows or blocks the transaction (**Policy Management**).
141
+
142
+ ## Prerequisites
143
+
144
+ - [Node.js](https://nodejs.org/en/download/) (v18 or later)
145
+ - [Foundry](https://book.getfoundry.sh/getting-started/installation) (v0.3.0 or later)
146
+ - [pnpm](https://pnpm.io/installation)
147
+
148
+ ## Setup
149
+
150
+ 1. **Clone and enter the repository:**
151
+
152
+ ```bash
153
+ git clone https://github.com/smartcontractkit/chainlink-ace.git
154
+ cd chainlink-ace
155
+ ```
156
+
157
+ 2. **Install dependencies:**
158
+
159
+ ```bash
160
+ pnpm install
161
+ ```
162
+
163
+ 3. **Build the project:**
164
+
165
+ ```bash
166
+ pnpm build
167
+ ```
168
+
169
+ ## Start anvil chain
170
+
171
+ From a new terminal, start a new anvil chain by running:
172
+
173
+ ```bash
174
+ anvil
175
+ ```
176
+
177
+ **Note:** Keep this terminal open - anvil runs on `http://localhost:8545` by default. Open a new terminal instance for the next steps.
178
+
179
+ ## Building Your Compliance System
180
+
181
+ ### Prerequisite: Setup your environment
182
+
183
+ For convenience, we've provided an example `.env` file that will simplify going through the rest of the guide.
184
+
185
+ 1. **Create a `.env` file:**
186
+ Copy the example environment file to a new `.env` file in the root of the repository.
187
+
188
+ ```bash
189
+ cp getting_started/advanced/.env.example .env
190
+ ```
191
+
192
+ > **Note:** The addresses in the `.env.example` file are deterministic and should be correct if you are using a fresh Anvil instance. As a best practice, we'll double-check that the addresses in your `.env` file match the contract addresses logged in your terminal after running the deployment scripts in the next steps.
193
+
194
+ 2. **Load the environment variables:**
195
+ Run the following command to export all the variables into your current terminal session. You'll need to do this for every new terminal you open.
196
+
197
+ ```bash
198
+ source .env
199
+ ```
200
+
201
+ ### Prerequisite: Deploy a Sanctions List as a Sanctions Provider
202
+
203
+ In a realistic scenario, a Sanctions Provider maintains its own sanctions list. They own the `SanctionsList` contract independently, and the Fund Manager's compliance system simply references it.
204
+
205
+ **As the Sanctions Provider**, deploy a [sanctions list](./SanctionsList.sol):
206
+
207
+ ```bash
208
+ export ETH_RPC_URL=http://localhost:8545
209
+ PRIVATE_KEY=$SANCTIONS_PROVIDER_PRIVATE_KEY forge script script/getting_started/advanced/DeploySanctionsList.s.sol:DeploySanctionsList \
210
+ --rpc-url $ETH_RPC_URL \
211
+ --broadcast
212
+ ```
213
+
214
+ **Expected output:** The script will log the deployed address:
215
+
216
+ ```
217
+ SanctionsList deployed at: 0x...
218
+ ```
219
+
220
+ > **Note:** If you're using a fresh Anvil instance, this address should match the `SANCTIONS_LIST_ADDRESS` in your `.env.example` file (deterministic deployment). If the addresses don't match, update your `.env` file with the actual deployed address and run `source .env` again.
221
+
222
+ The Fund Manager's deployment script will configure the `SanctionsPolicy` to point to this address.
223
+
224
+ ### Deploy Your Compliance System
225
+
226
+ Now, let's build the Tokenized MMF. We'll use a [deployment script](../../script/getting_started/advanced/DeployAdvancedGettingStarted.s.sol) specifically created for this more comprehensive scenario.
227
+
228
+ This single command will deploy all the necessary contracts as the Fund Manager, including the MMF token, the Policy Engine, the registries, and all the required policies. The script will configure the system to use the Sanctions List deployed by the Sanctions Provider in the prerequisite step above.
229
+
230
+ ```bash
231
+ PRIVATE_KEY=$FUND_MANAGER_PRIVATE_KEY forge script script/getting_started/advanced/DeployAdvancedGettingStarted.s.sol:DeployAdvancedGettingStarted \
232
+ --rpc-url $ETH_RPC_URL \
233
+ --broadcast
234
+ ```
235
+
236
+ The script will output the deployed contract addresses at the **beginning** of the console logs. Verify that the addresses set in your `.env` file match the addresses logged in your terminal.
237
+
238
+ ### Understanding Contract Ownership
239
+
240
+ After deployment, here's a summary of who controls what:
241
+
242
+ | Contract | Owner | What This Means |
243
+ | ----------------------- | -------------------------------- | -------------------------------------------------------- |
244
+ | **Policy Engine** | Fund Manager | Controls which policies are attached to which functions |
245
+ | **MMF Token** | Fund Manager (via Policy Engine) | Token admin functions are protected by policies |
246
+ | **Identity Registry** | Fund Manager (via Policy Engine) | Registration requires authorized sender policy |
247
+ | **Credential Registry** | Fund Manager (via Policy Engine) | Credential issuance requires authorized sender policy |
248
+ | **Sanctions List** | Sanctions Provider | Independently deployed and managed by the provider |
249
+ | **All Policies** | Fund Manager | Can configure policy parameters (pause, authorize, etc.) |
250
+
251
+ **Delegated Access:**
252
+
253
+ - **Credential Issuer**: Authorized by Fund Manager to call registry functions (doesn't own contracts)
254
+
255
+ **Key Insight:** The Policy Engine architecture lets the Fund Manager control the _compliance framework_ (what rules exist) while delegating _operational duties_ (KYC checks, sanctions updates) to specialized actors.
256
+
257
+ ## Testing the Compliance System: A Step-by-Step Walkthrough
258
+
259
+ Now, let's test the system by taking on our different roles.
260
+
261
+ ### Step 1 (as Fund Manager): Mint Initial Shares
262
+
263
+ Only the `Fund Manager` (or an authorized minter) can create new shares. Let's mint 1,000,000 MMF shares to the manager's address.
264
+
265
+ ```bash
266
+ cast send $TOKEN_ADDRESS "mint(address,uint256)" $FUND_MANAGER_ADDRESS 1000000000000000000000000 --private-key $FUND_MANAGER_PRIVATE_KEY
267
+ ```
268
+
269
+ **Why this works:** The `mint` function is protected by an `OnlyAuthorizedSenderPolicy`. Our script authorizes the `Fund Manager` as a minter by default, so the Policy Engine allows the transaction.
270
+
271
+ **Let's verify the policy is enforcing:** Now try minting with an unauthorized address (Alice):
272
+
273
+ ```bash
274
+ cast send $TOKEN_ADDRESS "mint(address,uint256)" $ALICE_ADDRESS 1000000000000000000000000 --private-key $ALICE_PRIVATE_KEY
275
+ ```
276
+
277
+ **Expected:** `Error: PolicyRunRejected` with reason `"sender is not authorized"`
278
+
279
+ This demonstrates that the policy is actively protecting the `mint` function. Only authorized minters can create new shares.
280
+
281
+ ### Step 2 (as Fund Manager): Authorize the Credential Issuer
282
+
283
+ The `IdentityRegistry` and `CredentialRegistry` are protected by the `identityOnlyAuthorizedSenderPolicy`. To allow our trusted issuer to register users, the `Fund Manager` must add the `CREDENTIAL_ISSUER_ADDRESS` to that policy's authorized list.
284
+
285
+ ```bash
286
+ cast send $IDENTITY_ONLY_AUTHORIZED_POLICY_ADDRESS "authorizeSender(address)" $CREDENTIAL_ISSUER_ADDRESS --private-key $FUND_MANAGER_PRIVATE_KEY
287
+ ```
288
+
289
+ ### Step 3 (as Credential Issuer): Onboard Investors
290
+
291
+ Now that the Credential Issuer is authorized by the policy, it can call the registry contracts to onboard Alice and Charlie.
292
+
293
+ ```bash
294
+ # Register Alice's Identity
295
+ cast send $IDENTITY_REGISTRY_ADDRESS "registerIdentity(bytes32,address,bytes)" $ALICE_CCID $ALICE_ADDRESS 0x --private-key $CREDENTIAL_ISSUER_PRIVATE_KEY
296
+
297
+ # Grant Alice KYC
298
+ cast send $CREDENTIAL_REGISTRY_ADDRESS "registerCredential(bytes32,bytes32,uint40,bytes,bytes)" $ALICE_CCID $KYC_CREDENTIAL 0 0x 0x --private-key $CREDENTIAL_ISSUER_PRIVATE_KEY
299
+
300
+ # Register Charlie's Identity
301
+ cast send $IDENTITY_REGISTRY_ADDRESS "registerIdentity(bytes32,address,bytes)" $CHARLIE_CCID $CHARLIE_ADDRESS 0x --private-key $CREDENTIAL_ISSUER_PRIVATE_KEY
302
+
303
+ # Grant Charlie KYC
304
+ cast send $CREDENTIAL_REGISTRY_ADDRESS "registerCredential(bytes32,bytes32,uint40,bytes,bytes)" $CHARLIE_CCID $KYC_CREDENTIAL 0 0x 0x --private-key $CREDENTIAL_ISSUER_PRIVATE_KEY
305
+ ```
306
+
307
+ **Why this works:** The transactions succeed because they are sent from an address (`CREDENTIAL_ISSUER_ADDRESS`) that we explicitly authorized in the `identityOnlyAuthorizedSenderPolicy` in the previous step.
308
+
309
+ ### Step 4 (as Sanctions Provider): Update Sanctions Data
310
+
311
+ As the owner of the `SanctionsList` contract, you can now add Charlie's address to the sanctions list.
312
+
313
+ ```bash
314
+ cast send $SANCTIONS_LIST_ADDRESS "add(address)" $CHARLIE_ADDRESS --private-key $SANCTIONS_PROVIDER_PRIVATE_KEY
315
+ ```
316
+
317
+ ### Step 5 (as Fund Manager): Distribute Shares
318
+
319
+ Now that the system is fully configured, **as the Fund Manager**, let's attempt to distribute shares to our investors.
320
+
321
+ **A. Transfer to Alice (Should Succeed)**
322
+
323
+ The Fund Manager sends 100 shares to Alice (100 shares = 100 \* 10^18 MMF tokens):
324
+
325
+ ```bash
326
+ cast send $TOKEN_ADDRESS "transfer(address,uint256)" $ALICE_ADDRESS 100000000000000000000 --private-key $FUND_MANAGER_PRIVATE_KEY
327
+ ```
328
+
329
+ You can confirm the transfer was successful by checking Alice's balance:
330
+
331
+ ```bash
332
+ cast call $TOKEN_ADDRESS "balanceOf(address)" $ALICE_ADDRESS | cast --from-wei
333
+ ```
334
+
335
+ This should return `100`.
336
+
337
+ **Expected Result:** ✅ **SUCCESS!** The `PolicyEngine` checks the policies for the `transfer` function:
338
+
339
+ 1. The `CredentialRegistryIdentityValidatorPolicy` confirms Alice has a `KYC` credential. It returns `Continue`.
340
+ 2. The `SanctionsPolicy` checks its configured `SanctionsList` and confirms Alice is **not** on it. It also returns `Continue`.
341
+
342
+ Since no policy reverted with a `PolicyRejected` error, the engine applies its default result, which was configured to allow by default (`true`). The transaction is approved.
343
+
344
+ **B. Transfer to Charlie (Should Fail)**
345
+
346
+ Now the Fund Manager attempts to send 100 shares to Charlie:
347
+
348
+ ```bash
349
+ cast send $TOKEN_ADDRESS "transfer(address,uint256)" $CHARLIE_ADDRESS 100000000000000000000 --private-key $FUND_MANAGER_PRIVATE_KEY
350
+ ```
351
+
352
+ **Expected Result:** ❌ **REVERT!** The transaction is blocked with `Error: PolicyRunRejected` and reason `"account sanctions validation failed"`.
353
+
354
+ 1. The `CredentialRegistryIdentityValidatorPolicy` confirms Charlie has a `KYC` credential. It returns `Continue`.
355
+ 2. The `SanctionsPolicy` checks its configured `SanctionsList` and sees that Charlie's address **is** on the list. It reverts with a `PolicyRejected` error, and the `PolicyEngine` immediately reverts the entire transaction with `PolicyRunRejected`.
356
+
357
+ **C. Investor-to-Investor Transfer to Charlie (Should Also Fail)**
358
+
359
+ Let's verify that the sanctions policy works regardless of who is sending. Alice will now attempt to send 50 shares to Charlie:
360
+
361
+ ```bash
362
+ cast send $TOKEN_ADDRESS "transfer(address,uint256)" $CHARLIE_ADDRESS 50000000000000000000 --private-key $ALICE_PRIVATE_KEY
363
+ ```
364
+
365
+ **Expected Result:** ❌ **REVERT!** The transaction is blocked with `Error: PolicyRunRejected` and reason `"account sanctions validation failed"`.
366
+
367
+ The same flow occurs: the KYC check passes, but the sanctions check fails. This demonstrates that **Charlie cannot receive tokens from anyone**—neither the Fund Manager nor other investors—because the sanctions policy evaluates the recipient address, not the sender.
368
+
369
+ ### Anatomy of a Compliance Check: How the Transfer to Charlie Was Blocked
370
+
371
+ The previous steps worked because of how we wired the components together in the deployment script. Let's break down exactly what happened behind the scenes during the failed transfer to Charlie, from setup to execution.
372
+
373
+ #### The Setup (from [`DeployAdvancedGettingStarted.s.sol`](../../script/getting_started/advanced/DeployAdvancedGettingStarted.s.sol))
374
+
375
+ Three critical connections were made when you ran the deployment script:
376
+
377
+ 1. **`setExtractor`**: We told the `PolicyEngine` that for any call to the `transfer` function, it must use the `ERC20TransferExtractor`. This extractor's job is to read the raw transaction calldata and pull out the `to` address parameter.
378
+ 2. **`addPolicy`**: We added two policies to the `transfer` function's execution chain: first the `CredentialRegistryIdentityValidatorPolicy`, then the `SanctionsPolicy`. We also told the engine that both policies require the `to` parameter, which the extractor provides.
379
+ 3. **`setSanctionsList`**: We told the `SanctionsPolicy` the direct address of the `SanctionsList` contract, linking the rule to its data.
380
+
381
+ These three steps set the stage for the dynamic, real-time compliance check.
382
+
383
+ #### The Live Flow: Alice transfers to Charlie
384
+
385
+ Here is the step-by-step journey of the transaction as it moves through the system:
386
+
387
+ 1. **The Call Starts**: Your wallet calls `mmfToken.transfer(CHARLIE_ADDRESS, 100)`.
388
+ 2. **`PolicyProtected` Intercepts**: The `runPolicy` modifier on the `transfer` function immediately halts execution and passes the entire transaction payload to the `PolicyEngine`, asking, "Is this allowed?"
389
+ 3. **Engine Consults Extractor**: The `PolicyEngine` sees the call is for the `transfer` selector (`0xa9059cbb`). It looks up which extractor to use and finds our [`ERC20TransferExtractor`](../../packages/policy-management/src/extractors/ERC20TransferExtractor.sol). It invokes the extractor, which parses the `calldata` and returns an array of named parameters—in this case, `(name: "from", value: SENDER_ADDRESS)`, `(name: "to", value: CHARLIE_ADDRESS)`, and `(name: "amount", value: 100)`. This makes all aspects of the transfer available for any policy to scrutinize.
390
+ 4. **Engine Runs Policy #1 (`CredentialRegistryIdentityValidatorPolicy`)**:
391
+ - The engine sees this is the first policy in the chain for `transfer` and that it requires the `to` parameter.
392
+ - It provides `CHARLIE_ADDRESS` to the policy's `run()` function.
393
+ - The policy checks the `IdentityRegistry` and `CredentialRegistry`, finds that Charlie has the required `KYC` credential, and returns `Continue`. The engine proceeds to the next policy.
394
+ 5. **Engine Runs Policy #2 (`SanctionsPolicy`)**:
395
+ - The engine moves to the second policy and sees it _also_ requires the `to` parameter.
396
+ - It again provides `CHARLIE_ADDRESS` to the `SanctionsPolicy`'s `run()` function.
397
+ 6. **`SanctionsPolicy` makes its decision**:
398
+ - The policy knows the address of the `SanctionsList` contract.
399
+ - It calls `SanctionsList.isSanctioned(CHARLIE_ADDRESS)`.
400
+ - The `SanctionsList` contract checks its internal mapping and returns `true`.
401
+ 7. **The Final Verdict**:
402
+ - The `SanctionsPolicy` sees the result is `true` and immediately reverts with `IPolicyEngine.PolicyRejected("account sanctions validation failed")`.
403
+ 8. **Engine Halts and Reverts**:
404
+ - The `PolicyEngine` catches the `PolicyRejected` error, wraps it in a `PolicyRunRejected` error with additional context (function selector, policy address, and reason), and reverts the entire transaction. Alice's transfer to Charlie fails.
405
+
406
+ This flow perfectly demonstrates the decoupling of the system: the `mmfToken` contract has no knowledge of sanctions.
407
+
408
+ This architectural separation is the key to the system's adaptability. If regulations change tomorrow, the compliance rules can be upgraded without ever touching or redeploying the core `mmfToken` contract. A new sanctions list could be configured, or a new policy requiring a different credential could be added to the `PolicyEngine`'s chain for the `transfer` function—all through governance transactions, providing a truly future-proof design.
409
+
410
+ ## 🎉 You've Built It. Now What?
411
+
412
+ Congratulations! You've successfully deployed a complete, modular compliance system and seen how the components work together to enforce onchain rules automatically.
413
+
414
+ The best way to understand how all the components were connected is to review the script you just ran:
415
+
416
+ - → **[Study the deployment script](../../script/getting_started/advanced/DeployAdvancedGettingStarted.s.sol)**
417
+
418
+ From there, your next step depends on your goal:
419
+
420
+ - **"I want to build my own token"**
421
+
422
+ - → Use the code in the [**`tokens` package**](../../packages/tokens) and [**`script` directory**](../../script) as templates for your own project.
423
+
424
+ - **"I want to understand a specific component better"**
425
+
426
+ - → [**Explore the Components**](../../README.md#explore-the-components) in the main README to dive into the documentation for each part of the toolkit.
427
+
428
+ - **"I want to see more code examples"**
429
+ - → Study the reference implementations in each component's `/src` folder.
430
+ - → Review the usage patterns in the `/test` folders for advanced examples.
431
+ - → Browse the [**ready-to-use policies**](../../packages/policy-management/src/policies/README.md) for plug-and-play modules.
@@ -0,0 +1,25 @@
1
+ // SPDX-License-Identifier: BUSL-1.1
2
+ pragma solidity 0.8.26;
3
+
4
+ import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
5
+
6
+ // This is a mock contract representing a public sanctions list.
7
+ // In a real-world scenario, this would be a highly secure contract managed by a trusted data provider.
8
+ contract SanctionsList is Ownable {
9
+ mapping(address => bool) public isSanctioned;
10
+
11
+ event AddedToSanctionsList(address indexed account);
12
+ event RemovedFromSanctionsList(address indexed account);
13
+
14
+ constructor() Ownable(msg.sender) {}
15
+
16
+ function add(address _account) public onlyOwner {
17
+ isSanctioned[_account] = true;
18
+ emit AddedToSanctionsList(_account);
19
+ }
20
+
21
+ function remove(address _account) public onlyOwner {
22
+ isSanctioned[_account] = false;
23
+ emit RemovedFromSanctionsList(_account);
24
+ }
25
+ }
@@ -0,0 +1,58 @@
1
+ // SPDX-License-Identifier: BUSL-1.1
2
+ pragma solidity 0.8.26;
3
+
4
+ import {Policy} from "../../packages/policy-management/src/core/Policy.sol";
5
+ import {IPolicyEngine} from "../../packages/policy-management/src/interfaces/IPolicyEngine.sol";
6
+ import {SanctionsList} from "./SanctionsList.sol";
7
+
8
+ contract SanctionsPolicy is Policy {
9
+ address public sanctionsList;
10
+
11
+ /**
12
+ * @notice Configures the policy with the sanctions list address.
13
+ * @dev This is called automatically during initialization.
14
+ * @param parameters ABI-encoded address of the SanctionsList contract.
15
+ */
16
+ function configure(bytes calldata parameters) internal override onlyInitializing {
17
+ require(parameters.length > 0, "SanctionsPolicy: configData required");
18
+ address _sanctionsList = abi.decode(parameters, (address));
19
+ _setSanctionsList(_sanctionsList);
20
+ }
21
+
22
+ /// @notice Allows updating the sanctions list address after deployment.
23
+ function setSanctionsList(address _listAddress) public onlyOwner {
24
+ _setSanctionsList(_listAddress);
25
+ }
26
+
27
+ /// @notice Internal function to validate and set the sanctions list address.
28
+ function _setSanctionsList(address _listAddress) private {
29
+ require(_listAddress != address(0), "SanctionsPolicy: Invalid address");
30
+ sanctionsList = _listAddress;
31
+ }
32
+
33
+ function run(
34
+ address, /* caller */
35
+ address, /* subject */
36
+ bytes4, /* selector */
37
+ bytes[] calldata parameters,
38
+ bytes calldata /* context */
39
+ )
40
+ public
41
+ view
42
+ override
43
+ returns (IPolicyEngine.PolicyResult)
44
+ {
45
+ require(parameters.length == 1, "SanctionsPolicy: Expected 1 parameter");
46
+ // This policy expects the "to" address as the first parameter
47
+ address recipient = abi.decode(parameters[0], (address));
48
+
49
+ SanctionsList _sl = SanctionsList(sanctionsList);
50
+
51
+ // If the recipient is on the list, reject the transaction.
52
+ if (_sl.isSanctioned(recipient)) {
53
+ revert IPolicyEngine.PolicyRejected("account sanctions validation failed");
54
+ }
55
+
56
+ return IPolicyEngine.PolicyResult.Continue;
57
+ }
58
+ }
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@chainlink/ace",
3
+ "version": "0.5.0",
4
+ "description": "Chainlink Automated Compliance Engine (ACE) Contracts",
5
+ "keywords": [
6
+ "chainlink",
7
+ "ace",
8
+ "compliance",
9
+ "contracts"
10
+ ],
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "https://github.com/smartcontractkit/chainlink-ace.git"
14
+ },
15
+ "license": "BUSL-1.1",
16
+ "author": "Chainlink Labs",
17
+ "scripts": {
18
+ "build": "forge build --sizes",
19
+ "clean": "rm -rf cache out",
20
+ "deploy:token:erc20": "forge script ./script/DeployComplianceTokenERC20.s.sol --via-ir --broadcast --rpc-url ${RPC_URL:=local}",
21
+ "deploy:token:erc3643": "forge script ./script/DeployComplianceTokenERC3643.s.sol --via-ir --broadcast --rpc-url ${RPC_URL:=local}",
22
+ "deploy:token:simple": "forge script ./script/DeploySimpleComplianceToken.s.sol --via-ir --broadcast --rpc-url ${RPC_URL:=local}",
23
+ "docs": "forge doc --serve --port 4000",
24
+ "fmt": "forge fmt",
25
+ "fmt:check": "forge fmt --check",
26
+ "foundryup": "foundryup --version `cat .foundry-version`",
27
+ "lint": "solhint packages/**/*.sol",
28
+ "lint:test": "solhint --config \".solhint-test.json\" --ignore-path \".solhintignore-test\" packages/**/*.t.sol",
29
+ "test": "forge test",
30
+ "test:coverage": "forge coverage",
31
+ "test:coverage:report": "forge coverage --report lcov && genhtml lcov.info --branch-coverage --output-dir coverage",
32
+ "wtf": "which forge"
33
+ },
34
+ "devDependencies": {
35
+ "@chainlink/contracts": "1.3.0",
36
+ "@openzeppelin/contracts": "^5.0.2",
37
+ "@openzeppelin/contracts-upgradeable": "^5.0.2",
38
+ "forge-std": "github:foundry-rs/forge-std#v1.9.4",
39
+ "solhint": "^5.0.4"
40
+ }
41
+ }