Files
zeekayandHanzo Dev 1778e53f00 the pin hashes what solc was fed, and names the upstream that was exercised
Settings belong in the preimage because source is not what deploys: turn
the optimizer off and every file stays byte-identical while the bytecode
changes. Hashing what solc recorded about its own inputs catches that, and
catches whatever moved them — a stray FOUNDRY_OPTIMIZER, a foundry.toml
edit, a toolchain default that shifted.

The protocol digest was stale: luxfi/liquid moved after it was written, so
deploy.sh refused, which is the guard doing its job. The new one is
written down having done what the file asks for it — the whole stack
deployed onto a local chain and the markets exercised against it: deposit,
borrow to the ceiling, the borrow past it refused on chain, repay,
withdraw, and every adversarial guard beside them.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-27 19:56:39 -07:00

99 lines
4.0 KiB
Bash
Executable File

#!/usr/bin/env bash
# Deploys the Lux Financial on-chain stack to one EVM chain and records the
# addresses under deploy/<chainId>.json.
#
# The chain is an argument, never a constant: Lux, Zoo and Hanzo are sovereign
# L1s with their own EVM chain IDs, and each gets its own deployment file. The
# bank resolves contracts by the chain ID it reads from its RPC, so adding a
# chain is a run of this script, not an edit anywhere.
#
# RPC=… PRIVATE_KEY=… OWNER=0x… ORACLE=0x… ./deploy.sh
#
# PRIVATE_KEY signs, and that is all it does — it owns nothing when this
# returns. OWNER ends up holding the protocol: the markets are nominated to it,
# the fees are paid to it, and it is the only account that can bridge collateral
# in or whitelist a minter. ORACLE ends up holding one thing, the yield index of
# each market, and can only ever raise it. Both are addresses you already
# control; a multisig for OWNER, the key that posts yield for ORACLE. In
# production PRIVATE_KEY comes from KMS (providers/<org>/deploy-mnemonic), not
# from a shell, and is never echoed.
set -euo pipefail
cd "$(dirname "$0")"
RPC="${RPC:-http://127.0.0.1:8645}"
export FOUNDRY_DISABLE_NIGHTLY_WARNING=1
for v in PRIVATE_KEY OWNER ORACLE; do
[ -n "${!v:-}" ] || { echo "$v is not set" >&2; exit 1; }
done
# The canonical contracts are the sibling checkouts, not a vendored copy — the
# tokens come from luxfi/standard and the protocol from luxfi/liquid.
for repo in standard liquid; do
[ -d "../../$repo/src" ] || [ -d "../../$repo/contracts" ] || {
echo "missing sibling checkout ../../$repo" >&2; exit 1; }
done
# What each step compiles, hashed: every file in its import closure by content,
# and the settings solc compiled them under. The checkouts are symlinks to
# whatever those repos happen to be at right now, so a released tag would not pin
# them and a branch name pins nothing at all. This does.
#
# Settings belong in the preimage because source is not what deploys. Turn the
# optimizer off and every file stays byte-identical while the tokens bytecode
# grows 1.85x — hashing sources alone would call that build the pinned one.
# Hashing what solc recorded about its own inputs catches it, and catches
# whatever moved them: a stray FOUNDRY_OPTIMIZER, a foundry.toml edit, a
# toolchain upgrade that shifts a default.
#
# Ask forge where it put the artifact rather than assuming. `forge build` honours
# FOUNDRY_OUT; a hardcoded path does not, and the gap between them is a stale
# artifact hashing green while a different build is the one that gets broadcast.
digest() {
local out
out=$(FOUNDRY_PROFILE="$1" forge config --json | jq -r .out)
jq -S '.metadata | del(.output) | .sources |= map_values(.keccak256)' "$out/$2.s.sol/$2.json" | shasum -a 256 | cut -c1-64
}
verify() {
FOUNDRY_PROFILE="$1" forge build >/dev/null
local is want
is=$(digest "$1" "$2")
want=$(awk -v k="$1" '$1 == k { print $2 }' pins)
[ "$is" = "$want" ] || {
echo "$1: upstream moved out from under this deploy" >&2
echo " pinned $want" >&2
echo " found $is" >&2
echo " re-run the market checks against it, then write the new digest into chain/pins" >&2
exit 1; }
echo "$is"
}
tokens=$(verify tokens Tokens)
protocol=$(verify protocol Protocol)
grants=$(verify grants Grants)
chain=$(cast chain-id --rpc-url "$RPC")
mkdir -p out deploy
echo "==> chain $chain via $RPC"
echo "==> owner $OWNER, oracle $ORACLE"
run() {
FOUNDRY_PROFILE="$1" forge script "script/$1/$2.s.sol:$2" \
--rpc-url "$RPC" --broadcast --slow >/dev/null
echo "==> $1 done"
}
run tokens Tokens
run protocol Protocol
run grants Grants
# The address book carries the sources it came from, so a chain deployed years
# apart from the next one can still say what it was built out of.
jq --arg t "$tokens" --arg p "$protocol" --arg g "$grants" \
'.sources = {tokens: $t, protocol: $p, grants: $g}' \
"deploy/$chain.json" > "out/$chain.json" && mv "out/$chain.json" "deploy/$chain.json"
echo "==> deploy/$chain.json"
cat "deploy/$chain.json"