@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.
- package/.github/workflows/npm-publish.yml +28 -0
- package/README.md +9 -1
- package/UPGRADE_GUIDE.md +754 -0
- package/getting_started/GETTING_STARTED.md +6 -0
- package/getting_started/MyVault.sol +2 -2
- package/getting_started/advanced/SanctionsPolicy.sol +6 -2
- package/package.json +3 -3
- package/packages/cross-chain-identity/docs/API_REFERENCE.md +2 -0
- package/packages/cross-chain-identity/src/CredentialRegistry.sol +9 -7
- package/packages/cross-chain-identity/src/CredentialRegistryFactory.sol +96 -0
- package/packages/cross-chain-identity/src/CredentialRegistryIdentityValidator.sol +15 -4
- package/packages/cross-chain-identity/src/CredentialRegistryIdentityValidatorPolicy.sol +10 -7
- package/packages/cross-chain-identity/src/IdentityRegistry.sol +33 -17
- package/packages/cross-chain-identity/src/IdentityRegistryFactory.sol +96 -0
- package/packages/cross-chain-identity/src/TrustedIssuerRegistry.sol +8 -6
- package/packages/cross-chain-identity/src/TrustedIssuerRegistryFactory.sol +96 -0
- package/packages/cross-chain-identity/src/interfaces/ICredentialRegistry.sol +6 -0
- package/packages/cross-chain-identity/src/interfaces/ICredentialRequirements.sol +2 -1
- package/packages/cross-chain-identity/src/interfaces/IIdentityRegistry.sol +7 -1
- package/packages/cross-chain-identity/src/interfaces/ITrustedIssuerRegistry.sol +6 -0
- package/packages/cross-chain-identity/test/CredentialRegistry.t.sol +1 -1
- package/packages/cross-chain-identity/test/CredentialRegistryFactory.t.sol +105 -0
- package/packages/cross-chain-identity/test/CredentialRegistryIdentityValidator.t.sol +16 -1
- package/packages/cross-chain-identity/test/CredentialRegistryIdentityValidatorPolicy.t.sol +58 -1
- package/packages/cross-chain-identity/test/IdentityRegistry.t.sol +140 -1
- package/packages/cross-chain-identity/test/IdentityRegistryFactory.t.sol +103 -0
- package/packages/cross-chain-identity/test/IdentityValidator.t.sol +1 -1
- package/packages/cross-chain-identity/test/TrustedIssuerRegistry.t.sol +1 -1
- package/packages/cross-chain-identity/test/TrustedIssuerRegistryFactory.t.sol +107 -0
- package/packages/cross-chain-identity/test/helpers/BaseProxyTest.sol +1 -1
- package/packages/cross-chain-identity/test/helpers/MockCredentialDataValidator.sol +1 -1
- package/packages/cross-chain-identity/test/helpers/MockCredentialRegistryReverting.sol +3 -1
- package/packages/policy-management/README.md +1 -0
- package/packages/policy-management/docs/API_GUIDE.md +2 -0
- package/packages/policy-management/docs/API_REFERENCE.md +3 -1
- package/packages/policy-management/docs/CUSTOM_POLICIES_TUTORIAL.md +10 -4
- package/packages/policy-management/src/core/Policy.sol +5 -4
- package/packages/policy-management/src/core/PolicyEngine.sol +113 -57
- package/packages/policy-management/src/core/PolicyEngineFactory.sol +102 -0
- package/packages/policy-management/src/core/PolicyFactory.sol +1 -1
- package/packages/policy-management/src/core/PolicyProtected.sol +28 -55
- package/packages/policy-management/src/core/PolicyProtectedUpgradeable.sol +134 -0
- package/packages/policy-management/src/extractors/ComplianceTokenForceTransferExtractor.sol +4 -1
- package/packages/policy-management/src/extractors/ComplianceTokenFreezeUnfreezeExtractor.sol +4 -1
- package/packages/policy-management/src/extractors/ComplianceTokenMintBurnExtractor.sol +4 -1
- package/packages/policy-management/src/extractors/ERC20ApproveExtractor.sol +4 -1
- package/packages/policy-management/src/extractors/ERC20TransferExtractor.sol +4 -1
- package/packages/policy-management/src/extractors/ERC3643ForcedTransferExtractor.sol +4 -1
- package/packages/policy-management/src/extractors/ERC3643FreezeUnfreezeExtractor.sol +4 -1
- package/packages/policy-management/src/extractors/ERC3643MintBurnExtractor.sol +4 -1
- package/packages/policy-management/src/extractors/ERC3643SetAddressFrozenExtractor.sol +4 -1
- package/packages/policy-management/src/interfaces/ICertifiedActionValidator.sol +110 -0
- package/packages/policy-management/src/interfaces/IExtractor.sol +6 -0
- package/packages/policy-management/src/interfaces/IMapper.sol +6 -0
- package/packages/policy-management/src/interfaces/IPolicy.sol +6 -0
- package/packages/policy-management/src/interfaces/IPolicyEngine.sol +90 -10
- package/packages/policy-management/src/interfaces/IPolicyProtected.sol +6 -0
- package/packages/policy-management/src/libraries/CertifiedActionLib.sol +47 -0
- package/packages/policy-management/src/policies/AllowPolicy.sol +12 -7
- package/packages/policy-management/src/policies/BypassPolicy.sol +23 -5
- package/packages/policy-management/src/policies/CertifiedActionDONValidatorPolicy.sol +156 -0
- package/packages/policy-management/src/policies/CertifiedActionERC20TransferValidatorPolicy.sol +202 -0
- package/packages/policy-management/src/policies/CertifiedActionValidatorPolicy.sol +365 -0
- package/packages/policy-management/src/policies/IntervalPolicy.sol +64 -48
- package/packages/policy-management/src/policies/MaxPolicy.sol +7 -5
- package/packages/policy-management/src/policies/OnlyAuthorizedSenderPolicy.sol +20 -4
- package/packages/policy-management/src/policies/OnlyOwnerPolicy.sol +4 -2
- package/packages/policy-management/src/policies/PausePolicy.sol +16 -15
- package/packages/policy-management/src/policies/RejectPolicy.sol +23 -5
- package/packages/policy-management/src/policies/RoleBasedAccessControlPolicy.sol +7 -5
- package/packages/policy-management/src/policies/SecureMintPolicy.sol +136 -48
- package/packages/policy-management/src/policies/VolumePolicy.sol +9 -5
- package/packages/policy-management/src/policies/VolumeRatePolicy.sol +11 -7
- package/packages/policy-management/test/PolicyEngine.t.sol +131 -23
- package/packages/policy-management/test/PolicyEngineFactory.t.sol +95 -0
- package/packages/policy-management/test/PolicyFactory.t.sol +1 -1
- package/packages/policy-management/test/{PolicyProtectedToken.t.sol → PolicyProtected.t.sol} +54 -8
- package/packages/policy-management/test/PolicyProtectedUpgradeable.t.sol +126 -0
- package/packages/policy-management/test/extractors/ComplianceTokenForceTransferExtractor.t.sol +1 -1
- package/packages/policy-management/test/extractors/ComplianceTokenFreezeUnfreezeExtractor.t.sol +1 -1
- package/packages/policy-management/test/extractors/ComplianceTokenMintBurnExtractor.t.sol +1 -1
- package/packages/policy-management/test/extractors/ERC20ApproveExtractor.t.sol +1 -1
- package/packages/policy-management/test/extractors/ERC3643ForcedTransferExtractor.t.sol +1 -1
- package/packages/policy-management/test/extractors/ERC3643FreezeUnfreezeExtractor.t.sol +1 -1
- package/packages/policy-management/test/extractors/ERC3643MintBurnExtractor.t.sol +1 -1
- package/packages/policy-management/test/extractors/ERC3643SetAddressFrozenExtractor.t.sol +1 -1
- package/packages/policy-management/test/helpers/BaseCertifiedActionTest.sol +44 -0
- package/packages/policy-management/test/helpers/BaseProxyTest.sol +88 -7
- package/packages/policy-management/test/helpers/CustomMapper.sol +3 -1
- package/packages/policy-management/test/helpers/DummyExtractor.sol +3 -1
- package/packages/policy-management/test/helpers/ExpectedParameterPolicy.sol +3 -1
- package/packages/policy-management/test/helpers/FaultyPolicyEngine.sol +54 -0
- package/packages/policy-management/test/helpers/MockCertifiedActionValidatorPolicyExtension.sol +46 -0
- package/packages/policy-management/test/helpers/MockToken.sol +2 -5
- package/packages/policy-management/test/helpers/MockTokenExtractor.sol +9 -4
- package/packages/policy-management/test/helpers/MockTokenUpgradeable.sol +66 -0
- package/packages/policy-management/test/helpers/PolicyAlwaysAllowed.sol +3 -1
- package/packages/policy-management/test/helpers/PolicyAlwaysContinue.sol +3 -1
- package/packages/policy-management/test/helpers/PolicyAlwaysRejected.sol +10 -1
- package/packages/policy-management/test/helpers/PolicyFailingRun.sol +3 -1
- package/packages/policy-management/test/policies/AllowPolicy.t.sol +39 -23
- package/packages/policy-management/test/policies/BypassPolicy.t.sol +48 -20
- package/packages/policy-management/test/policies/CertifiedActionDONValidatorPolicy.t.sol +172 -0
- package/packages/policy-management/test/policies/CertifiedActionERC20TransferValidatorPolicy.t.sol +217 -0
- package/packages/policy-management/test/policies/CertifiedActionValidatorPolicy.t.sol +1083 -0
- package/packages/policy-management/test/policies/IntervalPolicy.t.sol +105 -55
- package/packages/policy-management/test/policies/MaxPolicy.t.sol +15 -7
- package/packages/policy-management/test/policies/OnlyAuthorizedSenderPolicy.t.sol +91 -16
- package/packages/policy-management/test/policies/OnlyOwnerPolicy.t.sol +11 -7
- package/packages/policy-management/test/policies/PausePolicy.t.sol +46 -16
- package/packages/policy-management/test/policies/RejectPolicy.t.sol +52 -24
- package/packages/policy-management/test/policies/RoleBasedAccessControlPolicy.t.sol +50 -30
- package/packages/policy-management/test/policies/SecureMintPolicy.t.sol +211 -32
- package/packages/policy-management/test/policies/VolumePolicy.t.sol +20 -10
- package/packages/policy-management/test/policies/VolumeRatePolicy.t.sol +24 -18
- package/packages/tokens/erc-20/src/ComplianceTokenERC20.sol +4 -7
- package/packages/tokens/erc-20/src/ComplianceTokenStoreERC20.sol +4 -4
- package/packages/tokens/erc-20/test/ComplianceTokenERC20.t.sol +92 -82
- package/packages/tokens/erc-20/test/helpers/BaseProxyTest.sol +74 -1
- package/packages/tokens/erc-3643/src/ComplianceTokenERC3643.sol +12 -7
- package/packages/tokens/erc-3643/src/ComplianceTokenStoreERC3643.sol +4 -4
- package/packages/tokens/erc-3643/test/ComplianceTokenERC3643.t.sol +108 -119
- package/packages/tokens/erc-3643/test/helpers/BaseProxyTest.sol +74 -1
- package/packages/tokens/erc-3643/test/helpers/ExpectedContextPolicy.sol +3 -1
- package/remappings.txt +1 -0
- package/script/DeployCertifiedActionsComplianceTokenERC3643.s.sol +125 -0
package/packages/policy-management/src/policies/CertifiedActionERC20TransferValidatorPolicy.sol
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
// SPDX-License-Identifier: BUSL-1.1
|
|
2
|
+
pragma solidity ^0.8.20;
|
|
3
|
+
|
|
4
|
+
import {IPolicyEngine} from "../interfaces/IPolicyEngine.sol";
|
|
5
|
+
import {ICertifiedActionValidator} from "../interfaces/ICertifiedActionValidator.sol";
|
|
6
|
+
import {CertifiedActionValidatorPolicy} from "./CertifiedActionValidatorPolicy.sol";
|
|
7
|
+
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @title CertifiedActionERC20TransferValidatorPolicy
|
|
11
|
+
* @notice Policy to validate ERC20 token transfers using certified action permits.
|
|
12
|
+
* @dev This policy extends the CertifiedActionValidatorPolicy to specifically handle ERC20 token transfers.
|
|
13
|
+
* It overrides the intent hashing and validation logic to match based on the sender, token address and the
|
|
14
|
+
* ERC20 transfer from/to instead of the entire list of parameters supplied to the policy. Max transfer amount
|
|
15
|
+
* enforcement is performed in the validation hooks.
|
|
16
|
+
*
|
|
17
|
+
* NOTE: The permit parameters are encoded differently than the parameters that will be provided to the policy during
|
|
18
|
+
* validation. The permit parameters include the maximum amount allowed, while the policy parameters during validation
|
|
19
|
+
* will include the actual amount being transferred.
|
|
20
|
+
*
|
|
21
|
+
* The policy expects the following parameters:
|
|
22
|
+
* - parameters[0]: address of the sender/from (address)
|
|
23
|
+
* - parameters[1]: address of the recipient/to (address)
|
|
24
|
+
* - parameters[2]: amount to transfer (uint256)
|
|
25
|
+
*
|
|
26
|
+
* The permit's parameters are expected to be encoded as:
|
|
27
|
+
* - permit.parameters[0]: address of the from/sender (address)
|
|
28
|
+
* - permit.parameters[1]: address of the recipient/to (address)
|
|
29
|
+
* - permit.parameters[2]: maximum amount allowed to transfer (uint256)
|
|
30
|
+
*
|
|
31
|
+
* If the attempted transfer amount exceeds the maximum allowed amount, the validation will fail.
|
|
32
|
+
*/
|
|
33
|
+
contract CertifiedActionERC20TransferValidatorPolicy is CertifiedActionValidatorPolicy {
|
|
34
|
+
/// @custom:storage-location erc7201:chainlink.ace.CertifiedActionERC20TransferValidatorPolicy
|
|
35
|
+
struct CertifiedActionERC20TransferValidatorPolicyStorage {
|
|
36
|
+
mapping(bytes32 permitId => uint256 maxAmount) maxTransferAmounts;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// keccak256(abi.encode(uint256(keccak256("chainlink.ace.CertifiedActionERC20TransferValidatorPolicy")) - 1)) &
|
|
40
|
+
// ~bytes32(uint256(0xff))
|
|
41
|
+
// solhint-disable-next-line const-name-snakecase
|
|
42
|
+
bytes32 private constant CertifiedActionERC20TransferValidatorPolicyStorageLocation =
|
|
43
|
+
0xb6ec1194d7b5ec102788638ac9b642ebad3ce1075cc2da50b257f0984f300800;
|
|
44
|
+
|
|
45
|
+
function _getCertifiedActionERC20TransferValidatorPolicyStorage()
|
|
46
|
+
private
|
|
47
|
+
pure
|
|
48
|
+
returns (CertifiedActionERC20TransferValidatorPolicyStorage storage $)
|
|
49
|
+
{
|
|
50
|
+
// solhint-disable-next-line no-inline-assembly
|
|
51
|
+
assembly {
|
|
52
|
+
$.slot := CertifiedActionERC20TransferValidatorPolicyStorageLocation
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function typeAndVersion() public pure virtual override returns (string memory) {
|
|
57
|
+
return "CertifiedActionERC20TransferValidatorPolicy 1.0.0";
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* @dev Overrides the intent hash to be based on the ERC20 token address, the funds sender and recipient address.
|
|
62
|
+
* The selector is not included in the hash to allow the same permit to be used for both `transfer` and
|
|
63
|
+
* `transferFrom` but a check is performed to ensure the selector is either `transfer` or `transferFrom`.
|
|
64
|
+
* The parameters provided to the policy are expected to be:
|
|
65
|
+
* - parameters[0]: address of the sender/from (address)
|
|
66
|
+
* - parameters[1]: address of the recipient/to (address)
|
|
67
|
+
* - parameters[2]: amount to transfer (uint256)
|
|
68
|
+
*/
|
|
69
|
+
function _hashIntent(
|
|
70
|
+
address caller,
|
|
71
|
+
address subject,
|
|
72
|
+
bytes4 selector,
|
|
73
|
+
bytes[] memory parameters
|
|
74
|
+
)
|
|
75
|
+
internal
|
|
76
|
+
pure
|
|
77
|
+
virtual
|
|
78
|
+
override
|
|
79
|
+
returns (bytes32)
|
|
80
|
+
{
|
|
81
|
+
if (parameters.length != 3) {
|
|
82
|
+
revert InvalidParameters("expected 3 policy parameters");
|
|
83
|
+
}
|
|
84
|
+
// restrict to ERC20 transfer family (transfer and transferFrom)
|
|
85
|
+
if (selector != IERC20.transfer.selector && selector != IERC20.transferFrom.selector) {
|
|
86
|
+
revert InvalidParameters("unsupported selector");
|
|
87
|
+
}
|
|
88
|
+
return keccak256(abi.encode(caller, subject, parameters[0], parameters[1]));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* @dev Stores the maximum transfer amount from the permit's intent which will be used to validate the transfer amount
|
|
93
|
+
* during the validation hook.
|
|
94
|
+
*
|
|
95
|
+
* The permit parameters are expected to be encoded as:
|
|
96
|
+
* - permit.parameters[0]: address of the from/sender (address)
|
|
97
|
+
* - permit.parameters[1]: address of the recipient/to (address)
|
|
98
|
+
* - permit.parameters[2]: maximum amount allowed to transfer (uint256)
|
|
99
|
+
*
|
|
100
|
+
* If the intent is not correctly encoded, the permit presentation will revert.
|
|
101
|
+
*/
|
|
102
|
+
function _storePresentedPermitHook(Permit memory permit) internal virtual override {
|
|
103
|
+
if (permit.parameters.length != 3) {
|
|
104
|
+
revert InvalidParameters("expected 3 permit parameters");
|
|
105
|
+
}
|
|
106
|
+
uint256 maxAmount = abi.decode(permit.parameters[2], (uint256));
|
|
107
|
+
|
|
108
|
+
_getCertifiedActionERC20TransferValidatorPolicyStorage().maxTransferAmounts[permit.permitId] = maxAmount;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* @dev Validates that the attempted transfer amount does not exceed the maximum amount specified in the presented
|
|
113
|
+
* permit.
|
|
114
|
+
*
|
|
115
|
+
* The parameters provided to the policy are expected to be (extracted by ERC20TransferExtractor):
|
|
116
|
+
* - parameters[0]: address of the sender/from (address)
|
|
117
|
+
* - parameters[1]: address of the recipient/to (address)
|
|
118
|
+
* - parameters[2]: amount to transfer (uint256)
|
|
119
|
+
*/
|
|
120
|
+
function _validatePrePresentedPermitHook(
|
|
121
|
+
bytes32 permitId,
|
|
122
|
+
address, /*caller*/
|
|
123
|
+
address, /*subject*/
|
|
124
|
+
bytes4, /*selector*/
|
|
125
|
+
bytes[] memory parameters
|
|
126
|
+
)
|
|
127
|
+
internal
|
|
128
|
+
view
|
|
129
|
+
virtual
|
|
130
|
+
override
|
|
131
|
+
returns (bool)
|
|
132
|
+
{
|
|
133
|
+
if (parameters.length != 3) {
|
|
134
|
+
revert InvalidParameters("expected 3 policy parameters");
|
|
135
|
+
}
|
|
136
|
+
uint256 attemptedAmount = abi.decode(parameters[2], (uint256));
|
|
137
|
+
|
|
138
|
+
CertifiedActionERC20TransferValidatorPolicyStorage storage $ =
|
|
139
|
+
_getCertifiedActionERC20TransferValidatorPolicyStorage();
|
|
140
|
+
if ($.maxTransferAmounts[permitId] > 0 && attemptedAmount > $.maxTransferAmounts[permitId]) {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* @dev Validates that the attempted transfer amount does not exceed the maximum amount specified in the permit's
|
|
149
|
+
* intent.
|
|
150
|
+
*
|
|
151
|
+
* The parameters provided to the policy are expected to be (extracted by ERC20TransferExtractor):
|
|
152
|
+
* - parameters[0]: address of the sender/from (address)
|
|
153
|
+
* - parameters[1]: address of the recipient/to (address)
|
|
154
|
+
* - parameters[2]: amount to transfer (uint256)
|
|
155
|
+
*
|
|
156
|
+
* The permit parameters are expected to have been encoded as:
|
|
157
|
+
* - permit.parameters[0]: address of the from/sender (address)
|
|
158
|
+
* - permit.parameters[1]: address of the recipient/to (address)
|
|
159
|
+
* - permit.parameters[2]: maximum amount allowed to transfer (uint256)
|
|
160
|
+
*/
|
|
161
|
+
function _validateSignedPermitHook(
|
|
162
|
+
Permit memory permit,
|
|
163
|
+
address, /*caller*/
|
|
164
|
+
address, /*subject*/
|
|
165
|
+
bytes4, /*selector*/
|
|
166
|
+
bytes[] memory parameters
|
|
167
|
+
)
|
|
168
|
+
internal
|
|
169
|
+
pure
|
|
170
|
+
virtual
|
|
171
|
+
override
|
|
172
|
+
returns (bool)
|
|
173
|
+
{
|
|
174
|
+
if (parameters.length != 3) {
|
|
175
|
+
revert InvalidParameters("expected 3 policy parameters");
|
|
176
|
+
}
|
|
177
|
+
uint256 attemptedAmount = abi.decode(parameters[2], (uint256));
|
|
178
|
+
|
|
179
|
+
if (permit.parameters.length != 3) {
|
|
180
|
+
revert InvalidParameters("expected 3 permit parameters");
|
|
181
|
+
}
|
|
182
|
+
uint256 max = abi.decode(permit.parameters[2], (uint256));
|
|
183
|
+
|
|
184
|
+
if (max > 0 && attemptedAmount > max) {
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return true;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/// @inheritdoc ICertifiedActionValidator
|
|
192
|
+
/// @dev Overrides the check function to ensure the permit is for an ERC20 transfer/transferFrom operation.
|
|
193
|
+
function check(Permit memory permit, bytes memory signature) public view virtual override returns (bool) {
|
|
194
|
+
if (
|
|
195
|
+
permit.parameters.length != 3
|
|
196
|
+
|| (permit.selector != IERC20.transfer.selector && permit.selector != IERC20.transferFrom.selector)
|
|
197
|
+
) {
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
return super.check(permit, signature);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
// SPDX-License-Identifier: BUSL-1.1
|
|
2
|
+
pragma solidity ^0.8.20;
|
|
3
|
+
|
|
4
|
+
import {IPolicyEngine} from "../interfaces/IPolicyEngine.sol";
|
|
5
|
+
import {ICertifiedActionValidator} from "../interfaces/ICertifiedActionValidator.sol";
|
|
6
|
+
import {CertifiedActionLib} from "../libraries/CertifiedActionLib.sol";
|
|
7
|
+
import {Policy} from "../core/Policy.sol";
|
|
8
|
+
import {EIP712Upgradeable} from "@openzeppelin/contracts-upgradeable/utils/cryptography/EIP712Upgradeable.sol";
|
|
9
|
+
import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @title CertifiedActionValidatorPolicy
|
|
13
|
+
* @notice Policy to validate actions using certified action permits.
|
|
14
|
+
* @dev This policy implements the ICertifiedActionValidator interface to manage and validate permits for actions.
|
|
15
|
+
* It uses EIP-712 for secure off-chain signing of permits and OpenZeppelin's ECDSA library for signature
|
|
16
|
+
* verification.
|
|
17
|
+
*
|
|
18
|
+
* The policy maintains a list of authorized issuers who can sign permits. Each permit includes details such as
|
|
19
|
+
* the caller, subject, action selector, parameters, metadata, maximum uses, and expiry time.
|
|
20
|
+
*
|
|
21
|
+
* The policy supports two modes of operation:
|
|
22
|
+
* 1. Pre-presented permits: Permits that have been presented and stored on-chain before the action is attempted.
|
|
23
|
+
* Note that the act of presenting a permit overrides any ability for a context permit for the same intent.
|
|
24
|
+
* 2. Contextual permits: Permits that are provided in the context of the action attempt.
|
|
25
|
+
*
|
|
26
|
+
* The policy checks the validity of the permit based on its signature, expiry, and usage limits before allowing
|
|
27
|
+
* the action to proceed.
|
|
28
|
+
*
|
|
29
|
+
* Custom implementations can extend this base policy to add additional validation logic or storage as needed.
|
|
30
|
+
*/
|
|
31
|
+
contract CertifiedActionValidatorPolicy is Policy, EIP712Upgradeable, ICertifiedActionValidator {
|
|
32
|
+
using CertifiedActionLib for ICertifiedActionValidator.Permit;
|
|
33
|
+
|
|
34
|
+
/// @notice The readable name of the EIP712 signing domain.
|
|
35
|
+
string private constant EIP712_DOMAIN = "CertifiedActionValidator";
|
|
36
|
+
/// @notice The version of the EIP712 signing domain.
|
|
37
|
+
string private constant EIP712_VERSION = "1";
|
|
38
|
+
|
|
39
|
+
struct StoredPermit {
|
|
40
|
+
uint64 maxUses;
|
|
41
|
+
uint48 expiry;
|
|
42
|
+
uint64 uses;
|
|
43
|
+
bool revoked;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/// @custom:storage-location erc7201:chainlink.ace.CertifiedActionValidatorPolicy
|
|
47
|
+
struct CertifiedActionValidatorPolicyStorage {
|
|
48
|
+
mapping(bytes32 permitId => StoredPermit storedPermit) storedPermits;
|
|
49
|
+
mapping(address issuerKey => bool allowed) issuers;
|
|
50
|
+
mapping(bytes32 intentHash => bytes32 permitId) intentToPermit;
|
|
51
|
+
mapping(bytes32 permitId => address issuer) permitIssuer;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/// @notice Error emitted when a permit signature format is invalid.
|
|
55
|
+
error InvalidSignatureFormat(ECDSA.RecoverError error, bytes32 errArg);
|
|
56
|
+
|
|
57
|
+
// keccak256(abi.encode(uint256(keccak256("chainlink.ace.CertifiedActionValidatorPolicy")) - 1)) &
|
|
58
|
+
// ~bytes32(uint256(0xff))
|
|
59
|
+
// solhint-disable-next-line const-name-snakecase
|
|
60
|
+
bytes32 private constant CertifiedActionValidatorPolicyStorageLocation =
|
|
61
|
+
0x44c002381c586ebdb00ba1d2de58b2e7b37c8c224e74aff0ea92c173550eb800;
|
|
62
|
+
|
|
63
|
+
function _getCertifiedActionValidatorPolicyStorage()
|
|
64
|
+
internal
|
|
65
|
+
pure
|
|
66
|
+
returns (CertifiedActionValidatorPolicyStorage storage $)
|
|
67
|
+
{
|
|
68
|
+
// solhint-disable-next-line no-inline-assembly
|
|
69
|
+
assembly {
|
|
70
|
+
$.slot := CertifiedActionValidatorPolicyStorageLocation
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function typeAndVersion() public pure virtual override returns (string memory) {
|
|
75
|
+
return "CertifiedActionValidatorPolicy 1.0.0";
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* @notice Configures the policy by initializing the EIP712 domain separator and setting the contract owner as the
|
|
80
|
+
* initial signer.
|
|
81
|
+
* @dev No parameters are expected or decoded from the input. The EIP712 domain is initialized with the constants
|
|
82
|
+
* `EIP712_DOMAIN` and `EIP712_VERSION. The initial owner is added as the first authorized signer for approving
|
|
83
|
+
* requests.
|
|
84
|
+
*/
|
|
85
|
+
function configure(bytes calldata) internal virtual override onlyInitializing {
|
|
86
|
+
__EIP712_init(EIP712_DOMAIN, EIP712_VERSION);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/// @inheritdoc ICertifiedActionValidator
|
|
90
|
+
function present(Permit memory permit, bytes memory signature) public virtual {
|
|
91
|
+
CertifiedActionValidatorPolicyStorage storage $ = _getCertifiedActionValidatorPolicyStorage();
|
|
92
|
+
if ($.permitIssuer[permit.permitId] != address(0)) {
|
|
93
|
+
revert PermitAlreadyPresented(permit.permitId);
|
|
94
|
+
} else if ($.storedPermits[permit.permitId].revoked) {
|
|
95
|
+
revert PermitAlreadyRevoked(permit.permitId);
|
|
96
|
+
} else if (!_validatePermitSignature(hashTypedDataV4Permit(permit), signature)) {
|
|
97
|
+
revert InvalidSignature(permit.permitId);
|
|
98
|
+
}
|
|
99
|
+
bytes32 intentHash = _hashIntent(permit.caller, permit.subject, permit.selector, permit.parameters);
|
|
100
|
+
$.intentToPermit[intentHash] = permit.permitId;
|
|
101
|
+
$.storedPermits[permit.permitId] =
|
|
102
|
+
StoredPermit(permit.maxUses, permit.expiry, $.storedPermits[permit.permitId].uses, false);
|
|
103
|
+
$.permitIssuer[permit.permitId] = _recoverIssuer(hashTypedDataV4Permit(permit), signature);
|
|
104
|
+
_storePresentedPermitHook(permit);
|
|
105
|
+
emit PermitStored(permit.permitId);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/// @inheritdoc ICertifiedActionValidator
|
|
109
|
+
function check(Permit memory permit, bytes memory signature) public view virtual returns (bool) {
|
|
110
|
+
return _validatePermitSignature(hashTypedDataV4Permit(permit), signature);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/// @inheritdoc ICertifiedActionValidator
|
|
114
|
+
function revoke(bytes32 permitId) public virtual onlyOwner {
|
|
115
|
+
CertifiedActionValidatorPolicyStorage storage $ = _getCertifiedActionValidatorPolicyStorage();
|
|
116
|
+
$.storedPermits[permitId].revoked = true;
|
|
117
|
+
emit PermitRevoked(permitId);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/// @inheritdoc ICertifiedActionValidator
|
|
121
|
+
function getUsage(bytes32 permitId) public view virtual returns (uint256) {
|
|
122
|
+
CertifiedActionValidatorPolicyStorage storage $ = _getCertifiedActionValidatorPolicyStorage();
|
|
123
|
+
return $.storedPermits[permitId].uses;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/// @inheritdoc ICertifiedActionValidator
|
|
127
|
+
function allowIssuer(bytes memory issuerKey) public virtual onlyOwner {
|
|
128
|
+
CertifiedActionValidatorPolicyStorage storage $ = _getCertifiedActionValidatorPolicyStorage();
|
|
129
|
+
address issuerAddress = abi.decode(issuerKey, (address));
|
|
130
|
+
if ($.issuers[issuerAddress]) {
|
|
131
|
+
revert IssuerAllowedAlreadySet(issuerKey, true);
|
|
132
|
+
}
|
|
133
|
+
$.issuers[issuerAddress] = true;
|
|
134
|
+
emit IssuerAllowed(issuerKey);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/// @inheritdoc ICertifiedActionValidator
|
|
138
|
+
function disAllowIssuer(bytes memory issuerKey) public virtual onlyOwner {
|
|
139
|
+
CertifiedActionValidatorPolicyStorage storage $ = _getCertifiedActionValidatorPolicyStorage();
|
|
140
|
+
address issuerAddress = abi.decode(issuerKey, (address));
|
|
141
|
+
if (!$.issuers[issuerAddress]) {
|
|
142
|
+
revert IssuerAllowedAlreadySet(issuerKey, false);
|
|
143
|
+
}
|
|
144
|
+
$.issuers[issuerAddress] = false;
|
|
145
|
+
emit IssuerDisAllowed(issuerKey);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/// @inheritdoc ICertifiedActionValidator
|
|
149
|
+
function getIssuerAllowed(bytes memory issuerKey) public view virtual returns (bool) {
|
|
150
|
+
address issuerAddress = abi.decode(issuerKey, (address));
|
|
151
|
+
return _getCertifiedActionValidatorPolicyStorage().issuers[issuerAddress];
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* @notice Computes the EIP-712 typed data hash for a Permit
|
|
156
|
+
* @dev Public function that combines operation hashing with EIP-712 domain hashing.
|
|
157
|
+
* Used for signature generation and verification.
|
|
158
|
+
* @param permit The operation to hash
|
|
159
|
+
* @return The EIP-712 compliant typed data hash ready for signing
|
|
160
|
+
*/
|
|
161
|
+
function hashTypedDataV4Permit(Permit memory permit) public view returns (bytes32) {
|
|
162
|
+
return _hashTypedDataV4(permit._hashPermit());
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function _validatePrePresentedPermit(
|
|
166
|
+
address caller,
|
|
167
|
+
address subject,
|
|
168
|
+
bytes4 selector,
|
|
169
|
+
bytes[] memory parameters
|
|
170
|
+
)
|
|
171
|
+
internal
|
|
172
|
+
view
|
|
173
|
+
returns (bool)
|
|
174
|
+
{
|
|
175
|
+
CertifiedActionValidatorPolicyStorage storage $ = _getCertifiedActionValidatorPolicyStorage();
|
|
176
|
+
bytes32 intentHash = _hashIntent(caller, subject, selector, parameters);
|
|
177
|
+
|
|
178
|
+
bytes32 permitId = $.intentToPermit[intentHash];
|
|
179
|
+
if (permitId == 0) {
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
StoredPermit storage storedPermit = $.storedPermits[permitId];
|
|
183
|
+
return _validatePermitNotRevoked(permitId) && _validatePermitIssuerNotRevoked($.permitIssuer[permitId])
|
|
184
|
+
&& _validatePermitExpiry(storedPermit.expiry) && _validatePermitMaxUses(storedPermit.uses, storedPermit.maxUses)
|
|
185
|
+
&& _validatePrePresentedPermitHook(permitId, caller, subject, selector, parameters);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* @notice Validates a signed permit against the action parameters.
|
|
190
|
+
* @dev This function does not verify whether a pre-presented permit exists for the same intent. The `run()` function
|
|
191
|
+
* performs that check before invoking this function. Callers invoking this function directly must ensure no
|
|
192
|
+
* pre-presented permit exists for the intent to preserve permit priority semantics.
|
|
193
|
+
*/
|
|
194
|
+
function _validateSignedPermit(
|
|
195
|
+
address caller,
|
|
196
|
+
address subject,
|
|
197
|
+
bytes4 selector,
|
|
198
|
+
bytes[] memory parameters,
|
|
199
|
+
SignedPermit memory signedPermit
|
|
200
|
+
)
|
|
201
|
+
internal
|
|
202
|
+
view
|
|
203
|
+
returns (bool)
|
|
204
|
+
{
|
|
205
|
+
CertifiedActionValidatorPolicyStorage storage $ = _getCertifiedActionValidatorPolicyStorage();
|
|
206
|
+
|
|
207
|
+
Permit memory permit = signedPermit.permit;
|
|
208
|
+
bytes32 permitIntentHash = _hashIntent(permit.caller, permit.subject, permit.selector, permit.parameters);
|
|
209
|
+
|
|
210
|
+
// ensures the permit exactly matches the intended action
|
|
211
|
+
if (_hashIntent(caller, subject, selector, parameters) != permitIntentHash) {
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
bytes32 permitHash = hashTypedDataV4Permit(permit);
|
|
216
|
+
// _validatePermitSignature already checks if the issuer is allowed
|
|
217
|
+
if (!_validatePermitSignature(permitHash, signedPermit.signature)) {
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return _validatePermitNotRevoked(permit.permitId) && _validatePermitExpiry(permit.expiry)
|
|
222
|
+
&& _validatePermitMaxUses($.storedPermits[permit.permitId].uses, permit.maxUses)
|
|
223
|
+
&& _validateSignedPermitHook(permit, caller, subject, selector, parameters);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function _validatePermitNotRevoked(bytes32 permitId) internal view returns (bool) {
|
|
227
|
+
CertifiedActionValidatorPolicyStorage storage $ = _getCertifiedActionValidatorPolicyStorage();
|
|
228
|
+
return !$.storedPermits[permitId].revoked;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function _validatePermitExpiry(uint48 expiry) internal view returns (bool) {
|
|
232
|
+
return expiry == 0 || block.timestamp <= expiry;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function _validatePermitMaxUses(uint64 currentUses, uint64 maxUses) internal pure returns (bool) {
|
|
236
|
+
return maxUses == 0 || currentUses < maxUses;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function _validatePermitSignature(bytes32 permitHash, bytes memory signature) internal view virtual returns (bool) {
|
|
240
|
+
address issuer = _recoverIssuer(permitHash, signature);
|
|
241
|
+
return _validatePermitIssuerNotRevoked(issuer);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function _recoverIssuer(bytes32 permitHash, bytes memory signature) internal view virtual returns (address) {
|
|
245
|
+
(address recovered, ECDSA.RecoverError error, bytes32 errArg) = ECDSA.tryRecover(permitHash, signature);
|
|
246
|
+
|
|
247
|
+
if (error != ECDSA.RecoverError.NoError) {
|
|
248
|
+
revert InvalidSignatureFormat(error, errArg);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return recovered;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function _validatePermitIssuerNotRevoked(address issuer) internal view returns (bool) {
|
|
255
|
+
return issuer != address(0) && _getCertifiedActionValidatorPolicyStorage().issuers[issuer];
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function _hashIntent(
|
|
259
|
+
address caller,
|
|
260
|
+
address subject,
|
|
261
|
+
bytes4 selector,
|
|
262
|
+
bytes[] memory parameters
|
|
263
|
+
)
|
|
264
|
+
internal
|
|
265
|
+
pure
|
|
266
|
+
virtual
|
|
267
|
+
returns (bytes32)
|
|
268
|
+
{
|
|
269
|
+
return keccak256(abi.encode(caller, subject, selector, parameters));
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// other implementations can extend this to store additional data
|
|
273
|
+
function _storePresentedPermitHook(Permit memory /*permit*/ ) internal virtual {}
|
|
274
|
+
|
|
275
|
+
// other implementations can extend this to validate the pre-presented permit further
|
|
276
|
+
function _validatePrePresentedPermitHook(
|
|
277
|
+
bytes32, /*permitId*/
|
|
278
|
+
address, /*caller*/
|
|
279
|
+
address, /*subject*/
|
|
280
|
+
bytes4, /*selector*/
|
|
281
|
+
bytes[] memory /*parameters*/
|
|
282
|
+
)
|
|
283
|
+
internal
|
|
284
|
+
view
|
|
285
|
+
virtual
|
|
286
|
+
returns (bool)
|
|
287
|
+
{
|
|
288
|
+
return true;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// other implementations can extend this to validate the permit further
|
|
292
|
+
function _validateSignedPermitHook(
|
|
293
|
+
Permit memory, /*permit*/
|
|
294
|
+
address, /*caller*/
|
|
295
|
+
address, /*subject*/
|
|
296
|
+
bytes4, /*selector*/
|
|
297
|
+
bytes[] memory /*parameters*/
|
|
298
|
+
)
|
|
299
|
+
internal
|
|
300
|
+
view
|
|
301
|
+
virtual
|
|
302
|
+
returns (bool)
|
|
303
|
+
{
|
|
304
|
+
return true;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function run(
|
|
308
|
+
address caller,
|
|
309
|
+
address subject,
|
|
310
|
+
bytes4 selector,
|
|
311
|
+
bytes[] calldata parameters,
|
|
312
|
+
bytes calldata context
|
|
313
|
+
)
|
|
314
|
+
public
|
|
315
|
+
view
|
|
316
|
+
override
|
|
317
|
+
returns (IPolicyEngine.PolicyResult)
|
|
318
|
+
{
|
|
319
|
+
bytes32 intentHash = _hashIntent(caller, subject, selector, parameters);
|
|
320
|
+
if (_getCertifiedActionValidatorPolicyStorage().intentToPermit[intentHash] != 0) {
|
|
321
|
+
// check for and validate a pre-presented permit
|
|
322
|
+
if (!_validatePrePresentedPermit(caller, subject, selector, parameters)) {
|
|
323
|
+
revert IPolicyEngine.PolicyRejected("no valid pre-presented permit found");
|
|
324
|
+
}
|
|
325
|
+
} else if (context.length > 0) {
|
|
326
|
+
// attempt to decode a permit from the context
|
|
327
|
+
if (!_validateSignedPermit(caller, subject, selector, parameters, abi.decode(context, (SignedPermit)))) {
|
|
328
|
+
revert IPolicyEngine.PolicyRejected("invalid signed permit in context");
|
|
329
|
+
}
|
|
330
|
+
} else {
|
|
331
|
+
revert IPolicyEngine.PolicyRejected("no valid permit found");
|
|
332
|
+
}
|
|
333
|
+
return IPolicyEngine.PolicyResult.Continue;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* @notice Runs after the policy check if the check was successful to update usage counts.
|
|
338
|
+
*/
|
|
339
|
+
function postRun(
|
|
340
|
+
address caller,
|
|
341
|
+
address subject,
|
|
342
|
+
bytes4 selector,
|
|
343
|
+
bytes[] calldata parameters,
|
|
344
|
+
bytes calldata context
|
|
345
|
+
)
|
|
346
|
+
public
|
|
347
|
+
override
|
|
348
|
+
onlyPolicyEngine
|
|
349
|
+
{
|
|
350
|
+
CertifiedActionValidatorPolicyStorage storage $ = _getCertifiedActionValidatorPolicyStorage();
|
|
351
|
+
// Always use intent hash to determine which permit was validated
|
|
352
|
+
bytes32 intentHash = _hashIntent(caller, subject, selector, parameters);
|
|
353
|
+
bytes32 permitId = $.intentToPermit[intentHash];
|
|
354
|
+
if (permitId != 0) {
|
|
355
|
+
// Pre-presented permit was used
|
|
356
|
+
$.storedPermits[permitId].uses++;
|
|
357
|
+
emit PermitUsed(permitId);
|
|
358
|
+
} else if (context.length > 0) {
|
|
359
|
+
// Contextual permit was used and validated
|
|
360
|
+
SignedPermit memory signedPermit = abi.decode(context, (SignedPermit));
|
|
361
|
+
$.storedPermits[signedPermit.permit.permitId].uses++;
|
|
362
|
+
emit PermitUsed(signedPermit.permit.permitId);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|