@interest-protocol/blast-core 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/dist/aborts.d.ts +13 -0
  2. package/dist/aborts.d.ts.map +1 -0
  3. package/dist/aborts.js +32 -0
  4. package/dist/aborts.js.map +1 -0
  5. package/dist/bcs.d.ts +226 -7
  6. package/dist/bcs.d.ts.map +1 -1
  7. package/dist/bcs.js +227 -1
  8. package/dist/bcs.js.map +1 -1
  9. package/dist/bps.d.ts +25 -0
  10. package/dist/bps.d.ts.map +1 -0
  11. package/dist/bps.js +64 -0
  12. package/dist/bps.js.map +1 -0
  13. package/dist/bytecode-template.d.ts +14 -3
  14. package/dist/bytecode-template.d.ts.map +1 -1
  15. package/dist/bytecode-template.js +61 -6
  16. package/dist/bytecode-template.js.map +1 -1
  17. package/dist/deployment.d.ts +59 -0
  18. package/dist/deployment.d.ts.map +1 -0
  19. package/dist/deployment.js +172 -0
  20. package/dist/deployment.js.map +1 -0
  21. package/dist/effects.d.ts +69 -0
  22. package/dist/effects.d.ts.map +1 -0
  23. package/dist/effects.js +143 -0
  24. package/dist/effects.js.map +1 -0
  25. package/dist/fee-split.d.ts +55 -0
  26. package/dist/fee-split.d.ts.map +1 -0
  27. package/dist/fee-split.js +69 -0
  28. package/dist/fee-split.js.map +1 -0
  29. package/dist/index.d.ts +10 -1
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +10 -1
  32. package/dist/index.js.map +1 -1
  33. package/dist/integers.d.ts +2 -0
  34. package/dist/integers.d.ts.map +1 -1
  35. package/dist/integers.js +8 -0
  36. package/dist/integers.js.map +1 -1
  37. package/dist/merkle.d.ts +6 -1
  38. package/dist/merkle.d.ts.map +1 -1
  39. package/dist/merkle.js +8 -1
  40. package/dist/merkle.js.map +1 -1
  41. package/dist/migration-math.d.ts +13 -2
  42. package/dist/migration-math.d.ts.map +1 -1
  43. package/dist/migration-math.js +34 -6
  44. package/dist/migration-math.js.map +1 -1
  45. package/dist/migration.d.ts +25 -0
  46. package/dist/migration.d.ts.map +1 -0
  47. package/dist/migration.js +43 -0
  48. package/dist/migration.js.map +1 -0
  49. package/dist/payout.d.ts +21 -0
  50. package/dist/payout.d.ts.map +1 -0
  51. package/dist/payout.js +36 -0
  52. package/dist/payout.js.map +1 -0
  53. package/dist/protocol-fees.d.ts +63 -0
  54. package/dist/protocol-fees.d.ts.map +1 -0
  55. package/dist/protocol-fees.js +117 -0
  56. package/dist/protocol-fees.js.map +1 -0
  57. package/dist/rewards/allocations.d.ts +16 -17
  58. package/dist/rewards/allocations.d.ts.map +1 -1
  59. package/dist/rewards/allocations.js +47 -43
  60. package/dist/rewards/allocations.js.map +1 -1
  61. package/dist/roles.d.ts +17 -0
  62. package/dist/roles.d.ts.map +1 -0
  63. package/dist/roles.js +32 -0
  64. package/dist/roles.js.map +1 -0
  65. package/dist/treasury.d.ts +58 -0
  66. package/dist/treasury.d.ts.map +1 -0
  67. package/dist/treasury.js +61 -0
  68. package/dist/treasury.js.map +1 -0
  69. package/package.json +39 -11
  70. package/src/aborts.ts +38 -0
  71. package/src/bcs.ts +374 -1
  72. package/src/bps.ts +92 -0
  73. package/src/bytecode-template.ts +78 -6
  74. package/src/deployment.ts +230 -0
  75. package/src/effects.ts +247 -0
  76. package/src/fee-split.ts +103 -0
  77. package/src/index.ts +10 -1
  78. package/src/integers.ts +15 -0
  79. package/src/merkle.ts +11 -1
  80. package/src/migration-math.ts +38 -6
  81. package/src/migration.ts +69 -0
  82. package/src/payout.ts +57 -0
  83. package/src/protocol-fees.ts +174 -0
  84. package/src/rewards/allocations.ts +52 -67
  85. package/src/roles.ts +39 -0
  86. package/src/treasury.ts +94 -0
  87. package/dist/rewards/keeper.d.ts +0 -17
  88. package/dist/rewards/keeper.d.ts.map +0 -1
  89. package/dist/rewards/keeper.js +0 -21
  90. package/dist/rewards/keeper.js.map +0 -1
  91. package/dist/rewards/state.d.ts +0 -55
  92. package/dist/rewards/state.d.ts.map +0 -1
  93. package/dist/rewards/state.js +0 -80
  94. package/dist/rewards/state.js.map +0 -1
  95. package/dist/timelock.d.ts +0 -15
  96. package/dist/timelock.d.ts.map +0 -1
  97. package/dist/timelock.js +0 -30
  98. package/dist/timelock.js.map +0 -1
  99. package/src/rewards/keeper.ts +0 -45
  100. package/src/rewards/state.ts +0 -113
  101. package/src/timelock.ts +0 -47
@@ -5,7 +5,18 @@ const Q128 = 1n << 128n;
5
5
  /** Atoms of the other asset `auto_lp_fixed_side` keeps beyond its estimate. */
6
6
  export const AUTO_LP_MARGIN = 2n;
7
7
 
8
- /** Mirrors `blast_fun_migrators::migration_math::sqrt_price_x64` over atomic Meme/Quote units. */
8
+ /**
9
+ * `tick_math` sqrt-price bounds, Q64.64, shared by Cetus and Bluefin
10
+ * (`blast_core::constants::{min,max}_sqrt_price_x64`).
11
+ */
12
+ const MIN_SQRT_PRICE_X64 = 4_295_048_016n;
13
+ const MAX_SQRT_PRICE_X64 = 79_226_673_515_401_279_992_447_579_055n;
14
+
15
+ /**
16
+ * Mirrors `blast_core::migration::sqrt_price_x64` over atomic Meme/Quote
17
+ * units: `floor(sqrt(B / A) * 2^64)`, B / A being Quote / Meme when Meme is
18
+ * coin A and Meme / Quote otherwise.
19
+ */
9
20
  export function sqrtPriceX64(
10
21
  memeAmount: bigint,
11
22
  quoteAmount: bigint,
@@ -15,7 +26,21 @@ export function sqrtPriceX64(
15
26
  }
16
27
 
17
28
  /**
18
- * Mirrors `migration_math::auto_lp_fixed_side`, which Bluefin auto-LP uses to
29
+ * Mirrors `migration::is_seedable_price`: whether Cetus and Bluefin can both
30
+ * seed a full-range pool at `sqrtPriceX64`, within
31
+ * `[2 * MIN_SQRT_PRICE_X64, MAX_SQRT_PRICE_X64 / 2]` (the upper bound rounds
32
+ * down).
33
+ */
34
+ export function isSeedablePrice(sqrtPriceX64: bigint): boolean {
35
+ requireU128("sqrtPriceX64", sqrtPriceX64);
36
+ return (
37
+ sqrtPriceX64 >= MIN_SQRT_PRICE_X64 * 2n &&
38
+ sqrtPriceX64 <= MAX_SQRT_PRICE_X64 / 2n
39
+ );
40
+ }
41
+
42
+ /**
43
+ * Mirrors `blast_migrators::bluefin`'s `auto_lp_fixed_side`, which Bluefin auto-LP uses to
19
44
  * choose the fixed side from the pool's and the position's Q64.64 sqrt prices.
20
45
  * Returns `{ fixA, amount }`; a zero amount means nothing is added now.
21
46
  * Divisions round down except the other side's requirement, which rounds up.
@@ -58,10 +83,17 @@ function ratioQ128(
58
83
  quoteAmount: bigint,
59
84
  memeIsCoinA: boolean,
60
85
  ): bigint {
61
- requirePositiveU64("memeAmount", memeAmount);
62
- requirePositiveU64("quoteAmount", quoteAmount);
63
- const numerator = memeIsCoinA ? quoteAmount : memeAmount;
64
- const denominator = memeIsCoinA ? memeAmount : quoteAmount;
86
+ // Move divides by the coin-A side (it aborts at zero) and allows a zero
87
+ // numerator, whose price is 0 and not seedable.
88
+ const [numerator, denominator] = memeIsCoinA
89
+ ? [
90
+ requireU64("quoteAmount", quoteAmount),
91
+ requirePositiveU64("memeAmount", memeAmount),
92
+ ]
93
+ : [
94
+ requireU64("memeAmount", memeAmount),
95
+ requirePositiveU64("quoteAmount", quoteAmount),
96
+ ];
65
97
  return (numerator << 128n) / denominator;
66
98
  }
67
99
 
@@ -0,0 +1,69 @@
1
+ import { bcs } from "@mysten/sui/bcs";
2
+ import { moveTypeNameBcs, suiObjectIdBcs } from "./bcs.ts";
3
+ import { bpsBcs } from "./bps.ts";
4
+ import type { FeeSplit } from "./fee-split.ts";
5
+ import { feeSplitBcs } from "./fee-split.ts";
6
+
7
+ /**
8
+ * `blast_core::migration::ResidualPolicy`: migration residuals the pool does
9
+ * not take are paid to Blast (`Protocol`) or held for the owner (`Owner`).
10
+ * `VenueTicket`, `Migration` and `OwnerProof` have no abilities: they exist
11
+ * only inside one PTB, as transaction results, and have no BCS here.
12
+ */
13
+ export type ResidualPolicy = "Protocol" | "Owner";
14
+
15
+ export const residualPolicyBcs = bcs
16
+ .enum("ResidualPolicy", { Protocol: null, Owner: null })
17
+ .transform({
18
+ input: (policy: ResidualPolicy) =>
19
+ policy === "Protocol"
20
+ ? { Protocol: true as const }
21
+ : { Owner: true as const },
22
+ output: (policy): ResidualPolicy => policy.$kind,
23
+ });
24
+
25
+ /**
26
+ * `blast_core::migration::VaultTerms`: what a vault snapshots from the sale
27
+ * that sealed its terms. `origin` is the launch or presale ID and
28
+ * `packageWitness` the original type of the product's package witness.
29
+ */
30
+ export type VaultTerms = {
31
+ origin: string;
32
+ packageWitness: string;
33
+ feeSplit: FeeSplit;
34
+ protocolMemeBps: bigint;
35
+ protocolQuoteBps: bigint;
36
+ partnerId: string | null;
37
+ residuals: ResidualPolicy;
38
+ };
39
+
40
+ export const vaultTermsBcs = bcs
41
+ .struct("VaultTerms", {
42
+ origin: suiObjectIdBcs,
43
+ package_witness: moveTypeNameBcs,
44
+ fee_split: feeSplitBcs,
45
+ protocol_meme_bps: bpsBcs,
46
+ protocol_quote_bps: bpsBcs,
47
+ partner_id: bcs.option(suiObjectIdBcs),
48
+ residuals: residualPolicyBcs,
49
+ })
50
+ .transform({
51
+ input: (terms: VaultTerms) => ({
52
+ origin: terms.origin,
53
+ package_witness: terms.packageWitness,
54
+ fee_split: terms.feeSplit,
55
+ protocol_meme_bps: terms.protocolMemeBps,
56
+ protocol_quote_bps: terms.protocolQuoteBps,
57
+ partner_id: terms.partnerId,
58
+ residuals: terms.residuals,
59
+ }),
60
+ output: (terms): VaultTerms => ({
61
+ origin: terms.origin,
62
+ packageWitness: terms.package_witness,
63
+ feeSplit: terms.fee_split,
64
+ protocolMemeBps: terms.protocol_meme_bps,
65
+ protocolQuoteBps: terms.protocol_quote_bps,
66
+ partnerId: terms.partner_id,
67
+ residuals: terms.residuals,
68
+ }),
69
+ });
package/src/payout.ts ADDED
@@ -0,0 +1,57 @@
1
+ import { bcs } from "@mysten/sui/bcs";
2
+ import { addressBcs, u64Bcs } from "./bcs.ts";
3
+
4
+ /**
5
+ * `blast_core::payout::VestingTerms`: `periods` equal steps of
6
+ * `periodDurationMs` each, the first available `cliffDelayMs` after the
7
+ * payout.
8
+ */
9
+ export type VestingTerms = {
10
+ cliffDelayMs: bigint;
11
+ periodDurationMs: bigint;
12
+ periods: bigint;
13
+ };
14
+
15
+ /**
16
+ * `blast_core::payout::Recipient`: where a payout goes, paid outright when
17
+ * `vestingTerms` is null and locked under them otherwise.
18
+ */
19
+ export type Recipient = {
20
+ destination: string;
21
+ vestingTerms: VestingTerms | null;
22
+ };
23
+
24
+ export const vestingTermsBcs = bcs
25
+ .struct("VestingTerms", {
26
+ cliff_delay_ms: u64Bcs,
27
+ period_duration_ms: u64Bcs,
28
+ periods: u64Bcs,
29
+ })
30
+ .transform({
31
+ input: (terms: VestingTerms) => ({
32
+ cliff_delay_ms: terms.cliffDelayMs,
33
+ period_duration_ms: terms.periodDurationMs,
34
+ periods: terms.periods,
35
+ }),
36
+ output: (terms): VestingTerms => ({
37
+ cliffDelayMs: terms.cliff_delay_ms,
38
+ periodDurationMs: terms.period_duration_ms,
39
+ periods: terms.periods,
40
+ }),
41
+ });
42
+
43
+ export const recipientBcs = bcs
44
+ .struct("Recipient", {
45
+ destination: addressBcs,
46
+ vesting_terms: bcs.option(vestingTermsBcs),
47
+ })
48
+ .transform({
49
+ input: (recipient: Recipient) => ({
50
+ destination: recipient.destination,
51
+ vesting_terms: recipient.vestingTerms,
52
+ }),
53
+ output: (recipient): Recipient => ({
54
+ destination: recipient.destination,
55
+ vestingTerms: recipient.vesting_terms,
56
+ }),
57
+ });
@@ -0,0 +1,174 @@
1
+ import { bcs } from "@mysten/sui/bcs";
2
+ import type { SuiClientTypes } from "@mysten/sui/client";
3
+ import {
4
+ normalizeStructTag,
5
+ normalizeSuiAddress,
6
+ parseStructTag,
7
+ } from "@mysten/sui/utils";
8
+ import {
9
+ addressBcs,
10
+ balanceBcs,
11
+ moveTypeNameBcs,
12
+ suiObjectIdBcs,
13
+ u64Bcs,
14
+ } from "./bcs.ts";
15
+ import { applyBpsUp } from "./bps.ts";
16
+ import type { SuiEventPosition } from "./events.ts";
17
+ import { normalizeSuiEventPosition } from "./events.ts";
18
+ import { BASIS_POINTS_DENOMINATOR, mulDivDown } from "./integers.ts";
19
+
20
+ /**
21
+ * `blast_core::protocol_fees::Ledger<Meme, Quote>`: Blast's held fees of one
22
+ * launch, presale or vault. `packageWitness` is the original type of the
23
+ * package witness that created it; `partnerId` is fixed at creation.
24
+ */
25
+ export const ledgerBcs = bcs
26
+ .struct("Ledger", {
27
+ package_witness: moveTypeNameBcs,
28
+ origin: suiObjectIdBcs,
29
+ partner_id: bcs.option(suiObjectIdBcs),
30
+ meme: balanceBcs,
31
+ quote: balanceBcs,
32
+ sui: balanceBcs,
33
+ })
34
+ .transform({
35
+ input: (ledger: Ledger) => ({
36
+ package_witness: ledger.packageWitness,
37
+ origin: ledger.origin,
38
+ partner_id: ledger.partnerId,
39
+ meme: ledger.meme,
40
+ quote: ledger.quote,
41
+ sui: ledger.sui,
42
+ }),
43
+ output: (ledger): Ledger => ({
44
+ packageWitness: ledger.package_witness,
45
+ origin: ledger.origin,
46
+ partnerId: ledger.partner_id,
47
+ meme: ledger.meme,
48
+ quote: ledger.quote,
49
+ sui: ledger.sui,
50
+ }),
51
+ });
52
+
53
+ export type Ledger = {
54
+ packageWitness: string;
55
+ origin: string;
56
+ partnerId: string | null;
57
+ meme: bigint;
58
+ quote: bigint;
59
+ sui: bigint;
60
+ };
61
+
62
+ /**
63
+ * Mirrors the amount `protocol_fees::take_cut` moves into the ledger from
64
+ * one fee of one asset: `ceil(amount * bps / 10,000)`; the fee keeps the rest.
65
+ */
66
+ export function takeCut(amount: bigint, bps: bigint): bigint {
67
+ return applyBpsUp(amount, bps);
68
+ }
69
+
70
+ /** `blast_core::blast_core_events::ProtocolFeesPaid<Meme, Quote>`, in Move field order. */
71
+ export const protocolFeesPaidBcs = bcs.struct("ProtocolFeesPaid", {
72
+ package_witness: moveTypeNameBcs,
73
+ origin: suiObjectIdBcs,
74
+ treasury: addressBcs,
75
+ meme_amount: u64Bcs,
76
+ quote_amount: u64Bcs,
77
+ sui_amount: u64Bcs,
78
+ partner_id: bcs.option(suiObjectIdBcs),
79
+ partner_meme_amount: u64Bcs,
80
+ partner_quote_amount: u64Bcs,
81
+ partner_sui_amount: u64Bcs,
82
+ });
83
+
84
+ export type ProtocolFeesPaid = ReturnType<typeof protocolFeesPaidBcs.parse>;
85
+
86
+ export type DecodedProtocolFeesPaid = {
87
+ source: "core";
88
+ kind: "ProtocolFeesPaid";
89
+ memeType: string;
90
+ quoteType: string;
91
+ payload: ProtocolFeesPaid;
92
+ sender: string;
93
+ position: SuiEventPosition;
94
+ };
95
+
96
+ /**
97
+ * Decodes one `<core>::blast_core_events::ProtocolFeesPaid<Meme, Quote>`.
98
+ * `payload.origin` is the launch, presale or vault whose ledger paid, and
99
+ * `payload.package_witness` the package that created the ledger.
100
+ */
101
+ export function decodeProtocolFeesPaid(
102
+ event: SuiClientTypes.EventEntry,
103
+ core: { originalPackageId: string },
104
+ ): DecodedProtocolFeesPaid {
105
+ const tag = parseStructTag(event.eventType);
106
+ const [meme, quote, ...extra] = tag.typeParams;
107
+ if (
108
+ tag.address !== normalizeSuiAddress(core.originalPackageId) ||
109
+ tag.module !== "blast_core_events" ||
110
+ tag.name !== "ProtocolFeesPaid" ||
111
+ meme === undefined ||
112
+ quote === undefined ||
113
+ extra.length > 0
114
+ ) {
115
+ throw new TypeError(`unexpected core event type ${event.eventType}`);
116
+ }
117
+ return {
118
+ source: "core",
119
+ kind: "ProtocolFeesPaid",
120
+ memeType: normalizeStructTag(meme),
121
+ quoteType: normalizeStructTag(quote),
122
+ payload: protocolFeesPaidBcs.parse(event.bcs),
123
+ sender: normalizeSuiAddress(event.sender),
124
+ position: normalizeSuiEventPosition(event),
125
+ };
126
+ }
127
+
128
+ /**
129
+ * Previews `protocol_fees::pay` / `pay_with_partner` over a decoded ledger:
130
+ * the bound partner's cut of each asset first (`partnerShareBps` of it,
131
+ * rounded down per asset as `blast_partners::pay`), Blast's treasury the
132
+ * rest. `partnerShareBps` is required exactly when the ledger names a
133
+ * partner. Null when the ledger is empty: the contract sends and emits
134
+ * nothing. Omits `package_witness`, `origin` and `treasury`, which the
135
+ * ledger and the treasury object carry.
136
+ */
137
+ export function previewProtocolFeesPaid(
138
+ ledger: Ledger,
139
+ partnerShareBps: bigint | null,
140
+ ): Pick<
141
+ ProtocolFeesPaid,
142
+ | "meme_amount"
143
+ | "quote_amount"
144
+ | "sui_amount"
145
+ | "partner_id"
146
+ | "partner_meme_amount"
147
+ | "partner_quote_amount"
148
+ | "partner_sui_amount"
149
+ > | null {
150
+ if ((ledger.partnerId === null) !== (partnerShareBps === null)) {
151
+ throw new TypeError(
152
+ "a partner share is required exactly when the ledger names a partner",
153
+ );
154
+ }
155
+ if (ledger.meme === 0n && ledger.quote === 0n && ledger.sui === 0n) {
156
+ return null;
157
+ }
158
+ const cut = (amount: bigint) =>
159
+ partnerShareBps === null
160
+ ? 0n
161
+ : mulDivDown(amount, partnerShareBps, BASIS_POINTS_DENOMINATOR);
162
+ const partnerMeme = cut(ledger.meme);
163
+ const partnerQuote = cut(ledger.quote);
164
+ const partnerSui = cut(ledger.sui);
165
+ return {
166
+ meme_amount: ledger.meme - partnerMeme,
167
+ quote_amount: ledger.quote - partnerQuote,
168
+ sui_amount: ledger.sui - partnerSui,
169
+ partner_id: ledger.partnerId,
170
+ partner_meme_amount: partnerMeme,
171
+ partner_quote_amount: partnerQuote,
172
+ partner_sui_amount: partnerSui,
173
+ };
174
+ }
@@ -10,6 +10,7 @@ import { requireU64 } from "../integers.ts";
10
10
  import {
11
11
  blake2b256,
12
12
  merkleLayers,
13
+ merkleLeaf,
13
14
  merkleNode,
14
15
  merkleProof,
15
16
  merkleRoot,
@@ -30,7 +31,7 @@ export type RewardProof = {
30
31
  proof: string[];
31
32
  };
32
33
  export type RewardManifest = {
33
- version: 1;
34
+ version: 2;
34
35
  vaultId: string;
35
36
  number: string;
36
37
  startCheckpoint: string;
@@ -45,7 +46,6 @@ export type RewardManifest = {
45
46
  /** Complete commitment after durable paged construction; it does not contain all proofs in memory. */
46
47
  export type RewardManifestSummary = Omit<RewardManifest, "allocations"> & {
47
48
  leafCount: number;
48
- leafDomain: RewardLeafDomain;
49
49
  };
50
50
 
51
51
  /**
@@ -93,14 +93,16 @@ const manifestHeaderBcs = bcs.struct("RewardManifestHeader", {
93
93
  start: bcs.u64(),
94
94
  end: bcs.u64(),
95
95
  });
96
- /** Leaf domain of `blast_fun_migrators` holder-reward rounds. */
97
- export const MIGRATOR_REWARD_LEAF_DOMAIN = "\x00blast-holder-rewards-v1";
98
- /** Leaf domain of `blast_presale::holder_rewards` rounds; same leaf layout. */
99
- export const PRESALE_REWARD_LEAF_DOMAIN = "\x00blast-presale-holder-rewards-v1";
100
- /** The package whose rounds a manifest pays: each hashes leaves under its own domain. */
101
- export type RewardLeafDomain =
102
- | typeof MIGRATOR_REWARD_LEAF_DOMAIN
103
- | typeof PRESALE_REWARD_LEAF_DOMAIN;
96
+ /**
97
+ * `blast_vault::constants::reward_leaf_domain`: every vault's rounds hash
98
+ * their leaves as `merkle::leaf(REWARD_LEAF_DOMAIN, bcs(leaf))`.
99
+ */
100
+ export const REWARD_LEAF_DOMAIN = "blast-holder-rewards-v2";
101
+
102
+ /** One leaf digest: `blake2b256(0x00 || REWARD_LEAF_DOMAIN || bcs(leaf))`. */
103
+ function rewardLeaf(bytes: Uint8Array): Uint8Array {
104
+ return merkleLeaf(REWARD_LEAF_DOMAIN, bytes);
105
+ }
104
106
 
105
107
  /** Exact atomic-unit allocations, rounded down. Dust stays in the vault rewards balance. */
106
108
  export function allocateHolderRewards(
@@ -136,21 +138,25 @@ export function allocateHolderRewards(
136
138
  }
137
139
 
138
140
  /**
139
- * Quote atoms = floor(budget * wallet weight / frozen global weight), never a page denominator.
140
- * Remainders stay holder-owned; sum quoteAllocated over every page to calculate global dust.
141
- * Inputs are distinct qualifying wallets in canonical order; carry lastAddress into the next page.
141
+ * Atoms of each asset = floor(budget * wallet weight / frozen global weight), never a page
142
+ * denominator. Remainders stay holder-owned; sum memeAllocated and quoteAllocated over every page
143
+ * to calculate global dust. Inputs are distinct qualifying wallets in canonical order; carry
144
+ * lastAddress into the next page.
142
145
  */
143
146
  export function allocateHolderRewardPage(
144
147
  holders: readonly HolderWeight[],
148
+ memeBudget: bigint,
145
149
  quoteBudget: bigint,
146
150
  totalWeight: bigint,
147
151
  afterAddress?: string,
148
152
  ) {
153
+ requireU64("memeBudget", memeBudget);
149
154
  requireU64("quoteBudget", quoteBudget);
150
155
  if (totalWeight <= 0n) throw new RangeError("global weight must be positive");
151
156
  let lastAddress =
152
157
  afterAddress === undefined ? undefined : rewardAddress(afterAddress);
153
158
  let pageWeight = 0n;
159
+ let memeAllocated = 0n;
154
160
  let quoteAllocated = 0n;
155
161
  const allocations: RewardAllocation[] = [];
156
162
  for (const holder of holders) {
@@ -163,13 +169,21 @@ export function allocateHolderRewardPage(
163
169
  pageWeight += holder.weight;
164
170
  if (pageWeight > totalWeight)
165
171
  throw new RangeError("page weight exceeds global weight");
172
+ const memeAmount = (memeBudget * holder.weight) / totalWeight;
166
173
  const quoteAmount = (quoteBudget * holder.weight) / totalWeight;
167
- if (quoteAmount > 0n)
168
- allocations.push({ beneficiary, memeAmount: 0n, quoteAmount });
174
+ if (memeAmount > 0n || quoteAmount > 0n)
175
+ allocations.push({ beneficiary, memeAmount, quoteAmount });
176
+ memeAllocated += memeAmount;
169
177
  quoteAllocated += quoteAmount;
170
178
  lastAddress = beneficiary;
171
179
  }
172
- return { allocations, pageWeight, quoteAllocated, lastAddress };
180
+ return {
181
+ allocations,
182
+ pageWeight,
183
+ memeAllocated,
184
+ quoteAllocated,
185
+ lastAddress,
186
+ };
173
187
  }
174
188
 
175
189
  /** Builds the public payout list and every positional proof, independent of the signing key. */
@@ -179,9 +193,7 @@ export function buildRewardManifest(options: {
179
193
  startCheckpoint: bigint;
180
194
  endCheckpoint: bigint;
181
195
  allocations: readonly RewardAllocation[];
182
- leafDomain: RewardLeafDomain;
183
196
  }): RewardManifest {
184
- const leafPrefix = new TextEncoder().encode(options.leafDomain);
185
197
  const vaultId = rewardAddress(options.vaultId);
186
198
  requireU64("number", options.number);
187
199
  requireU64("startCheckpoint", options.startCheckpoint);
@@ -221,9 +233,7 @@ export function buildRewardManifest(options: {
221
233
  quote: allocation.quoteAmount,
222
234
  }));
223
235
  const layers = merkleLayers(
224
- leaves.map((leaf) =>
225
- blake2b256(concat(leafPrefix, leafBcs.serialize(leaf).toBytes())),
226
- ),
236
+ leaves.map((leaf) => rewardLeaf(leafBcs.serialize(leaf).toBytes())),
227
237
  );
228
238
  const memeAmount = requireU64(
229
239
  "total Meme",
@@ -234,7 +244,7 @@ export function buildRewardManifest(options: {
234
244
  allocations.reduce((sum, item) => sum + item.quoteAmount, 0n),
235
245
  );
236
246
  return {
237
- version: 1,
247
+ version: 2,
238
248
  vaultId,
239
249
  number: options.number.toString(),
240
250
  startCheckpoint: options.startCheckpoint.toString(),
@@ -298,7 +308,6 @@ export async function buildRewardManifestPages(
298
308
  options.pageSize > MAX_REWARD_LEAVES
299
309
  )
300
310
  throw new RangeError("invalid page size");
301
- const leafPrefix = new TextEncoder().encode(options.leafDomain);
302
311
  const digest = blake2b.create({ dkLen: 32 });
303
312
  digest.update(
304
313
  manifestHeaderBcs
@@ -331,7 +340,6 @@ export async function buildRewardManifestPages(
331
340
  options,
332
341
  vaultId,
333
342
  digest,
334
- leafPrefix,
335
343
  store,
336
344
  writeHashes,
337
345
  );
@@ -343,7 +351,7 @@ export async function buildRewardManifestPages(
343
351
  const root = (await readHashes(level, 0, 1))[0];
344
352
  if (root === undefined) throw new Error("missing Merkle root");
345
353
  const summary: RewardManifestSummary = {
346
- version: 1,
354
+ version: 2,
347
355
  vaultId,
348
356
  number: options.number.toString(),
349
357
  startCheckpoint: options.startCheckpoint.toString(),
@@ -353,9 +361,8 @@ export async function buildRewardManifestPages(
353
361
  memeAmount: memeAmount.toString(),
354
362
  quoteAmount: quoteAmount.toString(),
355
363
  leafCount: options.leafCount,
356
- leafDomain: options.leafDomain,
357
364
  };
358
- await writeManifestProofs(options, summary, leafPrefix, store, readHashes);
365
+ await writeManifestProofs(options, summary, store, readHashes);
359
366
  return summary;
360
367
  }
361
368
 
@@ -402,7 +409,6 @@ async function writeManifestLeaves(
402
409
  options: PagedManifestOptions,
403
410
  vaultId: string,
404
411
  digest: ReturnType<typeof blake2b.create>,
405
- leafPrefix: Uint8Array,
406
412
  store: RewardManifestPageStore,
407
413
  writeHashes: WriteRewardHashes,
408
414
  ) {
@@ -428,7 +434,7 @@ async function writeManifestLeaves(
428
434
  quoteAmount + allocation.quoteAmount,
429
435
  );
430
436
  digest.update(bytes);
431
- hashes.push(toHex(blake2b256(concat(leafPrefix, bytes))));
437
+ hashes.push(toHex(rewardLeaf(bytes)));
432
438
  }
433
439
  await writeHashes(0, offset, hashes);
434
440
  }
@@ -466,7 +472,6 @@ async function buildManifestMerkleLayers(
466
472
  async function writeManifestProofs(
467
473
  options: PagedManifestOptions,
468
474
  summary: RewardManifestSummary,
469
- leafPrefix: Uint8Array,
470
475
  store: RewardManifestPageStore,
471
476
  readHashes: ReadRewardHashes,
472
477
  ) {
@@ -479,11 +484,8 @@ async function writeManifestProofs(
479
484
  const proofs = allocations.map((allocation, i): RewardProof => {
480
485
  if (
481
486
  toHex(
482
- blake2b256(
483
- concat(
484
- leafPrefix,
485
- rewardLeafBytes(options, summary.vaultId, allocation, offset + i),
486
- ),
487
+ rewardLeaf(
488
+ rewardLeafBytes(options, summary.vaultId, allocation, offset + i),
487
489
  ),
488
490
  ) !== hashes[i]
489
491
  )
@@ -498,7 +500,7 @@ async function writeManifestProofs(
498
500
  });
499
501
  await fillManifestProofs(options, offset, proofs, readHashes);
500
502
  for (const proof of proofs)
501
- if (!verifyRewardSummaryProof(summary, proof, options.leafDomain))
503
+ if (!verifyRewardSummaryProof(summary, proof))
502
504
  throw new Error("proof does not match constructed root");
503
505
  await store.writeProofs(offset, proofs);
504
506
  const stored = await store.readProofs(offset, count);
@@ -554,12 +556,10 @@ function rewardProofPagesMatch(
554
556
  export function verifyRewardProof(
555
557
  manifest: RewardManifest,
556
558
  allocation: RewardProof,
557
- leafDomain: RewardLeafDomain,
558
559
  ): boolean {
559
560
  return verifyRewardSummaryProof(
560
- { ...manifest, leafCount: manifest.allocations.length, leafDomain },
561
+ { ...manifest, leafCount: manifest.allocations.length },
561
562
  allocation,
562
- leafDomain,
563
563
  );
564
564
  }
565
565
 
@@ -567,10 +567,7 @@ export function verifyRewardProof(
567
567
  export function verifyRewardSummaryProof(
568
568
  manifest: RewardManifestSummary,
569
569
  allocation: RewardProof,
570
- leafDomain: RewardLeafDomain,
571
570
  ): boolean {
572
- if (manifest.leafDomain !== leafDomain) return false;
573
- const leafPrefix = new TextEncoder().encode(leafDomain);
574
571
  try {
575
572
  let index = Number(requireU64("index", BigInt(allocation.index)));
576
573
  const count = manifest.leafCount;
@@ -582,20 +579,17 @@ export function verifyRewardSummaryProof(
582
579
  allocation.proof.length !== Math.ceil(Math.log2(count))
583
580
  )
584
581
  return false;
585
- let digest = blake2b256(
586
- concat(
587
- leafPrefix,
588
- leafBcs
589
- .serialize({
590
- vault: rewardAddress(manifest.vaultId),
591
- number: requireU64("number", BigInt(manifest.number)),
592
- index: BigInt(index),
593
- beneficiary: rewardAddress(allocation.beneficiary),
594
- meme: requireU64("meme", BigInt(allocation.memeAmount)),
595
- quote: requireU64("quote", BigInt(allocation.quoteAmount)),
596
- })
597
- .toBytes(),
598
- ),
582
+ let digest = rewardLeaf(
583
+ leafBcs
584
+ .serialize({
585
+ vault: rewardAddress(manifest.vaultId),
586
+ number: requireU64("number", BigInt(manifest.number)),
587
+ index: BigInt(index),
588
+ beneficiary: rewardAddress(allocation.beneficiary),
589
+ meme: requireU64("meme", BigInt(allocation.memeAmount)),
590
+ quote: requireU64("quote", BigInt(allocation.quoteAmount)),
591
+ })
592
+ .toBytes(),
599
593
  );
600
594
  for (const sibling of allocation.proof) {
601
595
  const bytes = rewardDigest(sibling);
@@ -612,11 +606,8 @@ export function verifyRewardSummaryProof(
612
606
  /** Structural validation only; ready-plan evidence must prove durable construction upstream. */
613
607
  export function validateRewardManifestSummary(
614
608
  summary: RewardManifestSummary,
615
- leafDomain: RewardLeafDomain,
616
609
  ): RewardManifestSummary {
617
- if (summary.leafDomain !== leafDomain)
618
- throw new TypeError("manifest leaf domain mismatch");
619
- if (summary.version !== 1) throw new Error("unsupported manifest version");
610
+ if (summary.version !== 2) throw new Error("unsupported manifest version");
620
611
  if (summary.vaultId !== rewardAddress(summary.vaultId))
621
612
  throw new TypeError("noncanonical vault");
622
613
  for (const key of [
@@ -648,9 +639,8 @@ export function validateRewardManifestSummary(
648
639
  /** Rejects edited totals, roots, ordering, proofs and manifests before creating transactions. */
649
640
  export function validateRewardManifest(
650
641
  manifest: RewardManifest,
651
- leafDomain: RewardLeafDomain,
652
642
  ): RewardManifest {
653
- if (manifest.version !== 1) throw new Error("unsupported manifest version");
643
+ if (manifest.version !== 2) throw new Error("unsupported manifest version");
654
644
  const expected = buildRewardManifest({
655
645
  vaultId: manifest.vaultId,
656
646
  number: BigInt(manifest.number),
@@ -661,7 +651,6 @@ export function validateRewardManifest(
661
651
  memeAmount: BigInt(allocation.memeAmount),
662
652
  quoteAmount: BigInt(allocation.quoteAmount),
663
653
  })),
664
- leafDomain,
665
654
  });
666
655
  for (const key of [
667
656
  "vaultId",
@@ -704,7 +693,3 @@ export function rewardDigest(value: string): Uint8Array {
704
693
  throw new TypeError("digest must be 32 canonical hex bytes");
705
694
  return fromHex(value);
706
695
  }
707
-
708
- function concat(left: Uint8Array, right: Uint8Array): Uint8Array {
709
- return new Uint8Array([...left, ...right]);
710
- }