migration.gno
3.58 Kb · 88 lines
1// Package memba_points_migration_v1 is a one-time, owner-gated importer of legacy backend XP into the
2// soulbound memba_points_v1 ledger. It holds NO funds and mints NO points itself — it calls the audited
3// awarder path (memba_points_v1.Award) so every migrated grant lands as a normal PointsAwarded event
4// (source = this realm, reason = "xp_migration"). Idempotent per-address + permanently finalizable.
5//
6// The parsing / validation / dedup logic lives in the pure, unit-tested
7// p/samcrew/memba_points_migration_core_v1 package; this realm is the thin state + effect shell
8// (owner gate, migrated set, finalize latch, the Award loop). It can't be unit-tested under bare
9// `gno test` because it imports the unpublished points_v1 — its logic is covered by the core tests
10// plus a test13 ceremony dry-run.
11//
12// Rollout (owner ceremony, after points_v1 is deployed): points_v1.AddAwarder(this realm) →
13// MigrateBatch × N → FinalizeMigration → points_v1.RemoveAwarder(this realm). See README.md.
14package memba_points_migration_v1
15
16import (
17 "chain"
18
19 "gno.land/p/nt/avl/v0"
20 "gno.land/p/nt/ufmt/v0"
21 core "gno.land/p/samcrew/memba_points_migration_core_v1"
22 points "gno.land/r/samcrew/memba_points_v1"
23)
24
25// AdminAddress inits owner — the samcrew-core deploy multisig (same as memba_points_v1). Hand it to the
26// memba_dao executor post-deploy via a 2-step transfer if desired.
27const AdminAddress = "g1x7k4628w93a7wzdhqc06atzx0v50rnshweuxu0"
28
29const (
30 // MaxBatch bounds entries per MigrateBatch tx (gas).
31 MaxBatch = 100
32 // reasonXPMigration tags every migrated grant in the points_v1 audit trail.
33 reasonXPMigration = "xp_migration"
34)
35
36var (
37 owner address
38 migrated = avl.NewTree() // addr string -> true (idempotency ledger)
39 finalized bool // once true, MigrateBatch refuses forever
40)
41
42func init() { owner = address(AdminAddress) }
43
44// assertOwner uses the interrealm-v2 sanctioned primitives (statically scoped to this crossing frame,
45// cannot be spoofed) — matching the hardened memba_points_v1.
46func assertOwner(cur realm) {
47 if !cur.IsCurrent() {
48 panic("spoofed realm")
49 }
50 if cur.Previous().Address() != owner {
51 panic("unauthorized: owner only")
52 }
53}
54
55// MigrateBatch imports a batch of legacy XP as points. Owner only. `entries` is
56// "addr:amount,addr:amount" (≤ MaxBatch). Malformed input fails the whole tx (fail-fast); an
57// already-migrated address is skipped; the call is refused once finalized. Each fresh entry is awarded
58// through the audited memba_points_v1 awarder path.
59func MigrateBatch(cur realm, entries string) {
60 assertOwner(cur)
61 plan := core.PlanBatch(entries, points.MaxAward, MaxBatch, finalized, func(a string) bool {
62 return migrated.Has(a)
63 })
64 for _, e := range plan {
65 points.Award(cross(cur), address(e.Addr), e.Amount, reasonXPMigration)
66 migrated.Set(e.Addr, true)
67 chain.Emit("XPMigrated", "addr", e.Addr, "amount", ufmt.Sprintf("%d", e.Amount))
68 }
69}
70
71// FinalizeMigration permanently closes the migration — no further MigrateBatch. Owner only, irreversible.
72func FinalizeMigration(cur realm) {
73 assertOwner(cur)
74 finalized = true
75 chain.Emit("MigrationFinalized")
76}
77
78// IsMigrated reports whether an address has already been imported.
79func IsMigrated(addr string) bool { return migrated.Has(addr) }
80
81// IsFinalized reports whether the migration is permanently closed.
82func IsFinalized() bool { return finalized }
83
84// Render — minimal gnoweb status (owner is a fixed const-derived address; no untrusted input).
85func Render(_ string) string {
86 return ufmt.Sprintf("# Memba Points — XP Migration\n\n- Finalized: **%t**\n- Owner: `%s`\n",
87 finalized, owner.String())
88}