@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.
- package/dist/aborts.d.ts +13 -0
- package/dist/aborts.d.ts.map +1 -0
- package/dist/aborts.js +32 -0
- package/dist/aborts.js.map +1 -0
- package/dist/bcs.d.ts +226 -7
- package/dist/bcs.d.ts.map +1 -1
- package/dist/bcs.js +227 -1
- package/dist/bcs.js.map +1 -1
- package/dist/bps.d.ts +25 -0
- package/dist/bps.d.ts.map +1 -0
- package/dist/bps.js +64 -0
- package/dist/bps.js.map +1 -0
- package/dist/bytecode-template.d.ts +14 -3
- package/dist/bytecode-template.d.ts.map +1 -1
- package/dist/bytecode-template.js +61 -6
- package/dist/bytecode-template.js.map +1 -1
- package/dist/deployment.d.ts +59 -0
- package/dist/deployment.d.ts.map +1 -0
- package/dist/deployment.js +172 -0
- package/dist/deployment.js.map +1 -0
- package/dist/effects.d.ts +69 -0
- package/dist/effects.d.ts.map +1 -0
- package/dist/effects.js +143 -0
- package/dist/effects.js.map +1 -0
- package/dist/fee-split.d.ts +55 -0
- package/dist/fee-split.d.ts.map +1 -0
- package/dist/fee-split.js +69 -0
- package/dist/fee-split.js.map +1 -0
- package/dist/index.d.ts +10 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -1
- package/dist/index.js.map +1 -1
- package/dist/integers.d.ts +2 -0
- package/dist/integers.d.ts.map +1 -1
- package/dist/integers.js +8 -0
- package/dist/integers.js.map +1 -1
- package/dist/merkle.d.ts +6 -1
- package/dist/merkle.d.ts.map +1 -1
- package/dist/merkle.js +8 -1
- package/dist/merkle.js.map +1 -1
- package/dist/migration-math.d.ts +13 -2
- package/dist/migration-math.d.ts.map +1 -1
- package/dist/migration-math.js +34 -6
- package/dist/migration-math.js.map +1 -1
- package/dist/migration.d.ts +25 -0
- package/dist/migration.d.ts.map +1 -0
- package/dist/migration.js +43 -0
- package/dist/migration.js.map +1 -0
- package/dist/payout.d.ts +21 -0
- package/dist/payout.d.ts.map +1 -0
- package/dist/payout.js +36 -0
- package/dist/payout.js.map +1 -0
- package/dist/protocol-fees.d.ts +63 -0
- package/dist/protocol-fees.d.ts.map +1 -0
- package/dist/protocol-fees.js +117 -0
- package/dist/protocol-fees.js.map +1 -0
- package/dist/rewards/allocations.d.ts +16 -17
- package/dist/rewards/allocations.d.ts.map +1 -1
- package/dist/rewards/allocations.js +47 -43
- package/dist/rewards/allocations.js.map +1 -1
- package/dist/roles.d.ts +17 -0
- package/dist/roles.d.ts.map +1 -0
- package/dist/roles.js +32 -0
- package/dist/roles.js.map +1 -0
- package/dist/treasury.d.ts +58 -0
- package/dist/treasury.d.ts.map +1 -0
- package/dist/treasury.js +61 -0
- package/dist/treasury.js.map +1 -0
- package/package.json +39 -11
- package/src/aborts.ts +38 -0
- package/src/bcs.ts +374 -1
- package/src/bps.ts +92 -0
- package/src/bytecode-template.ts +78 -6
- package/src/deployment.ts +230 -0
- package/src/effects.ts +247 -0
- package/src/fee-split.ts +103 -0
- package/src/index.ts +10 -1
- package/src/integers.ts +15 -0
- package/src/merkle.ts +11 -1
- package/src/migration-math.ts +38 -6
- package/src/migration.ts +69 -0
- package/src/payout.ts +57 -0
- package/src/protocol-fees.ts +174 -0
- package/src/rewards/allocations.ts +52 -67
- package/src/roles.ts +39 -0
- package/src/treasury.ts +94 -0
- package/dist/rewards/keeper.d.ts +0 -17
- package/dist/rewards/keeper.d.ts.map +0 -1
- package/dist/rewards/keeper.js +0 -21
- package/dist/rewards/keeper.js.map +0 -1
- package/dist/rewards/state.d.ts +0 -55
- package/dist/rewards/state.d.ts.map +0 -1
- package/dist/rewards/state.js +0 -80
- package/dist/rewards/state.js.map +0 -1
- package/dist/timelock.d.ts +0 -15
- package/dist/timelock.d.ts.map +0 -1
- package/dist/timelock.js +0 -30
- package/dist/timelock.js.map +0 -1
- package/src/rewards/keeper.ts +0 -45
- package/src/rewards/state.ts +0 -113
- package/src/timelock.ts +0 -47
package/src/migration-math.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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 `
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
const numerator = memeIsCoinA
|
|
64
|
-
|
|
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
|
|
package/src/migration.ts
ADDED
|
@@ -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:
|
|
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
|
-
/**
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
*
|
|
140
|
-
* Remainders stay holder-owned; sum quoteAllocated over every page
|
|
141
|
-
* Inputs are distinct qualifying wallets in canonical order; carry
|
|
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
|
|
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 {
|
|
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:
|
|
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:
|
|
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,
|
|
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(
|
|
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
|
-
|
|
483
|
-
|
|
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
|
|
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
|
|
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 =
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
.
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
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.
|
|
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 !==
|
|
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
|
-
}
|