Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
196 changes: 196 additions & 0 deletions staking-dashboard/scripts/multi-rollup-test/seed-fork.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
/**
* Seed multi-rollup reward state on a forked-mainnet anvil.
*
* Auto-discovers rollups by reading `Registry.CanonicalRollupUpdated` events,
* then writes `sequencerRewards[TARGET_ADDRESS]` on each rollup via
* `anvil_setStorageAt`. The fork already has real fee-asset balances and
* `isRewardsClaimable=true` from mainnet state, so nothing else needs touching.
*
* Run from repo root:
* npx tsx staking-dashboard/scripts/multi-rollup-test/seed-fork.ts
*
* Env overrides:
* TARGET_ADDRESS - address to seed rewards for (default: anvil account 0)
* RPC_URL - anvil RPC URL (default: http://127.0.0.1:8545)
* REWARD_AMOUNTS - comma-separated reward amounts in whole tokens, oldest
* rollup first (default: "5,10" — 5 on v1, 10 on v2)
*/

import {
createPublicClient,
createTestClient,
http,
keccak256,
encodeAbiParameters,
numberToHex,
stringToHex,
parseAbi,
type Address,
type Hex,
type Log,
} from "viem";
import { mainnet } from "viem/chains";
import { readFileSync } from "fs";
import { resolve, dirname } from "path";
import { fileURLToPath } from "url";

const __dirname = dirname(fileURLToPath(import.meta.url));
const REPO_ROOT = resolve(__dirname, "../../..");
const ADDRS_FILE = resolve(REPO_ROOT, "atp-indexer/contract_addresses.json");

const TARGET_ADDRESS = (
process.env.TARGET_ADDRESS ?? "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"
) as Address;
const RPC_URL = process.env.RPC_URL ?? "http://127.0.0.1:8545";
const REWARD_AMOUNTS = (process.env.REWARD_AMOUNTS ?? "5,10")
.split(",")
.map((s) => BigInt(s.trim()));

const addrs = JSON.parse(readFileSync(ADDRS_FILE, "utf-8"));
const REGISTRY_ADDRESS = addrs.registryAddress as Address;
const REGISTRY_DEPLOY_BLOCK = BigInt(addrs.registryDeploymentBlock);

// ERC-7201-style namespaced storage. Base = keccak256 of the raw UTF-8 string
// (NOT abi.encode("aztec.reward.storage"), which produces a different hash).
// slot 0: mapping(address => uint256) sequencerRewards
const REWARD_STORAGE_BASE = keccak256(stringToHex("aztec.reward.storage"));
const SEQUENCER_REWARDS_SLOT = BigInt(REWARD_STORAGE_BASE);

const CANONICAL_UPDATED_EVENT = {
type: "event",
name: "CanonicalRollupUpdated",
inputs: [
{ name: "instance", type: "address", indexed: true },
{ name: "version", type: "uint256", indexed: true },
],
} as const;

const rollupAbi = parseAbi([
"function getSequencerRewards(address) view returns (uint256)",
"function isRewardsClaimable() view returns (bool)",
"function getVersion() view returns (uint256)",
"function getFeeAsset() view returns (address)",
]);

const erc20Abi = parseAbi(["function balanceOf(address) view returns (uint256)"]);

const publicClient = createPublicClient({ chain: mainnet, transport: http(RPC_URL) });
const testClient = createTestClient({ chain: mainnet, mode: "anvil", transport: http(RPC_URL) });

function mappingSlot(key: Address, baseSlot: bigint): Hex {
return keccak256(
encodeAbiParameters([{ type: "address" }, { type: "uint256" }], [key, baseSlot]),
);
}

async function discoverRollups(): Promise<{ address: Address; version: bigint; blockNumber: bigint }[]> {
const logs = await publicClient.getLogs({
address: REGISTRY_ADDRESS,
event: CANONICAL_UPDATED_EVENT,
fromBlock: REGISTRY_DEPLOY_BLOCK,
toBlock: "latest",
});

// Dedupe by rollup address. Keep oldest event per rollup (first canonical).
const seen = new Map<Address, { address: Address; version: bigint; blockNumber: bigint }>();
for (const log of logs as Log[]) {
const args = (log as unknown as { args: { instance: Address; version: bigint } }).args;
const addr = args.instance.toLowerCase() as Address;
if (!seen.has(addr)) {
seen.set(addr, {
address: args.instance,
version: args.version,
blockNumber: log.blockNumber!,
});
}
}
return [...seen.values()].sort((a, b) => Number(a.blockNumber - b.blockNumber));
}

async function main() {
console.log(`\nSeeding multi-rollup rewards on fork`);
console.log(` RPC: ${RPC_URL}`);
console.log(` target: ${TARGET_ADDRESS}\n`);

const chainId = await publicClient.getChainId();
if (chainId !== 1) {
throw new Error(`expected chainId 1 (mainnet fork), got ${chainId}`);
}

const rollups = await discoverRollups();
if (rollups.length === 0) {
throw new Error(`no CanonicalRollupUpdated events found on Registry ${REGISTRY_ADDRESS}`);
}
console.log(`Discovered ${rollups.length} rollup(s) from Registry events:`);
for (const r of rollups) {
console.log(` - ${r.address} (version=${r.version}, block=${r.blockNumber})`);
}
console.log();

if (REWARD_AMOUNTS.length < rollups.length) {
console.warn(
`Note: ${rollups.length} rollups discovered but only ${REWARD_AMOUNTS.length} reward amount(s) given; ` +
`remaining rollups will be seeded with the last value (${REWARD_AMOUNTS[REWARD_AMOUNTS.length - 1]}).`,
);
}

const slot = mappingSlot(TARGET_ADDRESS, SEQUENCER_REWARDS_SLOT);
for (let i = 0; i < rollups.length; i++) {
const rollup = rollups[i];
const amount = REWARD_AMOUNTS[Math.min(i, REWARD_AMOUNTS.length - 1)] * 10n ** 18n;

await testClient.setStorageAt({
address: rollup.address,
index: slot,
value: numberToHex(amount, { size: 32 }),
});

// Verify via the contract getter (covers both slot calc and the fork actually accepting the write)
const got = await publicClient.readContract({
address: rollup.address,
abi: rollupAbi,
functionName: "getSequencerRewards",
args: [TARGET_ADDRESS],
});
if (got !== amount) {
throw new Error(
`verification failed for ${rollup.address}: wrote ${amount} but getSequencerRewards returned ${got}`,
);
}

// Sanity: claimSequencerRewards transfers fee asset from the rollup, so the
// rollup needs to hold at least `amount` of it. On a fresh mainnet fork this
// is true (rollups already hold the AZTEC token). Warn if it's not.
const feeAsset = await publicClient.readContract({
address: rollup.address,
abi: rollupAbi,
functionName: "getFeeAsset",
});
const feeBal = await publicClient.readContract({
address: feeAsset,
abi: erc20Abi,
functionName: "balanceOf",
args: [rollup.address],
});
const claimable = await publicClient.readContract({
address: rollup.address,
abi: rollupAbi,
functionName: "isRewardsClaimable",
});

console.log(
` ✓ ${rollup.address} rewards=${amount} claimable=${claimable} ` +
`feeBal=${feeBal} ${feeBal < amount ? "(LOW — claim will revert)" : ""}`,
);
}

const lsKey = `rewards_coinbase_addresses_${TARGET_ADDRESS.toLowerCase()}`;
const lsValue = JSON.stringify([TARGET_ADDRESS.toLowerCase()]);
console.log(`\nDone. In the dashboard's DevTools console, paste:`);
console.log(` localStorage.setItem('${lsKey}', '${lsValue}'); location.reload();`);
}

main().catch((err) => {
console.error(`\nError: ${err.message ?? err}\n`);
process.exit(1);
});
141 changes: 141 additions & 0 deletions staking-dashboard/scripts/multi-rollup-test/setup-fork.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
#!/bin/bash
# Set up a forked-mainnet anvil environment for end-to-end multi-rollup testing.
#
# Assumes anvil is already running at http://127.0.0.1:8545 forking mainnet.
# Generates atp-indexer/.env and staking-dashboard/.env pointing at the local
# anvil + local indexer, then seeds sequencer rewards for the target address
# on every rollup the Registry has ever made canonical.
#
# Usage:
# bash staking-dashboard/scripts/multi-rollup-test/setup-fork.sh
#
# Env overrides:
# TARGET_ADDRESS address to seed rewards for (default: anvil account 0)
# RPC_URL anvil RPC URL (default: http://127.0.0.1:8545)
# INDEXER_PORT port for the indexer API (default: 42068)

set -eu

ROOT=$(git rev-parse --show-toplevel)
INDEXER=$ROOT/atp-indexer
DASHBOARD=$ROOT/staking-dashboard
ADDRS_FILE=$INDEXER/contract_addresses.json

TARGET_ADDRESS="${TARGET_ADDRESS:-0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266}"
RPC_URL="${RPC_URL:-http://127.0.0.1:8545}"
INDEXER_PORT="${INDEXER_PORT:-42068}"

source "$ROOT/scripts/logging.sh"

# ---------- 1. Verify anvil ----------
log_step "Checking anvil at $RPC_URL..."
ANVIL_CHAIN_ID=$(cast chain-id --rpc-url "$RPC_URL" 2>/dev/null || echo "")
if [ -z "$ANVIL_CHAIN_ID" ]; then
echo "Error: could not reach anvil at $RPC_URL"
exit 1
fi
if [ "$ANVIL_CHAIN_ID" != "1" ]; then
echo "Error: anvil chain id is $ANVIL_CHAIN_ID, expected 1 (mainnet fork)."
echo "Restart anvil with: anvil --fork-url \$MAINNET_RPC --fork-block-number <pinned-block>"
exit 1
fi
ANVIL_BLOCK=$(cast block-number --rpc-url "$RPC_URL")
log_success " anvil OK: chain $ANVIL_CHAIN_ID @ block $ANVIL_BLOCK"

# ---------- 2. Validate addresses file ----------
if [ ! -f "$ADDRS_FILE" ]; then
echo "Error: $ADDRS_FILE not found"
exit 1
fi

ATP_FACTORY=$(jq -r '.atpFactory' "$ADDRS_FILE")
ATP_FACTORY_AUCTION=$(jq -r '.atpFactoryAuction' "$ADDRS_FILE")
ATP_FACTORY_MATP=$(jq -r '.atpFactoryMatp' "$ADDRS_FILE")
ATP_FACTORY_LATP=$(jq -r '.atpFactoryLatp' "$ADDRS_FILE")
ATP_REGISTRY=$(jq -r '.atpRegistry' "$ADDRS_FILE")
ATP_REGISTRY_AUCTION=$(jq -r '.atpRegistryAuction' "$ADDRS_FILE")
STAKING_REGISTRY=$(jq -r '.stakingRegistry' "$ADDRS_FILE")
REGISTRY=$(jq -r '.registryAddress' "$ADDRS_FILE")
ATP_FACTORY_BLOCK=$(jq -r '.atpFactoryDeploymentBlock' "$ADDRS_FILE")
REGISTRY_BLOCK=$(jq -r '.registryDeploymentBlock' "$ADDRS_FILE")
ATP_STAKER=$(jq -r '.atpWithdrawableAndClaimableStaker' "$ADDRS_FILE")
GENESIS_SALE=$(jq -r '.genesisSequencerSale' "$ADDRS_FILE")
GOVERNANCE=$(jq -r '.governanceAddress' "$ADDRS_FILE")
GSE=$(jq -r '.gseAddress' "$ADDRS_FILE")

# ---------- 3. Write atp-indexer/.env ----------
log_step "Writing $INDEXER/.env (start blocks pinned to mainnet deployment)..."
[ -f "$INDEXER/.env" ] && cp "$INDEXER/.env" "$INDEXER/.env.pre-fork"
cat > "$INDEXER/.env" <<EOF
# Generated by scripts/multi-rollup-test/setup-fork.sh
POSTGRES_CONNECTION_STRING=

RPC_URL=$RPC_URL
CHAIN_ID=1

ATP_FACTORY_ADDRESS=$ATP_FACTORY
ATP_FACTORY_AUCTION_ADDRESS=$ATP_FACTORY_AUCTION
ATP_FACTORY_MATP_ADDRESS=$ATP_FACTORY_MATP
ATP_FACTORY_LATP_ADDRESS=$ATP_FACTORY_LATP
STAKING_REGISTRY_ADDRESS=$STAKING_REGISTRY
REGISTRY_ADDRESS=$REGISTRY

# Pin start blocks to mainnet deployment so Ponder doesn't try to walk genesis.
START_BLOCK=$ATP_FACTORY_BLOCK
REGISTRY_START_BLOCK=$REGISTRY_BLOCK

PORT=$INDEXER_PORT

NODE_ENV=development
LOG_LEVEL=info
EOF

# ---------- 4. Write staking-dashboard/.env ----------
log_step "Writing $DASHBOARD/.env (RPC pointed at local anvil)..."
[ -f "$DASHBOARD/.env" ] && cp "$DASHBOARD/.env" "$DASHBOARD/.env.pre-fork"
cat > "$DASHBOARD/.env" <<EOF
# Generated by scripts/multi-rollup-test/setup-fork.sh
VITE_CHAIN_ID=1
VITE_RPC_URL=$RPC_URL
VITE_API_HOST=http://localhost:$INDEXER_PORT
VITE_EXPLORER_URL=https://etherscan.io
VITE_VALIDATOR_DASHBOARD_URL=https://dashtec.xyz
VITE_WALLETCONNECT_PROJECT_ID=

VITE_ATP_FACTORY_ADDRESS=$ATP_FACTORY
VITE_ATP_FACTORY_AUCTION_ADDRESS=$ATP_FACTORY_AUCTION
VITE_ATP_REGISTRY_ADDRESS=$ATP_REGISTRY
VITE_ATP_REGISTRY_AUCTION_ADDRESS=$ATP_REGISTRY_AUCTION
VITE_STAKING_REGISTRY_ADDRESS=$STAKING_REGISTRY
VITE_ATP_NON_WITHDRAWABLE_STAKER_ADDRESS=$ATP_STAKER
VITE_ATP_WITHDRAWABLE_STAKER_ADDRESS=$ATP_STAKER
VITE_ATP_WITHDRAWABLE_AND_CLAIMABLE_STAKER_ADDRESS=$ATP_STAKER
VITE_GENESIS_SEQUENCER_SALE_ADDRESS=$GENESIS_SALE
VITE_GOVERNANCE_ADDRESS=$GOVERNANCE
VITE_GSE_ADDRESS=$GSE

NODE_ENV=development
EOF

# ---------- 5. Seed rewards ----------
log_step "Seeding sequencer rewards via anvil_setStorageAt..."
cd "$ROOT"
TARGET_ADDRESS="$TARGET_ADDRESS" RPC_URL="$RPC_URL" \
npx tsx "$DASHBOARD/scripts/multi-rollup-test/seed-fork.ts"

# ---------- 6. Next steps ----------
log_success ""
log_success "Setup complete. To run the stack:"
echo ""
echo " Terminal 2 (indexer):"
echo " cd $INDEXER && yarn install && yarn dev"
echo ""
echo " Terminal 3 (dashboard):"
echo " cd $DASHBOARD && yarn install && yarn dev"
echo ""
echo "After indexer catches up past block $REGISTRY_BLOCK, the dashboard's"
echo "/api/rollups call will succeed. The localStorage snippet above pre-fills"
echo "the saved coinbase address so the rewards section is populated immediately."
echo ""
echo "Re-running just the seed (e.g. after anvil reset):"
echo " npx tsx $DASHBOARD/scripts/multi-rollup-test/seed-fork.ts"
Loading
Loading