@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,442 @@
1
+ // SPDX-License-Identifier: BUSL-1.1
2
+ pragma solidity 0.8.26;
3
+
4
+ import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
5
+ import {IPolicyEngine} from "@chainlink/policy-management/interfaces/IPolicyEngine.sol";
6
+ import {PolicyEngine} from "@chainlink/policy-management/core/PolicyEngine.sol";
7
+ import {ERC3643MintBurnExtractor} from "@chainlink/policy-management/extractors/ERC3643MintBurnExtractor.sol";
8
+ import {SecureMintPolicy} from "@chainlink/policy-management/policies/SecureMintPolicy.sol";
9
+ import {MockToken} from "../helpers/MockToken.sol";
10
+ import {MockAggregatorV3} from "../helpers/MockAggregatorV3.sol";
11
+ import {BaseProxyTest} from "../helpers/BaseProxyTest.sol";
12
+
13
+ contract SecureMintPolicyTest is BaseProxyTest {
14
+ PolicyEngine public policyEngine;
15
+ SecureMintPolicy public policy;
16
+ ERC3643MintBurnExtractor public extractor;
17
+ MockToken public token;
18
+ MockAggregatorV3 public porFeed;
19
+ address public deployer;
20
+ address public recipient;
21
+
22
+ function setUp() public {
23
+ deployer = makeAddr("deployer");
24
+ recipient = makeAddr("recipient");
25
+
26
+ vm.startPrank(deployer, deployer);
27
+
28
+ policyEngine = _deployPolicyEngine(true, deployer);
29
+
30
+ token = MockToken(_deployMockToken(address(policyEngine)));
31
+
32
+ porFeed = new MockAggregatorV3(42 ether, 18);
33
+
34
+ extractor = new ERC3643MintBurnExtractor();
35
+ bytes32[] memory parameterOutputFormat = new bytes32[](1);
36
+ parameterOutputFormat[0] = extractor.PARAM_AMOUNT();
37
+
38
+ SecureMintPolicy policyImpl = new SecureMintPolicy();
39
+ policy = SecureMintPolicy(
40
+ _deployPolicy(
41
+ address(policyImpl),
42
+ address(policyEngine),
43
+ deployer,
44
+ abi.encode(address(porFeed), SecureMintPolicy.ReserveMarginMode.None, 0, 0)
45
+ )
46
+ );
47
+
48
+ policyEngine.setExtractor(MockToken.mint.selector, address(extractor));
49
+ policyEngine.addPolicy(address(token), MockToken.mint.selector, address(policy), parameterOutputFormat);
50
+
51
+ vm.warp(1737583804);
52
+ }
53
+
54
+ function test_initialize_configEvents_succeeds() public {
55
+ SecureMintPolicy policyImpl = new SecureMintPolicy();
56
+
57
+ // Set the reserves feed to a new address
58
+ vm.expectEmit();
59
+ emit SecureMintPolicy.ReservesFeedSet(address(porFeed));
60
+ vm.expectEmit();
61
+ emit SecureMintPolicy.ReserveMarginSet(SecureMintPolicy.ReserveMarginMode.None, 0);
62
+ vm.expectEmit();
63
+ emit SecureMintPolicy.MaxStalenessSecondsSet(600);
64
+ policy = SecureMintPolicy(
65
+ _deployPolicy(
66
+ address(policyImpl),
67
+ address(policyEngine),
68
+ deployer,
69
+ abi.encode(address(porFeed), SecureMintPolicy.ReserveMarginMode.None, 0, 600)
70
+ )
71
+ );
72
+ }
73
+
74
+ function test_setReservesFeed_succeeds() public {
75
+ MockAggregatorV3 newPorFeed = new MockAggregatorV3(42 ether, 18);
76
+
77
+ vm.startPrank(deployer, deployer);
78
+
79
+ // Set the reserves feed to a new address
80
+ vm.expectEmit();
81
+ emit SecureMintPolicy.ReservesFeedSet(address(newPorFeed));
82
+ policy.setReservesFeed(address(newPorFeed));
83
+ vm.assertEq(address(policy.reservesFeed()), address(newPorFeed));
84
+ }
85
+
86
+ function test_setReservesFeed_notOwner_reverts() public {
87
+ MockAggregatorV3 newPorFeed = new MockAggregatorV3(42 ether, 18);
88
+
89
+ vm.startPrank(recipient, recipient);
90
+
91
+ vm.expectPartialRevert(OwnableUpgradeable.OwnableUnauthorizedAccount.selector);
92
+ policy.setReservesFeed(address(newPorFeed));
93
+ }
94
+
95
+ function test_setReservesFeed_sameAsCurrent_reverts() public {
96
+ vm.startPrank(deployer, deployer);
97
+
98
+ // Set the reserves feed to the same address
99
+ vm.expectRevert("feed same as current");
100
+ policy.setReservesFeed(address(porFeed));
101
+ }
102
+
103
+ function test_setReserveMargin_succeeds() public {
104
+ vm.startPrank(deployer, deployer);
105
+
106
+ vm.expectEmit();
107
+ emit SecureMintPolicy.ReserveMarginSet(SecureMintPolicy.ReserveMarginMode.PositivePercentage, 1000);
108
+ policy.setReserveMargin(SecureMintPolicy.ReserveMarginMode.PositivePercentage, 1000);
109
+ assertEq(uint256(policy.reserveMarginMode()), 1);
110
+ assertEq(policy.reserveMarginAmount(), 1000);
111
+ }
112
+
113
+ function test_setReserveMargin_notOwner_reverts() public {
114
+ vm.startPrank(recipient, recipient);
115
+
116
+ vm.expectPartialRevert(OwnableUpgradeable.OwnableUnauthorizedAccount.selector);
117
+ policy.setReserveMargin(SecureMintPolicy.ReserveMarginMode.PositivePercentage, 1000);
118
+ }
119
+
120
+ function test_setReserveMargin_sameAsCurrent_reverts() public {
121
+ vm.startPrank(deployer, deployer);
122
+
123
+ vm.expectRevert("margin same as current");
124
+ policy.setReserveMargin(SecureMintPolicy.ReserveMarginMode.None, 0);
125
+ }
126
+
127
+ function test_setReserveMargin_positivePercentageExceedsMax_reverts() public {
128
+ vm.startPrank(deployer, deployer);
129
+
130
+ vm.expectRevert("margin must be <= BASIS_POINTS for percentage modes");
131
+ policy.setReserveMargin(SecureMintPolicy.ReserveMarginMode.PositivePercentage, 10001);
132
+ }
133
+
134
+ function test_setReserveMargin_negativePercentageExceedsMax_reverts() public {
135
+ vm.startPrank(deployer, deployer);
136
+
137
+ vm.expectRevert("margin must be <= BASIS_POINTS for percentage modes");
138
+ policy.setReserveMargin(SecureMintPolicy.ReserveMarginMode.NegativePercentage, 10001);
139
+ }
140
+
141
+ function test_setReserveMargin_positiveAbsoluteZeroAmount_reverts() public {
142
+ vm.startPrank(deployer, deployer);
143
+
144
+ vm.expectRevert("margin must be > 0 for absolute modes");
145
+ policy.setReserveMargin(SecureMintPolicy.ReserveMarginMode.PositiveAbsolute, 0);
146
+ }
147
+
148
+ function test_setReserveMargin_negativeAbsoluteZeroAmount_reverts() public {
149
+ vm.startPrank(deployer, deployer);
150
+
151
+ vm.expectRevert("margin must be > 0 for absolute modes");
152
+ policy.setReserveMargin(SecureMintPolicy.ReserveMarginMode.NegativeAbsolute, 0);
153
+ }
154
+
155
+ function test_setMaxStalenessSeconds_succeeds() public {
156
+ vm.startPrank(deployer, deployer);
157
+
158
+ vm.expectEmit();
159
+ emit SecureMintPolicy.MaxStalenessSecondsSet(600);
160
+ policy.setMaxStalenessSeconds(600);
161
+ assertEq(policy.maxStalenessSeconds(), 600);
162
+ }
163
+
164
+ function test_setMaxStalenessSeconds_notOwner_reverts() public {
165
+ vm.startPrank(recipient, recipient);
166
+
167
+ vm.expectPartialRevert(OwnableUpgradeable.OwnableUnauthorizedAccount.selector);
168
+ policy.setMaxStalenessSeconds(600);
169
+ }
170
+
171
+ function test_setMaxStalenessSeconds_sameAsCurrent_reverts() public {
172
+ vm.startPrank(deployer, deployer);
173
+
174
+ vm.expectRevert("value same as current");
175
+ policy.setMaxStalenessSeconds(0);
176
+ }
177
+
178
+ function test_mint_marginModeNone_succeeds() public {
179
+ vm.startPrank(deployer, deployer);
180
+
181
+ // 0 + 40 <= 42, succeeds
182
+ token.mint(recipient, 40 ether);
183
+ assertEq(token.balanceOf(recipient), 40 ether);
184
+
185
+ // 40 + 2 <= 42, succeeds
186
+ token.mint(recipient, 2 ether);
187
+ assertEq(token.balanceOf(recipient), 42 ether);
188
+ }
189
+
190
+ function test_mint_marginModeNone_reverts() public {
191
+ vm.startPrank(deployer, deployer);
192
+
193
+ // 0 + 40 <= 42, succeeds
194
+ token.mint(recipient, 40 ether);
195
+ assertEq(token.balanceOf(recipient), 40 ether);
196
+
197
+ // 40 + 3 > 42, reverts
198
+ vm.expectRevert(
199
+ _encodeRejectedRevert(MockToken.mint.selector, address(policy), "mint would exceed available reserves")
200
+ );
201
+ token.mint(recipient, 3 ether);
202
+
203
+ // 40 + 2 <= 42, succeeds
204
+ token.mint(recipient, 2 ether);
205
+ assertEq(token.balanceOf(recipient), 42 ether);
206
+ }
207
+
208
+ function test_mint_marginModePositivePercentage_succeeds() public {
209
+ vm.startPrank(deployer, deployer);
210
+
211
+ // set margin (mintable amount = 42 ether * (1 - 20%) = 33.6 ether)
212
+ policy.setReserveMargin(SecureMintPolicy.ReserveMarginMode.PositivePercentage, 2000); // 20%
213
+
214
+ // 0 + 30 <= 33.6, succeeds
215
+ token.mint(recipient, 30 ether);
216
+ assertEq(token.balanceOf(recipient), 30 ether);
217
+
218
+ // 30 + 3.6 <= 33.6, succeeds
219
+ token.mint(recipient, 3.6 ether);
220
+ assertEq(token.balanceOf(recipient), 33.6 ether);
221
+ }
222
+
223
+ function test_mint_marginModePositivePercentage100_reverts() public {
224
+ vm.startPrank(deployer, deployer);
225
+
226
+ // set margin (mintable amount = 42 ether * (1 - 100%) = 0 ether)
227
+ policy.setReserveMargin(SecureMintPolicy.ReserveMarginMode.PositivePercentage, 10000); // 100%
228
+
229
+ // 0 + 0.1 > 0, reverts
230
+ vm.expectRevert(
231
+ _encodeRejectedRevert(MockToken.mint.selector, address(policy), "mint would exceed available reserves")
232
+ );
233
+ token.mint(recipient, 0.1 ether);
234
+ }
235
+
236
+ function test_mint_marginModePositivePercentage_reverts() public {
237
+ vm.startPrank(deployer, deployer);
238
+
239
+ // set margin (mintable amount = 42 ether * (1 - 20%) = 33.6 ether)
240
+ policy.setReserveMargin(SecureMintPolicy.ReserveMarginMode.PositivePercentage, 2000); // 20%
241
+
242
+ // 0 + 30 <= 33.6, succeeds
243
+ token.mint(recipient, 30 ether);
244
+ assertEq(token.balanceOf(recipient), 30 ether);
245
+
246
+ // 30 + 4 > 33.6, reverts
247
+ vm.expectRevert(
248
+ _encodeRejectedRevert(MockToken.mint.selector, address(policy), "mint would exceed available reserves")
249
+ );
250
+ token.mint(recipient, 4 ether);
251
+
252
+ // 30 + 3 <= 33.6, succeeds
253
+ token.mint(recipient, 3 ether);
254
+ assertEq(token.balanceOf(recipient), 33 ether);
255
+
256
+ // 35 + 1 > 33.6, reverts
257
+ vm.expectRevert(
258
+ _encodeRejectedRevert(MockToken.mint.selector, address(policy), "mint would exceed available reserves")
259
+ );
260
+ token.mint(recipient, 1 ether);
261
+ }
262
+
263
+ function test_mint_marginModePositiveAbsolute_succeeds() public {
264
+ vm.startPrank(deployer, deployer);
265
+
266
+ // set margin (mintable amount = 42 ether - 2 ether = 40 ether)
267
+ policy.setReserveMargin(SecureMintPolicy.ReserveMarginMode.PositiveAbsolute, 2 ether); // 2 ether
268
+
269
+ // 0 + 30 <= 40, succeeds
270
+ token.mint(recipient, 30 ether);
271
+ assertEq(token.balanceOf(recipient), 30 ether);
272
+
273
+ // 30 + 10 <= 40, succeeds
274
+ token.mint(recipient, 10 ether);
275
+ assertEq(token.balanceOf(recipient), 40 ether);
276
+ }
277
+
278
+ function test_mint_marginModePositiveAbsolute_reverts() public {
279
+ vm.startPrank(deployer, deployer);
280
+
281
+ // set margin (mintable amount = 42 ether - 2 ether = 40 ether)
282
+ policy.setReserveMargin(SecureMintPolicy.ReserveMarginMode.PositiveAbsolute, 2 ether); // 2 ether
283
+
284
+ // 0 + 30 <= 40, succeeds
285
+ token.mint(recipient, 30 ether);
286
+ assertEq(token.balanceOf(recipient), 30 ether);
287
+
288
+ // 30 + 15 > 40, reverts
289
+ vm.expectRevert(
290
+ _encodeRejectedRevert(MockToken.mint.selector, address(policy), "mint would exceed available reserves")
291
+ );
292
+ token.mint(recipient, 12 ether);
293
+
294
+ // 30 + 10 <= 40, succeeds
295
+ token.mint(recipient, 10 ether);
296
+ assertEq(token.balanceOf(recipient), 40 ether);
297
+
298
+ // 40 + 1 > 40, reverts
299
+ vm.expectRevert(
300
+ _encodeRejectedRevert(MockToken.mint.selector, address(policy), "mint would exceed available reserves")
301
+ );
302
+ token.mint(recipient, 1 ether);
303
+ }
304
+
305
+ function test_mint_marginModePositiveAbsoluteExceedsReserves_reverts() public {
306
+ vm.startPrank(deployer, deployer);
307
+
308
+ // set margin greater than reserves (mintable amount = max(0, 42 ether - 50 ether) = 0 ether)
309
+ // 50 ether > 42 ether reserves
310
+ policy.setReserveMargin(SecureMintPolicy.ReserveMarginMode.PositiveAbsolute, 50 ether);
311
+
312
+ // 0 + 0.1 > 0, reverts (any minting should be blocked)
313
+ vm.expectRevert(
314
+ _encodeRejectedRevert(MockToken.mint.selector, address(policy), "mint would exceed available reserves")
315
+ );
316
+ token.mint(recipient, 0.1 ether);
317
+ }
318
+
319
+ function test_mint_marginModeNegativePercentage_succeeds() public {
320
+ vm.startPrank(deployer, deployer);
321
+
322
+ // set margin (mintable amount = 42 ether * (1 - (-20%)) = 50.4 ether)
323
+ policy.setReserveMargin(SecureMintPolicy.ReserveMarginMode.NegativePercentage, 2000); // -20%
324
+
325
+ // 0 + 50 <= 50.4, succeeds
326
+ token.mint(recipient, 50 ether);
327
+ assertEq(token.balanceOf(recipient), 50 ether);
328
+
329
+ // 50 + 0.4 <= 50.4, succeeds
330
+ token.mint(recipient, 0.4 ether);
331
+ assertEq(token.balanceOf(recipient), 50.4 ether);
332
+ }
333
+
334
+ function test_mint_marginModeNegativePercentage100_succeeds() public {
335
+ vm.startPrank(deployer, deployer);
336
+
337
+ // set margin (mintable amount = 42 ether * (1 - (-100%)) = 84 ether)
338
+ policy.setReserveMargin(SecureMintPolicy.ReserveMarginMode.NegativePercentage, 10000); // -100%
339
+
340
+ // 0 + 50 <= 84, succeeds
341
+ token.mint(recipient, 50 ether);
342
+ assertEq(token.balanceOf(recipient), 50 ether);
343
+
344
+ // 50 + 34 <= 84, succeeds
345
+ token.mint(recipient, 34 ether);
346
+ assertEq(token.balanceOf(recipient), 84 ether);
347
+ }
348
+
349
+ function test_mint_marginModeNegativePercentage_reverts() public {
350
+ vm.startPrank(deployer, deployer);
351
+
352
+ // set margin (mintable amount = 42 ether * (1 - (-20%)) = 50.4 ether)
353
+ policy.setReserveMargin(SecureMintPolicy.ReserveMarginMode.NegativePercentage, 2000); // -20%
354
+
355
+ // 0 + 50 <= 50.4, succeeds
356
+ token.mint(recipient, 50 ether);
357
+ assertEq(token.balanceOf(recipient), 50 ether);
358
+
359
+ // 50 + 0.5 <= 50.4, reverts
360
+ vm.expectRevert(
361
+ _encodeRejectedRevert(MockToken.mint.selector, address(policy), "mint would exceed available reserves")
362
+ );
363
+ token.mint(recipient, 0.5 ether);
364
+
365
+ // 50 + 0.4 <= 50.4, succeeds
366
+ token.mint(recipient, 0.4 ether);
367
+ assertEq(token.balanceOf(recipient), 50.4 ether);
368
+ }
369
+
370
+ function test_mint_marginModeNegativeAbsolute_succeeds() public {
371
+ vm.startPrank(deployer, deployer);
372
+
373
+ // set margin (mintable amount = 42 ether + 2 ether = 44 ether)
374
+ policy.setReserveMargin(SecureMintPolicy.ReserveMarginMode.NegativeAbsolute, 2 ether); // 2 ether
375
+
376
+ // 0 + 30 <= 44, succeeds
377
+ token.mint(recipient, 30 ether);
378
+ assertEq(token.balanceOf(recipient), 30 ether);
379
+
380
+ // 30 + 14 <= 44, succeeds
381
+ token.mint(recipient, 14 ether);
382
+ assertEq(token.balanceOf(recipient), 44 ether);
383
+ }
384
+
385
+ function test_mint_marginModeNegativeAbsolute_reverts() public {
386
+ vm.startPrank(deployer, deployer);
387
+
388
+ // set margin (mintable amount = 42 ether + 2 ether = 44 ether)
389
+ policy.setReserveMargin(SecureMintPolicy.ReserveMarginMode.NegativeAbsolute, 2 ether); // 2 ether
390
+
391
+ // 0 + 30 <= 44, succeeds
392
+ token.mint(recipient, 30 ether);
393
+ assertEq(token.balanceOf(recipient), 30 ether);
394
+
395
+ // 30 + 14.1 <= 44, reverts
396
+ vm.expectRevert(
397
+ _encodeRejectedRevert(MockToken.mint.selector, address(policy), "mint would exceed available reserves")
398
+ );
399
+ token.mint(recipient, 14.1 ether);
400
+
401
+ // 30 + 14 <= 44, succeeds
402
+ token.mint(recipient, 14 ether);
403
+ assertEq(token.balanceOf(recipient), 44 ether);
404
+
405
+ // 44 + 1 > 44, reverts
406
+ vm.expectRevert(
407
+ _encodeRejectedRevert(MockToken.mint.selector, address(policy), "mint would exceed available reserves")
408
+ );
409
+ token.mint(recipient, 1 ether);
410
+ }
411
+
412
+ function test_mint_maxStalenessSeconds_succeeds() public {
413
+ vm.startPrank(deployer, deployer);
414
+
415
+ policy.setMaxStalenessSeconds(600);
416
+
417
+ porFeed.setUpdatedAt(block.timestamp - 600);
418
+ token.mint(recipient, 1 ether);
419
+ assertEq(token.balanceOf(recipient), 1 ether);
420
+ }
421
+
422
+ function test_mint_maxStalenessSeconds_reverts() public {
423
+ vm.startPrank(deployer, deployer);
424
+
425
+ policy.setMaxStalenessSeconds(600);
426
+
427
+ porFeed.setUpdatedAt(block.timestamp - 601);
428
+ vm.expectRevert(_encodeRejectedRevert(MockToken.mint.selector, address(policy), "reserve data is stale"));
429
+ token.mint(recipient, 1 ether);
430
+ }
431
+
432
+ function test_mint_negativeReserve_reverts() public {
433
+ vm.startPrank(deployer, deployer);
434
+
435
+ // mocks a malfunctioned that returns a negative value
436
+ porFeed.setPrice(-1 ether);
437
+
438
+ // should revert because the reserve is negative
439
+ vm.expectRevert(_encodeRejectedRevert(MockToken.mint.selector, address(policy), "reserve value is negative"));
440
+ token.mint(recipient, 1 ether);
441
+ }
442
+ }
@@ -0,0 +1,158 @@
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 {PolicyEngine} from "@chainlink/policy-management/core/PolicyEngine.sol";
6
+ import {VolumePolicy} from "@chainlink/policy-management/policies/VolumePolicy.sol";
7
+ import {ERC20TransferExtractor} from "@chainlink/policy-management/extractors/ERC20TransferExtractor.sol";
8
+ import {MockToken} from "../helpers/MockToken.sol";
9
+ import {BaseProxyTest} from "../helpers/BaseProxyTest.sol";
10
+
11
+ contract VolumePolicyTest is BaseProxyTest {
12
+ PolicyEngine public policyEngine;
13
+ VolumePolicy public volumePolicy;
14
+ ERC20TransferExtractor public extractor;
15
+ MockToken public token;
16
+ address public deployer;
17
+ address public txSender;
18
+
19
+ function setUp() public {
20
+ deployer = makeAddr("deployer");
21
+ txSender = makeAddr("txSender");
22
+
23
+ vm.startPrank(deployer);
24
+
25
+ policyEngine = _deployPolicyEngine(true, deployer);
26
+
27
+ token = MockToken(_deployMockToken(address(policyEngine)));
28
+
29
+ extractor = new ERC20TransferExtractor();
30
+ bytes32[] memory parameterOutputFormat = new bytes32[](1);
31
+ parameterOutputFormat[0] = extractor.PARAM_AMOUNT();
32
+
33
+ VolumePolicy volumePolicyImpl = new VolumePolicy();
34
+ volumePolicy =
35
+ VolumePolicy(_deployPolicy(address(volumePolicyImpl), address(policyEngine), deployer, abi.encode(100, 200)));
36
+
37
+ policyEngine.setExtractor(MockToken.transfer.selector, address(extractor));
38
+
39
+ policyEngine.addPolicy(address(token), MockToken.transfer.selector, address(volumePolicy), parameterOutputFormat);
40
+ }
41
+
42
+ function test_policy_initMaxBelowMin_fails() public {
43
+ vm.startPrank(deployer);
44
+ VolumePolicy invalidVolumePolicyImpl = new VolumePolicy();
45
+ vm.expectRevert("maxAmount must be greater than minAmount");
46
+ _deployPolicy(address(invalidVolumePolicyImpl), address(policyEngine), deployer, abi.encode(200, 100));
47
+ }
48
+
49
+ function test_policy_initMaxEqualsMin_fails() public {
50
+ vm.startPrank(deployer);
51
+ VolumePolicy invalidVolumePolicyImpl = new VolumePolicy();
52
+ vm.expectRevert("maxAmount must be greater than minAmount");
53
+ _deployPolicy(address(invalidVolumePolicyImpl), address(policyEngine), deployer, abi.encode(200, 200));
54
+ }
55
+
56
+ function test_setMax_succeeds() public {
57
+ vm.startPrank(deployer, deployer);
58
+
59
+ // Set the max amount to 100
60
+ vm.expectEmit();
61
+ emit VolumePolicy.MaxVolumeSet(150);
62
+ volumePolicy.setMax(150);
63
+ vm.assertEq(volumePolicy.getMax(), 150);
64
+ }
65
+
66
+ function test_setMax_sameAsCurrent_fails() public {
67
+ vm.startPrank(deployer, deployer);
68
+
69
+ // Set the max amount to 100 (sanity check)
70
+ vm.expectEmit();
71
+ emit VolumePolicy.MaxVolumeSet(150);
72
+ volumePolicy.setMax(150);
73
+ vm.assertEq(volumePolicy.getMax(), 150);
74
+
75
+ // Set the max amount to 100 again (revert)
76
+ vm.expectRevert("maxAmount cannot be the same as current maxAmount");
77
+ volumePolicy.setMax(150);
78
+ }
79
+
80
+ function test_setMin_succeeds() public {
81
+ vm.startPrank(deployer, deployer);
82
+
83
+ // Set the min amount to 1
84
+ vm.expectEmit();
85
+ emit VolumePolicy.MinVolumeSet(1);
86
+ volumePolicy.setMin(1);
87
+ vm.assertEq(volumePolicy.getMin(), 1);
88
+ }
89
+
90
+ function test_setMin_sameAsCurrent_fails() public {
91
+ vm.startPrank(deployer, deployer);
92
+
93
+ // Set the min amount to 1 (sanity check)
94
+ vm.expectEmit();
95
+ emit VolumePolicy.MinVolumeSet(1);
96
+ volumePolicy.setMin(1);
97
+ vm.assertEq(volumePolicy.getMin(), 1);
98
+
99
+ // Set the min amount to 1 again (revert)
100
+ vm.expectRevert("minAmount cannot be the same as current minAmount");
101
+ volumePolicy.setMin(1);
102
+ }
103
+
104
+ function test_transfer_extractorWithoutPriceFeedAndAmountBelowMaxVolumePolicy_succeeds() public {
105
+ address recipient = makeAddr("recipient");
106
+
107
+ vm.startPrank(txSender);
108
+
109
+ token.transfer(recipient, 199);
110
+
111
+ assert(token.balanceOf(recipient) == 199);
112
+ }
113
+
114
+ function test_transfer_extractorWithoutPriceFeedAndAmountAboveMaxVolumePolicy_reverts() public {
115
+ address recipient = makeAddr("recipient");
116
+
117
+ vm.startPrank(txSender);
118
+
119
+ vm.expectRevert(
120
+ _encodeRejectedRevert(MockToken.transfer.selector, address(volumePolicy), "amount outside allowed volume limits")
121
+ );
122
+
123
+ token.transfer(recipient, 201);
124
+ }
125
+
126
+ function test_transfer_extractorWithoutPriceAndAmountAboveMinVolumePolicy_succeeds() public {
127
+ address recipient = makeAddr("recipient");
128
+
129
+ vm.startPrank(txSender);
130
+
131
+ token.transfer(recipient, 101);
132
+
133
+ assert(token.balanceOf(recipient) == 101);
134
+ }
135
+
136
+ function test_transfer_extractorWithoutPriceAndAmountBelowMinVolumePolicy_reverts() public {
137
+ address recipient = makeAddr("recipient");
138
+
139
+ vm.startPrank(txSender);
140
+
141
+ vm.expectRevert(
142
+ _encodeRejectedRevert(MockToken.transfer.selector, address(volumePolicy), "amount outside allowed volume limits")
143
+ );
144
+
145
+ token.transfer(recipient, 99);
146
+ }
147
+
148
+ function test_transfer_setMaxBelowMinVolumePolicy_reverts() public {
149
+ vm.expectRevert("maxAmount must be greater than minAmount");
150
+ volumePolicy.setMax(99);
151
+ }
152
+
153
+ function test_transfer_setMinAboveMaxVolumePolicy_reverts() public {
154
+ vm.expectRevert("minAmount must be less than maxAmount");
155
+
156
+ volumePolicy.setMin(201);
157
+ }
158
+ }