create-aztec-privacy-template 0.1.1 → 0.2.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/dist/constants.js +1 -0
- package/dist/helpers/post-init.js +11 -2
- package/dist/templates/index.js +18 -1
- package/package.json +1 -1
- package/scaffold/.env.deploy.example +28 -0
- package/scaffold/Makefile +33 -1
- package/scaffold/README.md +14 -5
- package/scaffold/contracts/l1/BasePortal.sol +56 -12
- package/scaffold/contracts/l1/GenericActionExecutor.sol +97 -0
- package/scaffold/contracts/l1/GenericPortal.sol +4 -2
- package/scaffold/contracts/l1/README.md +12 -1
- package/scaffold/contracts/l1/test/BasePortal.t.sol +39 -14
- package/scaffold/contracts/l1/test/EscapeHatch.t.sol +1 -1
- package/scaffold/contracts/l1/test/GenericPortal.t.sol +13 -11
- package/scaffold/docs/DEPLOYMENT.md +86 -6
- package/scaffold/scripts/deploy.sh +328 -0
- package/scaffold/scripts/integration-test-deployment.sh +394 -0
- package/scaffold/scripts/verify-deployment.sh +196 -0
package/dist/constants.js
CHANGED
|
@@ -4,6 +4,7 @@ export const OVERLAY_EXAMPLES_DIR = 'overlays/examples';
|
|
|
4
4
|
export const EXAMPLE_OVERLAY_ORDER = ['aave', 'lido', 'uniswap'];
|
|
5
5
|
export const SUPPORTED_EXAMPLE_SELECTIONS = ['none', ...EXAMPLE_OVERLAY_ORDER, 'all'];
|
|
6
6
|
export const TEMPLATE_COPY_ENTRIES = [
|
|
7
|
+
'.env.deploy.example',
|
|
7
8
|
'.solhint.json',
|
|
8
9
|
'gitignore',
|
|
9
10
|
'Makefile',
|
|
@@ -2,7 +2,7 @@ import { access, chmod } from 'node:fs/promises';
|
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
const POST_INIT_HOOKS = [
|
|
4
4
|
verifyRequiredLayoutHook,
|
|
5
|
-
|
|
5
|
+
ensureScriptExecutablesHook,
|
|
6
6
|
];
|
|
7
7
|
export async function runPostInitHooks(context) {
|
|
8
8
|
for (const hook of POST_INIT_HOOKS) {
|
|
@@ -16,6 +16,9 @@ async function verifyRequiredLayoutHook(context) {
|
|
|
16
16
|
join('contracts', 'l1'),
|
|
17
17
|
join('contracts', 'aztec'),
|
|
18
18
|
join('scripts', 'compile-aztec-contract.sh'),
|
|
19
|
+
join('scripts', 'deploy.sh'),
|
|
20
|
+
join('scripts', 'integration-test-deployment.sh'),
|
|
21
|
+
join('scripts', 'verify-deployment.sh'),
|
|
19
22
|
];
|
|
20
23
|
if (context.exampleSelection === 'all') {
|
|
21
24
|
requiredPaths.push(join('examples', 'aave'), join('examples', 'lido'), join('examples', 'uniswap'));
|
|
@@ -27,7 +30,13 @@ async function verifyRequiredLayoutHook(context) {
|
|
|
27
30
|
await access(join(context.absoluteTargetPath, relativePath));
|
|
28
31
|
}
|
|
29
32
|
}
|
|
30
|
-
async function
|
|
33
|
+
async function ensureScriptExecutablesHook(context) {
|
|
31
34
|
const compileScript = join(context.absoluteTargetPath, 'scripts', 'compile-aztec-contract.sh');
|
|
35
|
+
const deployScript = join(context.absoluteTargetPath, 'scripts', 'deploy.sh');
|
|
36
|
+
const integrationDeploymentScript = join(context.absoluteTargetPath, 'scripts', 'integration-test-deployment.sh');
|
|
37
|
+
const verifyDeploymentScript = join(context.absoluteTargetPath, 'scripts', 'verify-deployment.sh');
|
|
32
38
|
await chmod(compileScript, 0o755);
|
|
39
|
+
await chmod(deployScript, 0o755);
|
|
40
|
+
await chmod(integrationDeploymentScript, 0o755);
|
|
41
|
+
await chmod(verifyDeploymentScript, 0o755);
|
|
33
42
|
}
|
package/dist/templates/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { cp, mkdir } from 'node:fs/promises';
|
|
2
|
-
import { join } from 'node:path';
|
|
2
|
+
import { join, relative, sep } from 'node:path';
|
|
3
3
|
import { EXAMPLE_OVERLAY_ORDER, OVERLAY_EXAMPLES_DIR, SCAFFOLD_DIR, TEMPLATE_COPY_ENTRIES, } from '../constants.js';
|
|
4
4
|
export function resolveTemplateInstallPlan(generatorRoot, exampleSelection) {
|
|
5
5
|
return {
|
|
@@ -18,6 +18,7 @@ export async function installTemplatePlan(absoluteTargetPath, plan) {
|
|
|
18
18
|
errorOnExist: true,
|
|
19
19
|
force: false,
|
|
20
20
|
preserveTimestamps: true,
|
|
21
|
+
filter: createTemplateCopyFilter(plan.base.sourceDir),
|
|
21
22
|
});
|
|
22
23
|
}
|
|
23
24
|
for (const overlay of plan.overlays) {
|
|
@@ -26,9 +27,25 @@ export async function installTemplatePlan(absoluteTargetPath, plan) {
|
|
|
26
27
|
errorOnExist: false,
|
|
27
28
|
force: true,
|
|
28
29
|
preserveTimestamps: true,
|
|
30
|
+
filter: createTemplateCopyFilter(overlay.sourceDir),
|
|
29
31
|
});
|
|
30
32
|
}
|
|
31
33
|
}
|
|
34
|
+
const GENERATED_ARTIFACT_PREFIXES = [
|
|
35
|
+
'contracts/l1/cache',
|
|
36
|
+
'contracts/l1/out',
|
|
37
|
+
'contracts/aztec/target',
|
|
38
|
+
];
|
|
39
|
+
function createTemplateCopyFilter(sourceRoot) {
|
|
40
|
+
return (sourcePath) => {
|
|
41
|
+
const relPath = relative(sourceRoot, sourcePath);
|
|
42
|
+
if (relPath === '') {
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
const normalizedRelPath = relPath.split(sep).join('/');
|
|
46
|
+
return !GENERATED_ARTIFACT_PREFIXES.some((prefix) => normalizedRelPath === prefix || normalizedRelPath.startsWith(`${prefix}/`));
|
|
47
|
+
};
|
|
48
|
+
}
|
|
32
49
|
function getBaseTemplate(generatorRoot) {
|
|
33
50
|
return {
|
|
34
51
|
mode: 'base',
|
package/package.json
CHANGED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Copy this file to .env.deploy and edit values for your environment.
|
|
2
|
+
# Usage example:
|
|
3
|
+
# set -a && source .env.deploy && set +a && bash scripts/deploy.sh
|
|
4
|
+
|
|
5
|
+
# Common
|
|
6
|
+
PROTOCOL_ID=0xe9b9beae393069af7271ea16e89d289254d64491636d5137dcc231679d9eb042
|
|
7
|
+
RELAYER_ADDRESS=
|
|
8
|
+
AZTEC_ACCOUNT_ALIAS=
|
|
9
|
+
AZTEC_CONTRACT_ALIAS=generic-privacy-adapter
|
|
10
|
+
WALLET_DATA_DIR=.aztec-wallet
|
|
11
|
+
# Integration test helpers (used by make test-deployment)
|
|
12
|
+
RELAYER_PRIVATE_KEY=
|
|
13
|
+
OPERATOR_PRIVATE_KEY=
|
|
14
|
+
INTEGRATION_AMOUNT_WEI=1000000000000000
|
|
15
|
+
FAILURE_TIMEOUT_BLOCKS=1
|
|
16
|
+
# FAILURE_TARGET=0x000000000000000000000000000000000000dEaD
|
|
17
|
+
|
|
18
|
+
# Local devnet defaults (aztec start --local-network)
|
|
19
|
+
L1_RPC_URL=http://127.0.0.1:8545
|
|
20
|
+
AZTEC_NODE_URL=http://127.0.0.1:8080
|
|
21
|
+
DEPLOYER_PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
|
|
22
|
+
SKIP_BUILD=0
|
|
23
|
+
|
|
24
|
+
# Sepolia / Aztec testnet example values
|
|
25
|
+
# L1_RPC_URL=https://ethereum-sepolia-rpc.publicnode.com
|
|
26
|
+
# AZTEC_NODE_URL=https://devnet-6.aztec-labs.com
|
|
27
|
+
# DEPLOYER_PRIVATE_KEY=0x...
|
|
28
|
+
# SPONSORED_FPC_ADDRESS=0x1586f476995be97f07ebd415340a14be48dc28c6c661cc6bdddb80ae790caa4e
|
package/scaffold/Makefile
CHANGED
|
@@ -10,7 +10,7 @@ SOLIDITY_FMT_FILES := $(shell find contracts/l1 -type f -name '*.sol' -not -path
|
|
|
10
10
|
|
|
11
11
|
AZTEC_CONTRACT_DIRS := contracts/aztec
|
|
12
12
|
|
|
13
|
-
.PHONY: help install verify-toolchain fmt fmt-check lint test _test-core _test-flow build build-aztec check clean
|
|
13
|
+
.PHONY: help install verify-toolchain fmt fmt-check lint test _test-core _test-flow build build-aztec deploy test-deployment verify-deployment integration-deployment check clean
|
|
14
14
|
|
|
15
15
|
help:
|
|
16
16
|
@printf "Aztec Privacy Starter Make Targets\n\n"
|
|
@@ -28,6 +28,11 @@ help:
|
|
|
28
28
|
@printf "Build\n"
|
|
29
29
|
@printf " make build Build Solidity + Aztec artifacts\n"
|
|
30
30
|
@printf " make build-aztec Compile Aztec Noir contract\n\n"
|
|
31
|
+
@printf "Deploy\n"
|
|
32
|
+
@printf " make deploy Deploy scaffold contracts (local or sepolia/testnet)\n"
|
|
33
|
+
@printf " make test-deployment Run live post-deploy integration checks\n"
|
|
34
|
+
@printf " make verify-deployment Run deployment invariant checks only\n"
|
|
35
|
+
@printf " make integration-deployment Run state-changing integration flow only\n\n"
|
|
31
36
|
@printf "Cleanup\n"
|
|
32
37
|
@printf " make clean Remove build artifacts\n"
|
|
33
38
|
|
|
@@ -147,6 +152,32 @@ build:
|
|
|
147
152
|
@mkdir -p target
|
|
148
153
|
@echo "Build complete."
|
|
149
154
|
|
|
155
|
+
deploy:
|
|
156
|
+
@bash scripts/deploy.sh
|
|
157
|
+
|
|
158
|
+
test-deployment:
|
|
159
|
+
@if [ -n "$(DEPLOYMENT_FILE)" ]; then \
|
|
160
|
+
bash scripts/verify-deployment.sh "$(DEPLOYMENT_FILE)"; \
|
|
161
|
+
bash scripts/integration-test-deployment.sh "$(DEPLOYMENT_FILE)"; \
|
|
162
|
+
else \
|
|
163
|
+
bash scripts/verify-deployment.sh; \
|
|
164
|
+
bash scripts/integration-test-deployment.sh; \
|
|
165
|
+
fi
|
|
166
|
+
|
|
167
|
+
verify-deployment:
|
|
168
|
+
@if [ -n "$(DEPLOYMENT_FILE)" ]; then \
|
|
169
|
+
bash scripts/verify-deployment.sh "$(DEPLOYMENT_FILE)"; \
|
|
170
|
+
else \
|
|
171
|
+
bash scripts/verify-deployment.sh; \
|
|
172
|
+
fi
|
|
173
|
+
|
|
174
|
+
integration-deployment:
|
|
175
|
+
@if [ -n "$(DEPLOYMENT_FILE)" ]; then \
|
|
176
|
+
bash scripts/integration-test-deployment.sh "$(DEPLOYMENT_FILE)"; \
|
|
177
|
+
else \
|
|
178
|
+
bash scripts/integration-test-deployment.sh; \
|
|
179
|
+
fi
|
|
180
|
+
|
|
150
181
|
check:
|
|
151
182
|
@$(MAKE) fmt-check
|
|
152
183
|
@$(MAKE) lint
|
|
@@ -157,4 +188,5 @@ clean:
|
|
|
157
188
|
@rm -rf target
|
|
158
189
|
@rm -rf cache out
|
|
159
190
|
@rm -rf contracts/l1/cache contracts/l1/out
|
|
191
|
+
@rm -rf contracts/aztec/target
|
|
160
192
|
@rm -rf coverage .turbo .nyc_output
|
package/scaffold/README.md
CHANGED
|
@@ -42,13 +42,16 @@ Important integration boundary:
|
|
|
42
42
|
```text
|
|
43
43
|
.
|
|
44
44
|
|-- contracts/
|
|
45
|
-
| |-- l1/ # BasePortal, EscapeHatch, GenericPortal + tests
|
|
45
|
+
| |-- l1/ # BasePortal, EscapeHatch, GenericPortal, GenericActionExecutor + tests
|
|
46
46
|
| `-- aztec/ # Generic Noir adapter skeleton
|
|
47
47
|
|-- docs/
|
|
48
|
-
| |-- DEPLOYMENT.md # deployment/configuration runbook
|
|
48
|
+
| |-- DEPLOYMENT.md # deployment/configuration runbook + script usage
|
|
49
49
|
| `-- RELAYER_SPEC.md # minimal relayer operational spec
|
|
50
50
|
|-- scripts/
|
|
51
|
-
|
|
|
51
|
+
| |-- compile-aztec-contract.sh
|
|
52
|
+
| |-- deploy.sh
|
|
53
|
+
| |-- integration-test-deployment.sh
|
|
54
|
+
| `-- verify-deployment.sh
|
|
52
55
|
|-- Makefile
|
|
53
56
|
`-- package.json
|
|
54
57
|
```
|
|
@@ -85,10 +88,16 @@ Compile Solidity and Aztec contract artifacts.
|
|
|
85
88
|
2. `make test`
|
|
86
89
|
Run all starter Solidity tests (`BasePortal`, `EscapeHatch`, `GenericPortal` flow).
|
|
87
90
|
|
|
91
|
+
3. `bash scripts/deploy.sh`
|
|
92
|
+
Deploy scaffold contracts to local Aztec/Anvil or Aztec testnet + Sepolia.
|
|
93
|
+
|
|
94
|
+
4. `make test-deployment`
|
|
95
|
+
Run live post-deploy checks (invariants + state-changing integration flow).
|
|
96
|
+
|
|
88
97
|
## Adaptation workflow
|
|
89
98
|
|
|
90
99
|
1. Start from `contracts/l1/GenericPortal.sol`.
|
|
91
|
-
2.
|
|
100
|
+
2. Adapt `contracts/l1/GenericActionExecutor.sol` target allowlist + forwarding payload to your protocol integration.
|
|
92
101
|
3. Extend `contracts/aztec/src/main.nr` intent fields to match your private flow.
|
|
93
102
|
4. Define a single completion payload schema and keep it identical in `GenericPortal` success emission, relayer transport,
|
|
94
103
|
and Noir `finalize_action` hashing.
|
|
@@ -119,7 +128,7 @@ Use `docs/DEPLOYMENT.md` as the source of truth for:
|
|
|
119
128
|
3. address dependency planning between L1 and L2 contracts
|
|
120
129
|
4. deployment validation checks
|
|
121
130
|
|
|
122
|
-
|
|
131
|
+
Use `bash scripts/deploy.sh` for scaffold deployment automation, and keep `docs/DEPLOYMENT.md` as the source of truth for environment values.
|
|
123
132
|
|
|
124
133
|
## Relayer Operational Spec
|
|
125
134
|
|
|
@@ -20,10 +20,12 @@ abstract contract BasePortal {
|
|
|
20
20
|
|
|
21
21
|
/// @notice protocol identifier used in message derivation.
|
|
22
22
|
bytes32 public immutable PROTOCOL_ID;
|
|
23
|
-
/// @notice bound L2 portal contract address.
|
|
24
|
-
|
|
23
|
+
/// @notice bound L2 portal contract identifier (Aztec contract address bytes32).
|
|
24
|
+
bytes32 public L2_CONTRACT;
|
|
25
25
|
/// @notice authorized relayer address.
|
|
26
26
|
address public immutable RELAYER;
|
|
27
|
+
/// @notice one-time configuration admin for deferred L2 binding.
|
|
28
|
+
address public immutable CONFIG_ADMIN;
|
|
27
29
|
/// @notice monotonic message nonce.
|
|
28
30
|
uint64 public messageNonce;
|
|
29
31
|
|
|
@@ -33,35 +35,40 @@ abstract contract BasePortal {
|
|
|
33
35
|
error EmptyRecipient();
|
|
34
36
|
error InvalidL2Contract();
|
|
35
37
|
error InvalidRelayer();
|
|
38
|
+
error L2ContractAlreadyConfigured();
|
|
39
|
+
error L2ContractNotConfigured();
|
|
36
40
|
error MessageAlreadyIssued();
|
|
37
41
|
error MessageAlreadyConsumed();
|
|
38
42
|
error MessageNotIssued();
|
|
43
|
+
error UnauthorizedConfigAdmin();
|
|
39
44
|
error UnauthorizedCaller();
|
|
40
45
|
|
|
46
|
+
event L2ContractConfigured(bytes32 indexed l2Contract);
|
|
47
|
+
|
|
41
48
|
/// @notice Initializes the portal with immutable protocol metadata.
|
|
42
49
|
/// @param protocolId_ protocol identifier used for hashing messages.
|
|
43
|
-
/// @param l2Contract_ bound L2 contract
|
|
50
|
+
/// @param l2Contract_ bound L2 contract identifier (`bytes32` Aztec address for this flow).
|
|
51
|
+
/// `bytes32(0)` is allowed for deferred one-time initialization via `setL2Contract`.
|
|
44
52
|
/// @param relayer_ authorized relayer/service address.
|
|
45
53
|
/// @dev Value source guidance is documented in scaffold docs/DEPLOYMENT.md.
|
|
46
|
-
constructor(bytes32 protocolId_,
|
|
47
|
-
if (l2Contract_ == address(0)) {
|
|
48
|
-
revert InvalidL2Contract();
|
|
49
|
-
}
|
|
50
|
-
|
|
54
|
+
constructor(bytes32 protocolId_, bytes32 l2Contract_, address relayer_) {
|
|
51
55
|
if (relayer_ == address(0)) {
|
|
52
56
|
revert InvalidRelayer();
|
|
53
57
|
}
|
|
54
58
|
|
|
55
59
|
PROTOCOL_ID = protocolId_;
|
|
56
|
-
L2_CONTRACT = l2Contract_;
|
|
57
60
|
RELAYER = relayer_;
|
|
61
|
+
CONFIG_ADMIN = msg.sender;
|
|
58
62
|
messageNonce = 0;
|
|
63
|
+
|
|
64
|
+
if (l2Contract_ != bytes32(0)) {
|
|
65
|
+
L2_CONTRACT = l2Contract_;
|
|
66
|
+
emit L2ContractConfigured(l2Contract_);
|
|
67
|
+
}
|
|
59
68
|
}
|
|
60
69
|
|
|
61
70
|
modifier onlyRelayer() {
|
|
62
|
-
|
|
63
|
-
revert UnauthorizedCaller();
|
|
64
|
-
}
|
|
71
|
+
_onlyRelayer();
|
|
65
72
|
_;
|
|
66
73
|
}
|
|
67
74
|
|
|
@@ -70,6 +77,8 @@ abstract contract BasePortal {
|
|
|
70
77
|
/// @param sender Message sender address.
|
|
71
78
|
/// @return messageHash Derived message hash.
|
|
72
79
|
function _sendL1ToL2Message(bytes32 content, address sender) internal returns (bytes32 messageHash) {
|
|
80
|
+
_assertL2ContractConfigured();
|
|
81
|
+
|
|
73
82
|
if (sender == address(0)) {
|
|
74
83
|
revert EmptyRecipient();
|
|
75
84
|
}
|
|
@@ -85,6 +94,7 @@ abstract contract BasePortal {
|
|
|
85
94
|
}
|
|
86
95
|
|
|
87
96
|
function _consumeL2ToL1Message(bytes32 content, address sender, uint64 nonce) internal onlyRelayer {
|
|
97
|
+
_assertL2ContractConfigured();
|
|
88
98
|
bytes32 messageHash = _buildMessageHash(content, sender, nonce);
|
|
89
99
|
|
|
90
100
|
if (!issuedMessages[messageHash]) {
|
|
@@ -105,9 +115,43 @@ abstract contract BasePortal {
|
|
|
105
115
|
/// @param nonce Monotonic nonce.
|
|
106
116
|
/// @return Hashed message payload.
|
|
107
117
|
function _buildMessageHash(bytes32 content, address sender, uint64 nonce) internal view returns (bytes32) {
|
|
118
|
+
_assertL2ContractConfigured();
|
|
119
|
+
// forge-lint: disable-next-line(asm-keccak256)
|
|
108
120
|
return keccak256(abi.encodePacked(PROTOCOL_ID, L2_CONTRACT, content, sender, nonce));
|
|
109
121
|
}
|
|
110
122
|
|
|
123
|
+
function _onlyRelayer() private view {
|
|
124
|
+
if (msg.sender != RELAYER) {
|
|
125
|
+
revert UnauthorizedCaller();
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function _assertL2ContractConfigured() private view {
|
|
130
|
+
if (L2_CONTRACT == bytes32(0)) {
|
|
131
|
+
revert L2ContractNotConfigured();
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/// @notice Configures the L2 contract binding exactly once.
|
|
136
|
+
/// @param l2Contract_ Aztec contract address (`bytes32`) bound to this portal.
|
|
137
|
+
function setL2Contract(bytes32 l2Contract_) external {
|
|
138
|
+
if (msg.sender != CONFIG_ADMIN) {
|
|
139
|
+
revert UnauthorizedConfigAdmin();
|
|
140
|
+
}
|
|
141
|
+
if (l2Contract_ == bytes32(0)) {
|
|
142
|
+
revert InvalidL2Contract();
|
|
143
|
+
}
|
|
144
|
+
if (L2_CONTRACT != bytes32(0)) {
|
|
145
|
+
revert L2ContractAlreadyConfigured();
|
|
146
|
+
}
|
|
147
|
+
if (messageNonce != 0) {
|
|
148
|
+
revert MessageAlreadyIssued();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
L2_CONTRACT = l2Contract_;
|
|
152
|
+
emit L2ContractConfigured(l2Contract_);
|
|
153
|
+
}
|
|
154
|
+
|
|
111
155
|
/// @notice Checks if a message hash was already issued.
|
|
112
156
|
/// @param messageHash Message hash.
|
|
113
157
|
/// @return Whether the message has been issued.
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.33;
|
|
3
|
+
|
|
4
|
+
import {IGenericActionExecutor} from "./GenericPortal.sol";
|
|
5
|
+
|
|
6
|
+
/// @title GenericActionExecutor
|
|
7
|
+
/// @author aztec-privacy-template
|
|
8
|
+
/// @notice Generic L1 executor that forwards validated actions to allowlisted targets.
|
|
9
|
+
/// @dev `actionData` schema: `abi.encode(address target, uint256 value, bytes callData)`.
|
|
10
|
+
contract GenericActionExecutor is IGenericActionExecutor {
|
|
11
|
+
/// @notice Current operator allowed to configure this executor.
|
|
12
|
+
address public operator;
|
|
13
|
+
/// @notice Generic portal authorized to invoke `executeAction`.
|
|
14
|
+
address public portal;
|
|
15
|
+
/// @notice Allowlist of protocol targets callable by this executor.
|
|
16
|
+
mapping(address target => bool allowed) public allowedTargets;
|
|
17
|
+
|
|
18
|
+
error InvalidAddress();
|
|
19
|
+
error UnauthorizedCaller();
|
|
20
|
+
error PortalAlreadyConfigured();
|
|
21
|
+
error TargetNotAllowed(address target);
|
|
22
|
+
|
|
23
|
+
event OperatorUpdated(address indexed previousOperator, address indexed newOperator);
|
|
24
|
+
event PortalConfigured(address indexed portal);
|
|
25
|
+
event TargetPermissionSet(address indexed target, bool allowed);
|
|
26
|
+
event ActionForwarded(address indexed actor, uint256 amount, address indexed target, uint256 value, bool success);
|
|
27
|
+
|
|
28
|
+
constructor(address operator_) {
|
|
29
|
+
if (operator_ == address(0)) {
|
|
30
|
+
revert InvalidAddress();
|
|
31
|
+
}
|
|
32
|
+
operator = operator_;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
modifier onlyOperator() {
|
|
36
|
+
if (msg.sender != operator) {
|
|
37
|
+
revert UnauthorizedCaller();
|
|
38
|
+
}
|
|
39
|
+
_;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
modifier onlyPortal() {
|
|
43
|
+
if (msg.sender != portal) {
|
|
44
|
+
revert UnauthorizedCaller();
|
|
45
|
+
}
|
|
46
|
+
_;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/// @notice Sets the portal once after deployment.
|
|
50
|
+
function setPortal(address portal_) external onlyOperator {
|
|
51
|
+
if (portal_ == address(0)) {
|
|
52
|
+
revert InvalidAddress();
|
|
53
|
+
}
|
|
54
|
+
if (portal != address(0)) {
|
|
55
|
+
revert PortalAlreadyConfigured();
|
|
56
|
+
}
|
|
57
|
+
portal = portal_;
|
|
58
|
+
emit PortalConfigured(portal_);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/// @notice Transfers executor configuration rights.
|
|
62
|
+
function setOperator(address nextOperator) external onlyOperator {
|
|
63
|
+
if (nextOperator == address(0)) {
|
|
64
|
+
revert InvalidAddress();
|
|
65
|
+
}
|
|
66
|
+
address previous = operator;
|
|
67
|
+
operator = nextOperator;
|
|
68
|
+
emit OperatorUpdated(previous, nextOperator);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/// @notice Sets target allowlist status for forwarded calls.
|
|
72
|
+
function setTargetPermission(address target, bool allowed) external onlyOperator {
|
|
73
|
+
if (target == address(0)) {
|
|
74
|
+
revert InvalidAddress();
|
|
75
|
+
}
|
|
76
|
+
allowedTargets[target] = allowed;
|
|
77
|
+
emit TargetPermissionSet(target, allowed);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/// @inheritdoc IGenericActionExecutor
|
|
81
|
+
function executeAction(address actor, uint256 amount, bytes calldata actionData)
|
|
82
|
+
external
|
|
83
|
+
onlyPortal
|
|
84
|
+
returns (bool)
|
|
85
|
+
{
|
|
86
|
+
(address target, uint256 value, bytes memory callData) = abi.decode(actionData, (address, uint256, bytes));
|
|
87
|
+
if (!allowedTargets[target]) {
|
|
88
|
+
revert TargetNotAllowed(target);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
(bool success,) = target.call{value: value}(callData);
|
|
92
|
+
emit ActionForwarded(actor, amount, target, value, success);
|
|
93
|
+
return success;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
receive() external payable {}
|
|
97
|
+
}
|
|
@@ -73,11 +73,12 @@ contract GenericPortal is BasePortal, EscapeHatch {
|
|
|
73
73
|
|
|
74
74
|
/// @notice Creates a new generic portal.
|
|
75
75
|
/// @param protocolId_ Protocol identifier used in deterministic message hashing.
|
|
76
|
-
/// @param l2Contract_ L2 adapter contract address for this flow.
|
|
76
|
+
/// @param l2Contract_ L2 adapter contract identifier (`bytes32` Aztec contract address) for this flow.
|
|
77
|
+
/// `bytes32(0)` enables deferred one-time binding via `setL2Contract`.
|
|
77
78
|
/// @param relayer_ Relayer/service address authorized to execute inbound messages.
|
|
78
79
|
/// @param executor_ Protocol execution hook for concrete integration.
|
|
79
80
|
/// @dev See scaffold docs/DEPLOYMENT.md for value sources and address planning guidance.
|
|
80
|
-
constructor(bytes32 protocolId_,
|
|
81
|
+
constructor(bytes32 protocolId_, bytes32 l2Contract_, address relayer_, address executor_)
|
|
81
82
|
BasePortal(protocolId_, l2Contract_, relayer_)
|
|
82
83
|
{
|
|
83
84
|
if (executor_ == address(0)) {
|
|
@@ -140,6 +141,7 @@ contract GenericPortal is BasePortal, EscapeHatch {
|
|
|
140
141
|
}
|
|
141
142
|
|
|
142
143
|
bytes32 messageHash = _buildMessageHash(content, sender, nonce);
|
|
144
|
+
// forge-lint: disable-next-line(asm-keccak256)
|
|
143
145
|
bytes32 actionHash = keccak256(actionData);
|
|
144
146
|
|
|
145
147
|
_assertFlowRequest(messageHash, sender, amount, actionHash);
|
|
@@ -5,6 +5,7 @@ This folder contains all Solidity contracts for the starter:
|
|
|
5
5
|
1. `BasePortal.sol`
|
|
6
6
|
2. `EscapeHatch.sol`
|
|
7
7
|
3. `GenericPortal.sol`
|
|
8
|
+
4. `GenericActionExecutor.sol`
|
|
8
9
|
|
|
9
10
|
It also includes tests under `test/`.
|
|
10
11
|
|
|
@@ -20,6 +21,8 @@ It also includes tests under `test/`.
|
|
|
20
21
|
These contracts define deterministic message content and local request state. For real cross-chain delivery, integrate
|
|
21
22
|
your relayer with Aztec canonical Inbox/Outbox contracts (or their current equivalent on your target network).
|
|
22
23
|
|
|
24
|
+
`BasePortal` supports deferred one-time L2 binding via `setL2Contract(bytes32)` for deployment sequencing.
|
|
25
|
+
|
|
23
26
|
## Deployment parameters
|
|
24
27
|
|
|
25
28
|
See `../../docs/DEPLOYMENT.md` for constructor input requirements and value sources for:
|
|
@@ -37,10 +40,18 @@ See `../../docs/RELAYER_SPEC.md` for minimum relayer responsibilities, nonce/ide
|
|
|
37
40
|
|
|
38
41
|
1. Keep hash construction consistent between Aztec and L1.
|
|
39
42
|
2. Keep `onlyRelayer` on execution/finalization entrypoints.
|
|
40
|
-
3.
|
|
43
|
+
3. Adapt `GenericActionExecutor` target allowlist + payload schema to your protocol integration.
|
|
41
44
|
4. Preserve escape registration behavior on execution failure.
|
|
42
45
|
5. Extend tests in `test/GenericPortal.t.sol` for protocol-specific invariants.
|
|
43
46
|
|
|
47
|
+
## Default executor payload
|
|
48
|
+
|
|
49
|
+
`GenericActionExecutor` expects:
|
|
50
|
+
|
|
51
|
+
1. `actionData = abi.encode(address target, uint256 value, bytes callData)`
|
|
52
|
+
2. `target` must be allowlisted via `setTargetPermission(target, true)`
|
|
53
|
+
3. `executeAction` forwards `callData` to `target` and returns call success
|
|
54
|
+
|
|
44
55
|
## Personalization examples
|
|
45
56
|
|
|
46
57
|
Example A: lend/repay style flow
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
2
|
pragma solidity ^0.8.33;
|
|
3
3
|
|
|
4
|
-
import "../BasePortal.sol";
|
|
4
|
+
import {BasePortal} from "../BasePortal.sol";
|
|
5
5
|
|
|
6
6
|
contract BasePortalHarness is BasePortal {
|
|
7
|
-
constructor(bytes32 protocolId,
|
|
7
|
+
constructor(bytes32 protocolId, bytes32 l2Contract, address relayer) BasePortal(protocolId, l2Contract, relayer) {}
|
|
8
8
|
|
|
9
9
|
function sendMessage(bytes32 content, address sender) external returns (bytes32) {
|
|
10
10
|
return _sendL1ToL2Message(content, sender);
|
|
@@ -20,23 +20,22 @@ contract BasePortalHarness is BasePortal {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
contract UnauthorizedPortalCaller {
|
|
23
|
+
function configure(BasePortalHarness portal, bytes32 l2Contract) external {
|
|
24
|
+
portal.setL2Contract(l2Contract);
|
|
25
|
+
}
|
|
26
|
+
|
|
23
27
|
function consume(BasePortalHarness portal, bytes32 content, address sender, uint64 nonce) external {
|
|
24
28
|
portal.consumeMessage(content, sender, nonce);
|
|
25
29
|
}
|
|
26
30
|
}
|
|
27
31
|
|
|
28
32
|
contract BasePortalTest {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
try new BasePortalHarness(bytes32("BASE_PORTAL"), address(0), address(this)) {
|
|
32
|
-
invalidL2 = false;
|
|
33
|
-
} catch {
|
|
34
|
-
invalidL2 = true;
|
|
35
|
-
}
|
|
36
|
-
assert(invalidL2);
|
|
33
|
+
bytes32 private constant PROTOCOL_ID = keccak256("BASE_PORTAL");
|
|
34
|
+
bytes32 private constant L2_CONTRACT_ID = keccak256("L2_CONTRACT");
|
|
37
35
|
|
|
36
|
+
function testBasePortalConstructorRejectsInvalidRelayer() external {
|
|
38
37
|
bool invalidRelayer = false;
|
|
39
|
-
try new BasePortalHarness(
|
|
38
|
+
try new BasePortalHarness(PROTOCOL_ID, L2_CONTRACT_ID, address(0)) {
|
|
40
39
|
invalidRelayer = false;
|
|
41
40
|
} catch {
|
|
42
41
|
invalidRelayer = true;
|
|
@@ -44,8 +43,34 @@ contract BasePortalTest {
|
|
|
44
43
|
assert(invalidRelayer);
|
|
45
44
|
}
|
|
46
45
|
|
|
46
|
+
function testBasePortalDeferredL2Configuration() external {
|
|
47
|
+
BasePortalHarness portal = new BasePortalHarness(PROTOCOL_ID, bytes32(0), address(this));
|
|
48
|
+
UnauthorizedPortalCaller unauthorized = new UnauthorizedPortalCaller();
|
|
49
|
+
|
|
50
|
+
assert(portal.L2_CONTRACT() == bytes32(0));
|
|
51
|
+
|
|
52
|
+
bool unauthorizedReverted = false;
|
|
53
|
+
try unauthorized.configure(portal, L2_CONTRACT_ID) {
|
|
54
|
+
unauthorizedReverted = false;
|
|
55
|
+
} catch {
|
|
56
|
+
unauthorizedReverted = true;
|
|
57
|
+
}
|
|
58
|
+
assert(unauthorizedReverted);
|
|
59
|
+
|
|
60
|
+
portal.setL2Contract(L2_CONTRACT_ID);
|
|
61
|
+
assert(portal.L2_CONTRACT() == L2_CONTRACT_ID);
|
|
62
|
+
|
|
63
|
+
bool cannotReconfigure = false;
|
|
64
|
+
try portal.setL2Contract(keccak256("other")) {
|
|
65
|
+
cannotReconfigure = false;
|
|
66
|
+
} catch {
|
|
67
|
+
cannotReconfigure = true;
|
|
68
|
+
}
|
|
69
|
+
assert(cannotReconfigure);
|
|
70
|
+
}
|
|
71
|
+
|
|
47
72
|
function testBasePortalSendGeneratesDeterministicHashesAndMonotonicNonce() external {
|
|
48
|
-
BasePortalHarness portal = new BasePortalHarness(
|
|
73
|
+
BasePortalHarness portal = new BasePortalHarness(PROTOCOL_ID, L2_CONTRACT_ID, address(this));
|
|
49
74
|
|
|
50
75
|
bytes32 content = keccak256("content-key");
|
|
51
76
|
address sender = address(0xB0B);
|
|
@@ -66,7 +91,7 @@ contract BasePortalTest {
|
|
|
66
91
|
}
|
|
67
92
|
|
|
68
93
|
function testBasePortalRejectsEmptyRecipientMessageSend() external {
|
|
69
|
-
BasePortalHarness portal = new BasePortalHarness(
|
|
94
|
+
BasePortalHarness portal = new BasePortalHarness(PROTOCOL_ID, L2_CONTRACT_ID, address(this));
|
|
70
95
|
|
|
71
96
|
bool reverted = false;
|
|
72
97
|
|
|
@@ -80,7 +105,7 @@ contract BasePortalTest {
|
|
|
80
105
|
}
|
|
81
106
|
|
|
82
107
|
function testBasePortalConsumptionFlow() external {
|
|
83
|
-
BasePortalHarness portal = new BasePortalHarness(
|
|
108
|
+
BasePortalHarness portal = new BasePortalHarness(PROTOCOL_ID, L2_CONTRACT_ID, address(this));
|
|
84
109
|
UnauthorizedPortalCaller unauthorized = new UnauthorizedPortalCaller();
|
|
85
110
|
|
|
86
111
|
bytes32 content = keccak256("consume-key");
|