Repository navigation
Expand file tree
/
Copy pathMakefile
More file actions
387 lines (343 loc) · 30.6 KB
/
Copy pathMakefile
File metadata and controls
387 lines (343 loc) · 30.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
# Makefile — golden path over the chain-config tooling (script/config/*).
#
# Every target is a thin wrapper: the raw `forge script` / `bash` commands it runs are documented
# in README.md ("Chain config tooling") and remain the escape hatch. `FOUNDRY_PROFILE=sync` is set
# inside each recipe (the sync profile enables `ffi` for the curl+jq API fetch), never exported.
#
# NOTE on exit codes: the canonical drift-check exit contract (0 clean / 1 drift / 2 API
# unreachable) belongs to `bash script/config/sync-check.sh` — GNU make remaps ANY failing recipe
# to its own exit code 2, so `make sync-check` is pass/fail only. CI calls the script directly.
CONFIG_DIR := config/chains
# Real chain configs only: exclude the gitignored `zz-scratch-*` files the test suites write into
# config/chains/ (they carry fake selectors and would break the sync/discover tooling that scans the
# directory). This matches the `.gitignore` pattern; a leftover scratch file from a test run is ignored.
KNOWN_CHAINS := $(filter-out zz-scratch-%,$(basename $(notdir $(wildcard $(CONFIG_DIR)/*.json))))
SYNC_SCRIPT := script/config/SyncCcipConfig.s.sol
# Per-chain EVM version. `foundry.toml` pins the repo default (see the comment there); a chain whose
# network never activated an opcode the default emits declares an optional `"evmVersion"` in its
# config/chains/<CHAIN>.json, and every forge run scoped to that chain forwards it as --evm-version.
# It matters most on the deploy path, where it is the compile target for bytecode that has to run on
# that chain; on the read paths it is the local interpreter the simulation executes in.
#
# One resolver for every caller: `script/config/evm-version.sh <chain>` (the shell tooling calls it
# directly). It falls back to `forge config`'s own value, so a chain that declares nothing is passed
# the version forge would have chosen anyway - a no-op flag, never a second default to keep in sync.
#
# `$(call evm-version,<chain>)` is deliberately recursive (`=`, not `:=`): it must expand when a recipe
# runs and CHAIN is known, not when the Makefile is parsed.
# Read/diagnose targets resolve LENIENTLY: an unrecognized declaration degrades to the repo default
# with a stderr diagnostic, so `make doctor` still runs and FAILs the schema rung by name instead of
# dying on a forge CLI error about a flag the operator never typed. The shell macros below that
# BROADCAST use the strict form (no --lenient) and abort.
evm-version = $(shell bash script/config/evm-version.sh "$(1)" --lenient)
evm-version-flag = --evm-version $(call evm-version,$(1))
# Optional token group. GROUP=<name> selects one of N token groups
# (project/<group>/<selectorName>.json); unset is the flat default (project/<selectorName>.json). It
# threads to the scripts as PROJECT_GROUP; GROUP_DIR locates the same file for the jq repair steps here.
# Honored by the project targets (add-lane, remove-lane, adopt-token, snapshot-chain, doctor,
# roles-check) and the deploy targets (deploy-token/pool/lockbox/lockrelease-pool/siloed-pool, deploy-new-chain); the
# chain-facts targets (add-chain, sync*) ignore it (config/chains is group-independent).
GROUP_DIR := $(if $(GROUP),$(GROUP)/,)
.DEFAULT_GOAL := help
.PHONY: probe-chain adopt-token forget-deployment help tools discover discover-tokens add-chain add-lane remove-lane sync sync-preview sync-all sync-check doctor fmt-config clean-scratch snapshot-chain roles-check roles-check-all deploy-token deploy-pool deploy-lockbox deploy-lockrelease-pool deploy-siloed-pool deploy-new-chain preflight verify verify-args verify-execution
# Deploy-time parameters are read by the forge scripts from the environment (vm.env*). Forward a value
# passed on the make command line (make deploy-token TOKEN_NAME=...) to the forge subprocess; a value
# already exported in the shell is inherited either way. The deploy targets resolve only --rpc-url and
# --account for you; these carry the token/pool parameters through unchanged.
#
# Export ONLY vars that actually have a value. A blanket `export FOO` for an unset FOO exports it as an
# EMPTY STRING on GNU Make, which the scripts would then read as "present" and use instead of their
# vm.envOr(KEY, default) fallback - reverting on an empty address/uint or deploying an empty name. The
# conditional export keeps unset vars unset so the script defaults (JSON config / registry / address(0))
# still apply.
DEPLOY_VARS := TOKEN_NAME TOKEN_SYMBOL TOKEN_DECIMALS TOKEN_MAX_SUPPLY TOKEN_PRE_MINT \
TOKEN_PRE_MINT_RECIPIENT CCIP_ADMIN_ADDRESS ROLES_RECIPIENT TOKEN TOKEN_POOL LOCK_BOX DECIMALS \
POOL_HOOKS AUTHORIZED_CALLERS FORCE_REDEPLOY REANCHOR SILO
$(foreach v,$(DEPLOY_VARS),$(if $(strip $($(v))),$(eval export $(v))))
# Recipe-time guard: the CHAIN's config file must exist (helpful list + add-chain hint on a miss).
define require-chain-config
@test -f "$(CONFIG_DIR)/$(CHAIN).json" || { \
echo "unknown chain '$(CHAIN)' - known chains: $(KNOWN_CHAINS)"; \
echo "New chain? make add-chain CHAIN=<selectorName> SELECTOR=<selector> (both from the 'make discover' API NAME + SELECTOR columns)"; \
exit 1; }
endef
# Recipe-time guard for the targets that FORK the chain: refuse a non-EVM chainFamily before forge is
# handed an endpoint. Without it, `make deploy-token CHAIN=<a solana chain>` sends eth_chainId to a
# Solana node and dies on `error code -32601: Method not found` - a transport-looking failure for a
# config-level answer. The other family-sensitive targets (add-lane, snapshot-chain, sync, doctor,
# probe-chain, verify-args, detect-evm-version) already answer by family in their own script.
# $(1) = chain name, $(2) = where the operator should go instead.
# Only an EXPLICIT non-evm declaration fires: chain-family.sh reads an absent or unparseable
# declaration as `evm`, so a broken config is still reported by the target that owns it.
define require-evm-chain
@fam="$$(bash script/config/chain-family.sh "$(1)")"; \
test "$$fam" = "evm" || { \
echo "$@: $(1) is chainFamily '$$fam' - this target is EVM-only."; \
echo "$(2)"; \
exit 1; }
endef
# Non-EVM chains are destination-only here (docs/config-schema.md, "Non-EVM (Solana) chain file").
NON_EVM_DEPLOY_HINT = Deploy it with that chain's own tooling, then record it: make adopt-token CHAIN=$(CHAIN) TOKEN_B58=<that chain's address> [POOL_B58=<that chain's address>]
# Canonical JSON format for config/chains/*.json: `jq --indent 2 -S .` (2-space indent, sorted keys,
# trailing newline — jq always emits one). The committed files use this exact style, and every target
# that writes a config re-canonicalizes it as its last step, so a no-drift `make sync` produces ZERO
# git diff (Foundry's `vm.writeJson` has its own style; raw `forge script` runs bypass the reformat —
# `make fmt-config` restores canon).
define canon-chain-config
@tmp="$$(mktemp)" && jq --indent 2 -S . "$(CONFIG_DIR)/$(CHAIN).json" > "$$tmp" && mv "$$tmp" "$(CONFIG_DIR)/$(CHAIN).json"
endef
# Canonical JSON for project/*.json: sorted keys, 2-space indent, and NO trailing newline (forge's
# `vm.writeJson` — the ONLY writer of project files — omits the trailing newline, and project state is
# never round-tripped through jq in the normal flow, so its canonical form is the writer's exact output;
# see docs/deployed-addresses.md). This REPAIR target strips jq's trailing newline to match. Repair
# tool only — the writers already emit this form on the direct forge path.
define canon-project
@test -f "project/$(GROUP_DIR)$(CHAIN).json" && { tmp="$$(mktemp)" && jq --indent 2 -S . "project/$(GROUP_DIR)$(CHAIN).json" > "$$tmp" && printf '%s' "$$(cat "$$tmp")" > "project/$(GROUP_DIR)$(CHAIN).json" && rm -f "$$tmp"; } || true
endef
help: ## List the available targets
@echo "Chain-config tooling golden path (raw commands: README.md > Chain config tooling):"
@awk 'BEGIN {FS = ":.*## "} /^[a-z][a-z-]*:.*## / {printf " %-16s %s\n", $$1, $$2}' $(MAKEFILE_LIST)
# The chain this invocation targets, if any: CHAIN for the single-chain targets, LOCAL for the lane
# targets. Recursive (`=`) so it expands when the recipe runs.
TOOLS_CHAIN = $(or $(CHAIN),$(LOCAL))
tools: ## Check the required tools are installed (forge, curl, jq; CHAIN= adds that chain's family-specific check)
@command -v forge > /dev/null || { echo "missing: forge - install Foundry: https://book.getfoundry.sh/getting-started/installation"; exit 2; }
@command -v curl > /dev/null || { echo "missing: curl - install it (usually preinstalled; else brew install curl / apt install curl)"; exit 2; }
@command -v jq > /dev/null || { echo "missing: jq - install it (e.g. brew install jq / apt install jq)"; exit 2; }
@echo "tools: forge, curl and jq are all present"
@# forge/curl/jq are the baseline for every family - the config tooling itself is forge-based and
@# still runs against a non-EVM chain (sync SKIPs, doctor checks the schema). Anything
@# family-specific hangs off the chain argument, so an EVM-only user never sees another family's
@# stack, and never pays for the lookup. svm needs nothing extra TODAY: this repo has no non-EVM
@# write path, and the Solana CLI the docs mention is advisory, never installed or invoked here.
@test -z "$(TOOLS_CHAIN)" || { \
fam="$$(bash script/config/chain-family.sh "$(TOOLS_CHAIN)")"; \
test "$$fam" = "evm" || \
echo "tools: $(TOOLS_CHAIN) is chainFamily '$$fam' - destination-only here, no extra toolchain required (docs/config-schema.md)"; }
discover: tools ## List the CCIP API chain catalog vs local configs, both planes (FILTER=<term> narrows; ENVIRONMENT=<testnet|mainnet> narrows the plane)
@FILTER="$(FILTER)" ENVIRONMENT="$(ENVIRONMENT)" bash script/config/sync-discover.sh
discover-tokens: tools ## List the CCIP API token catalog for an operator (ADMIN=, SYMBOL=, CHAIN_SELECTOR=, ENVIRONMENT= all narrow; unreviewed tokens included; POOL=1 adds the pool, its API type and version, one request per row)
@ADMIN="$(ADMIN)" SYMBOL="$(SYMBOL)" CHAIN_SELECTOR="$(CHAIN_SELECTOR)" ENVIRONMENT="$(ENVIRONMENT)" POOL="$(POOL)" bash script/config/discover-tokens.sh
add-chain: tools ## Generate config/chains/<CHAIN>.json from the live API (CHAIN= and SELECTOR= required)
$(if $(CHAIN),,$(error CHAIN is required: make add-chain CHAIN=<selectorName> SELECTOR=<selector> - both from the make discover API NAME + SELECTOR columns))
$(if $(SELECTOR),,$(error SELECTOR is required - find it with: make discover FILTER=<term>))
FOUNDRY_PROFILE=sync forge script $(SYNC_SCRIPT) $(call evm-version-flag,$(CHAIN)) --sig "init(string,uint256)" "$(CHAIN)" "$(SELECTOR)"
$(canon-chain-config)
@bash script/config/detect-evm-version.sh "$(CHAIN)" || true
detect-evm-version: tools ## Re-probe CHAIN for PUSH0 support and pin evmVersion if the chain needs it
$(if $(CHAIN),,$(error CHAIN is required: make detect-evm-version CHAIN=<selectorName>))
@bash script/config/detect-evm-version.sh "$(CHAIN)" || true
add-lane: tools ## Append a lanes{} policy entry LOCAL -> REMOTE (LOCAL= REMOTE= CAPACITY= RATE= required; INBOUND_CAPACITY= + INBOUND_RATE= add the inbound block; BOTH=1 adds the reciprocal; GROUP= scopes to a token group)
$(if $(LOCAL),,$(error LOCAL is required: make add-lane LOCAL=<name> REMOTE=<name> CAPACITY=<wei> RATE=<wei> [INBOUND_CAPACITY=<wei> INBOUND_RATE=<wei>] [BOTH=1]))
$(if $(REMOTE),,$(error REMOTE is required: make add-lane LOCAL=<name> REMOTE=<name> CAPACITY=<wei> RATE=<wei> [INBOUND_CAPACITY=<wei> INBOUND_RATE=<wei>] [BOTH=1]))
$(if $(CAPACITY),,$(error CAPACITY is required - the outbound rate-limit bucket capacity in wei))
$(if $(RATE),,$(error RATE is required - the outbound rate-limit refill rate in wei per second))
ifdef INBOUND_CAPACITY
$(if $(INBOUND_RATE),,$(error INBOUND_RATE is required when INBOUND_CAPACITY is set - a declared inbound block carries both fields))
endif
ifdef INBOUND_RATE
$(if $(INBOUND_CAPACITY),,$(error INBOUND_CAPACITY is required when INBOUND_RATE is set - a declared inbound block carries both fields))
endif
@for c in "$(LOCAL)" "$(REMOTE)"; do \
test -f "$(CONFIG_DIR)/$$c.json" || { \
echo "unknown chain '$$c' - known chains: $(KNOWN_CHAINS)"; \
echo "New chain? make add-chain CHAIN=<selectorName> SELECTOR=<selector> (both from the 'make discover' API NAME + SELECTOR columns)"; \
exit 1; }; \
done
ifdef INBOUND_CAPACITY
FOUNDRY_PROFILE=sync PROJECT_GROUP="$(GROUP)" forge script $(SYNC_SCRIPT) $(call evm-version-flag,$(LOCAL)) --sig "addLane(string,string,uint256,uint256,uint256,uint256)" "$(LOCAL)" "$(REMOTE)" "$(CAPACITY)" "$(RATE)" "$(INBOUND_CAPACITY)" "$(INBOUND_RATE)"
else
FOUNDRY_PROFILE=sync PROJECT_GROUP="$(GROUP)" forge script $(SYNC_SCRIPT) $(call evm-version-flag,$(LOCAL)) --sig "addLane(string,string,uint256,uint256)" "$(LOCAL)" "$(REMOTE)" "$(CAPACITY)" "$(RATE)"
endif
ifdef BOTH
ifdef INBOUND_CAPACITY
FOUNDRY_PROFILE=sync PROJECT_GROUP="$(GROUP)" forge script $(SYNC_SCRIPT) $(call evm-version-flag,$(REMOTE)) --sig "addLane(string,string,uint256,uint256,uint256,uint256)" "$(REMOTE)" "$(LOCAL)" "$(CAPACITY)" "$(RATE)" "$(INBOUND_CAPACITY)" "$(INBOUND_RATE)"
else
FOUNDRY_PROFILE=sync PROJECT_GROUP="$(GROUP)" forge script $(SYNC_SCRIPT) $(call evm-version-flag,$(REMOTE)) --sig "addLane(string,string,uint256,uint256)" "$(REMOTE)" "$(LOCAL)" "$(CAPACITY)" "$(RATE)"
endif
endif
@for c in "$(LOCAL)" "$(REMOTE)"; do \
tmp="$$(mktemp)" && jq --indent 2 -S . "$(CONFIG_DIR)/$$c.json" > "$$tmp" && mv "$$tmp" "$(CONFIG_DIR)/$$c.json"; \
done
@echo "review the lane policy diff (lanes{} = owner policy), then: make doctor CHAIN=$(LOCAL)$(if $(GROUP), GROUP=$(GROUP),)"
remove-lane: tools ## Remove a lanes{} policy entry LOCAL -> REMOTE from the declaration (LOCAL= REMOTE= required; BOTH=1 removes the reciprocal; GROUP= scopes to a token group; on-chain removal via RemoveChain, or RemoveRemotePool for a single pool, is a separate step)
$(if $(LOCAL),,$(error LOCAL is required: make remove-lane LOCAL=<name> REMOTE=<name> [BOTH=1]))
$(if $(REMOTE),,$(error REMOTE is required: make remove-lane LOCAL=<name> REMOTE=<name> [BOTH=1]))
FOUNDRY_PROFILE=sync PROJECT_GROUP="$(GROUP)" forge script $(SYNC_SCRIPT) $(call evm-version-flag,$(LOCAL)) --sig "removeLane(string,string)" "$(LOCAL)" "$(REMOTE)"
ifdef BOTH
FOUNDRY_PROFILE=sync PROJECT_GROUP="$(GROUP)" forge script $(SYNC_SCRIPT) $(call evm-version-flag,$(REMOTE)) --sig "removeLane(string,string)" "$(REMOTE)" "$(LOCAL)"
endif
@for c in "$(LOCAL)" "$(REMOTE)"; do \
test -f "$(CONFIG_DIR)/$$c.json" || continue; \
tmp="$$(mktemp)" && jq --indent 2 -S . "$(CONFIG_DIR)/$$c.json" > "$$tmp" && mv "$$tmp" "$(CONFIG_DIR)/$$c.json"; \
done
@echo "review the lane policy diff (lanes{} = owner policy), then: make doctor CHAIN=$(LOCAL)$(if $(GROUP), GROUP=$(GROUP),)"
adopt-token: tools ## Adopt an externally deployed token into project/[<GROUP>/]<CHAIN>.json (EVM: CHAIN= TOKEN= [TOKEN_POOL=]; non-EVM: CHAIN= TOKEN_B58= [POOL_B58=]; GROUP= for a second token)
$(if $(CHAIN),,$(error CHAIN is required: make adopt-token CHAIN=<name> TOKEN=<addr> [TOKEN_POOL=<addr>], or non-EVM: TOKEN_B58=<base58> [POOL_B58=<base58>]))
$(require-chain-config)
ifdef TOKEN_B58
FOUNDRY_PROFILE=sync PROJECT_GROUP="$(GROUP)" forge script script/config/AdoptToken.s.sol $(call evm-version-flag,$(CHAIN)) --sig "runNonEvm(string,string,string)" "$(CHAIN)" "$(TOKEN_B58)" "$(POOL_B58)"
else
$(if $(TOKEN),,$(error TOKEN is required - the externally deployed token address to adopt (or TOKEN_B58 for a non-EVM chain)))
$(call require-evm-chain,$(CHAIN),TOKEN= is an EVM address - pass this family's own form: make adopt-token CHAIN=$(CHAIN) TOKEN_B58=<that chain's address> [POOL_B58=<that chain's address>])
@FOUNDRY_PROFILE=sync PROJECT_GROUP="$(GROUP)" bash script/config/forge-fork.sh "$(CHAIN)" -- forge script script/config/AdoptToken.s.sol $(call evm-version-flag,$(CHAIN)) --sig "run(string,address,address)" "$(CHAIN)" "$(TOKEN)" "$(or $(TOKEN_POOL),0x0000000000000000000000000000000000000000)"
endif
forget-deployment: tools ## Remove a retired entry from project/[<GROUP>/]<CHAIN>.json deployments{} (CHAIN= NAME=<deployments key> required; refuses an active, registered, still-laned or funded artifact; PREVIEW=1 changes nothing; local edit, nothing sent)
$(if $(CHAIN),,$(error CHAIN is required: make forget-deployment CHAIN=<name> NAME=<deployments key>))
$(if $(NAME),,$(error NAME is required - the addresses.deployments key to remove, e.g. WBTC_BurnMintTokenPool_1.5.1))
$(require-chain-config)
@FOUNDRY_PROFILE=sync PROJECT_GROUP="$(GROUP)" forge script script/config/ForgetDeployment.s.sol $(call evm-version-flag,$(CHAIN)) --sig "run(string,string,bool)" "$(CHAIN)" "$(NAME)" $(if $(PREVIEW),true,false)
sync: tools ## Refresh <CHAIN>'s ccip{} block from the live API (CHAIN= required)
$(if $(CHAIN),,$(error CHAIN is required: make sync CHAIN=<name>))
$(require-chain-config)
FOUNDRY_PROFILE=sync forge script $(SYNC_SCRIPT) $(call evm-version-flag,$(CHAIN)) --sig "run(string)" "$(CHAIN)"
$(canon-chain-config)
sync-preview: tools ## Fetch + log <CHAIN>'s ccip{} from the API without writing (CHAIN= required)
$(if $(CHAIN),,$(error CHAIN is required: make sync-preview CHAIN=<name>))
$(require-chain-config)
FOUNDRY_PROFILE=sync forge script $(SYNC_SCRIPT) $(call evm-version-flag,$(CHAIN)) --sig "preview(string)" "$(CHAIN)"
sync-all: tools ## Refresh every configured chain (non-EVM chains SKIP; failures are collected)
@failed=""; for f in $(CONFIG_DIR)/*.json; do \
name="$$(basename "$$f" .json)"; \
case "$$name" in zz-scratch-*) continue ;; esac; \
echo ">> sync $$name"; \
evm="$$(bash script/config/evm-version.sh "$$name" --lenient)"; \
FOUNDRY_PROFILE=sync forge script $(SYNC_SCRIPT) --evm-version "$$evm" --sig "run(string)" "$$name" || failed="$$failed $$name"; \
tmp="$$(mktemp)" && jq --indent 2 -S . "$$f" > "$$tmp" && mv "$$tmp" "$$f"; \
done; \
if [ -n "$$failed" ]; then echo "sync-all: FAILED for:$$failed"; exit 1; fi; \
echo "sync-all: OK - every configured chain synced (or SKIPped)"
fmt-config: tools ## Repair canonical JSON: config/chains/*.json (jq -S + trailing newline) AND project files, all groups (jq -S, NO trailing newline)
@for f in $(CONFIG_DIR)/*.json; do \
case "$$f" in *zz-scratch-*) continue ;; esac; \
tmp="$$(mktemp)" && jq --indent 2 -S . "$$f" > "$$tmp" && mv "$$tmp" "$$f"; \
done; \
for f in project/*.json project/*/*.json; do \
[ -e "$$f" ] || continue; \
case "$$f" in *zz-scratch-*|*.example.json) continue ;; esac; \
tmp="$$(mktemp)" && jq --indent 2 -S . "$$f" > "$$tmp" && printf '%s' "$$(cat "$$tmp")" > "$$f" && rm -f "$$tmp"; \
done; \
echo "fmt-config: canonicalized $(CONFIG_DIR)/*.json and project files (all groups)"
# Explicit patterns only - NEVER `git clean -X` here: the user's REAL project/history state is
# gitignored by design, so an ignore-based sweep would delete live project files along with scratch.
# `batches/` holds the operator's OWN run artifacts under the same action-shaped names the suite uses,
# so only the `zz-scratch-` prefix is swept there - never the bare action names.
clean-scratch: ## Remove gitignored test-scratch fixtures (zz-scratch-*, zz-tt-*, local-*) from config/chains/, project/, history/ and batches/
@rm -f $(CONFIG_DIR)/zz-scratch-*.json project/zz-scratch-*.json project/local-*.json
@rm -rf project/zz-scratch-*/ project/zz-tt-*/ history/*/zz-scratch-*
@rm -f batches/zz-scratch-*.json
@echo "clean-scratch: removed test-scratch fixtures from $(CONFIG_DIR)/, project/, history/ and batches/"
sync-check: tools ## Read-only drift check (CHAIN= optional; pass/fail only - CI uses the script for 0/1/2)
@bash script/config/sync-check.sh $(CHAIN)
# Convenience sugar over the documented direct commands (README "Verifying deployed contracts"):
# `verify-args` prints the composed verifier flags for a chain; `verify` backfills one contract.
# The direct `forge verify-contract` / `forge script ... --verify` commands work without make.
verify-args: tools ## Print the forge verifier flags composed from config/chains/<CHAIN>.json (CHAIN= required)
$(if $(CHAIN),,$(error CHAIN is required: make verify-args CHAIN=<name>))
@bash script/config/verify-args.sh "$(CHAIN)"
verify: tools ## Source-verify an already-deployed contract on <CHAIN>'s explorer backend (CHAIN= ADDRESS= CONTRACT= required; CONSTRUCTOR_ARGS= optional, else guessed via the RPC)
$(if $(CHAIN),,$(error CHAIN is required: make verify CHAIN=<name> ADDRESS=<addr> CONTRACT=<path:Name>))
$(if $(ADDRESS),,$(error ADDRESS is required - the deployed contract address))
$(if $(CONTRACT),,$(error CONTRACT is required - e.g. CONTRACT=src/CrossChainToken.sol:CrossChainToken))
@bash script/config/verify-contract.sh "$(CHAIN)" "$(ADDRESS)" "$(CONTRACT)" $(if $(CONSTRUCTOR_ARGS),"$(CONSTRUCTOR_ARGS)",)
probe-chain: tools ## Read a chain's CCIP wiring over plain JSON-RPC without forking it (CHAIN= required; read-only, reports rather than verifies; works where doctor's fork cannot reach the chain; pass/fail only - CI uses the script for 0/1/2)
$(if $(CHAIN),,$(error CHAIN is required: make probe-chain CHAIN=<name>))
$(require-chain-config)
@bash script/config/probe-chain.sh "$(CHAIN)"
doctor: tools ## Layered verification of one chain's config (CHAIN= required; GROUP= scopes to one token group)
$(if $(CHAIN),,$(error CHAIN is required: make doctor CHAIN=<name>))
$(require-chain-config)
@FOUNDRY_PROFILE=sync PROJECT_GROUP="$(GROUP)" bash script/config/forge-fork.sh "$(CHAIN)" -- forge script script/config/VerifyChain.s.sol $(call evm-version-flag,$(CHAIN)) --tc VerifyChain --sig "run(string)" "$(CHAIN)"
# The one target that spans TWO chains in one forge process, so it cannot use "the chain's" evm
# version - it picks the LATER of the two. That is sound because preflight only simulates: an
# interpreter always executes bytecode built for an earlier EVM, and a chain that rejects the later
# version's opcodes cannot be hosting contracts that contain them in the first place. An unrecognized
# value (a version newer than this list) ranks highest, so an explicit declaration always wins.
preflight: ## Preflight a token transfer before sending: destination-side simulation via ccip-cli, nothing sent (SOURCE_CHAIN= DEST_CHAIN= AMOUNT= RECEIVER= required; opt TOKEN= WALLET=; needs ccip-cli; GO/NO-GO/UNRESOLVED - CI uses the script for 0/1/2)
$(if $(SOURCE_CHAIN),,$(error SOURCE_CHAIN is required: make preflight SOURCE_CHAIN=<name> DEST_CHAIN=<name> AMOUNT=<wei> RECEIVER=<addr>))
$(if $(DEST_CHAIN),,$(error DEST_CHAIN is required: make preflight SOURCE_CHAIN=<name> DEST_CHAIN=<name> AMOUNT=<wei> RECEIVER=<addr>))
$(if $(AMOUNT),,$(error AMOUNT is required in wei: make preflight SOURCE_CHAIN=<name> DEST_CHAIN=<name> AMOUNT=<wei> RECEIVER=<addr>))
$(if $(RECEIVER),,$(error RECEIVER is required: make preflight SOURCE_CHAIN=<name> DEST_CHAIN=<name> AMOUNT=<wei> RECEIVER=<addr>))
@bash script/config/preflight-transfer.sh "$(SOURCE_CHAIN)" "$(DEST_CHAIN)" "$(AMOUNT)" "$(RECEIVER)"
# No `tools:` prereq (unlike the sibling targets): this needs ccip-cli + jq, not the forge/curl that
# `tools` checks, and the script self-checks its own dependencies.
verify-execution: ## Check whether a sent message executed on its destination (MESSAGE_ID= DEST_CHAIN= required; read-only, no keystore; needs ccip-cli; pass/fail only - CI uses the script for 0/1/2/3)
$(if $(MESSAGE_ID),,$(error MESSAGE_ID is required: make verify-execution MESSAGE_ID=0x... DEST_CHAIN=<name>))
$(if $(DEST_CHAIN),,$(error DEST_CHAIN is required: make verify-execution MESSAGE_ID=0x... DEST_CHAIN=<name>))
@bash script/config/verify-execution.sh "$(MESSAGE_ID)" "$(DEST_CHAIN)"
# ------------------------------------------------------------------------- deploy lifecycle golden path
# The deploy targets close the DX gap the config golden path (add-chain/doctor) left: they resolve
# --rpc-url from the chain file's `rpcEnv` field and --account from KEYSTORE_NAME, so no per-chain RPC
# is hand-exported before each `forge script`. The raw `forge script` command each wraps stays
# documented in README.md ("What this runs") as the escape hatch. Address persistence and the redeploy
# guard are the scripts' own RegistryWriter behavior, reused unchanged.
#
# $(call run-deploy,<script-path>): resolve the chain's RPC + keystore, then broadcast. VERIFY=1 appends
# --verify plus the config-driven verifier flags (script/config/verify-args.sh) so deploy and explorer
# verification are one step; ETHERSCAN_API_KEY is read from the environment and never echoed.
# The evmVersion declaration is resolved BEFORE the RPC and keystore are required. A broken value in
# config/chains is wrong wherever it is read, so reporting it does not depend on having an endpoint or a
# signer to hand, and an operator fixing their config should not have to satisfy unrelated prerequisites
# first to see the message. It also keeps this reachable where no RPC is configured, such as CI.
define run-deploy
@case "$(CHAIN)" in ""|*[!a-z0-9-]*) echo "invalid CHAIN '$(CHAIN)' - use lowercase letters, digits, and hyphens only"; exit 1;; esac; \
rpc_env="$$(jq -r '.rpcEnv // empty' "$(CONFIG_DIR)/$(CHAIN).json")"; \
test -n "$$rpc_env" || { echo "chain '$(CHAIN)' declares no rpcEnv - run: make sync CHAIN=$(CHAIN)"; exit 1; }; \
evm_version="$$(bash script/config/evm-version.sh "$(CHAIN)")" || exit 1; \
rpc_url="$$(bash script/config/dotenv-get.sh "$$rpc_env")"; \
test -n "$$rpc_url" || { echo "RPC URL not set - set $$rpc_env=<url> in ./.env, or export it (the rpcEnv field named in $(CONFIG_DIR)/$(CHAIN).json)"; exit 1; }; \
keystore="$(KEYSTORE_NAME)"; \
test -n "$$keystore" || keystore="$$(bash script/config/dotenv-get.sh KEYSTORE_NAME)"; \
test -n "$$keystore" || { echo "KEYSTORE_NAME is required - set it in ./.env, or export it (create an account with: cast wallet import)"; exit 1; }; \
verify=""; \
if [ -n "$(VERIFY)" ]; then verify="--verify $$(bash script/config/verify-args.sh "$(CHAIN)")" || { echo "could not compose verifier flags for $(CHAIN)"; exit 1; }; fi; \
echo ">> deploy $(1) on $(CHAIN) (rpc: $$rpc_env, account: $$keystore, evm: $$evm_version)"; \
PROJECT_GROUP="$(GROUP)" forge script $(1) --rpc-url "$$rpc_url" --evm-version "$$evm_version" --account "$$keystore" --broadcast $$verify
endef
deploy-token: tools ## Deploy a cross-chain token on <CHAIN> (CHAIN= + KEYSTORE_NAME= required; token params via env TOKEN_NAME= TOKEN_SYMBOL= ...; VERIFY=1 source-verifies; FORCE_REDEPLOY=1 overrides the redeploy guard; GROUP= scopes to a token group)
$(if $(CHAIN),,$(error CHAIN is required: make deploy-token CHAIN=<name> (token params via env: TOKEN_NAME= TOKEN_SYMBOL= TOKEN_DECIMALS= ...)))
$(require-chain-config)
$(call require-evm-chain,$(CHAIN),$(NON_EVM_DEPLOY_HINT))
$(call run-deploy,script/deploy/DeployToken.s.sol)
deploy-pool: tools ## Deploy a BurnMint token pool on <CHAIN> (CHAIN= + KEYSTORE_NAME= required; token resolved from the registry, else TOKEN=; opt POOL_HOOKS=; VERIFY=1; FORCE_REDEPLOY=1; GROUP= scopes to a token group)
$(if $(CHAIN),,$(error CHAIN is required: make deploy-pool CHAIN=<name>))
$(require-chain-config)
$(call require-evm-chain,$(CHAIN),$(NON_EVM_DEPLOY_HINT))
$(call run-deploy,script/deploy/DeployBurnMintTokenPool.s.sol)
deploy-lockbox: tools ## Deploy an ERC20 LockBox on <CHAIN> for the LockRelease liquidity model (CHAIN= + KEYSTORE_NAME= required; token from the registry, else TOKEN=; opt AUTHORIZED_CALLERS=; VERIFY=1; GROUP= scopes to a token group)
$(if $(CHAIN),,$(error CHAIN is required: make deploy-lockbox CHAIN=<name>))
$(require-chain-config)
$(call require-evm-chain,$(CHAIN),$(NON_EVM_DEPLOY_HINT))
$(call run-deploy,script/deploy/DeployERC20LockBox.s.sol)
deploy-lockrelease-pool: tools ## Deploy a LockRelease token pool on <CHAIN> (CHAIN= + KEYSTORE_NAME= required; token + lock box from the registry, else TOKEN= LOCK_BOX=; opt POOL_HOOKS=; VERIFY=1; FORCE_REDEPLOY=1; GROUP= scopes to a token group)
$(if $(CHAIN),,$(error CHAIN is required: make deploy-lockrelease-pool CHAIN=<name>))
$(require-chain-config)
$(call require-evm-chain,$(CHAIN),$(NON_EVM_DEPLOY_HINT))
$(call run-deploy,script/deploy/DeployLockReleaseTokenPool.s.sol)
deploy-siloed-pool: tools ## Deploy a SiloedLockRelease token pool on <CHAIN>, no lock box (CHAIN= + KEYSTORE_NAME= required; token from the registry, else TOKEN=; opt POOL_HOOKS=; VERIFY=1; FORCE_REDEPLOY=1; GROUP=). Then deploy-lockbox SILO=<label> per silo and configure/siloed/ConfigureLockBoxes
$(if $(CHAIN),,$(error CHAIN is required: make deploy-siloed-pool CHAIN=<name>))
$(require-chain-config)
$(call require-evm-chain,$(CHAIN),$(NON_EVM_DEPLOY_HINT))
$(call run-deploy,script/deploy/DeploySiloedLockReleaseTokenPool.s.sol)
deploy-new-chain: tools ## Guided deploy: add-chain -> deploy-token -> deploy-pool -> doctor (CHAIN= SELECTOR= + KEYSTORE_NAME= required; token params + VERIFY= via env). Register, set-pool, and wire-lane come next - see docs/workflows/greenfield-deploy.md; a green run means deployed, not yet cross-chain-live
$(if $(CHAIN),,$(error CHAIN is required: make deploy-new-chain CHAIN=<selectorName> SELECTOR=<selector> (token params via env)))
$(if $(SELECTOR),,$(error SELECTOR is required - find it with: make discover FILTER=<term>))
@$(MAKE) --no-print-directory add-chain CHAIN=$(CHAIN) SELECTOR=$(SELECTOR)
@$(MAKE) --no-print-directory deploy-token CHAIN=$(CHAIN) $(if $(GROUP),GROUP=$(GROUP),)
@$(MAKE) --no-print-directory deploy-pool CHAIN=$(CHAIN) $(if $(GROUP),GROUP=$(GROUP),)
@$(MAKE) --no-print-directory doctor CHAIN=$(CHAIN) $(if $(GROUP),GROUP=$(GROUP),)
# ---------------------------------------------------------------- authority durable store (roles{})
# The `roles{}` subtree is the DECLARED authority surface, versioned in git. `snapshot-chain` is the
# ONLY writer (backfill FROM chain); `roles-check` is READ-ONLY (reconcile declared vs live). Same
# exit-remap note as sync-check: the 0/1/2 contract lives in `script/config/roles-check.sh`; CI calls
# the script directly, `make roles-check` is pass/fail only.
snapshot-chain: tools ## Backfill the declared roles{} authority block FROM chain (CHAIN= required; GROUP= scopes to one token group; opt: TOKEN= TOKEN_POOL= TAR= SCAN_FROM_BLOCK= REANCHOR=true)
$(if $(CHAIN),,$(error CHAIN is required: make snapshot-chain CHAIN=<name>))
$(require-chain-config)
@FOUNDRY_PROFILE=sync PROJECT_GROUP="$(GROUP)" bash script/config/forge-fork.sh "$(CHAIN)" -- forge script script/config/SnapshotChain.s.sol $(call evm-version-flag,$(CHAIN)) --sig "run(string)" "$(CHAIN)"
$(canon-project)
@echo "review the roles{} diff in project/$(GROUP_DIR)$(CHAIN).json (roles{} = declared authority), then reconcile: make roles-check CHAIN=$(CHAIN)$(if $(GROUP), GROUP=$(GROUP),)"
roles-check: tools ## READ-ONLY reconcile of a chain's declared roles{} vs the live chain (CHAIN= optional; GROUP= scopes to one token group; pass/fail only - CI uses the script for 0/1/2)
@PROJECT_GROUP="$(GROUP)" bash script/config/roles-check.sh $(CHAIN)
roles-check-all: tools ## READ-ONLY reconcile of every chain that declares roles{}, across all token groups (exit contract = script/config/roles-check.sh)
@bash script/config/roles-check.sh