mirror of
https://github.com/luxfi/migration.git
synced 2026-10-30 15:25:07 +00:00
140 lines
7.0 KiB
Bash
Executable File
140 lines
7.0 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
#
|
|
# v3-to-v4-migrate.sh — WITHDRAW side of the Lux DAO V3->V4 LP migration.
|
|
#
|
|
# Drives the staged Safe MultiSendCallOnly batch in v3-to-v4-withdraw.json.
|
|
# The batch: for each live V3 position held by the DAO Safe, decreaseLiquidity(full)+collect(max);
|
|
# an optional cleanup batch burns all 74 emptied NFTs.
|
|
#
|
|
# SAFETY: this script NEVER sends to real mainnet. It has exactly two modes:
|
|
# (default) DRY RUN — print the plan, decode the batch, compute the Safe tx hash. No sends.
|
|
# --execute-fork FORK ONLY — reset + execute against the local anvil fork on 127.0.0.1:8545 only.
|
|
# Add --with-burns to also run the burn-all cleanup batch.
|
|
#
|
|
# The real mainnet execution is performed by the Safe owner signing execTransaction in the Safe UI /
|
|
# their own signer — NOT by this script. See "MAINNET EXECUTION" printed in dry-run mode.
|
|
#
|
|
set -euo pipefail
|
|
|
|
CAST=/Users/z/.foundry/bin/cast
|
|
FORK="http://127.0.0.1:8545" # the ONLY endpoint this script will ever send to
|
|
UPSTREAM="http://127.0.0.1:19630/v1/chain/C/rpc"
|
|
FORK_BLOCK=1082950
|
|
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
JSON="$HERE/v3-to-v4-withdraw.json"
|
|
export FOUNDRY_DISABLE_NIGHTLY_WARNING=1
|
|
|
|
SAFE=$(python3 -c "import json;print(json.load(open('$JSON'))['meta']['safe'])")
|
|
OWNER=$(python3 -c "import json;print(json.load(open('$JSON'))['meta']['safeOwner'])")
|
|
MSCO=$(python3 -c "import json;print(json.load(open('$JSON'))['meta']['multiSendCallOnly'])")
|
|
NFPM=$(python3 -c "import json;print(json.load(open('$JSON'))['meta']['nonfungiblePositionManager'])")
|
|
Z=0x0000000000000000000000000000000000000000
|
|
|
|
jget() { python3 -c "import json;d=json.load(open('$JSON'));print(eval(\"d$1\"))"; }
|
|
|
|
live_ids() { python3 -c "import json;print(' '.join(str(p['tokenId']) for p in json.load(open('$JSON'))['positions']))"; }
|
|
|
|
liq() { $CAST call $NFPM 'positions(uint256)(uint96,address,address,address,uint24,int24,int24,uint128,uint256,uint256,uint128,uint128)' "$1" --rpc-url "$FORK" | awk 'NR==8{print $1}'; }
|
|
|
|
safe_tx_hash() { # $1=data
|
|
local data="$1" nonce
|
|
nonce=$($CAST call $SAFE 'nonce()(uint256)' --rpc-url "$FORK")
|
|
$CAST call $SAFE \
|
|
'getTransactionHash(address,uint256,bytes,uint8,uint256,uint256,uint256,address,address,uint256)(bytes32)' \
|
|
$MSCO 0 "$data" 1 0 0 0 $Z $Z "$nonce" --rpc-url "$FORK"
|
|
}
|
|
|
|
exec_batch() { # $1=data $2=label
|
|
local data="$1" label="$2" txhash sig res
|
|
txhash=$(safe_tx_hash "$data")
|
|
echo " [$label] safeTxHash=$txhash"
|
|
$CAST send $SAFE 'approveHash(bytes32)' "$txhash" --from $OWNER --unlocked --rpc-url "$FORK" >/dev/null
|
|
sig="0x000000000000000000000000${OWNER#0x}$(printf '0%.0s' {1..64})01"
|
|
res=$($CAST send $SAFE \
|
|
'execTransaction(address,uint256,bytes,uint8,uint256,uint256,uint256,address,address,bytes)(bool)' \
|
|
$MSCO 0 "$data" 1 0 0 0 $Z $Z "$sig" --from $OWNER --unlocked --rpc-url "$FORK" --json)
|
|
echo " [$label] exec.status=$(echo "$res" | python3 -c 'import sys,json;print(json.load(sys.stdin)["status"])') gasUsed=$(echo "$res" | python3 -c 'import sys,json;print(int(json.load(sys.stdin)["gasUsed"],16))')"
|
|
}
|
|
|
|
print_plan() {
|
|
echo "=============================================================="
|
|
echo " Lux DAO V3->V4 migration — WITHDRAW side"
|
|
echo " Safe: $SAFE (owner $OWNER, threshold 1)"
|
|
echo " NFPM: $NFPM"
|
|
echo " MultiSendCallOnly: $MSCO"
|
|
echo "--------------------------------------------------------------"
|
|
echo " Live positions to withdraw:"
|
|
python3 - "$JSON" <<'PY'
|
|
import json,sys
|
|
d=json.load(open(sys.argv[1]))
|
|
for p in d["positions"]:
|
|
fp=p["forkProven"]
|
|
print(f" id {p['tokenId']:<4} {p['pair']:<12} liq={p['liquidity']:<30} "
|
|
f"-> {fp['withdrawn_token0']} {p['token0']['symbol']} + {fp['withdrawn_token1']} {p['token1']['symbol']} clean={fp['clean']}")
|
|
print(" totals withdrawn:")
|
|
for s,v in d["totalsWithdrawnByToken"].items():
|
|
print(f" {s:<6} {v['human']}")
|
|
print(f" empties to burn (cleanup batch): {d['meta']['emptyCount']}")
|
|
PY
|
|
echo "--------------------------------------------------------------"
|
|
echo " Withdraw batch: to=$MSCO operation=1(delegatecall) subCalls=$(jget "['batches']['withdraw']['subCallCount']")"
|
|
echo " Burn-all batch: to=$MSCO operation=1(delegatecall) subCalls=$(jget "['batches']['burnAll']['subCallCount']")"
|
|
echo "=============================================================="
|
|
}
|
|
|
|
MODE="${1:-dry}"
|
|
|
|
case "$MODE" in
|
|
dry|--dry|--dry-run|"")
|
|
print_plan
|
|
echo ""
|
|
echo "DRY RUN — no transactions sent."
|
|
if $CAST block-number --rpc-url "$FORK" >/dev/null 2>&1; then
|
|
DATA=$(jget "['batches']['withdraw']['execTransactionData']")
|
|
echo "Computed Safe tx hash for the withdraw batch (against fork read-only):"
|
|
echo " $(safe_tx_hash "$DATA")"
|
|
else
|
|
echo "(fork on $FORK not running — skip live tx-hash computation; reference hash in JSON forkProof.safeTxHash_at_nonce0)"
|
|
fi
|
|
echo ""
|
|
echo "MAINNET EXECUTION (performed by the Safe owner, NOT this script):"
|
|
echo " 1. Load Safe $SAFE in the Safe UI (or your signer)."
|
|
echo " 2. New transaction -> Contract interaction / raw:"
|
|
echo " to = $MSCO (MultiSendCallOnly)"
|
|
echo " value = 0"
|
|
echo " operation = 1 (delegatecall) <-- REQUIRED so inner calls run as the Safe"
|
|
echo " data = batches.withdraw.execTransactionData (in v3-to-v4-withdraw.json)"
|
|
echo " 3. Owner $OWNER signs (threshold 1) and executes."
|
|
echo " 4. Optionally repeat with batches.burnAll.execTransactionData to burn the 74 emptied NFTs."
|
|
;;
|
|
|
|
--execute-fork)
|
|
echo "### EXECUTE ON FORK ONLY ($FORK) ###"
|
|
if ! $CAST block-number --rpc-url "$FORK" >/dev/null 2>&1; then
|
|
echo "ERROR: no anvil fork on $FORK. Launch:"
|
|
echo " anvil --fork-url $UPSTREAM --chain-id 96369 --auto-impersonate --silent --port 8545 --fork-block-number $FORK_BLOCK --no-rate-limit"
|
|
exit 1
|
|
fi
|
|
echo "resetting fork to block $FORK_BLOCK ..."
|
|
$CAST rpc anvil_reset "{\"forking\":{\"jsonRpcUrl\":\"$UPSTREAM\",\"blockNumber\":$FORK_BLOCK}}" --rpc-url "$FORK" >/dev/null
|
|
echo "live liquidity BEFORE:"; for t in $(live_ids); do echo " id $t liq=$(liq $t)"; done
|
|
exec_batch "$(jget "['batches']['withdraw']['execTransactionData']")" "withdraw"
|
|
ALLZERO=1
|
|
echo "live liquidity AFTER (must all be 0):"
|
|
for t in $(live_ids); do L=$(liq $t); echo " id $t liq=$L"; [ "$L" != "0" ] && ALLZERO=0; done
|
|
if [ "${2:-}" = "--with-burns" ]; then
|
|
echo "running burn-all cleanup batch ..."
|
|
exec_batch "$(jget "['batches']['burnAll']['execTransactionData']")" "burnAll"
|
|
echo "Safe NFPM balanceOf after burns = $($CAST call $NFPM 'balanceOf(address)(uint256)' $SAFE --rpc-url "$FORK")"
|
|
fi
|
|
echo ""
|
|
echo "### ALL_LIVE_LIQUIDITY_ZERO=$ALLZERO (1 = success) ###"
|
|
;;
|
|
|
|
*)
|
|
echo "usage: $0 [dry] # default: print plan + tx hash, no sends"
|
|
echo " $0 --execute-fork [--with-burns] # execute against 127.0.0.1:8545 fork ONLY"
|
|
exit 2
|
|
;;
|
|
esac
|