@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,89 @@
1
+ // SPDX-License-Identifier: BUSL-1.1
2
+ pragma solidity 0.8.26;
3
+
4
+ import {IPolicyEngine} from "@chainlink/policy-management/interfaces/IPolicyEngine.sol";
5
+ import {Policy} from "@chainlink/policy-management/core/Policy.sol";
6
+
7
+ /**
8
+ * @title RejectPolicy
9
+ * @notice A policy that rejects method calls if one of the addresses is on the list.
10
+ */
11
+ contract RejectPolicy is Policy {
12
+ /// @custom:storage-location erc7201:policy-management.RejectPolicy
13
+ struct RejectPolicyStorage {
14
+ /// @notice If the address is on this list, method calls will always be rejected.
15
+ mapping(address account => bool isRejected) rejectList;
16
+ }
17
+
18
+ // keccak256(abi.encode(uint256(keccak256("policy-management.RejectPolicy")) - 1)) & ~bytes32(uint256(0xff))
19
+ bytes32 private constant RejectPolicyStorageLocation =
20
+ 0x616c7ef46b4b6f234c99b5c7b6e2de9ba93899829ad2bcb8a5a37d5cddf44600;
21
+
22
+ function _getRejectPolicyStorage() private pure returns (RejectPolicyStorage storage $) {
23
+ assembly {
24
+ $.slot := RejectPolicyStorageLocation
25
+ }
26
+ }
27
+
28
+ /**
29
+ * @notice Adds the account to the reject list.
30
+ * @dev Throws if the account is already in the reject list.
31
+ * @param account The address to add to the reject list.
32
+ */
33
+ function rejectAddress(address account) public onlyOwner {
34
+ RejectPolicyStorage storage $ = _getRejectPolicyStorage();
35
+ require(!$.rejectList[account], "Account already in reject list");
36
+ $.rejectList[account] = true;
37
+ }
38
+
39
+ /**
40
+ * @notice Removes the account from the reject list.
41
+ * @dev Throws if the account is not in the reject list.
42
+ * @param account The address to remove from the reject list.
43
+ */
44
+ function unrejectAddress(address account) public onlyOwner {
45
+ RejectPolicyStorage storage $ = _getRejectPolicyStorage();
46
+ require($.rejectList[account], "Account not in reject list");
47
+ $.rejectList[account] = false;
48
+ }
49
+
50
+ /**
51
+ * @notice Checks if the account is on the reject list.
52
+ * @param account The address to check.
53
+ * @return addressRejected if the account is on the reject list, false otherwise.
54
+ */
55
+ function addressRejected(address account) public view returns (bool) {
56
+ RejectPolicyStorage storage $ = _getRejectPolicyStorage();
57
+ return $.rejectList[account];
58
+ }
59
+
60
+ /**
61
+ * @notice Function to be called by the policy engine to check if execution is allowed.
62
+ * @param parameters encoded policy parameters.
63
+ * [account(address),...] List of addresses to check for present on the reject list.
64
+ * @return result The result of the policy check.
65
+ */
66
+ function run(
67
+ address, /*caller*/
68
+ address, /*subject*/
69
+ bytes4, /*selector*/
70
+ bytes[] calldata parameters,
71
+ bytes calldata /*context*/
72
+ )
73
+ public
74
+ view
75
+ override
76
+ returns (IPolicyEngine.PolicyResult)
77
+ {
78
+ require(parameters.length >= 1, "expected at least 1 parameter");
79
+ // Gas optimization: load storage reference once
80
+ RejectPolicyStorage storage $ = _getRejectPolicyStorage();
81
+ for (uint256 i = 0; i < parameters.length; i++) {
82
+ address account = abi.decode(parameters[i], (address));
83
+ if ($.rejectList[account]) {
84
+ revert IPolicyEngine.PolicyRejected("address is on reject list");
85
+ }
86
+ }
87
+ return IPolicyEngine.PolicyResult.Continue;
88
+ }
89
+ }
@@ -0,0 +1,162 @@
1
+ // SPDX-License-Identifier: BUSL-1.1
2
+ pragma solidity 0.8.26;
3
+
4
+ import {AccessControlUpgradeable} from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol";
5
+ import {IPolicyEngine} from "@chainlink/policy-management/interfaces/IPolicyEngine.sol";
6
+ import {Policy} from "@chainlink/policy-management/core/Policy.sol";
7
+
8
+ /**
9
+ * @title RoleBasedAccessControlPolicy
10
+ * @notice A policy that allows or denies method calls based on the roles of the sender.
11
+ * @dev This policy uses OpenZeppelin's AccessControlUpgradeable to manage roles and permissions. The initial owner of
12
+ * the policy is granted the DEFAULT_ADMIN_ROLE, which allows them to grant and revoke any role. Users can also make use
13
+ * of roleAdmin to grant account permission to manage specific roles.
14
+ *
15
+ * # Usage
16
+ * Grant the operation allowance to a role. This will allow any account with that role to perform the operation.
17
+ * ```
18
+ * bytes32 role = keccak256(abi.encode("roleName"));
19
+ * grantOperationAllowanceToRole(Token.transfer.selector, role);
20
+ * grantRole(role, account);
21
+ * ```
22
+ */
23
+ contract RoleBasedAccessControlPolicy is Policy, AccessControlUpgradeable {
24
+ /**
25
+ * @notice Emitted when the operation allowance is granted to a role.
26
+ * @param operation The operation allowance to be granted.
27
+ * @param role The role that is granted the operation allowance.
28
+ */
29
+ event OperationAllowanceGrantedToRole(bytes4 operation, bytes32 role);
30
+ /**
31
+ * @notice Emitted when the operation allowance is removed from a role.
32
+ * @param operation The operation allowance to be removed.
33
+ * @param role The role that is removed the operation allowance.
34
+ */
35
+ event OperationAllowanceRemovedFromRole(bytes4 operation, bytes32 role);
36
+
37
+ /// @custom:storage-location erc7201:policy-management.RoleBasedAccessControlPolicy
38
+ struct RoleBasedAccessControlPolicyStorage {
39
+ /// @notice The mapping of operation allowances to roles. Each operation can have multiple roles that are allowed to
40
+ /// perform it.
41
+ mapping(bytes4 operation => bytes32[] roles) rolesByOperation;
42
+ }
43
+
44
+ // keccak256(abi.encode(uint256(keccak256("policy-management.RoleBasedAccessControlPolicy")) - 1)) &
45
+ // ~bytes32(uint256(0xff))
46
+ bytes32 private constant RoleBasedAccessControlPolicyStorageLocation =
47
+ 0xcebcf55b92595d67a9ed71c4c21ff1d547eba811760bfc05d1f636b4330cd900;
48
+
49
+ function _getRoleBasedAccessControlPolicyStorage()
50
+ private
51
+ pure
52
+ returns (RoleBasedAccessControlPolicyStorage storage $)
53
+ {
54
+ assembly {
55
+ $.slot := RoleBasedAccessControlPolicyStorageLocation
56
+ }
57
+ }
58
+
59
+ /**
60
+ * @notice Configures the policy by granting the initial owner the `DEFAULT_ADMIN_ROLE`.
61
+ * @dev No parameters are expected or decoded from the input. The owner is granted the `DEFAULT_ADMIN_ROLE`
62
+ * using OpenZeppelin's AccessControl mechanism. This role allows the owner to grant and revoke other roles.
63
+ */
64
+ function configure(bytes calldata) internal override {
65
+ address owner = owner();
66
+ bool granted = _grantRole(DEFAULT_ADMIN_ROLE, owner);
67
+ require(granted, "failed to grant DEFAULT_ADMIN_ROLE");
68
+ }
69
+
70
+ /**
71
+ * @notice Grants the operation allowance to a role. This will allow any account with that role to perform the
72
+ * operation.
73
+ * @param operation The operation allowance to be granted.
74
+ * @param role The role that is granted the operation allowance.
75
+ */
76
+ function grantOperationAllowanceToRole(bytes4 operation, bytes32 role) public onlyOwner {
77
+ RoleBasedAccessControlPolicyStorage storage $ = _getRoleBasedAccessControlPolicyStorage();
78
+ uint256 length = $.rolesByOperation[operation].length;
79
+ for (uint256 i = 0; i < length; i++) {
80
+ if ($.rolesByOperation[operation][i] == role) {
81
+ revert("Role already has operation allowance");
82
+ }
83
+ }
84
+ $.rolesByOperation[operation].push(role);
85
+ emit OperationAllowanceGrantedToRole(operation, role);
86
+ }
87
+
88
+ /**
89
+ * @notice Removes the operation allowance from a role. This will revoke the ability of any account with that role to
90
+ * perform the operation.
91
+ * @param operation The operation allowance to be removed.
92
+ * @param role The role that is removed the operation allowance.
93
+ */
94
+ function removeOperationAllowanceFromRole(bytes4 operation, bytes32 role) public onlyOwner {
95
+ RoleBasedAccessControlPolicyStorage storage $ = _getRoleBasedAccessControlPolicyStorage();
96
+ uint256 length = $.rolesByOperation[operation].length;
97
+ for (uint256 i = 0; i < length; i++) {
98
+ if ($.rolesByOperation[operation][i] == role) {
99
+ $.rolesByOperation[operation][i] = $.rolesByOperation[operation][length - 1];
100
+ $.rolesByOperation[operation].pop();
101
+ emit OperationAllowanceRemovedFromRole(operation, role);
102
+ return;
103
+ }
104
+ }
105
+ revert("Role does not have operation allowance");
106
+ }
107
+
108
+ /**
109
+ * @notice Checks if the account has any of the roles that are allowed to perform the operation.
110
+ * @param operation The function selector of the operation.
111
+ * @param account The address of the account to check.
112
+ * @return isAllowed true if the account has any of the roles that are allowed to perform the operation, false
113
+ * otherwise.
114
+ */
115
+ function hasAllowedRole(bytes4 operation, address account) public view returns (bool) {
116
+ RoleBasedAccessControlPolicyStorage storage $ = _getRoleBasedAccessControlPolicyStorage();
117
+ bytes32[] memory roles = $.rolesByOperation[operation];
118
+ uint256 length = roles.length;
119
+ for (uint256 i = 0; i < length; i++) {
120
+ if (hasRole(roles[i], account)) {
121
+ return true;
122
+ }
123
+ }
124
+ return false;
125
+ }
126
+
127
+ /**
128
+ * @notice Function to be called by the policy engine to check if execution is allowed.
129
+ * @param caller The address of the sender.
130
+ * @param selector The function selector of the method being called.
131
+ * @param parameters None expected for this policy.
132
+ * @return result The result of the policy check.
133
+ */
134
+ function run(
135
+ address caller,
136
+ address, /*subject*/
137
+ bytes4 selector,
138
+ bytes[] calldata parameters,
139
+ bytes calldata /*context*/
140
+ )
141
+ public
142
+ view
143
+ override
144
+ returns (IPolicyEngine.PolicyResult)
145
+ {
146
+ // expected parameters: none
147
+ // solhint-disable-next-line gas-custom-errors
148
+ if (parameters.length != 0) {
149
+ revert IPolicyEngine.InvalidConfiguration("expected 0 parameters");
150
+ }
151
+
152
+ if (!hasAllowedRole(selector, caller)) {
153
+ revert IPolicyEngine.PolicyRejected("caller lacks required role for operation");
154
+ }
155
+
156
+ return IPolicyEngine.PolicyResult.Continue;
157
+ }
158
+
159
+ function supportsInterface(bytes4 interfaceId) public view override(Policy, AccessControlUpgradeable) returns (bool) {
160
+ return super.supportsInterface(interfaceId);
161
+ }
162
+ }
@@ -0,0 +1,271 @@
1
+ // SPDX-License-Identifier: BUSL-1.1
2
+ pragma solidity 0.8.26;
3
+
4
+ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
5
+ import {AggregatorV3Interface} from "@chainlink/contracts/src/v0.8/shared/interfaces/AggregatorV3Interface.sol";
6
+ import {IPolicyEngine} from "@chainlink/policy-management/interfaces/IPolicyEngine.sol";
7
+ import {Policy} from "@chainlink/policy-management/core/Policy.sol";
8
+
9
+ /**
10
+ * @title SecureMintPolicy
11
+ * @notice A policy that ensures new token minting does not exceed available reserves.
12
+ * @dev Extends Chainlink's `Policy` reference implementation.
13
+ * This policy checks if minting a specified amount would cause the total supply of a token to exceed the reserve value
14
+ * provided by a Chainlink price feed.
15
+ *
16
+ * ## Core Parameters
17
+ *
18
+ * - `s_reservesFeed`: The Chainlink AggregatorV3 price feed contract address used to retrieve the latest reserve value.
19
+ * - `s_reserveMarginMode`: Specifies how the reserve margin is calculated. A positive reserve margin means that the
20
+ * reserves must exceed the total supply of the token by a certain amount, while a negative reserve margin means that
21
+ * the reserves can be less than the total supply by a certain amount.
22
+ * - `s_reserveMarginAmount`: The margin amount used in the reserve margin calculation. If `s_reserveMarginMode` is
23
+ * percentage-based, this represents a hundredth of a percent.
24
+ * - `s_maxStalenessSeconds`: The maximum staleness seconds for the reserve price feed. 0 means no staleness check.
25
+ *
26
+ * ## Dependencies:
27
+ * - **AggregatorV3Interface**: Used to retrieve the latest reserve value.
28
+ * - **IERC20**: The policy assumes the `subject` contract implements ERC-20 and supports `totalSupply()`.
29
+ */
30
+ contract SecureMintPolicy is Policy {
31
+ /**
32
+ * @notice Emitted when the PoR feed contract address is set.
33
+ * @param reservesFeed The new Chainlink AggregatorV3 price feed contract address.
34
+ */
35
+ event ReservesFeedSet(address reservesFeed);
36
+ /**
37
+ * @notice Emitted when the margin mode is set.
38
+ * @param mode The new margin mode.
39
+ * @param amount The new margin amount.
40
+ */
41
+ event ReserveMarginSet(ReserveMarginMode mode, uint256 amount);
42
+ /**
43
+ * @notice Emitted when the max staleness seconds is set.
44
+ * @param maxStalenessSeconds The new max staleness seconds. 0 means no staleness check.
45
+ */
46
+ event MaxStalenessSecondsSet(uint256 maxStalenessSeconds);
47
+
48
+ /**
49
+ * @notice The ReserveMarginMode enum specifies how the reserve margin is calculated. A positive reserve margin means
50
+ * that the reserves must exceed the total supply of the token by a certain amount, while a negative reserve margin
51
+ * means that the reserves can be less than the total supply by a certain amount.
52
+ * @param None No margin is applied. Total mintable amount is equal to reserves.
53
+ * @param PositivePercentage A positive percentage margin is applied. Total mintable amount is
54
+ * reserves * (BASIS_POINTS - margin) / BASIS_POINTS.
55
+ * @param PositiveAbsolute A positive absolute margin is applied. Total mintable amount is reserves - margin.
56
+ * @param NegativePercentage A negative percentage margin is applied. Total mintable amount is
57
+ * reserves * (BASIS_POINTS + margin) / BASIS_POINTS.
58
+ * @param NegativeAbsolute A negative absolute margin is applied. Total mintable amount is reserves + margin.
59
+ */
60
+ enum ReserveMarginMode {
61
+ None,
62
+ PositivePercentage,
63
+ PositiveAbsolute,
64
+ NegativePercentage,
65
+ NegativeAbsolute
66
+ }
67
+
68
+ /// @notice Basis points scale used for percentage calculations (1 basis point = 0.01%)
69
+ uint256 private constant BASIS_POINTS = 10_000;
70
+
71
+ /// @custom:storage-location erc7201:policy-management.SecureMintPolicy
72
+ struct SecureMintPolicyStorage {
73
+ /// @notice Chainlink AggregatorV3 price feed contract address.
74
+ AggregatorV3Interface reservesFeed;
75
+ /// @notice Specifies how the reserve margin is calculated.
76
+ ReserveMarginMode reserveMarginMode;
77
+ /// @notice The margin amount used in the reserve margin calculation. If reserveMarginMode is percentage-based, this
78
+ /// represents a hundredth of a percent.
79
+ uint256 reserveMarginAmount;
80
+ /// @notice The maximum staleness seconds for the reserve price feed. 0 means no staleness check.
81
+ uint256 maxStalenessSeconds;
82
+ }
83
+
84
+ // keccak256(abi.encode(uint256(keccak256("policy-management.SecureMintPolicy")) - 1)) & ~bytes32(uint256(0xff))
85
+ bytes32 private constant SecureMintPolicyStorageLocation =
86
+ 0xce7aed3b7d424da898685a1d407ca1286fb1f81e854eae77e5e2276c63944900;
87
+
88
+ function _getSecureMintPolicyStorage() private pure returns (SecureMintPolicyStorage storage $) {
89
+ assembly {
90
+ $.slot := SecureMintPolicyStorageLocation
91
+ }
92
+ }
93
+
94
+ /**
95
+ * @notice Configures the policy by setting the reserves feed and margin.
96
+ * @param parameters ABI-encoded bytes containing [address reservesFeed, ReserveMarginMode reserveMarginMode, uint256
97
+ * marginAmount, uint256 maxStalenessSeconds].
98
+ */
99
+ function configure(bytes calldata parameters) internal override {
100
+ (address reservesFeed, ReserveMarginMode reserveMarginMode, uint256 marginAmount, uint256 maxStalenessSeconds) =
101
+ abi.decode(parameters, (address, ReserveMarginMode, uint256, uint256));
102
+
103
+ SecureMintPolicyStorage storage $ = _getSecureMintPolicyStorage();
104
+ $.reservesFeed = AggregatorV3Interface(reservesFeed);
105
+ emit ReservesFeedSet(reservesFeed);
106
+
107
+ _setReserveMargin(reserveMarginMode, marginAmount);
108
+
109
+ $.maxStalenessSeconds = maxStalenessSeconds;
110
+ emit MaxStalenessSecondsSet(maxStalenessSeconds);
111
+ }
112
+
113
+ /**
114
+ * @notice Updates the Chainlink price feed used for reserve validation.
115
+ * @dev Throws when address is the same as the current one.
116
+ * @param reservesFeed The new Chainlink AggregatorV3 price feed contract address.
117
+ */
118
+ function setReservesFeed(address reservesFeed) external onlyOwner {
119
+ SecureMintPolicyStorage storage $ = _getSecureMintPolicyStorage(); // Gas optimization: single storage reference
120
+ require(reservesFeed != address($.reservesFeed), "feed same as current");
121
+ $.reservesFeed = AggregatorV3Interface(reservesFeed);
122
+ emit ReservesFeedSet(reservesFeed);
123
+ }
124
+
125
+ function _setReserveMargin(ReserveMarginMode mode, uint256 amount) internal {
126
+ require(uint256(mode) <= 4, "Invalid margin mode");
127
+ if (mode == ReserveMarginMode.PositivePercentage || mode == ReserveMarginMode.NegativePercentage) {
128
+ require(amount <= BASIS_POINTS, "margin must be <= BASIS_POINTS for percentage modes");
129
+ } else if (mode == ReserveMarginMode.PositiveAbsolute || mode == ReserveMarginMode.NegativeAbsolute) {
130
+ require(amount > 0, "margin must be > 0 for absolute modes");
131
+ }
132
+ SecureMintPolicyStorage storage $ = _getSecureMintPolicyStorage(); // Gas optimization: single storage reference
133
+ $.reserveMarginMode = mode;
134
+ $.reserveMarginAmount = amount;
135
+ emit ReserveMarginSet(mode, amount);
136
+ }
137
+
138
+ /**
139
+ * @notice Updates the reserve margin mode and amount.
140
+ * @dev Throws when mode is invalid or both the mode and amount are the same as the current values.
141
+ * @param mode The new reserve margin mode.
142
+ * @param amount The new reserve margin amount. When mode is percentage-based, this represents a hundredth of a
143
+ * percent.
144
+ * @dev Precision Warning: When using percentage-based modes (PositivePercentage/NegativePercentage),
145
+ * be aware that very small reserve values combined with high margin percentages may result in
146
+ * zero mintable supply due to integer division rounding. Consider the minimum expected reserve
147
+ * value and price feed decimals when setting percentage margins.
148
+ *
149
+ * For feeds with low precision or small values, consider using absolute margin modes instead.
150
+ */
151
+ function setReserveMargin(ReserveMarginMode mode, uint256 amount) external onlyOwner {
152
+ SecureMintPolicyStorage storage $ = _getSecureMintPolicyStorage(); // Gas optimization: single storage reference
153
+ require(mode != $.reserveMarginMode || amount != $.reserveMarginAmount, "margin same as current");
154
+ _setReserveMargin(mode, amount);
155
+ }
156
+
157
+ /**
158
+ * @notice Updates the maximum staleness seconds for the reserve price feed.
159
+ * @dev Throws when the value is the same as the current value.
160
+ * @param value The new maximum staleness seconds. 0 means no staleness check.
161
+ */
162
+ function setMaxStalenessSeconds(uint256 value) external onlyOwner {
163
+ SecureMintPolicyStorage storage $ = _getSecureMintPolicyStorage(); // Gas optimization: single storage reference
164
+ require(value != $.maxStalenessSeconds, "value same as current");
165
+ $.maxStalenessSeconds = value;
166
+ emit MaxStalenessSecondsSet(value);
167
+ }
168
+
169
+ /**
170
+ * @notice Returns the current Chainlink price feed used for reserve validation.
171
+ * @return address The address of the current Chainlink AggregatorV3 price feed contract.
172
+ */
173
+ function reservesFeed() external view returns (address) {
174
+ SecureMintPolicyStorage storage $ = _getSecureMintPolicyStorage();
175
+ return address($.reservesFeed);
176
+ }
177
+
178
+ /**
179
+ * @notice Returns the current margin mode.
180
+ * @return reserveMarginMode The current margin mode.
181
+ */
182
+ function reserveMarginMode() external view returns (ReserveMarginMode) {
183
+ SecureMintPolicyStorage storage $ = _getSecureMintPolicyStorage();
184
+ return $.reserveMarginMode;
185
+ }
186
+
187
+ /**
188
+ * @notice Returns the current margin amount.
189
+ * @return reserveMarginAmount The current margin amount.
190
+ */
191
+ function reserveMarginAmount() external view returns (uint256) {
192
+ SecureMintPolicyStorage storage $ = _getSecureMintPolicyStorage();
193
+ return $.reserveMarginAmount;
194
+ }
195
+
196
+ /**
197
+ * @notice Returns the current max staleness seconds.
198
+ * @return maxStalenessSeconds The current max staleness seconds.
199
+ */
200
+ function maxStalenessSeconds() external view returns (uint256) {
201
+ SecureMintPolicyStorage storage $ = _getSecureMintPolicyStorage();
202
+ return $.maxStalenessSeconds;
203
+ }
204
+
205
+ /**
206
+ * @notice Calculates the total mintable amount based on the reserves and reserve margin mode.
207
+ * @param reserves The current reserves value.
208
+ * @return The total mintable amount.
209
+ */
210
+ function totalMintableSupply(uint256 reserves) internal view returns (uint256) {
211
+ SecureMintPolicyStorage storage $ = _getSecureMintPolicyStorage(); // Gas optimization: single storage reference
212
+ if ($.reserveMarginMode == ReserveMarginMode.None) {
213
+ return reserves;
214
+ } else if ($.reserveMarginMode == ReserveMarginMode.PositivePercentage) {
215
+ // WARNING: May round to zero for very small reserves with high margins
216
+ // e.g., reserves=1, margin=9999 → 1 * 1 / BASIS_POINTS = 0
217
+ return reserves * (BASIS_POINTS - $.reserveMarginAmount) / BASIS_POINTS;
218
+ } else if ($.reserveMarginMode == ReserveMarginMode.PositiveAbsolute) {
219
+ if (reserves < $.reserveMarginAmount) {
220
+ return 0;
221
+ }
222
+ return reserves - $.reserveMarginAmount;
223
+ } else if ($.reserveMarginMode == ReserveMarginMode.NegativePercentage) {
224
+ return reserves * (BASIS_POINTS + $.reserveMarginAmount) / BASIS_POINTS;
225
+ } else if ($.reserveMarginMode == ReserveMarginMode.NegativeAbsolute) {
226
+ return reserves + $.reserveMarginAmount;
227
+ }
228
+ revert("Invalid margin mode");
229
+ }
230
+
231
+ /**
232
+ * @notice Function to be called by the policy engine to check if execution is allowed.
233
+ * @param subject The address of the protected contract.
234
+ * @param parameters [to(address),amount(uint256)] The parameters of the called method.
235
+ * @return result The result of the policy check.
236
+ */
237
+ function run(
238
+ address, /*caller*/
239
+ address subject, /*subject*/
240
+ bytes4, /*selector*/
241
+ bytes[] calldata parameters,
242
+ bytes calldata /*context*/
243
+ )
244
+ public
245
+ view
246
+ override
247
+ returns (IPolicyEngine.PolicyResult)
248
+ {
249
+ require(parameters.length == 1, "expected 1 parameter");
250
+ uint256 amount = abi.decode(parameters[0], (uint256));
251
+
252
+ SecureMintPolicyStorage storage $ = _getSecureMintPolicyStorage(); // Gas optimization: single storage reference
253
+ (, int256 reserve,, uint256 updatedAt,) = $.reservesFeed.latestRoundData();
254
+
255
+ // reserve is not expected to be negative
256
+ if (reserve < 0) {
257
+ revert IPolicyEngine.PolicyRejected("reserve value is negative");
258
+ }
259
+
260
+ if ($.maxStalenessSeconds > 0 && block.timestamp - updatedAt > $.maxStalenessSeconds) {
261
+ revert IPolicyEngine.PolicyRejected("reserve data is stale");
262
+ }
263
+
264
+ IERC20 token = IERC20(subject);
265
+ if (amount + token.totalSupply() > totalMintableSupply(uint256(reserve))) {
266
+ revert IPolicyEngine.PolicyRejected("mint would exceed available reserves");
267
+ }
268
+
269
+ return IPolicyEngine.PolicyResult.Continue;
270
+ }
271
+ }
@@ -0,0 +1,133 @@
1
+ // SPDX-License-Identifier: BUSL-1.1
2
+ pragma solidity 0.8.26;
3
+
4
+ import {IPolicyEngine} from "@chainlink/policy-management/interfaces/IPolicyEngine.sol";
5
+ import {Policy} from "@chainlink/policy-management/core/Policy.sol";
6
+
7
+ /**
8
+ * @title VolumePolicy
9
+ * @notice A policy that validates transaction amount parameters against configured minimum and maximum limits.
10
+ * @dev This policy operates on the amount argument passed to protected functions, not on actual token transfers.
11
+ * For tokens with transfer fees, deflationary mechanisms, or other non-standard behaviors, the actual transferred
12
+ * amount may differ from the amount parameter this policy validates against.
13
+ */
14
+ contract VolumePolicy is Policy {
15
+ /**
16
+ * @notice Emitted when the maximum volume limit is set.
17
+ * @param maxAmount The maximum amount parameter limit. If set to 0, there is no maximum limit.
18
+ */
19
+ event MaxVolumeSet(uint256 maxAmount);
20
+ /**
21
+ * @notice Emitted when the minimum volume limit is set.
22
+ * @param minAmount The minimum amount parameter limit. If set to 0, there is no minimum limit.
23
+ */
24
+ event MinVolumeSet(uint256 minAmount);
25
+
26
+ /// @custom:storage-location erc7201:policy-management.VolumePolicy
27
+ struct VolumePolicyStorage {
28
+ /// @notice The maximum amount parameter limit. If set to 0, there is no maximum limit.
29
+ uint256 maxAmount;
30
+ /// @notice The minimum amount parameter limit. If set to 0, there is no minimum limit.
31
+ uint256 minAmount;
32
+ }
33
+
34
+ // keccak256(abi.encode(uint256(keccak256("policy-management.VolumePolicy")) - 1)) & ~bytes32(uint256(0xff))
35
+ bytes32 private constant VolumePolicyStorageLocation =
36
+ 0x5bb13fe9284039d01609a8e5ed913ad65a3c270b5906b4fd9932815137778200;
37
+
38
+ function _getVolumePolicyStorage() private pure returns (VolumePolicyStorage storage $) {
39
+ assembly {
40
+ $.slot := VolumePolicyStorageLocation
41
+ }
42
+ }
43
+
44
+ /**
45
+ * @notice Configures the policy by setting minimum and maximum amount parameter limits.
46
+ * @param parameters ABI-encoded bytes containing two `uint256` values: the min and max amount limits.
47
+ * - `minAmount`: The minimum amount parameter limit, 0 for no minimum.
48
+ * - `maxAmount`: The maximum amount parameter limit, 0 for no maximum.
49
+ * @dev These limits apply to function parameters, not actual token transfer amounts.
50
+ */
51
+ function configure(bytes calldata parameters) internal override {
52
+ VolumePolicyStorage storage $ = _getVolumePolicyStorage();
53
+ ($.minAmount, $.maxAmount) = abi.decode(parameters, (uint256, uint256));
54
+ require($.maxAmount > $.minAmount || $.maxAmount == 0, "maxAmount must be greater than minAmount");
55
+ }
56
+
57
+ /**
58
+ * @notice Sets the maximum amount parameter limit for the policy.
59
+ * @param maxAmount The maximum amount parameter limit.
60
+ * @dev Reverts if the new max amount is less than or equal to the min amount (unless maxAmount is 0),
61
+ * or if it is the same as the current max amount.
62
+ */
63
+ function setMax(uint256 maxAmount) public onlyOwner {
64
+ VolumePolicyStorage storage $ = _getVolumePolicyStorage();
65
+ require(maxAmount > $.minAmount || maxAmount == 0, "maxAmount must be greater than minAmount");
66
+ require(maxAmount != $.maxAmount, "maxAmount cannot be the same as current maxAmount");
67
+ $.maxAmount = maxAmount;
68
+ emit MaxVolumeSet(maxAmount);
69
+ }
70
+
71
+ /**
72
+ * @notice Gets the current maximum amount parameter limit for the policy.
73
+ * @return maxAmount The current maximum amount parameter limit. If 0, there is no maximum limit.
74
+ */
75
+ function getMax() public view returns (uint256) {
76
+ VolumePolicyStorage storage $ = _getVolumePolicyStorage();
77
+ return $.maxAmount;
78
+ }
79
+
80
+ /**
81
+ * @notice Sets the minimum amount parameter limit for the policy.
82
+ * @param minAmount The minimum amount parameter limit.
83
+ * @dev Reverts if the new min amount is greater than or equal to the max amount (unless maxAmount is 0),
84
+ * or if it is the same as the current min amount.
85
+ */
86
+ function setMin(uint256 minAmount) public onlyOwner {
87
+ VolumePolicyStorage storage $ = _getVolumePolicyStorage();
88
+ require(minAmount < $.maxAmount || $.maxAmount == 0, "minAmount must be less than maxAmount");
89
+ require(minAmount != $.minAmount, "minAmount cannot be the same as current minAmount");
90
+ $.minAmount = minAmount;
91
+ emit MinVolumeSet(minAmount);
92
+ }
93
+
94
+ /**
95
+ * @notice Gets the current minimum amount parameter limit for the policy.
96
+ * @return minAmount The current minimum amount parameter limit. If 0, there is no minimum limit.
97
+ */
98
+ function getMin() public view returns (uint256) {
99
+ VolumePolicyStorage storage $ = _getVolumePolicyStorage();
100
+ return $.minAmount;
101
+ }
102
+
103
+ /**
104
+ * @notice Function called by the policy engine to validate amount parameters against configured limits.
105
+ * @param parameters [amount(uint256)] The parameters of the called method.
106
+ * @return result The result of the policy check.
107
+ * @dev This policy validates the amount argument passed to a protected function, not actual token transfers.
108
+ * For tokens with fees, rebasing, or other non-standard behaviors, the actual transferred amount may differ.
109
+ */
110
+ function run(
111
+ address, /* caller */
112
+ address, /* subject */
113
+ bytes4, /*selector*/
114
+ bytes[] calldata parameters,
115
+ bytes calldata /* context */
116
+ )
117
+ public
118
+ view
119
+ override
120
+ returns (IPolicyEngine.PolicyResult)
121
+ {
122
+ require(parameters.length == 1, "expected 1 parameter");
123
+ uint256 amount = abi.decode(parameters[0], (uint256));
124
+
125
+ // Gas optimization: load storage reference once
126
+ VolumePolicyStorage storage $ = _getVolumePolicyStorage();
127
+ if (($.maxAmount != 0 && amount > $.maxAmount) || amount < $.minAmount) {
128
+ revert IPolicyEngine.PolicyRejected("amount outside allowed volume limits");
129
+ }
130
+
131
+ return IPolicyEngine.PolicyResult.Continue;
132
+ }
133
+ }