@chainlink/ace 0.5.0 → 1.0.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 (126) hide show
  1. package/.github/workflows/npm-publish.yml +28 -0
  2. package/README.md +9 -1
  3. package/UPGRADE_GUIDE.md +754 -0
  4. package/getting_started/GETTING_STARTED.md +6 -0
  5. package/getting_started/MyVault.sol +2 -2
  6. package/getting_started/advanced/SanctionsPolicy.sol +6 -2
  7. package/package.json +3 -3
  8. package/packages/cross-chain-identity/docs/API_REFERENCE.md +2 -0
  9. package/packages/cross-chain-identity/src/CredentialRegistry.sol +9 -7
  10. package/packages/cross-chain-identity/src/CredentialRegistryFactory.sol +96 -0
  11. package/packages/cross-chain-identity/src/CredentialRegistryIdentityValidator.sol +15 -4
  12. package/packages/cross-chain-identity/src/CredentialRegistryIdentityValidatorPolicy.sol +10 -7
  13. package/packages/cross-chain-identity/src/IdentityRegistry.sol +33 -17
  14. package/packages/cross-chain-identity/src/IdentityRegistryFactory.sol +96 -0
  15. package/packages/cross-chain-identity/src/TrustedIssuerRegistry.sol +8 -6
  16. package/packages/cross-chain-identity/src/TrustedIssuerRegistryFactory.sol +96 -0
  17. package/packages/cross-chain-identity/src/interfaces/ICredentialRegistry.sol +6 -0
  18. package/packages/cross-chain-identity/src/interfaces/ICredentialRequirements.sol +2 -1
  19. package/packages/cross-chain-identity/src/interfaces/IIdentityRegistry.sol +7 -1
  20. package/packages/cross-chain-identity/src/interfaces/ITrustedIssuerRegistry.sol +6 -0
  21. package/packages/cross-chain-identity/test/CredentialRegistry.t.sol +1 -1
  22. package/packages/cross-chain-identity/test/CredentialRegistryFactory.t.sol +105 -0
  23. package/packages/cross-chain-identity/test/CredentialRegistryIdentityValidator.t.sol +16 -1
  24. package/packages/cross-chain-identity/test/CredentialRegistryIdentityValidatorPolicy.t.sol +58 -1
  25. package/packages/cross-chain-identity/test/IdentityRegistry.t.sol +140 -1
  26. package/packages/cross-chain-identity/test/IdentityRegistryFactory.t.sol +103 -0
  27. package/packages/cross-chain-identity/test/IdentityValidator.t.sol +1 -1
  28. package/packages/cross-chain-identity/test/TrustedIssuerRegistry.t.sol +1 -1
  29. package/packages/cross-chain-identity/test/TrustedIssuerRegistryFactory.t.sol +107 -0
  30. package/packages/cross-chain-identity/test/helpers/BaseProxyTest.sol +1 -1
  31. package/packages/cross-chain-identity/test/helpers/MockCredentialDataValidator.sol +1 -1
  32. package/packages/cross-chain-identity/test/helpers/MockCredentialRegistryReverting.sol +3 -1
  33. package/packages/policy-management/README.md +1 -0
  34. package/packages/policy-management/docs/API_GUIDE.md +2 -0
  35. package/packages/policy-management/docs/API_REFERENCE.md +3 -1
  36. package/packages/policy-management/docs/CUSTOM_POLICIES_TUTORIAL.md +10 -4
  37. package/packages/policy-management/src/core/Policy.sol +5 -4
  38. package/packages/policy-management/src/core/PolicyEngine.sol +113 -57
  39. package/packages/policy-management/src/core/PolicyEngineFactory.sol +102 -0
  40. package/packages/policy-management/src/core/PolicyFactory.sol +1 -1
  41. package/packages/policy-management/src/core/PolicyProtected.sol +28 -55
  42. package/packages/policy-management/src/core/PolicyProtectedUpgradeable.sol +134 -0
  43. package/packages/policy-management/src/extractors/ComplianceTokenForceTransferExtractor.sol +4 -1
  44. package/packages/policy-management/src/extractors/ComplianceTokenFreezeUnfreezeExtractor.sol +4 -1
  45. package/packages/policy-management/src/extractors/ComplianceTokenMintBurnExtractor.sol +4 -1
  46. package/packages/policy-management/src/extractors/ERC20ApproveExtractor.sol +4 -1
  47. package/packages/policy-management/src/extractors/ERC20TransferExtractor.sol +4 -1
  48. package/packages/policy-management/src/extractors/ERC3643ForcedTransferExtractor.sol +4 -1
  49. package/packages/policy-management/src/extractors/ERC3643FreezeUnfreezeExtractor.sol +4 -1
  50. package/packages/policy-management/src/extractors/ERC3643MintBurnExtractor.sol +4 -1
  51. package/packages/policy-management/src/extractors/ERC3643SetAddressFrozenExtractor.sol +4 -1
  52. package/packages/policy-management/src/interfaces/ICertifiedActionValidator.sol +110 -0
  53. package/packages/policy-management/src/interfaces/IExtractor.sol +6 -0
  54. package/packages/policy-management/src/interfaces/IMapper.sol +6 -0
  55. package/packages/policy-management/src/interfaces/IPolicy.sol +6 -0
  56. package/packages/policy-management/src/interfaces/IPolicyEngine.sol +90 -10
  57. package/packages/policy-management/src/interfaces/IPolicyProtected.sol +6 -0
  58. package/packages/policy-management/src/libraries/CertifiedActionLib.sol +47 -0
  59. package/packages/policy-management/src/policies/AllowPolicy.sol +12 -7
  60. package/packages/policy-management/src/policies/BypassPolicy.sol +23 -5
  61. package/packages/policy-management/src/policies/CertifiedActionDONValidatorPolicy.sol +156 -0
  62. package/packages/policy-management/src/policies/CertifiedActionERC20TransferValidatorPolicy.sol +202 -0
  63. package/packages/policy-management/src/policies/CertifiedActionValidatorPolicy.sol +365 -0
  64. package/packages/policy-management/src/policies/IntervalPolicy.sol +64 -48
  65. package/packages/policy-management/src/policies/MaxPolicy.sol +7 -5
  66. package/packages/policy-management/src/policies/OnlyAuthorizedSenderPolicy.sol +20 -4
  67. package/packages/policy-management/src/policies/OnlyOwnerPolicy.sol +4 -2
  68. package/packages/policy-management/src/policies/PausePolicy.sol +16 -15
  69. package/packages/policy-management/src/policies/RejectPolicy.sol +23 -5
  70. package/packages/policy-management/src/policies/RoleBasedAccessControlPolicy.sol +7 -5
  71. package/packages/policy-management/src/policies/SecureMintPolicy.sol +136 -48
  72. package/packages/policy-management/src/policies/VolumePolicy.sol +9 -5
  73. package/packages/policy-management/src/policies/VolumeRatePolicy.sol +11 -7
  74. package/packages/policy-management/test/PolicyEngine.t.sol +131 -23
  75. package/packages/policy-management/test/PolicyEngineFactory.t.sol +95 -0
  76. package/packages/policy-management/test/PolicyFactory.t.sol +1 -1
  77. package/packages/policy-management/test/{PolicyProtectedToken.t.sol → PolicyProtected.t.sol} +54 -8
  78. package/packages/policy-management/test/PolicyProtectedUpgradeable.t.sol +126 -0
  79. package/packages/policy-management/test/extractors/ComplianceTokenForceTransferExtractor.t.sol +1 -1
  80. package/packages/policy-management/test/extractors/ComplianceTokenFreezeUnfreezeExtractor.t.sol +1 -1
  81. package/packages/policy-management/test/extractors/ComplianceTokenMintBurnExtractor.t.sol +1 -1
  82. package/packages/policy-management/test/extractors/ERC20ApproveExtractor.t.sol +1 -1
  83. package/packages/policy-management/test/extractors/ERC3643ForcedTransferExtractor.t.sol +1 -1
  84. package/packages/policy-management/test/extractors/ERC3643FreezeUnfreezeExtractor.t.sol +1 -1
  85. package/packages/policy-management/test/extractors/ERC3643MintBurnExtractor.t.sol +1 -1
  86. package/packages/policy-management/test/extractors/ERC3643SetAddressFrozenExtractor.t.sol +1 -1
  87. package/packages/policy-management/test/helpers/BaseCertifiedActionTest.sol +44 -0
  88. package/packages/policy-management/test/helpers/BaseProxyTest.sol +88 -7
  89. package/packages/policy-management/test/helpers/CustomMapper.sol +3 -1
  90. package/packages/policy-management/test/helpers/DummyExtractor.sol +3 -1
  91. package/packages/policy-management/test/helpers/ExpectedParameterPolicy.sol +3 -1
  92. package/packages/policy-management/test/helpers/FaultyPolicyEngine.sol +54 -0
  93. package/packages/policy-management/test/helpers/MockCertifiedActionValidatorPolicyExtension.sol +46 -0
  94. package/packages/policy-management/test/helpers/MockToken.sol +2 -5
  95. package/packages/policy-management/test/helpers/MockTokenExtractor.sol +9 -4
  96. package/packages/policy-management/test/helpers/MockTokenUpgradeable.sol +66 -0
  97. package/packages/policy-management/test/helpers/PolicyAlwaysAllowed.sol +3 -1
  98. package/packages/policy-management/test/helpers/PolicyAlwaysContinue.sol +3 -1
  99. package/packages/policy-management/test/helpers/PolicyAlwaysRejected.sol +10 -1
  100. package/packages/policy-management/test/helpers/PolicyFailingRun.sol +3 -1
  101. package/packages/policy-management/test/policies/AllowPolicy.t.sol +39 -23
  102. package/packages/policy-management/test/policies/BypassPolicy.t.sol +48 -20
  103. package/packages/policy-management/test/policies/CertifiedActionDONValidatorPolicy.t.sol +172 -0
  104. package/packages/policy-management/test/policies/CertifiedActionERC20TransferValidatorPolicy.t.sol +217 -0
  105. package/packages/policy-management/test/policies/CertifiedActionValidatorPolicy.t.sol +1083 -0
  106. package/packages/policy-management/test/policies/IntervalPolicy.t.sol +105 -55
  107. package/packages/policy-management/test/policies/MaxPolicy.t.sol +15 -7
  108. package/packages/policy-management/test/policies/OnlyAuthorizedSenderPolicy.t.sol +91 -16
  109. package/packages/policy-management/test/policies/OnlyOwnerPolicy.t.sol +11 -7
  110. package/packages/policy-management/test/policies/PausePolicy.t.sol +46 -16
  111. package/packages/policy-management/test/policies/RejectPolicy.t.sol +52 -24
  112. package/packages/policy-management/test/policies/RoleBasedAccessControlPolicy.t.sol +50 -30
  113. package/packages/policy-management/test/policies/SecureMintPolicy.t.sol +211 -32
  114. package/packages/policy-management/test/policies/VolumePolicy.t.sol +20 -10
  115. package/packages/policy-management/test/policies/VolumeRatePolicy.t.sol +24 -18
  116. package/packages/tokens/erc-20/src/ComplianceTokenERC20.sol +4 -7
  117. package/packages/tokens/erc-20/src/ComplianceTokenStoreERC20.sol +4 -4
  118. package/packages/tokens/erc-20/test/ComplianceTokenERC20.t.sol +92 -82
  119. package/packages/tokens/erc-20/test/helpers/BaseProxyTest.sol +74 -1
  120. package/packages/tokens/erc-3643/src/ComplianceTokenERC3643.sol +12 -7
  121. package/packages/tokens/erc-3643/src/ComplianceTokenStoreERC3643.sol +4 -4
  122. package/packages/tokens/erc-3643/test/ComplianceTokenERC3643.t.sol +108 -119
  123. package/packages/tokens/erc-3643/test/helpers/BaseProxyTest.sol +74 -1
  124. package/packages/tokens/erc-3643/test/helpers/ExpectedContextPolicy.sol +3 -1
  125. package/remappings.txt +1 -0
  126. package/script/DeployCertifiedActionsComplianceTokenERC3643.s.sol +125 -0
@@ -436,6 +436,12 @@ You now understand the core ACE integration pattern:
436
436
 
437
437
  Choose your path based on what you want to build:
438
438
 
439
+ #### **Upgrade an existing contract**
440
+
441
+ Already have a deployed contract that needs ACE compliance? Follow our step-by-step upgrade guide:
442
+
443
+ - **[Upgrade Guide](../UPGRADE_GUIDE.md)** - Add ACE to existing deployed contracts without disrupting functionality
444
+
439
445
  #### **Learn more about Policy Management**
440
446
 
441
447
  You've seen one policy (`PausePolicy`) on two functions. Ready to level up?
@@ -1,7 +1,7 @@
1
1
  // SPDX-License-Identifier: BUSL-1.1
2
2
  pragma solidity 0.8.26;
3
3
 
4
- import {PolicyProtected} from "@chainlink/policy-management/core/PolicyProtected.sol";
4
+ import {PolicyProtectedUpgradeable} from "@chainlink/policy-management/core/PolicyProtectedUpgradeable.sol";
5
5
 
6
6
  /**
7
7
  * @title MyVault
@@ -10,7 +10,7 @@ import {PolicyProtected} from "@chainlink/policy-management/core/PolicyProtected
10
10
  * Both the `deposit` and `withdraw` functions are protected with the `runPolicy` modifier.
11
11
  * This contract is designed to be deployed behind a proxy for upgradeability.
12
12
  */
13
- contract MyVault is PolicyProtected {
13
+ contract MyVault is PolicyProtectedUpgradeable {
14
14
  mapping(address => uint256) public deposits;
15
15
 
16
16
  function initialize(address initialOwner, address policyEngine) public initializer {
@@ -6,7 +6,9 @@ import {IPolicyEngine} from "../../packages/policy-management/src/interfaces/IPo
6
6
  import {SanctionsList} from "./SanctionsList.sol";
7
7
 
8
8
  contract SanctionsPolicy is Policy {
9
- address public sanctionsList;
9
+ string public constant override typeAndVersion = "SanctionsPolicy 1.0.0";
10
+
11
+ address public sanctionsList;
10
12
 
11
13
  /**
12
14
  * @notice Configures the policy with the sanctions list address.
@@ -42,7 +44,9 @@ contract SanctionsPolicy is Policy {
42
44
  override
43
45
  returns (IPolicyEngine.PolicyResult)
44
46
  {
45
- require(parameters.length == 1, "SanctionsPolicy: Expected 1 parameter");
47
+ if (parameters.length != 1) {
48
+ revert InvalidParameters("SanctionsPolicy: Expected 1 parameter");
49
+ }
46
50
  // This policy expects the "to" address as the first parameter
47
51
  address recipient = abi.decode(parameters[0], (address));
48
52
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chainlink/ace",
3
- "version": "0.5.0",
3
+ "version": "1.0.0",
4
4
  "description": "Chainlink Automated Compliance Engine (ACE) Contracts",
5
5
  "keywords": [
6
6
  "chainlink",
@@ -13,7 +13,7 @@
13
13
  "url": "https://github.com/smartcontractkit/chainlink-ace.git"
14
14
  },
15
15
  "license": "BUSL-1.1",
16
- "author": "Chainlink Labs",
16
+ "author": "smartcontractkit",
17
17
  "scripts": {
18
18
  "build": "forge build --sizes",
19
19
  "clean": "rm -rf cache out",
@@ -32,7 +32,7 @@
32
32
  "wtf": "which forge"
33
33
  },
34
34
  "devDependencies": {
35
- "@chainlink/contracts": "1.3.0",
35
+ "@chainlink/contracts": "1.5.0",
36
36
  "@openzeppelin/contracts": "^5.0.2",
37
37
  "@openzeppelin/contracts-upgradeable": "^5.0.2",
38
38
  "forge-std": "github:foundry-rs/forge-std#v1.9.4",
@@ -58,6 +58,8 @@ struct Credential {
58
58
 
59
59
  ```solidity
60
60
  interface ICredentialRegistry {
61
+ function typeAndVersion() external pure returns (string memory);
62
+
61
63
  function registerCredential(
62
64
  bytes32 ccid,
63
65
  bytes32 credentialTypeId,
@@ -1,22 +1,24 @@
1
1
  // SPDX-License-Identifier: BUSL-1.1
2
- pragma solidity 0.8.26;
2
+ pragma solidity ^0.8.20;
3
3
 
4
4
  import {ICredentialRegistry} from "./interfaces/ICredentialRegistry.sol";
5
5
  import {ICredentialValidator} from "./interfaces/ICredentialValidator.sol";
6
- import {PolicyProtected} from "@chainlink/policy-management/core/PolicyProtected.sol";
6
+ import {PolicyProtectedUpgradeable} from "@chainlink/policy-management/core/PolicyProtectedUpgradeable.sol";
7
7
 
8
- contract CredentialRegistry is PolicyProtected, ICredentialRegistry {
9
- /// @custom:storage-location erc7201:cross-chain-identity.CredentialRegistry
8
+ contract CredentialRegistry is PolicyProtectedUpgradeable, ICredentialRegistry {
9
+ string public constant override typeAndVersion = "CredentialRegistry 1.0.0";
10
+
11
+ /// @custom:storage-location erc7201:chainlink.ace.CredentialRegistry
10
12
  struct CredentialRegistryStorage {
11
13
  mapping(bytes32 ccid => bytes32[] credentialTypeIds) credentialTypeIdsByCCID;
12
14
  mapping(bytes32 ccid => mapping(bytes32 credentialTypeId => Credential credentials)) credentials;
13
15
  }
14
16
 
15
- // keccak256(abi.encode(uint256(keccak256("cross-chain-identity.CredentialRegistry")) - 1)) &
17
+ // keccak256(abi.encode(uint256(keccak256("chainlink.ace.CredentialRegistry")) - 1)) &
16
18
  // ~bytes32(uint256(0xff))
17
19
  // solhint-disable-next-line const-name-snakecase
18
20
  bytes32 private constant credentialRegistryStorageLocation =
19
- 0xda878a21d431ff897bdb535b211ae68088a4b0265066b239bc4db2e51d9a8200;
21
+ 0xa402d759ab0c43f5f6ba3a2cdc5bb0f98c15af3daf1f94f873714cacbe789800;
20
22
 
21
23
  function _credentialRegistryStorage() private pure returns (CredentialRegistryStorage storage $) {
22
24
  // solhint-disable-next-line no-inline-assembly
@@ -168,7 +170,7 @@ contract CredentialRegistry is PolicyProtected, ICredentialRegistry {
168
170
  view
169
171
  returns (Credential[] memory)
170
172
  {
171
- uint8 length = uint8(credentialTypeIds.length);
173
+ uint256 length = credentialTypeIds.length;
172
174
  Credential[] memory credentials = new Credential[](length);
173
175
  for (uint256 i = 0; i < length; i++) {
174
176
  credentials[i] = getCredential(ccid, credentialTypeIds[i]);
@@ -0,0 +1,96 @@
1
+ // SPDX-License-Identifier: BUSL-1.1
2
+ pragma solidity ^0.8.20;
3
+
4
+ import {CredentialRegistry} from "./CredentialRegistry.sol";
5
+ import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol";
6
+
7
+ /**
8
+ * @title CredentialRegistryFactory
9
+ * @notice Factory contract for creating deterministic minimal proxy clones of credential registry implementations.
10
+ * @dev Uses OpenZeppelin's Clones library to create deterministic minimal proxies (EIP-1167) of credential
11
+ * registry contracts.
12
+ * Each registry is deployed with a unique salt derived from the creator's address and a unique registry ID,
13
+ * ensuring deterministic addresses and preventing duplicate deployments.
14
+ */
15
+ contract CredentialRegistryFactory {
16
+ /// @notice Emitted when a new credential registry is created
17
+ event CredentialRegistryCreated(address registry);
18
+
19
+ /// @notice Emitted when registry initialization fails
20
+ error RegistryInitializationFailed(bytes reason);
21
+
22
+ /// @notice Emitted when implementation address is zero
23
+ error ImplementationIsZeroAddress();
24
+
25
+ /**
26
+ * @notice Creates a new credential registry contract using deterministic minimal proxy cloning.
27
+ * @dev If a registry with the same salt already exists, returns the existing address instead of reverting.
28
+ * Uses CREATE2 for deterministic deployment addresses. The registry is automatically initialized
29
+ * with the provided parameters after deployment.
30
+ * @param implementation The address of the credential registry implementation contract to clone
31
+ * @param uniqueRegistryId A unique identifier for this registry (combined with msg.sender to create salt)
32
+ * @param policyEngine The address of the policy engine that will manage this registry
33
+ * @param initialOwner The address that will own the newly created registry contract
34
+ * @return registryAddress The address of the created (or existing) registry contract
35
+ */
36
+ function createCredentialRegistry(
37
+ address implementation,
38
+ bytes32 uniqueRegistryId,
39
+ address policyEngine,
40
+ address initialOwner
41
+ )
42
+ public
43
+ returns (address registryAddress)
44
+ {
45
+ if (implementation == address(0)) revert ImplementationIsZeroAddress();
46
+
47
+ bytes32 salt = getSalt(msg.sender, uniqueRegistryId);
48
+ registryAddress = Clones.predictDeterministicAddress(implementation, salt);
49
+ if (registryAddress.code.length > 0) {
50
+ return registryAddress;
51
+ }
52
+
53
+ registryAddress = Clones.cloneDeterministic(implementation, salt);
54
+
55
+ try CredentialRegistry(registryAddress).initialize(policyEngine, initialOwner) {
56
+ emit CredentialRegistryCreated(registryAddress);
57
+ } catch (bytes memory reason) {
58
+ revert RegistryInitializationFailed(reason);
59
+ }
60
+ }
61
+
62
+ /**
63
+ * @notice Predicts the deterministic address where a credential registry would be deployed.
64
+ * @dev Useful for calculating registry addresses before deployment or checking if a registry already exists.
65
+ * Uses the same salt generation as createCredentialRegistry to ensure address consistency.
66
+ * @param creator The address of the account that would create the registry
67
+ * @param implementation The address of the credential registry implementation contract
68
+ * @param uniqueRegistryId The unique identifier for the registry
69
+ * @return The predicted address where the registry would be deployed
70
+ */
71
+ function predictRegistryAddress(
72
+ address creator,
73
+ address implementation,
74
+ bytes32 uniqueRegistryId
75
+ )
76
+ public
77
+ view
78
+ returns (address)
79
+ {
80
+ bytes32 salt = getSalt(creator, uniqueRegistryId);
81
+ return Clones.predictDeterministicAddress(implementation, salt);
82
+ }
83
+
84
+ /**
85
+ * @notice Generates a deterministic salt for registry deployment.
86
+ * @dev Combines the sender address and unique registry ID to create a unique salt.
87
+ * This ensures that the same creator cannot deploy multiple registries with the same ID,
88
+ * while allowing different creators to use the same registry ID.
89
+ * @param sender The address of the registry creator
90
+ * @param uniqueRegistryId The unique identifier for the registry
91
+ * @return The generated salt for deterministic deployment
92
+ */
93
+ function getSalt(address sender, bytes32 uniqueRegistryId) public pure returns (bytes32) {
94
+ return keccak256(abi.encodePacked(sender, uniqueRegistryId));
95
+ }
96
+ }
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: BUSL-1.1
2
- pragma solidity 0.8.26;
2
+ pragma solidity ^0.8.20;
3
3
 
4
4
  import {ICredentialRequirements} from "./interfaces/ICredentialRequirements.sol";
5
5
  import {IIdentityValidator} from "./interfaces/IIdentityValidator.sol";
@@ -11,19 +11,20 @@ import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/Own
11
11
  contract CredentialRegistryIdentityValidator is OwnableUpgradeable, ICredentialRequirements, IIdentityValidator {
12
12
  uint256 private constant MAX_REQUIREMENTS = 8;
13
13
  uint256 private constant MAX_REQUIREMENT_SOURCES = 8;
14
+ uint256 private constant MAX_CREDENTIAL_TYPES_PER_REQUIREMENT = 32;
14
15
 
15
- /// @custom:storage-location erc7201:cross-chain-identity.CredentialRegistryIdentityValidator
16
+ /// @custom:storage-location erc7201:chainlink.ace.CredentialRegistryIdentityValidator
16
17
  struct CredentialRegistryIdentityValidatorStorage {
17
18
  bytes32[] requirements;
18
19
  mapping(bytes32 requirementId => CredentialRequirement credentialRequirement) credentialRequirementMap;
19
20
  mapping(bytes32 credential => CredentialSource[] sources) credentialSources;
20
21
  }
21
22
 
22
- // keccak256(abi.encode(uint256(keccak256("cross-chain-identity.CredentialRegistryIdentityValidator")) - 1)) &
23
+ // keccak256(abi.encode(uint256(keccak256("chainlink.ace.CredentialRegistryIdentityValidator")) - 1)) &
23
24
  // ~bytes32(uint256(0xff))
24
25
  // solhint-disable-next-line const-name-snakecase
25
26
  bytes32 private constant credentialRegistryIdentityValidatorStorageLocation =
26
- 0xc27301a28eb510a5458d7558b8bccbf4cdde3a4546d3bf041997133950e7d200;
27
+ 0xd345ab5a3e8073283824dcc06e7ac0c586290818c864e9d2ee66361ecca47600;
27
28
 
28
29
  function _credentialRegistryIdentityValidatorStorage()
29
30
  private
@@ -36,6 +37,11 @@ contract CredentialRegistryIdentityValidator is OwnableUpgradeable, ICredentialR
36
37
  }
37
38
  }
38
39
 
40
+ constructor() {
41
+ // disabling initializers on the implementation contract itself
42
+ _disableInitializers();
43
+ }
44
+
39
45
  /**
40
46
  * @dev Initializes the credential validator and sets the initial credential sources and requirements.
41
47
  * @param credentialSourceInputs The credential sources to add.
@@ -96,6 +102,11 @@ contract CredentialRegistryIdentityValidator is OwnableUpgradeable, ICredentialR
96
102
  }
97
103
  }
98
104
  bytes32[] memory credentialTypeIds = input.credentialTypeIds;
105
+ uint256 credentialTypesLength = credentialTypeIds.length;
106
+
107
+ if (credentialTypesLength == 0 || credentialTypesLength > MAX_CREDENTIAL_TYPES_PER_REQUIREMENT) {
108
+ revert InvalidRequirementConfiguration("Invalid credential types length");
109
+ }
99
110
  _credentialRegistryIdentityValidatorStorage().requirements.push(requirementId);
100
111
  _credentialRegistryIdentityValidatorStorage().credentialRequirementMap[requirementId] =
101
112
  CredentialRequirement(credentialTypeIds, minValidations, input.invert);
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: BUSL-1.1
2
- pragma solidity 0.8.26;
2
+ pragma solidity ^0.8.20;
3
3
 
4
4
  import {CredentialRegistryIdentityValidator} from "./CredentialRegistryIdentityValidator.sol";
5
5
  import {IPolicyEngine} from "@chainlink/policy-management/interfaces/IPolicyEngine.sol";
@@ -7,6 +7,8 @@ import {Policy} from "@chainlink/policy-management/core/Policy.sol";
7
7
  import {ICredentialRequirements} from "@chainlink/cross-chain-identity/interfaces/ICredentialRequirements.sol";
8
8
 
9
9
  contract CredentialRegistryIdentityValidatorPolicy is Policy, CredentialRegistryIdentityValidator {
10
+ string public constant override typeAndVersion = "CredentialRegistryIdentityValidatorPolicy 1.0.0";
11
+
10
12
  /**
11
13
  * @notice Configures the policy by setting up credential sources and credential requirements.
12
14
  * @dev The `parameters` input must be the ABI encoding of two dynamic arrays:
@@ -53,14 +55,15 @@ contract CredentialRegistryIdentityValidatorPolicy is Policy, CredentialRegistry
53
55
  override
54
56
  returns (IPolicyEngine.PolicyResult)
55
57
  {
56
- // expected parameters: [account(address)]
57
- if (parameters.length != 1) {
58
- revert IPolicyEngine.InvalidConfiguration("expected 1 parameter");
58
+ if (parameters.length < 1) {
59
+ revert InvalidParameters("expected at least 1 parameter");
59
60
  }
60
- address account = abi.decode(parameters[0], (address));
61
61
 
62
- if (!validate(account, context)) {
63
- revert IPolicyEngine.PolicyRejected("account identity validation failed");
62
+ for (uint256 i = 0; i < parameters.length; i++) {
63
+ address account = abi.decode(parameters[i], (address));
64
+ if (!validate(account, context)) {
65
+ revert IPolicyEngine.PolicyRejected("account identity validation failed");
66
+ }
64
67
  }
65
68
  return IPolicyEngine.PolicyResult.Continue;
66
69
  }
@@ -1,21 +1,25 @@
1
1
  // SPDX-License-Identifier: BUSL-1.1
2
- pragma solidity 0.8.26;
2
+ pragma solidity ^0.8.20;
3
3
 
4
4
  import {IIdentityRegistry} from "./interfaces/IIdentityRegistry.sol";
5
- import {PolicyProtected} from "@chainlink/policy-management/core/PolicyProtected.sol";
5
+ import {PolicyProtectedUpgradeable} from "@chainlink/policy-management/core/PolicyProtectedUpgradeable.sol";
6
6
 
7
- contract IdentityRegistry is PolicyProtected, IIdentityRegistry {
8
- /// @custom:storage-location erc7201:cross-chain-identity.IdentityRegistry
7
+ contract IdentityRegistry is PolicyProtectedUpgradeable, IIdentityRegistry {
8
+ string public constant override typeAndVersion = "IdentityRegistry 1.0.0";
9
+
10
+ /// @custom:storage-location erc7201:chainlink.ace.IdentityRegistry
9
11
  struct IdentityRegistryStorage {
10
12
  mapping(address account => bytes32 ccid) accountToCcid;
11
13
  mapping(bytes32 ccid => address[] accounts) ccidToAccounts;
14
+ // Maps ccid => account => index in ccidToAccounts array (index + 1, 0 means not found)
15
+ mapping(bytes32 ccid => mapping(address account => uint256 index)) accountIndex;
12
16
  }
13
17
 
14
- // keccak256(abi.encode(uint256(keccak256("cross-chain-identity.IdentityRegistry")) - 1)) &
18
+ // keccak256(abi.encode(uint256(keccak256("chainlink.ace.IdentityRegistry")) - 1)) &
15
19
  // ~bytes32(uint256(0xff))
16
20
  // solhint-disable-next-line const-name-snakecase
17
21
  bytes32 private constant identityRegistryStorageLocation =
18
- 0x95c7dd14054992de17881168e75df13bb7ed90a1eacfff26d4643d48ec30de00;
22
+ 0x31dacf4de1329b533dc7ad419a7ae424eacaaf799ae802e5d062dea412180200;
19
23
 
20
24
  function _identityRegistryStorage() private pure returns (IdentityRegistryStorage storage $) {
21
25
  // solhint-disable-next-line no-inline-assembly
@@ -83,6 +87,8 @@ contract IdentityRegistry is PolicyProtected, IIdentityRegistry {
83
87
  }
84
88
  _identityRegistryStorage().accountToCcid[account] = ccid;
85
89
  _identityRegistryStorage().ccidToAccounts[ccid].push(account);
90
+ // Store index + 1 (so 0 means not found)
91
+ _identityRegistryStorage().accountIndex[ccid][account] = _identityRegistryStorage().ccidToAccounts[ccid].length;
86
92
  emit IdentityRegistered(ccid, account);
87
93
  }
88
94
 
@@ -97,18 +103,28 @@ contract IdentityRegistry is PolicyProtected, IIdentityRegistry {
97
103
  override
98
104
  runPolicyWithContext(context)
99
105
  {
100
- uint256 length = _identityRegistryStorage().ccidToAccounts[ccid].length;
101
- for (uint256 i = 0; i < length; i++) {
102
- if (_identityRegistryStorage().ccidToAccounts[ccid][i] == account) {
103
- _identityRegistryStorage().ccidToAccounts[ccid][i] = _identityRegistryStorage().ccidToAccounts[ccid][length - 1];
104
- _identityRegistryStorage().ccidToAccounts[ccid].pop();
105
- delete _identityRegistryStorage().accountToCcid[account];
106
-
107
- emit IdentityRemoved(ccid, account);
108
- return;
109
- }
106
+ uint256 indexPlusOne = _identityRegistryStorage().accountIndex[ccid][account];
107
+ if (indexPlusOne == 0) {
108
+ revert IdentityNotFound(ccid, account);
109
+ }
110
+
111
+ uint256 index = indexPlusOne - 1;
112
+ uint256 lastIndex = _identityRegistryStorage().ccidToAccounts[ccid].length - 1;
113
+
114
+ if (index != lastIndex) {
115
+ // Move the last element to the position being removed
116
+ address lastAccount = _identityRegistryStorage().ccidToAccounts[ccid][lastIndex];
117
+ _identityRegistryStorage().ccidToAccounts[ccid][index] = lastAccount;
118
+ // Update the moved account's index
119
+ _identityRegistryStorage().accountIndex[ccid][lastAccount] = indexPlusOne;
110
120
  }
111
- revert IdentityNotFound(ccid, account);
121
+
122
+ // Remove the last element
123
+ _identityRegistryStorage().ccidToAccounts[ccid].pop();
124
+ delete _identityRegistryStorage().accountToCcid[account];
125
+ delete _identityRegistryStorage().accountIndex[ccid][account];
126
+
127
+ emit IdentityRemoved(ccid, account);
112
128
  }
113
129
 
114
130
  /// @inheritdoc IIdentityRegistry
@@ -0,0 +1,96 @@
1
+ // SPDX-License-Identifier: BUSL-1.1
2
+ pragma solidity ^0.8.20;
3
+
4
+ import {IdentityRegistry} from "./IdentityRegistry.sol";
5
+ import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol";
6
+
7
+ /**
8
+ * @title IdentityRegistryFactory
9
+ * @notice Factory contract for creating deterministic minimal proxy clones of identity registry implementations.
10
+ * @dev Uses OpenZeppelin's Clones library to create deterministic minimal proxies (EIP-1167) of identity
11
+ * registry contracts.
12
+ * Each registry is deployed with a unique salt derived from the creator's address and a unique registry ID,
13
+ * ensuring deterministic addresses and preventing duplicate deployments.
14
+ */
15
+ contract IdentityRegistryFactory {
16
+ /// @notice Emitted when a new identity registry is created
17
+ event IdentityRegistryCreated(address registry);
18
+
19
+ /// @notice Emitted when registry initialization fails
20
+ error RegistryInitializationFailed(bytes reason);
21
+
22
+ /// @notice Emitted when implementation address is zero
23
+ error ImplementationIsZeroAddress();
24
+
25
+ /**
26
+ * @notice Creates a new identity registry contract using deterministic minimal proxy cloning.
27
+ * @dev If a registry with the same salt already exists, returns the existing address instead of reverting.
28
+ * Uses CREATE2 for deterministic deployment addresses. The registry is automatically initialized
29
+ * with the provided parameters after deployment.
30
+ * @param implementation The address of the identity registry implementation contract to clone
31
+ * @param uniqueRegistryId A unique identifier for this registry (combined with msg.sender to create salt)
32
+ * @param policyEngine The address of the policy engine that will manage this registry
33
+ * @param initialOwner The address that will own the newly created registry contract
34
+ * @return registryAddress The address of the created (or existing) registry contract
35
+ */
36
+ function createIdentityRegistry(
37
+ address implementation,
38
+ bytes32 uniqueRegistryId,
39
+ address policyEngine,
40
+ address initialOwner
41
+ )
42
+ public
43
+ returns (address registryAddress)
44
+ {
45
+ if (implementation == address(0)) revert ImplementationIsZeroAddress();
46
+
47
+ bytes32 salt = getSalt(msg.sender, uniqueRegistryId);
48
+ registryAddress = Clones.predictDeterministicAddress(implementation, salt);
49
+ if (registryAddress.code.length > 0) {
50
+ return registryAddress;
51
+ }
52
+
53
+ registryAddress = Clones.cloneDeterministic(implementation, salt);
54
+
55
+ try IdentityRegistry(registryAddress).initialize(policyEngine, initialOwner) {
56
+ emit IdentityRegistryCreated(registryAddress);
57
+ } catch (bytes memory reason) {
58
+ revert RegistryInitializationFailed(reason);
59
+ }
60
+ }
61
+
62
+ /**
63
+ * @notice Predicts the deterministic address where an identity registry would be deployed.
64
+ * @dev Useful for calculating registry addresses before deployment or checking if a registry already exists.
65
+ * Uses the same salt generation as createIdentityRegistry to ensure address consistency.
66
+ * @param creator The address of the account that would create the registry
67
+ * @param implementation The address of the identity registry implementation contract
68
+ * @param uniqueRegistryId The unique identifier for the registry
69
+ * @return The predicted address where the registry would be deployed
70
+ */
71
+ function predictRegistryAddress(
72
+ address creator,
73
+ address implementation,
74
+ bytes32 uniqueRegistryId
75
+ )
76
+ public
77
+ view
78
+ returns (address)
79
+ {
80
+ bytes32 salt = getSalt(creator, uniqueRegistryId);
81
+ return Clones.predictDeterministicAddress(implementation, salt);
82
+ }
83
+
84
+ /**
85
+ * @notice Generates a deterministic salt for registry deployment.
86
+ * @dev Combines the sender address and unique registry ID to create a unique salt.
87
+ * This ensures that the same creator cannot deploy multiple registries with the same ID,
88
+ * while allowing different creators to use the same registry ID.
89
+ * @param sender The address of the registry creator
90
+ * @param uniqueRegistryId The unique identifier for the registry
91
+ * @return The generated salt for deterministic deployment
92
+ */
93
+ function getSalt(address sender, bytes32 uniqueRegistryId) public pure returns (bytes32) {
94
+ return keccak256(abi.encodePacked(sender, uniqueRegistryId));
95
+ }
96
+ }
@@ -1,25 +1,27 @@
1
1
  // SPDX-License-Identifier: BUSL-1.1
2
- pragma solidity 0.8.26;
2
+ pragma solidity ^0.8.20;
3
3
 
4
4
  import {ITrustedIssuerRegistry} from "./interfaces/ITrustedIssuerRegistry.sol";
5
- import {PolicyProtected} from "@chainlink/policy-management/core/PolicyProtected.sol";
5
+ import {PolicyProtectedUpgradeable} from "@chainlink/policy-management/core/PolicyProtectedUpgradeable.sol";
6
6
 
7
7
  /**
8
8
  * @title TrustedIssuerRegistry
9
9
  * @dev Implementation of the ITrustedIssuerRegistry interface using ERC-7201 storage pattern.
10
10
  */
11
- contract TrustedIssuerRegistry is PolicyProtected, ITrustedIssuerRegistry {
12
- /// @custom:storage-location erc7201:cross-chain-identity.TrustedIssuerRegistry
11
+ contract TrustedIssuerRegistry is PolicyProtectedUpgradeable, ITrustedIssuerRegistry {
12
+ string public constant override typeAndVersion = "TrustedIssuerRegistry 1.0.0";
13
+
14
+ /// @custom:storage-location erc7201:chainlink.ace.TrustedIssuerRegistry
13
15
  struct TrustedIssuerRegistryStorage {
14
16
  mapping(bytes32 issuerIdHash => bool isTrusted) trustedIssuers;
15
17
  bytes32[] issuerList;
16
18
  }
17
19
 
18
- // keccak256(abi.encode(uint256(keccak256("cross-chain-identity.TrustedIssuerRegistry")) - 1)) &
20
+ // keccak256(abi.encode(uint256(keccak256("chainlink.ace.TrustedIssuerRegistry")) - 1)) &
19
21
  // ~bytes32(uint256(0xff))
20
22
  // solhint-disable-next-line const-name-snakecase
21
23
  bytes32 private constant trustedIssuerRegistryStorageLocation =
22
- 0x68705e3417317ecc3ac2b3879fdff408d88085552fb211d60abc5b025809c200;
24
+ 0x24368af76f28f75e0d3c893af5175655a9b97a18e82e58c43f9152ef16421900;
23
25
 
24
26
  function _trustedIssuerRegistryStorage() private pure returns (TrustedIssuerRegistryStorage storage $) {
25
27
  assembly {
@@ -0,0 +1,96 @@
1
+ // SPDX-License-Identifier: BUSL-1.1
2
+ pragma solidity ^0.8.20;
3
+
4
+ import {TrustedIssuerRegistry} from "./TrustedIssuerRegistry.sol";
5
+ import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol";
6
+
7
+ /**
8
+ * @title TrustedIssuerRegistryFactory
9
+ * @notice Factory contract for creating deterministic minimal proxy clones of trusted issuer registry implementations.
10
+ * @dev Uses OpenZeppelin's Clones library to create deterministic minimal proxies (EIP-1167) of trusted issuer
11
+ * registry contracts.
12
+ * Each registry is deployed with a unique salt derived from the creator's address and a unique registry ID,
13
+ * ensuring deterministic addresses and preventing duplicate deployments.
14
+ */
15
+ contract TrustedIssuerRegistryFactory {
16
+ /// @notice Emitted when a new trusted issuer registry is created
17
+ event TrustedIssuerRegistryCreated(address registry);
18
+
19
+ /// @notice Emitted when registry initialization fails
20
+ error RegistryInitializationFailed(bytes reason);
21
+
22
+ /// @notice Emitted when implementation address is zero
23
+ error ImplementationIsZeroAddress();
24
+
25
+ /**
26
+ * @notice Creates a new trusted issuer registry contract using deterministic minimal proxy cloning.
27
+ * @dev If a registry with the same salt already exists, returns the existing address instead of reverting.
28
+ * Uses CREATE2 for deterministic deployment addresses. The registry is automatically initialized
29
+ * with the provided parameters after deployment.
30
+ * @param implementation The address of the trusted issuer registry implementation contract to clone
31
+ * @param uniqueRegistryId A unique identifier for this registry (combined with msg.sender to create salt)
32
+ * @param policyEngine The address of the policy engine that will manage this registry
33
+ * @param initialOwner The address that will own the newly created registry contract
34
+ * @return registryAddress The address of the created (or existing) registry contract
35
+ */
36
+ function createTrustedIssuerRegistry(
37
+ address implementation,
38
+ bytes32 uniqueRegistryId,
39
+ address policyEngine,
40
+ address initialOwner
41
+ )
42
+ public
43
+ returns (address registryAddress)
44
+ {
45
+ if (implementation == address(0)) revert ImplementationIsZeroAddress();
46
+
47
+ bytes32 salt = getSalt(msg.sender, uniqueRegistryId);
48
+ registryAddress = Clones.predictDeterministicAddress(implementation, salt);
49
+ if (registryAddress.code.length > 0) {
50
+ return registryAddress;
51
+ }
52
+
53
+ registryAddress = Clones.cloneDeterministic(implementation, salt);
54
+
55
+ try TrustedIssuerRegistry(registryAddress).initialize(policyEngine, initialOwner) {
56
+ emit TrustedIssuerRegistryCreated(registryAddress);
57
+ } catch (bytes memory reason) {
58
+ revert RegistryInitializationFailed(reason);
59
+ }
60
+ }
61
+
62
+ /**
63
+ * @notice Predicts the deterministic address where a trusted issuer registry would be deployed.
64
+ * @dev Useful for calculating registry addresses before deployment or checking if a registry already exists.
65
+ * Uses the same salt generation as createTrustedIssuerRegistry to ensure address consistency.
66
+ * @param creator The address of the account that would create the registry
67
+ * @param implementation The address of the trusted issuer registry implementation contract
68
+ * @param uniqueRegistryId The unique identifier for the registry
69
+ * @return The predicted address where the registry would be deployed
70
+ */
71
+ function predictRegistryAddress(
72
+ address creator,
73
+ address implementation,
74
+ bytes32 uniqueRegistryId
75
+ )
76
+ public
77
+ view
78
+ returns (address)
79
+ {
80
+ bytes32 salt = getSalt(creator, uniqueRegistryId);
81
+ return Clones.predictDeterministicAddress(implementation, salt);
82
+ }
83
+
84
+ /**
85
+ * @notice Generates a deterministic salt for registry deployment.
86
+ * @dev Combines the sender address and unique registry ID to create a unique salt.
87
+ * This ensures that the same creator cannot deploy multiple registries with the same ID,
88
+ * while allowing different creators to use the same registry ID.
89
+ * @param sender The address of the registry creator
90
+ * @param uniqueRegistryId The unique identifier for the registry
91
+ * @return The generated salt for deterministic deployment
92
+ */
93
+ function getSalt(address sender, bytes32 uniqueRegistryId) public pure returns (bytes32) {
94
+ return keccak256(abi.encodePacked(sender, uniqueRegistryId));
95
+ }
96
+ }
@@ -55,6 +55,12 @@ interface ICredentialRegistry is ICredentialValidator {
55
55
  bytes32 indexed ccid, bytes32 indexed credentialTypeId, uint40 previousExpiresAt, uint40 expiresAt
56
56
  );
57
57
 
58
+ /**
59
+ * @notice Returns the type and version of the credential registry.
60
+ * @return A string representing the type and version of the credential registry.
61
+ */
62
+ function typeAndVersion() external pure returns (string memory);
63
+
58
64
  /**
59
65
  * @notice Registers a credential for an account.
60
66
  *
@@ -49,7 +49,8 @@ interface ICredentialRequirements {
49
49
  * @param requirementId The identifier of the requirement.
50
50
  * @param credentialTypeIds The credential type identifier(s) that satisfy the requirement.
51
51
  * @param minValidations The minimum number of validations required for the requirement.
52
- * @param invert If the requirement is satisfied by the absence of all of the credential(s).
52
+ * @param invert If true, the requirement is satisfied when at least minValidations sources confirm the credential is
53
+ * absent. This is useful for requirements that are satisfied by the absence of a credential on multiple sources.
53
54
  */
54
55
  struct CredentialRequirementInput {
55
56
  bytes32 requirementId;