Skip to content

feat: re-extract the public-sale bindings, 11 more participants paid at genesis - #75

Closed
alexiscolin wants to merge 1 commit into
gnolang:mainfrom
alexiscolin:feat/publicsale-resnapshot-2026-09-11
Closed

alexiscolin wants to merge 1 commit into
gnolang:mainfrom
alexiscolin:feat/publicsale-resnapshot-2026-09-11

Conversation

@alexiscolin

@alexiscolin alexiscolin commented Sep 11, 2026 •

Copy link
Copy Markdown
Member

Eleven more of the 122 settled public-sale wallets have named a gno.land address since #65, one of them the largest unclaimed row in the sale (2,739,572.093023 GNOT). This pays them at genesis instead of leaving them to the multisig. Supply-neutral.

Opening this per @dongwon8247's request, so @moul can decide whether to take it now or after genesis. Nothing breaks if it is not merged: the eleven stay covered by the multisig row and are paid by hand instead.

before after
Paid directly at genesis 67 wallets, 12,266,287.839945 GNOT 78 wallets, 16,521,530.801184 GNOT
[sale-unclaimed] multisig 9,338,399.590158 GNOT 5,083,156.628919 GNOT
Sale total / total supply 21,604,687.430103 / 1,332,999,998.328067 unchanged / unchanged

The new extract is a new file, per inputs/README.md's own rule: inputs/publicsale-sonar-2026-09-11.csv, ending at address_bindings id 79. The 09-09 file is kept byte-for-byte. The four hand-maintained pins in allocate/process_test.go move with the data; no code behaviour changes (two one-line comments updated alongside).

Reviewing the 57 MB binary in thirty seconds

diff <(git show main:mkgenesis/balances.txt.gz | gzip -dc | sort) <(gzip -dc mkgenesis/balances.txt.gz | sort)

14 lines: ten insertions, the one participant who also holds an airdrop (that row gains exactly its sale amount, §132 suffix byte-identical, the airdrop half still vests), and the multisig decrement. Nothing else in the 3.26M rows moves. balances.txt.gz was rebuilt with GNU gzip 1.12, which reproduces your container bit-for-bit; Apple's gzip does not, so a macOS rebuild shows that one file dirty.

Also corrected, in files this change edits anyway

  1. The provenance dated its extract with a local (UTC+9) timestamp written as UTC, 9h out ("2026-09-09T06:46Z" was the local time the export was saved, 2026-09-08T21:47Z in UTC - five bindings dated earlier on 09-09 than the stated instant are visibly absent from that file). The cutoff is now the address_bindings id, which is checkable. Nothing is wrong with either extract; only the label was.
  2. The vesting caveat said dropping the one declared-schedule row makes a pre-331e17fc3 binary safe. It does not: 28 whitelisted addresses carry a vesting line (26 airdrop-overlapping sale rows + two §127 funds), so that removes 1 trigger of 28. The paragraph now says so, with a one-liner to list them.

Before you merge

  • mkgenesis/unrestricted.txt goes 71 -> 82 addresses: sha256 9bc496a3… -> 683891151692a86b82c6f30fab9c81aa2c9191a7252241f649201837a18ad265. balances.txt.gz: aaf850fd… -> 38a604f2434ebb1c15fdf68c79dcbf26425c037888813023a69293e23cce9067 (57,244,031 B). This repo's README records both as fetched by pinned URL from gnolang/gno deployment scripts; on current gno master I could only find gnoland1 and test13 gen-genesis.sh, pinning commit 9dec38a4 (pre-sale). Whichever script cuts mainnet genesis needs the bump - say the word and I open it.
  • docs/history.md is deliberately untouched: a row here would sit across a long gap of unrecorded number changes (fix: enforce the IBC escrow skip — 36.62M GNOT to 106 keyless accounts #55-feat: generate the Constitution §126 unrestricted-address list (re-land) #74). Better one backfill later than a lone row implying nothing else moved.
  • Still no cutoff date; this goes stale with the next binding. Name a date and the next extract can be the last.

Verified beyond CI: all eleven re-derived from the raw Ethereum event log (accepted USD from CommitmentIncreased deltas, base at 129/2000, tiered by replaying the 173 increases in order, 24h from the first BidPlaced; exact rational arithmetic) - all match to the ugnot; all 79 binding signatures recover to the paying wallet under EIP-191; gnogenesis built from a revision containing 331e17fc3 (gnolang/gno#6095) loads the sheet, reports "79 balances saved", verify passes, ;type=delayed survives verbatim.

Left out on purpose: a test tying mkgenesis/publicsale.txt to the settlement CSV (today, redirecting one destination and regenerating leaves the whole suite green - demonstrated). Written and mutation-tested; coming as its own PR so this one stays data-only.

…at genesis

The binding page has no deadline, so the list gnolang#65 froze keeps growing.
inputs/README.md already says what to do about it: "replace the file at a new
instant rather than adding rows to this one". This is the second extract.

Eleven more of the 122 settled wallets named a gno.land address, so 78 are now
paid directly at genesis and 44 stay with the [sale-unclaimed] multisig. One of
the eleven is the largest unclaimed row in the sale, 2,739,572.093023 GNOT.

SUPPLY-NEUTRAL, AS gnolang#65 WAS

The eleven move 4,255,242.961239 GNOT out of the multisig row and into rows of
their own. The sheet still totals 21,604,687.430103 GNOT,
TOTAL_PUBLIC_SALE_UGNOT is untouched, and total supply stays at
1,332,999,998.328067 GNOT. balances.txt goes 3,262,454 -> 3,262,464 rows: eleven
new addresses, one of which merges into an airdrop row it already held. That
merge takes the pinned airdrop overlap 25 -> 26 addresses, and the merged row
keeps its §132 schedule byte-for-byte over the airdrop portion alone, its sale
portion liquid as §128 requires.

THE NEW EXTRACT IS A NEW FILE

inputs/publicsale-sonar-2026-09-11.csv, ending at address_bindings id 79. The
09-09 file is kept byte-for-byte: inputs/ is immutable, and that one has already
been published and reviewed. The two differ only in which wallets had bound.

No new lockup: all eleven are lockup_12m = no. The single declared vesting
schedule is unchanged.

TWO THINGS ON main THAT THIS CORRECTS IN PASSING

Both live in files this change edits anyway.

- The provenance dated its extract with a local (UTC+9) file timestamp written
  as if it were UTC, 9 hours out. The cutoff is now quoted as the
  address_bindings id, which is checkable, rather than a wall-clock instant.
- The vesting caveat said dropping the one declared-schedule row makes a
  pre-331e17fc3 binary safe. It does not: 28 addresses are both in
  unrestricted.txt and carry a vesting line, so that removes 1 of 28 InitChain
  panic triggers.

VERIFIED

make verify passes. The eleven were re-derived from the Ethereum event log
rather than copied: accepted USD as the sum of each wallet's CommitmentIncreased
deltas, base at the 129/2000 clearing price, the tiered band by replaying the
173 increases in (block, logIndex) order, 24h eligibility from the first
BidPlaced. Exact rational arithmetic. All eleven match to the ugnot. All 79
binding signatures recover to the paying wallet under EIP-191. gno's own
GetBalancesFromSheet (at a revision containing 331e17fc3, gnolang/gno#6095)
reads the sheet as 79 balances / 21,604,687,430,103 ugnot / 1 vesting schedule,
and the gnogenesis binary reports "79 balances saved" with chain_id gnoland1 and
verify passing.
@alexiscolin
alexiscolin force-pushed the feat/publicsale-resnapshot-2026-09-11 branch from c92ff3f to 0911773 Compare September 11, 2026 07:13
@alexiscolin
alexiscolin requested a review from moul September 11, 2026 07:49
moul added a commit that referenced this pull request Sep 11, 2026
… time

inputs/README.md says a snapshot is "replaced wholesale". #75 kept the 09-09
file alongside the 09-11 one instead, on the grounds that it had already been
published and reviewed. Replaced means replaced: the id-68 extract is deleted,
and this directory holds exactly one public-sale extract from now on.

NOTHING IS LOST, AND THAT IS CHECKED RATHER THAN ASSUMED

  - the two hold the same 122 wallets with IDENTICAL amounts: the
    (ugnot_total, base, tiered_bonus, first24h_bonus, accepted_usd, lockup_12m,
    first_24h_eligible) tuples of the two files are the same multiset
  - every GENESIS row of the id-68 file -- address, bound_at and signer included
    -- is present VERBATIM in the id-79 one, so no binding was dropped or
    redirected between extracts
  - the file is still in git where it was captured, 9ecf4d3 (#65):
        git show 9ecf4d3:inputs/publicsale-sonar-2026-09-09.csv

Nothing in the repository read it. No Go file, no test, no Makefile rule
references either CSV by name -- they are reference material for humans, and the
loaded rows live in mkgenesis/publicsale.txt. The table in inputs/README.md had
already been corrected on main, where it claimed allocate/process_test.go
consumed the 09-09 file.

WHY DELETING IS THE SAFER OPTION, NOT THE BOLDER ONE

Which extract is live is not recorded in either CSV. With two in the tree a
reader has to work it out, and a stale file sitting next to a current one is the
exact shape of the drift the rest of inputs/'s rules exist to prevent -- the
same class of bug as a stale generated artifact. One file, named by the id it
stops at, cannot be read wrong.

inputs/README.md and README-publicsale-provenance.md say all of the above, so
the next extract does not have to rediscover it.

make verify green; no artifact changes -- the CSV is not an input to the build.
moul pushed a commit that referenced this pull request Sep 11, 2026
…at genesis

The binding page has no deadline, so the list #65 froze keeps growing.
inputs/README.md already says what to do about it: "replace the file at a new
instant rather than adding rows to this one". This is the second extract.

Eleven more of the 122 settled wallets named a gno.land address, so 78 are now
paid directly at genesis and 44 stay with the [sale-unclaimed] multisig. One of
the eleven is the largest unclaimed row in the sale, 2,739,572.093023 GNOT.

SUPPLY-NEUTRAL, AS #65 WAS

The eleven move 4,255,242.961239 GNOT out of the multisig row and into rows of
their own. The sheet still totals 21,604,687.430103 GNOT,
TOTAL_PUBLIC_SALE_UGNOT is untouched, and total supply stays at
1,332,999,998.328067 GNOT. balances.txt goes 3,262,463 -> 3,262,473 rows: eleven
new addresses, one of which merges into an airdrop row it already held. That
merge takes the pinned airdrop overlap 25 -> 26 addresses, and the merged row
keeps its §132 schedule byte-for-byte over the airdrop portion alone, its sale
portion liquid as §128 requires.

THE NEW EXTRACT REPLACES THE OLD ONE

inputs/publicsale-sonar-2026-09-11.csv, ending at address_bindings id 79. It
does not sit beside the id-68 extract: inputs/README.md says a snapshot is
"replaced wholesale", and the follow-up commit deletes that file. The two hold
the same 122 wallets with identical amounts and differ only in which had bound,
and git keeps the old one at 9ecf4d3 (#65) regardless.

No new lockup: all eleven are lockup_12m = no. The single declared vesting
schedule is unchanged.

REBASED ONTO #73, WHICH CHANGES WHAT THIS HAS TO TOUCH

Opened as #75 against main, where publicsale.txt was the only §136 carve-out.
#73 makes it one of two, so five things are different here and none of them is
a data change:

- The §136 closure is now a three-line sum, and TestTheThreeSection136Lines-
  SumToTheTranche is left completely untouched. That is the strongest statement
  available that this change is supply-neutral: an extract that moved a single
  ugnot across the sale/tranche boundary could not leave that test alone.
- verify/crosscheck is keyed by filename as of #73, so a fifth sheet no longer
  shifts genbalance's index. The version against main did not need that and
  would have silently reintroduced it the next time a sheet was added.
- The four hand-maintained pins move through #73's shared readSheet() rather
  than the copy readSaleSheet() used to be.
- mkgenesis/unrestricted.txt is GENERATED by #74 from both §136 sheets, so the
  eleven are not hand-added: `make -C mkgenesis` emits 80 -> 91 addresses.
- ABOUT.md, docs/pipeline.md and README.md reconciled across THREE sheets, not
  two. #73 added investors.txt without them, so the row arithmetic in ABOUT.md
  and the tree in pipeline.md were 9 rows short before this; they now balance:

      3,262,351 + (66 - 6) + (79 - 26) + 9 = 3,262,473

  investors.txt intersects nothing -- not the airdrop, not the other two sheets
  -- so all nine are new rows. Checked by set arithmetic, not assumed.

The rebase is checkable without trusting any of the above. Sorted, the sheet
built here differs from the one on #75 by exactly eleven lines: the nine
distribution rows, and INVESTORS_UNLOCKED_ADDRESS going 128,395,312.569897 ->
112,318,842.569897, which is -16,076,470 to the ugnot. Nothing else in 3.26M
rows moves, so this carries #73's change and #75's change and invents neither.

    diff <(gzip -dc <#75's balances.txt.gz> | sort) \
         <(gzip -dc mkgenesis/balances.txt.gz | sort)

TWO THINGS ON main THAT THIS CORRECTS IN PASSING

Both live in files this change edits anyway.

- The provenance dated its extract with a local (UTC+9) file timestamp written
  as if it were UTC, 9 hours out. The cutoff is now quoted as the
  address_bindings id, which is checkable, rather than a wall-clock instant.
- The vesting caveat said dropping the one declared-schedule row makes a
  pre-331e17fc3 binary safe. It does not: 28 addresses are both in
  unrestricted.txt and carry a vesting line, so that removes 1 of 28 InitChain
  panic triggers. Still 28 with the nine distribution rows on the list -- they
  are unlocked and none holds an airdrop entitlement -- and the paragraph now
  says to re-run the count rather than trust the number.

ARTIFACTS

  mkgenesis/balances.txt.gz   fa4a8124... -> be4ad951...  57,243,726 -> 57,243,893 B
  mkgenesis/unrestricted.txt  632de96e... -> 7b37a168...  80 -> 91 addresses

Rebuilt with GNU gzip 1.12, which reproduces #73's own container bit-for-bit
(verified by round-tripping it before rebuilding); Apple's gzip does not, so a
macOS rebuild without GNU gzip on PATH shows that one file dirty.

VERIFIED

make verify is green end to end: allocate and mkgenesis tests, including the
golden rebuild and #74's regenerated-list check, and crosscheck reports "Balance
files match". Rebuilding twice produces the same bytes.

The sale-side evidence is carried over from #75 rather than re-run, and it is
about the same bytes: mkgenesis/publicsale.txt and the CSV here are byte-
identical to that PR, checked by sha256. In alexis' words:

The eleven were re-derived from the Ethereum event log rather than copied:
accepted USD as the sum of each wallet's CommitmentIncreased deltas, base at the
129/2000 clearing price, the tiered band by replaying the 173 increases in
(block, logIndex) order, 24h eligibility from the first BidPlaced. Exact
rational arithmetic. All eleven match to the ugnot. All 79 binding signatures
recover to the paying wallet under EIP-191. gno's own GetBalancesFromSheet (at a
revision containing 331e17fc3, gnolang/gno#6095) reads the sheet as 79 balances
/ 21,604,687,430,103 ugnot / 1 vesting schedule, and the gnogenesis binary
reports "79 balances saved" with chain_id gnoland1 and verify passing.
moul added a commit that referenced this pull request Sep 11, 2026
… time

inputs/README.md says a snapshot is "replaced wholesale". #75 kept the 09-09
file alongside the 09-11 one instead, on the grounds that it had already been
published and reviewed. Replaced means replaced: the id-68 extract is deleted,
and this directory holds exactly one public-sale extract from now on.

NOTHING IS LOST, AND THAT IS CHECKED RATHER THAN ASSUMED

  - the two hold the same 122 wallets with IDENTICAL amounts: the
    (ugnot_total, base, tiered_bonus, first24h_bonus, accepted_usd, lockup_12m,
    first_24h_eligible) tuples of the two files are the same multiset
  - every GENESIS row of the id-68 file -- address, bound_at and signer included
    -- is present VERBATIM in the id-79 one, so no binding was dropped or
    redirected between extracts
  - the file is still in git where it was captured, 9ecf4d3 (#65):
        git show 9ecf4d3:inputs/publicsale-sonar-2026-09-09.csv

Nothing in the repository read it. No Go file, no test, no Makefile rule
references either CSV by name -- they are reference material for humans, and the
loaded rows live in mkgenesis/publicsale.txt. The table in inputs/README.md had
already been corrected on main, where it claimed allocate/process_test.go
consumed the 09-09 file.

WHY DELETING IS THE SAFER OPTION, NOT THE BOLDER ONE

Which extract is live is not recorded in either CSV. With two in the tree a
reader has to work it out, and a stale file sitting next to a current one is the
exact shape of the drift the rest of inputs/'s rules exist to prevent -- the
same class of bug as a stale generated artifact. One file, named by the id it
stops at, cannot be read wrong.

inputs/README.md and README-publicsale-provenance.md say all of the above, so
the next extract does not have to rediscover it.

make verify green; no artifact changes -- the CSV is not an input to the build.
moul added a commit that referenced this pull request Sep 11, 2026
…at genesis (#75 rebased on #73) (#76)

* feat: re-extract the public-sale bindings, 11 more participants paid at genesis

The binding page has no deadline, so the list #65 froze keeps growing.
inputs/README.md already says what to do about it: "replace the file at a new
instant rather than adding rows to this one". This is the second extract.

Eleven more of the 122 settled wallets named a gno.land address, so 78 are now
paid directly at genesis and 44 stay with the [sale-unclaimed] multisig. One of
the eleven is the largest unclaimed row in the sale, 2,739,572.093023 GNOT.

SUPPLY-NEUTRAL, AS #65 WAS

The eleven move 4,255,242.961239 GNOT out of the multisig row and into rows of
their own. The sheet still totals 21,604,687.430103 GNOT,
TOTAL_PUBLIC_SALE_UGNOT is untouched, and total supply stays at
1,332,999,998.328067 GNOT. balances.txt goes 3,262,463 -> 3,262,473 rows: eleven
new addresses, one of which merges into an airdrop row it already held. That
merge takes the pinned airdrop overlap 25 -> 26 addresses, and the merged row
keeps its §132 schedule byte-for-byte over the airdrop portion alone, its sale
portion liquid as §128 requires.

THE NEW EXTRACT REPLACES THE OLD ONE

inputs/publicsale-sonar-2026-09-11.csv, ending at address_bindings id 79. It
does not sit beside the id-68 extract: inputs/README.md says a snapshot is
"replaced wholesale", and the follow-up commit deletes that file. The two hold
the same 122 wallets with identical amounts and differ only in which had bound,
and git keeps the old one at 9ecf4d3 (#65) regardless.

No new lockup: all eleven are lockup_12m = no. The single declared vesting
schedule is unchanged.

REBASED ONTO #73, WHICH CHANGES WHAT THIS HAS TO TOUCH

Opened as #75 against main, where publicsale.txt was the only §136 carve-out.
#73 makes it one of two, so five things are different here and none of them is
a data change:

- The §136 closure is now a three-line sum, and TestTheThreeSection136Lines-
  SumToTheTranche is left completely untouched. That is the strongest statement
  available that this change is supply-neutral: an extract that moved a single
  ugnot across the sale/tranche boundary could not leave that test alone.
- verify/crosscheck is keyed by filename as of #73, so a fifth sheet no longer
  shifts genbalance's index. The version against main did not need that and
  would have silently reintroduced it the next time a sheet was added.
- The four hand-maintained pins move through #73's shared readSheet() rather
  than the copy readSaleSheet() used to be.
- mkgenesis/unrestricted.txt is GENERATED by #74 from both §136 sheets, so the
  eleven are not hand-added: `make -C mkgenesis` emits 80 -> 91 addresses.
- ABOUT.md, docs/pipeline.md and README.md reconciled across THREE sheets, not
  two. #73 added investors.txt without them, so the row arithmetic in ABOUT.md
  and the tree in pipeline.md were 9 rows short before this; they now balance:

      3,262,351 + (66 - 6) + (79 - 26) + 9 = 3,262,473

  investors.txt intersects nothing -- not the airdrop, not the other two sheets
  -- so all nine are new rows. Checked by set arithmetic, not assumed.

The rebase is checkable without trusting any of the above. Sorted, the sheet
built here differs from the one on #75 by exactly eleven lines: the nine
distribution rows, and INVESTORS_UNLOCKED_ADDRESS going 128,395,312.569897 ->
112,318,842.569897, which is -16,076,470 to the ugnot. Nothing else in 3.26M
rows moves, so this carries #73's change and #75's change and invents neither.

    diff <(gzip -dc <#75's balances.txt.gz> | sort) \
         <(gzip -dc mkgenesis/balances.txt.gz | sort)

TWO THINGS ON main THAT THIS CORRECTS IN PASSING

Both live in files this change edits anyway.

- The provenance dated its extract with a local (UTC+9) file timestamp written
  as if it were UTC, 9 hours out. The cutoff is now quoted as the
  address_bindings id, which is checkable, rather than a wall-clock instant.
- The vesting caveat said dropping the one declared-schedule row makes a
  pre-331e17fc3 binary safe. It does not: 28 addresses are both in
  unrestricted.txt and carry a vesting line, so that removes 1 of 28 InitChain
  panic triggers. Still 28 with the nine distribution rows on the list -- they
  are unlocked and none holds an airdrop entitlement -- and the paragraph now
  says to re-run the count rather than trust the number.

ARTIFACTS

  mkgenesis/balances.txt.gz   fa4a8124... -> be4ad951...  57,243,726 -> 57,243,893 B
  mkgenesis/unrestricted.txt  632de96e... -> 7b37a168...  80 -> 91 addresses

Rebuilt with GNU gzip 1.12, which reproduces #73's own container bit-for-bit
(verified by round-tripping it before rebuilding); Apple's gzip does not, so a
macOS rebuild without GNU gzip on PATH shows that one file dirty.

VERIFIED

make verify is green end to end: allocate and mkgenesis tests, including the
golden rebuild and #74's regenerated-list check, and crosscheck reports "Balance
files match". Rebuilding twice produces the same bytes.

The sale-side evidence is carried over from #75 rather than re-run, and it is
about the same bytes: mkgenesis/publicsale.txt and the CSV here are byte-
identical to that PR, checked by sha256. In alexis' words:

The eleven were re-derived from the Ethereum event log rather than copied:
accepted USD as the sum of each wallet's CommitmentIncreased deltas, base at the
129/2000 clearing price, the tiered band by replaying the 173 increases in
(block, logIndex) order, 24h eligibility from the first BidPlaced. Exact
rational arithmetic. All eleven match to the ugnot. All 79 binding signatures
recover to the paying wallet under EIP-191. gno's own GetBalancesFromSheet (at a
revision containing 331e17fc3, gnolang/gno#6095) reads the sheet as 79 balances
/ 21,604,687,430,103 ugnot / 1 vesting schedule, and the gnogenesis binary
reports "79 balances saved" with chain_id gnoland1 and verify passing.

* chore(inputs): delete the superseded id-68 extract — one extract at a time

inputs/README.md says a snapshot is "replaced wholesale". #75 kept the 09-09
file alongside the 09-11 one instead, on the grounds that it had already been
published and reviewed. Replaced means replaced: the id-68 extract is deleted,
and this directory holds exactly one public-sale extract from now on.

NOTHING IS LOST, AND THAT IS CHECKED RATHER THAN ASSUMED

  - the two hold the same 122 wallets with IDENTICAL amounts: the
    (ugnot_total, base, tiered_bonus, first24h_bonus, accepted_usd, lockup_12m,
    first_24h_eligible) tuples of the two files are the same multiset
  - every GENESIS row of the id-68 file -- address, bound_at and signer included
    -- is present VERBATIM in the id-79 one, so no binding was dropped or
    redirected between extracts
  - the file is still in git where it was captured, 9ecf4d3 (#65):
        git show 9ecf4d3:inputs/publicsale-sonar-2026-09-09.csv

Nothing in the repository read it. No Go file, no test, no Makefile rule
references either CSV by name -- they are reference material for humans, and the
loaded rows live in mkgenesis/publicsale.txt. The table in inputs/README.md had
already been corrected on main, where it claimed allocate/process_test.go
consumed the 09-09 file.

WHY DELETING IS THE SAFER OPTION, NOT THE BOLDER ONE

Which extract is live is not recorded in either CSV. With two in the tree a
reader has to work it out, and a stale file sitting next to a current one is the
exact shape of the drift the rest of inputs/'s rules exist to prevent -- the
same class of bug as a stale generated artifact. One file, named by the id it
stops at, cannot be read wrong.

inputs/README.md and README-publicsale-provenance.md say all of the above, so
the next extract does not have to rediscover it.

make verify green; no artifact changes -- the CSV is not an input to the build.

---------

Co-authored-by: alexiscolin <alexis@jaunebleu.co>
@moul moul closed this Sep 11, 2026
@moul

moul commented Sep 11, 2026

Copy link
Copy Markdown
Member

moving to #73

moul added a commit that referenced this pull request Sep 11, 2026
)

* feat: pay the settled investor and partner distributions at genesis

Nine obligations already owed to counterparties -- market making, exchange
listing and integration, ecosystem liquidity -- paid at genesis out of the §136
tranche, by the same reasoning as the public sale: not an eighth bucket, just the
part of that tranche already spoken for.

  16,076,470 GNOT across 9 addresses

Total supply is unchanged. INVESTORS_UNLOCKED_ADDRESS now holds the tranche NET
of both carve-outs, 128,395,312.569897 -> 112,318,842.569897, and the three §136
lines still sum to exactly 150,000,000:

  112,318,842.569897   INVESTORS_UNLOCKED_ADDRESS
   21,604,687.430103   public sale        (68 rows)
   16,076,470.000000   distributions      (9 rows)
  --------------------
  150,000,000.000000

TestTheThreeSection136LinesSumToTheTranche asserts that directly, so a future
carve-out cannot quietly exceed the grant.

COUNTERPARTIES ARE NOT NAMED -- ANYWHERE IN THIS DIFF

Several are commercial counterparties whose identity and commercial terms are not
ours to publish. Rows are labelled investor1..investor9 and ordered by ADDRESS,
so neither the labels nor the ordering carries information. The real mapping is
held privately and is not in this repository.

That has to include the test that enforces it. The obvious way to write
TestInvestorDistributionsMatchFile is a blocklist of forbidden names -- which
publishes the whole counterparty list, in a public repository, inside the check
meant to protect it. So the test asserts the SHAPE of a row instead:

  ^g1[0-9a-z]{38}=[0-9]+ugnot # investor[0-9]+$

Anything an edit could append -- a name, a desk, a ticker -- fails to parse, and
it fails for every counterparty, including the ones nobody thought to enumerate.
Strictly stronger than a blocklist, and it leaks nothing.

Be honest about the limit: the AMOUNTS are in the genesis and are therefore
public no matter what, and the GNOT price is knowable. What is withheld is WHICH
counterparty each row belongs to. A stronger property needs the payment moved
off the genesis sheet, not relabelled. The check also constrains the payable
rows, not the prose header above them; that stays a human review.

WHAT IS DELIBERATELY NOT HERE

Obligations settled AFTER genesis: one counterparty wants the full amount
post-genesis for regulatory reasons, and two more are still pending. The tranche
multisig can pay them later; a genesis row cannot be undone. Their absence is why
the constant is 16,076,470 rather than the 28,073,670 the working sheet totals.

LIQUID, AND WHITELISTED

These come out of the §136 UNLOCKED tranche, so build.go marks the whole file
`unlocked` -- no §132 schedule, exactly like the sale. They must also be in
mkgenesis/unrestricted.txt: at genesis $GNOT is non-transferable (§126), and an
obligation paid into an address that cannot move it is not paid.

ALSO: THE CROSSCHECK INDEXED BY POSITION

verify/crosscheck compared balanceFiles[0] against balanceFiles[3]. Adding a
fourth sheet shifted genbalance from [3] to [4], so it compared the wrong pair
and reported every address as missing. Now keyed by filename, which is how I
found it -- the failure was loud, but only by luck of ordering.

Verified: crosscheck passes, total 1,332,999,998.328067 GNOT, rows 3,262,454 ->
3,262,463.

REBASED ONTO #74

#74 landed first and generates mkgenesis/unrestricted.txt from publicsale.txt
plus investors.txt when it exists. It exists now, so the committed list is
regenerated here: 71 -> 80 addresses, the nine distribution rows. Without this
the `generated` check is red on arrival -- #74 tolerates the file's absence, not
a stale list.

#74's TestUnrestrictedCoversEveryPublicSaleRow also had to learn about the
second sheet. It asserted len(listed) == len(sale)+3, which is the right
property on a tree with no investors.txt and a red build on one that has it --
the generator supports both paths, the test pinned one. It now reads
investors.txt the same way the generator does, tolerating absence, and covers
its rows too: a distribution paid into an address that cannot move it is the
exact failure the test exists to prevent.

* feat: re-extract the public-sale bindings, 11 more participants paid at genesis (#75 rebased on #73) (#76)

* feat: re-extract the public-sale bindings, 11 more participants paid at genesis

The binding page has no deadline, so the list #65 froze keeps growing.
inputs/README.md already says what to do about it: "replace the file at a new
instant rather than adding rows to this one". This is the second extract.

Eleven more of the 122 settled wallets named a gno.land address, so 78 are now
paid directly at genesis and 44 stay with the [sale-unclaimed] multisig. One of
the eleven is the largest unclaimed row in the sale, 2,739,572.093023 GNOT.

SUPPLY-NEUTRAL, AS #65 WAS

The eleven move 4,255,242.961239 GNOT out of the multisig row and into rows of
their own. The sheet still totals 21,604,687.430103 GNOT,
TOTAL_PUBLIC_SALE_UGNOT is untouched, and total supply stays at
1,332,999,998.328067 GNOT. balances.txt goes 3,262,463 -> 3,262,473 rows: eleven
new addresses, one of which merges into an airdrop row it already held. That
merge takes the pinned airdrop overlap 25 -> 26 addresses, and the merged row
keeps its §132 schedule byte-for-byte over the airdrop portion alone, its sale
portion liquid as §128 requires.

THE NEW EXTRACT REPLACES THE OLD ONE

inputs/publicsale-sonar-2026-09-11.csv, ending at address_bindings id 79. It
does not sit beside the id-68 extract: inputs/README.md says a snapshot is
"replaced wholesale", and the follow-up commit deletes that file. The two hold
the same 122 wallets with identical amounts and differ only in which had bound,
and git keeps the old one at 9ecf4d3 (#65) regardless.

No new lockup: all eleven are lockup_12m = no. The single declared vesting
schedule is unchanged.

REBASED ONTO #73, WHICH CHANGES WHAT THIS HAS TO TOUCH

Opened as #75 against main, where publicsale.txt was the only §136 carve-out.
#73 makes it one of two, so five things are different here and none of them is
a data change:

- The §136 closure is now a three-line sum, and TestTheThreeSection136Lines-
  SumToTheTranche is left completely untouched. That is the strongest statement
  available that this change is supply-neutral: an extract that moved a single
  ugnot across the sale/tranche boundary could not leave that test alone.
- verify/crosscheck is keyed by filename as of #73, so a fifth sheet no longer
  shifts genbalance's index. The version against main did not need that and
  would have silently reintroduced it the next time a sheet was added.
- The four hand-maintained pins move through #73's shared readSheet() rather
  than the copy readSaleSheet() used to be.
- mkgenesis/unrestricted.txt is GENERATED by #74 from both §136 sheets, so the
  eleven are not hand-added: `make -C mkgenesis` emits 80 -> 91 addresses.
- ABOUT.md, docs/pipeline.md and README.md reconciled across THREE sheets, not
  two. #73 added investors.txt without them, so the row arithmetic in ABOUT.md
  and the tree in pipeline.md were 9 rows short before this; they now balance:

      3,262,351 + (66 - 6) + (79 - 26) + 9 = 3,262,473

  investors.txt intersects nothing -- not the airdrop, not the other two sheets
  -- so all nine are new rows. Checked by set arithmetic, not assumed.

The rebase is checkable without trusting any of the above. Sorted, the sheet
built here differs from the one on #75 by exactly eleven lines: the nine
distribution rows, and INVESTORS_UNLOCKED_ADDRESS going 128,395,312.569897 ->
112,318,842.569897, which is -16,076,470 to the ugnot. Nothing else in 3.26M
rows moves, so this carries #73's change and #75's change and invents neither.

    diff <(gzip -dc <#75's balances.txt.gz> | sort) \
         <(gzip -dc mkgenesis/balances.txt.gz | sort)

TWO THINGS ON main THAT THIS CORRECTS IN PASSING

Both live in files this change edits anyway.

- The provenance dated its extract with a local (UTC+9) file timestamp written
  as if it were UTC, 9 hours out. The cutoff is now quoted as the
  address_bindings id, which is checkable, rather than a wall-clock instant.
- The vesting caveat said dropping the one declared-schedule row makes a
  pre-331e17fc3 binary safe. It does not: 28 addresses are both in
  unrestricted.txt and carry a vesting line, so that removes 1 of 28 InitChain
  panic triggers. Still 28 with the nine distribution rows on the list -- they
  are unlocked and none holds an airdrop entitlement -- and the paragraph now
  says to re-run the count rather than trust the number.

ARTIFACTS

  mkgenesis/balances.txt.gz   fa4a8124... -> be4ad951...  57,243,726 -> 57,243,893 B
  mkgenesis/unrestricted.txt  632de96e... -> 7b37a168...  80 -> 91 addresses

Rebuilt with GNU gzip 1.12, which reproduces #73's own container bit-for-bit
(verified by round-tripping it before rebuilding); Apple's gzip does not, so a
macOS rebuild without GNU gzip on PATH shows that one file dirty.

VERIFIED

make verify is green end to end: allocate and mkgenesis tests, including the
golden rebuild and #74's regenerated-list check, and crosscheck reports "Balance
files match". Rebuilding twice produces the same bytes.

The sale-side evidence is carried over from #75 rather than re-run, and it is
about the same bytes: mkgenesis/publicsale.txt and the CSV here are byte-
identical to that PR, checked by sha256. In alexis' words:

The eleven were re-derived from the Ethereum event log rather than copied:
accepted USD as the sum of each wallet's CommitmentIncreased deltas, base at the
129/2000 clearing price, the tiered band by replaying the 173 increases in
(block, logIndex) order, 24h eligibility from the first BidPlaced. Exact
rational arithmetic. All eleven match to the ugnot. All 79 binding signatures
recover to the paying wallet under EIP-191. gno's own GetBalancesFromSheet (at a
revision containing 331e17fc3, gnolang/gno#6095) reads the sheet as 79 balances
/ 21,604,687,430,103 ugnot / 1 vesting schedule, and the gnogenesis binary
reports "79 balances saved" with chain_id gnoland1 and verify passing.

* chore(inputs): delete the superseded id-68 extract — one extract at a time

inputs/README.md says a snapshot is "replaced wholesale". #75 kept the 09-09
file alongside the 09-11 one instead, on the grounds that it had already been
published and reviewed. Replaced means replaced: the id-68 extract is deleted,
and this directory holds exactly one public-sale extract from now on.

NOTHING IS LOST, AND THAT IS CHECKED RATHER THAN ASSUMED

  - the two hold the same 122 wallets with IDENTICAL amounts: the
    (ugnot_total, base, tiered_bonus, first24h_bonus, accepted_usd, lockup_12m,
    first_24h_eligible) tuples of the two files are the same multiset
  - every GENESIS row of the id-68 file -- address, bound_at and signer included
    -- is present VERBATIM in the id-79 one, so no binding was dropped or
    redirected between extracts
  - the file is still in git where it was captured, 9ecf4d3 (#65):
        git show 9ecf4d3:inputs/publicsale-sonar-2026-09-09.csv

Nothing in the repository read it. No Go file, no test, no Makefile rule
references either CSV by name -- they are reference material for humans, and the
loaded rows live in mkgenesis/publicsale.txt. The table in inputs/README.md had
already been corrected on main, where it claimed allocate/process_test.go
consumed the 09-09 file.

WHY DELETING IS THE SAFER OPTION, NOT THE BOLDER ONE

Which extract is live is not recorded in either CSV. With two in the tree a
reader has to work it out, and a stale file sitting next to a current one is the
exact shape of the drift the rest of inputs/'s rules exist to prevent -- the
same class of bug as a stale generated artifact. One file, named by the id it
stops at, cannot be read wrong.

inputs/README.md and README-publicsale-provenance.md say all of the above, so
the next extract does not have to rediscover it.

make verify green; no artifact changes -- the CSV is not an input to the build.

---------

Co-authored-by: alexiscolin <alexis@jaunebleu.co>

---------

Co-authored-by: alexiscolin <alexis@jaunebleu.co>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants