hookr-sdk 0.2.1 → 1.1.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/CHANGELOG.md +70 -0
- package/LICENSE +21 -0
- package/MIGRATING.md +89 -0
- package/README.md +308 -105
- package/dist/core/abi.d.ts +10930 -0
- package/dist/core/abi.js +455 -0
- package/dist/core/advisories.d.ts +184 -0
- package/dist/core/advisories.js +291 -0
- package/dist/core/client.d.ts +55 -0
- package/dist/core/client.js +107 -0
- package/dist/core/generated.d.ts +14295 -0
- package/dist/core/generated.js +1058 -0
- package/dist/core/index.d.ts +17 -0
- package/dist/core/index.js +17 -0
- package/dist/core/launch.d.ts +200 -0
- package/dist/core/launch.js +427 -0
- package/dist/core/pair.d.ts +64 -0
- package/dist/core/pair.js +136 -0
- package/dist/core/params.d.ts +415 -0
- package/dist/core/params.js +1235 -0
- package/dist/core/policies.d.ts +136 -0
- package/dist/core/policies.js +125 -0
- package/dist/core/proofs.d.ts +25 -0
- package/dist/core/proofs.js +118 -0
- package/dist/core/quotes.d.ts +185 -0
- package/dist/core/quotes.js +550 -0
- package/dist/core/referrals.d.ts +32 -0
- package/dist/core/referrals.js +36 -0
- package/dist/core/registry.d.ts +357 -0
- package/dist/core/registry.js +378 -0
- package/dist/core/rules.d.ts +675 -0
- package/dist/core/rules.js +1049 -0
- package/dist/core/schema.d.ts +358 -0
- package/dist/core/schema.js +105 -0
- package/dist/core/stocks.generated.d.ts +11 -0
- package/dist/core/stocks.generated.js +1228 -0
- package/dist/core/vault.d.ts +59 -0
- package/dist/core/vault.js +112 -0
- package/dist/core/wallet.d.ts +142 -0
- package/dist/core/wallet.js +304 -0
- package/dist/deployments.d.ts +8 -0
- package/dist/deployments.js +26 -0
- package/dist/index.d.ts +42 -15
- package/dist/index.js +40 -15
- package/dist/releases.d.ts +219 -0
- package/dist/releases.js +365 -0
- package/dist/wallet-cap.d.ts +159 -0
- package/dist/wallet-cap.js +234 -0
- package/package.json +46 -10
- package/provenance.json +31 -0
- package/dist/abi.d.ts +0 -6404
- package/dist/abi.js +0 -8300
- package/dist/chain.d.ts +0 -87
- package/dist/chain.js +0 -86
- package/dist/claims.d.ts +0 -32
- package/dist/claims.js +0 -31
- package/dist/fees.d.ts +0 -59
- package/dist/fees.js +0 -142
- package/dist/links.d.ts +0 -48
- package/dist/links.js +0 -93
- package/dist/market.d.ts +0 -264
- package/dist/market.js +0 -310
- package/dist/reads.d.ts +0 -136
- package/dist/reads.js +0 -219
- package/dist/rules.d.ts +0 -84
- package/dist/rules.js +0 -296
- package/dist/swap.d.ts +0 -108
- package/dist/swap.js +0 -144
|
@@ -0,0 +1,1049 @@
|
|
|
1
|
+
import { decodeAbiParameters, decodeFunctionResult, encodeAbiParameters, encodeFunctionData, keccak256, stringToBytes, } from "viem";
|
|
2
|
+
import { recaptureAbi, rulesAbi, treasuryAbi } from "./abi.js";
|
|
3
|
+
import { RELEASE_CONSTANTS, STRUCT_COMPONENTS } from "./generated.js";
|
|
4
|
+
import { address, assert, uint, ZERO } from "./schema.js";
|
|
5
|
+
/** Gas every Rules admission, and so every pool bound to HookrRules, carries: exactly 2,000,000. */
|
|
6
|
+
export const RULES_GAS_LIMIT = RELEASE_CONSTANTS.REGISTRY_RULES_MIN_GAS_LIMIT;
|
|
7
|
+
/** Ceiling on any LP fee, in pips. */
|
|
8
|
+
export const MAX_LP_FEE_PIPS = 600_000;
|
|
9
|
+
/**
|
|
10
|
+
* Highest protocol share a pool binds with, in basis points (HookrRules._validate), and highest minimum a HookrRules
|
|
11
|
+
* can be built with (its constructor): 50%.
|
|
12
|
+
*/
|
|
13
|
+
export const MAX_PROTOCOL_SHARE_BPS = 5_000;
|
|
14
|
+
/**
|
|
15
|
+
* Rule-fee terms the treasury governs (HookrTreasury, r23): the highest governed floor of a pool's protocolShareBps,
|
|
16
|
+
* the highest integrator rate (in basis points of Hookr's rule-fee share; HookrRules refuses a larger one at bind too)
|
|
17
|
+
* and the rate the app pre-fills, and how long a proposed change waits and then stays acceptable, in seconds.
|
|
18
|
+
*/
|
|
19
|
+
export const MAX_RULE_SHARE_FLOOR_BPS = Number(RELEASE_CONSTANTS.TREASURY_MAX_RULE_SHARE_FLOOR_BPS);
|
|
20
|
+
export const MAX_INTEGRATOR_BPS = Number(RELEASE_CONSTANTS.RULES_MAX_INTEGRATOR_BPS);
|
|
21
|
+
export const DEFAULT_INTEGRATOR_BPS = Number(RELEASE_CONSTANTS.TREASURY_DEFAULT_INTEGRATOR_BPS);
|
|
22
|
+
export const FEE_TERMS_DELAY_SECONDS = Number(RELEASE_CONSTANTS.TREASURY_TERMS_DELAY);
|
|
23
|
+
export const FEE_TERMS_GRACE_SECONDS = Number(RELEASE_CONSTANTS.TREASURY_TERMS_GRACE);
|
|
24
|
+
/**
|
|
25
|
+
* The Hookr minimum (r25, HookrTreasury): the least Hookr takes of every trader swap on a Hookr token's pool, in pips
|
|
26
|
+
* of the swap, frozen at bind. The treasury holds a rate for pools without arb recapture and one for pools with it,
|
|
27
|
+
* each 0 to MAX_MIN_FEE_PIPS (10,000, 1%) and DEFAULT_MIN_FEE_PIPS (1,000, 0.1%) at deployment, and a per-integrator
|
|
28
|
+
* override (0 to 10,000; NO_MIN_FEE_OVERRIDE, 65,535, reads as none) that reaches only the pool ids that integrator
|
|
29
|
+
* approved (`approveMinFeePool`). HookrRules refuses a minimum above its own MAX_MIN_FEE_PIPS at bind.
|
|
30
|
+
*/
|
|
31
|
+
export const MAX_MIN_FEE_PIPS = Number(RELEASE_CONSTANTS.TREASURY_MAX_MIN_FEE_PIPS);
|
|
32
|
+
export const DEFAULT_MIN_FEE_PIPS = Number(RELEASE_CONSTANTS.TREASURY_DEFAULT_MIN_FEE_PIPS);
|
|
33
|
+
export const NO_MIN_FEE_OVERRIDE = Number(RELEASE_CONSTANTS.TREASURY_NO_MIN_FEE_OVERRIDE);
|
|
34
|
+
export const RULES_MAX_MIN_FEE_PIPS = Number(RELEASE_CONSTANTS.RULES_MAX_MIN_FEE_PIPS);
|
|
35
|
+
/**
|
|
36
|
+
* The runtime keccak256 of HookrToken, the token HookrLauncher creates. It has no immutables, so every HookrToken has
|
|
37
|
+
* this codehash; HookrRules compares a pool's subject's codehash with it at bind and freezes the Hookr minimum only
|
|
38
|
+
* on a match, and then only on a pool without arb recapture whose rules earn Hookr nothing (`bindMinFeePips`). A token
|
|
39
|
+
* of any other code binds none; HookrToken bytecode deployed outside the launcher binds it.
|
|
40
|
+
*/
|
|
41
|
+
export const HOOKR_TOKEN_RUNTIME_KECCAK256 = RELEASE_CONSTANTS.HOOKR_TOKEN_RUNTIME_KECCAK256;
|
|
42
|
+
/**
|
|
43
|
+
* Hookr's cut of every arb recapture, in basis points of the profit the root reads back from each push at the pool's
|
|
44
|
+
* frozen partner share (HookrRecapture.RECAPTURE_PROTOCOL_BPS): 25% since r31, on every pool, whatever its
|
|
45
|
+
* protocolShareBps or integrator.
|
|
46
|
+
*/
|
|
47
|
+
export const RECAPTURE_PROTOCOL_BPS = Number(RELEASE_CONSTANTS.RECAPTURE_PROTOCOL_BPS);
|
|
48
|
+
/**
|
|
49
|
+
* HookrRegistryChecks.MAX_LANE_PARTNER_BPS: the most a lane partner keeps of each arb recapture's profit, 25% since
|
|
50
|
+
* r31, checked at every lane opening.
|
|
51
|
+
*/
|
|
52
|
+
export const MAX_LANE_PARTNER_BPS = Number(RELEASE_CONSTANTS.MAX_LANE_PARTNER_BPS);
|
|
53
|
+
/** HookrRecapture.PRIZE_SHARE_OF_FEE_BPS: a King of the Pool prize reaches at most half the protocol fee it bounds. */
|
|
54
|
+
export const PRIZE_SHARE_OF_FEE_BPS = Number(RELEASE_CONSTANTS.RECAPTURE_PRIZE_SHARE_OF_FEE_BPS);
|
|
55
|
+
/**
|
|
56
|
+
* Longest Anti-Snipe decay any guard binds with, in seconds (HookrTypes.MAX_SNIPE_DECAY_SECONDS, an hour); since r31 a
|
|
57
|
+
* longer guard may decay over its whole length (`maxSnipeDecaySeconds`).
|
|
58
|
+
*/
|
|
59
|
+
export const MAX_SNIPE_DECAY_SECONDS = Number(RELEASE_CONSTANTS.RULES_MAX_SNIPE_DECAY_SECONDS);
|
|
60
|
+
/** HookrLauncher.DYNAMIC_FEE_LIQUIDITY_DIVISOR: a dynamic fee member's launch liquidity over this is its minimum. */
|
|
61
|
+
export const DYNAMIC_FEE_LIQUIDITY_DIVISOR = BigInt(RELEASE_CONSTANTS.LAUNCHER_DYNAMIC_FEE_LIQUIDITY_DIVISOR);
|
|
62
|
+
/** Hookr dynamic fee reference windows (HookrRules.dynamicFeeParameters), in seconds, basis points and ticks. */
|
|
63
|
+
export const DYNAMIC_FEE_PARAMETERS = {
|
|
64
|
+
window: Number(RELEASE_CONSTANTS.RULES_DYNAMIC_FEE_WINDOW),
|
|
65
|
+
reset: Number(RELEASE_CONSTANTS.RULES_DYNAMIC_FEE_RESET),
|
|
66
|
+
carryBps: Number(RELEASE_CONSTANTS.RULES_DYNAMIC_FEE_CARRY_BPS),
|
|
67
|
+
moveTicks: Number(RELEASE_CONSTANTS.RULES_DYNAMIC_FEE_MOVE_TICKS),
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* RulesKnobs bounds HookrRules enforces at bind (HookrTypes): a dynamic fee pool's window, its reset (a multiple of the
|
|
71
|
+
* reset step from the larger of twice the window and the reset floor), its carry (a multiple of the carry step) and its
|
|
72
|
+
* counted move; the Snipe curves; and the parent block length a guard's wall-clock length is counted in.
|
|
73
|
+
*/
|
|
74
|
+
export const RULES_KNOB_BOUNDS = {
|
|
75
|
+
minWindowSeconds: Number(RELEASE_CONSTANTS.RULES_MIN_WINDOW_SECONDS),
|
|
76
|
+
maxWindowSeconds: Number(RELEASE_CONSTANTS.RULES_MAX_WINDOW_SECONDS),
|
|
77
|
+
minResetSeconds: Number(RELEASE_CONSTANTS.RULES_MIN_RESET_SECONDS),
|
|
78
|
+
maxResetSeconds: Number(RELEASE_CONSTANTS.RULES_MAX_RESET_SECONDS),
|
|
79
|
+
resetStepSeconds: Number(RELEASE_CONSTANTS.RULES_RESET_STEP_SECONDS),
|
|
80
|
+
maxCarryBps: Number(RELEASE_CONSTANTS.RULES_MAX_CARRY_BPS),
|
|
81
|
+
carryStepBps: Number(RELEASE_CONSTANTS.RULES_CARRY_STEP_BPS),
|
|
82
|
+
minMoveTicks: Number(RELEASE_CONSTANTS.RULES_MIN_MOVE_TICKS),
|
|
83
|
+
maxMoveTicks: Number(RELEASE_CONSTANTS.RULES_MAX_MOVE_TICKS),
|
|
84
|
+
};
|
|
85
|
+
/** RulesKnobs.snipeCurve (HookrTypes.SNIPE_CURVE_*): linear (the default), front-loaded and step. */
|
|
86
|
+
export const SNIPE_CURVE = {
|
|
87
|
+
linear: Number(RELEASE_CONSTANTS.RULES_SNIPE_CURVE_LINEAR),
|
|
88
|
+
frontLoaded: Number(RELEASE_CONSTANTS.RULES_SNIPE_CURVE_FRONT_LOADED),
|
|
89
|
+
step: Number(RELEASE_CONSTANTS.RULES_SNIPE_CURVE_STEP),
|
|
90
|
+
};
|
|
91
|
+
/** HookrTypes.PARENT_BLOCK_SECONDS: a guard's length in parent blocks times this bounds a longer Snipe decay. */
|
|
92
|
+
export const PARENT_BLOCK_SECONDS = Number(RELEASE_CONSTANTS.RULES_PARENT_BLOCK_SECONDS);
|
|
93
|
+
/** The knobs of a pool bound without any (`abi.encode(RulesConfig)`): every field zero. */
|
|
94
|
+
export const NO_RULES_KNOBS = Object.freeze({
|
|
95
|
+
minDynamicFeeLiquidity: 0n,
|
|
96
|
+
windowSeconds: 0,
|
|
97
|
+
resetSeconds: 0,
|
|
98
|
+
carryBps: 0,
|
|
99
|
+
moveTicks: 0,
|
|
100
|
+
snipeCurve: 0,
|
|
101
|
+
});
|
|
102
|
+
/**
|
|
103
|
+
* HookrDynamicFee's reach bound (r31, N09): a dynamic fee pool binds only when its span (maxFeePips less the base fee)
|
|
104
|
+
* times its sensitivity squared is at most `maxReach`, and its span times its protocol share times (`reserveScale` less
|
|
105
|
+
* the sensitivity squared) at most `maxReserve` times `reserveScale`.
|
|
106
|
+
*/
|
|
107
|
+
export const DYNAMIC_FEE_REACH = {
|
|
108
|
+
maxReach: BigInt(RELEASE_CONSTANTS.DYNAMIC_FEE_MAX_REACH),
|
|
109
|
+
maxReserve: BigInt(RELEASE_CONSTANTS.DYNAMIC_FEE_MAX_RESERVE),
|
|
110
|
+
reserveScale: BigInt(RELEASE_CONSTANTS.DYNAMIC_FEE_RESERVE_SCALE),
|
|
111
|
+
};
|
|
112
|
+
const MAX_MIN_LIQUIDITY = (1n << 96n) - 1n;
|
|
113
|
+
const WAD = 10n ** 18n;
|
|
114
|
+
/** Longest launch guard, in blocks after the binding block. */
|
|
115
|
+
export const MAX_GUARD_BLOCKS = 100_000;
|
|
116
|
+
const PIPS = 1000000n;
|
|
117
|
+
const BPS = 10000n;
|
|
118
|
+
const MAX_LP = BigInt(MAX_LP_FEE_PIPS);
|
|
119
|
+
const RULES_TUPLE = [{ type: "tuple", components: STRUCT_COMPONENTS.RulesConfig }];
|
|
120
|
+
const KNOBS_RULES_TUPLE = [...RULES_TUPLE, { type: "tuple", components: STRUCT_COMPONENTS.RulesKnobs }];
|
|
121
|
+
const RECAPTURE_RULES_TUPLE = [
|
|
122
|
+
...KNOBS_RULES_TUPLE,
|
|
123
|
+
{ type: "tuple", components: STRUCT_COMPONENTS.RecaptureConfig },
|
|
124
|
+
];
|
|
125
|
+
/**
|
|
126
|
+
* RulesConfig's type string, `RulesConfig(uint40 guardEndBlock,...,address integrator)`, built from the struct's ABI
|
|
127
|
+
* components in declaration order (16 fields since r23 appended `integrator`).
|
|
128
|
+
*/
|
|
129
|
+
export const RULES_CONFIG_TYPE = `RulesConfig(${STRUCT_COMPONENTS.RulesConfig.map((f) => `${f.type} ${f.name}`).join(",")})`;
|
|
130
|
+
/**
|
|
131
|
+
* The Rules' schema id (r26): HookrRules.configSchemaHash() is the keccak256 of RULES_CONFIG_TYPE, and
|
|
132
|
+
* HookrFeeAdvisory.RULES_SCHEMA and HookrSessionAdvisory's RULES_SCHEMA read the same id. A Rules admission carries it
|
|
133
|
+
* as its `schemaHash` (the lane checks the admission against the Rules' own answer), and a reader that gates on it
|
|
134
|
+
* refuses any other layout.
|
|
135
|
+
*/
|
|
136
|
+
export const RULES_CONFIG_SCHEMA_HASH = RELEASE_CONSTANTS.RULES_CONFIG_SCHEMA_HASH;
|
|
137
|
+
/**
|
|
138
|
+
* The retired schema id of the 15-word layout before r23's `integrator` field, keccak256("hookr.rules.config"): these
|
|
139
|
+
* Rules never answer it, so an advisory or tool still pinned to it fails closed against them.
|
|
140
|
+
*/
|
|
141
|
+
export const RETIRED_RULES_CONFIG_SCHEMA_HASH = keccak256(stringToBytes("hookr.rules.config"));
|
|
142
|
+
/**
|
|
143
|
+
* Words of each form of rules data HookrRules binds (r31): the config alone, the config and its RulesKnobs, and both
|
|
144
|
+
* with a RecaptureConfig too (16, 22 and 30 words).
|
|
145
|
+
*/
|
|
146
|
+
export const RULES_DATA_WORDS = {
|
|
147
|
+
config: STRUCT_COMPONENTS.RulesConfig.length,
|
|
148
|
+
knobs: STRUCT_COMPONENTS.RulesConfig.length + STRUCT_COMPONENTS.RulesKnobs.length,
|
|
149
|
+
recapture: STRUCT_COMPONENTS.RulesConfig.length +
|
|
150
|
+
STRUCT_COMPONENTS.RulesKnobs.length +
|
|
151
|
+
STRUCT_COMPONENTS.RecaptureConfig.length,
|
|
152
|
+
};
|
|
153
|
+
/** The decoded config with its addresses in the lowercase form every SDK comparison uses. */
|
|
154
|
+
function normalized(c) {
|
|
155
|
+
return { ...c, royaltyTo: c.royaltyTo.toLowerCase(), integrator: c.integrator.toLowerCase() };
|
|
156
|
+
}
|
|
157
|
+
function int(value, bits, label) {
|
|
158
|
+
assert(Number.isSafeInteger(value) && value >= 0 && value < 2 ** bits, `Invalid ${label}`);
|
|
159
|
+
return BigInt(value);
|
|
160
|
+
}
|
|
161
|
+
function min(a, b) {
|
|
162
|
+
return a < b ? a : b;
|
|
163
|
+
}
|
|
164
|
+
/** ABI-encodes a RulesConfig alone: `abi.encode(RulesConfig)`, the rules data of a pool without dynamic fees. */
|
|
165
|
+
export function encodeRulesConfig(c) {
|
|
166
|
+
return encodeAbiParameters(RULES_TUPLE, [c]);
|
|
167
|
+
}
|
|
168
|
+
/** Decodes a config-only rules data (`abi.encode(RulesConfig)`), refusing any non-canonical encoding. */
|
|
169
|
+
export function decodeRulesConfig(data) {
|
|
170
|
+
const [c] = decodeAbiParameters(RULES_TUPLE, data);
|
|
171
|
+
const decoded = normalized(c);
|
|
172
|
+
assert(encodeRulesConfig(decoded).toLowerCase() === data.toLowerCase(), "Non-canonical rules config");
|
|
173
|
+
return decoded;
|
|
174
|
+
}
|
|
175
|
+
/** The knobs with every integer normalized: what an ABI decode and a caller's literal compare equal on. */
|
|
176
|
+
function knobsOf(k) {
|
|
177
|
+
return {
|
|
178
|
+
minDynamicFeeLiquidity: BigInt(k.minDynamicFeeLiquidity),
|
|
179
|
+
windowSeconds: Number(k.windowSeconds),
|
|
180
|
+
resetSeconds: Number(k.resetSeconds),
|
|
181
|
+
carryBps: Number(k.carryBps),
|
|
182
|
+
moveTicks: Number(k.moveTicks),
|
|
183
|
+
snipeCurve: Number(k.snipeCurve),
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
/** Whether `k` sets any knob: a pool whose knobs are all zero binds the config alone. */
|
|
187
|
+
function anyKnob(k) {
|
|
188
|
+
return (k.minDynamicFeeLiquidity !== 0n ||
|
|
189
|
+
k.windowSeconds !== 0 ||
|
|
190
|
+
k.resetSeconds !== 0 ||
|
|
191
|
+
k.carryBps !== 0 ||
|
|
192
|
+
k.moveTicks !== 0 ||
|
|
193
|
+
k.snipeCurve !== 0);
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Refuses knobs HookrRules._checkKnobs refuses at bind for `c` in the given form (`knobs`: abi.encode(RulesConfig,
|
|
197
|
+
* RulesKnobs); `recapture`: with a RecaptureConfig after them; `config`: the config alone, whose knobs read as zero). A
|
|
198
|
+
* pool without dynamic fees takes none of the five dynamic fee fields; a dynamic fee pool needs the knobbed form, a
|
|
199
|
+
* minimum dynamic fee liquidity of 1 to 2^96 - 1 and its tempo in bounds; a curve other than linear needs a decaying
|
|
200
|
+
* Snipe; and a knobbed pool without arb recapture or dynamic fees must set a knob off its default (the curve).
|
|
201
|
+
* Returns whether the pool stores knobs (`custom`) and whether its tempo is off the default (`tempo`).
|
|
202
|
+
*/
|
|
203
|
+
export function checkRulesKnobs(c, knobs, form) {
|
|
204
|
+
const k = knobsOf(knobs);
|
|
205
|
+
uint(k.minDynamicFeeLiquidity, 96);
|
|
206
|
+
int(k.windowSeconds, 16, "windowSeconds");
|
|
207
|
+
int(k.resetSeconds, 16, "resetSeconds");
|
|
208
|
+
int(k.carryBps, 16, "carryBps");
|
|
209
|
+
int(k.moveTicks, 24, "moveTicks");
|
|
210
|
+
int(k.snipeCurve, 8, "snipeCurve");
|
|
211
|
+
const knobbed = form !== "config";
|
|
212
|
+
if (!knobbed)
|
|
213
|
+
assert(!anyKnob(k), "Rules data without knobs carries no knob");
|
|
214
|
+
let tempo = false;
|
|
215
|
+
let custom = false;
|
|
216
|
+
if (c.dynamicFeeSens === 0) {
|
|
217
|
+
assert(k.minDynamicFeeLiquidity === 0n &&
|
|
218
|
+
k.windowSeconds === 0 &&
|
|
219
|
+
k.resetSeconds === 0 &&
|
|
220
|
+
k.carryBps === 0 &&
|
|
221
|
+
k.moveTicks === 0, "A pool without dynamic fees takes no dynamic fee knob");
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
const b = RULES_KNOB_BOUNDS;
|
|
225
|
+
assert(knobbed, "A dynamic fee pool binds its RulesKnobs (abi.encode(RulesConfig, RulesKnobs))");
|
|
226
|
+
assert(k.minDynamicFeeLiquidity > 0n, "A dynamic fee pool needs a minimum dynamic fee liquidity in [1, 2^96 - 1]");
|
|
227
|
+
assert(k.windowSeconds >= b.minWindowSeconds && k.windowSeconds <= b.maxWindowSeconds, `windowSeconds ${k.windowSeconds} outside [${b.minWindowSeconds}, ${b.maxWindowSeconds}]`);
|
|
228
|
+
assert(k.resetSeconds >= 2 * k.windowSeconds &&
|
|
229
|
+
k.resetSeconds >= b.minResetSeconds &&
|
|
230
|
+
k.resetSeconds <= b.maxResetSeconds &&
|
|
231
|
+
k.resetSeconds % b.resetStepSeconds === 0, `resetSeconds ${k.resetSeconds} outside [max(2 x window, ${b.minResetSeconds}), ${b.maxResetSeconds}] in steps of ${b.resetStepSeconds}`);
|
|
232
|
+
assert(k.carryBps <= b.maxCarryBps && k.carryBps % b.carryStepBps === 0, `carryBps ${k.carryBps} above ${b.maxCarryBps} or off its ${b.carryStepBps} step`);
|
|
233
|
+
assert(k.moveTicks >= b.minMoveTicks && k.moveTicks <= b.maxMoveTicks, `moveTicks ${k.moveTicks} outside [${b.minMoveTicks}, ${b.maxMoveTicks}]`);
|
|
234
|
+
const d = DYNAMIC_FEE_PARAMETERS;
|
|
235
|
+
tempo =
|
|
236
|
+
k.windowSeconds !== d.window ||
|
|
237
|
+
k.resetSeconds !== d.reset ||
|
|
238
|
+
k.carryBps !== d.carryBps ||
|
|
239
|
+
k.moveTicks !== d.moveTicks;
|
|
240
|
+
custom = tempo;
|
|
241
|
+
}
|
|
242
|
+
assert(k.snipeCurve <= SNIPE_CURVE.step, `Unknown Snipe curve ${k.snipeCurve}`);
|
|
243
|
+
assert(k.snipeCurve === 0 || c.snipeDecaySeconds !== 0, "A Snipe curve other than linear needs a decaying Snipe");
|
|
244
|
+
if (k.snipeCurve !== 0)
|
|
245
|
+
custom = true;
|
|
246
|
+
assert(!(form === "knobs" && !custom && c.dynamicFeeSens === 0), "Knobs at their defaults on a pool without dynamic fees or arb recapture bind as the config alone");
|
|
247
|
+
return { custom, tempo };
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* The canonical rules data HookrRules.bind takes for `c` (r31, three forms told apart by length): `abi.encode(RulesConfig)`
|
|
251
|
+
* for a pool without dynamic fees, arb recapture or a knob off its default; `abi.encode(RulesConfig, RulesKnobs)` for
|
|
252
|
+
* every other pool without arb recapture; `abi.encode(RulesConfig, RulesKnobs, RecaptureConfig)` for a recapture pool
|
|
253
|
+
* (`recapture.on`). Refuses knobs bind would refuse (`checkRulesKnobs`); the RecaptureConfig's own bounds are
|
|
254
|
+
* HookrRecapture's, checked at bind.
|
|
255
|
+
*/
|
|
256
|
+
export function encodeRulesData(c, knobs = NO_RULES_KNOBS, recapture) {
|
|
257
|
+
const k = knobsOf(knobs);
|
|
258
|
+
if (recapture?.on) {
|
|
259
|
+
checkRulesKnobs(c, k, "recapture");
|
|
260
|
+
return encodeAbiParameters(RECAPTURE_RULES_TUPLE, [c, k, recapture]);
|
|
261
|
+
}
|
|
262
|
+
if (c.dynamicFeeSens === 0 && !anyKnob(k))
|
|
263
|
+
return encodeRulesConfig(c);
|
|
264
|
+
checkRulesKnobs(c, k, "knobs");
|
|
265
|
+
return encodeAbiParameters(KNOBS_RULES_TUPLE, [c, k]);
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Decodes `RulesBound.config` in any form HookrRules.bind accepts (16, 22 or, for a recapture pool, 30 words) and
|
|
269
|
+
* refuses what bind refuses of its shape: a non-canonical encoding, knobs its config cannot take (`checkRulesKnobs`)
|
|
270
|
+
* and recapture data with recapture off. `knobs` is NO_RULES_KNOBS for the config-only form; `minLiquidity` is the
|
|
271
|
+
* knobs' minimum dynamic fee liquidity, zero without dynamic fees.
|
|
272
|
+
*/
|
|
273
|
+
export function decodeRulesData(data) {
|
|
274
|
+
const words = (data.length - 2) / 64;
|
|
275
|
+
if (words === RULES_DATA_WORDS.config)
|
|
276
|
+
return { config: decodeRulesConfig(data), knobs: { ...NO_RULES_KNOBS }, minLiquidity: 0n };
|
|
277
|
+
if (words === RULES_DATA_WORDS.recapture) {
|
|
278
|
+
const [c, k, rc] = decodeAbiParameters(RECAPTURE_RULES_TUPLE, data);
|
|
279
|
+
const config = normalized(c);
|
|
280
|
+
const knobs = knobsOf(k);
|
|
281
|
+
const recapture = { ...rc };
|
|
282
|
+
assert(encodeAbiParameters(RECAPTURE_RULES_TUPLE, [config, knobs, recapture]).toLowerCase() === data.toLowerCase(), "Non-canonical recapture rules data");
|
|
283
|
+
assert(recapture.on, "Recapture rules data with recapture off");
|
|
284
|
+
checkRulesKnobs(config, knobs, "recapture");
|
|
285
|
+
return { config, knobs, minLiquidity: knobs.minDynamicFeeLiquidity, recapture };
|
|
286
|
+
}
|
|
287
|
+
assert(words === RULES_DATA_WORDS.knobs, "Rules data is none of the forms HookrRules binds");
|
|
288
|
+
const [c, k] = decodeAbiParameters(KNOBS_RULES_TUPLE, data);
|
|
289
|
+
const config = normalized(c);
|
|
290
|
+
const knobs = knobsOf(k);
|
|
291
|
+
assert(encodeAbiParameters(KNOBS_RULES_TUPLE, [config, knobs]).toLowerCase() === data.toLowerCase(), "Non-canonical rules data");
|
|
292
|
+
checkRulesKnobs(config, knobs, "knobs");
|
|
293
|
+
return { config, knobs, minLiquidity: knobs.minDynamicFeeLiquidity };
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* HookrLauncher's minimum dynamic fee liquidity for a member launched with `liquidity`: liquidity / divisor, in
|
|
297
|
+
* [1, 2^96 - 1]. `divisor` is launchFamily's MemberKnobs.dynamicFeeLiquidityDivisor; zero (and every other launch) is
|
|
298
|
+
* DYNAMIC_FEE_LIQUIDITY_DIVISOR.
|
|
299
|
+
*/
|
|
300
|
+
export function dynamicFeeMinLiquidity(liquidity, divisor = 0) {
|
|
301
|
+
int(divisor, 16, "dynamicFeeLiquidityDivisor");
|
|
302
|
+
const minimum = liquidity / (divisor === 0 ? DYNAMIC_FEE_LIQUIDITY_DIVISOR : BigInt(divisor));
|
|
303
|
+
if (minimum === 0n)
|
|
304
|
+
return 1n;
|
|
305
|
+
return minimum > MAX_MIN_LIQUIDITY ? MAX_MIN_LIQUIDITY : minimum;
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* The rules data HookrLauncher binds for a member with `rules` and launch `liquidity` (HookrLauncher._rulesData, r31):
|
|
309
|
+
* launchFamily's MemberKnobs pass the member's Rules knobs as given (`knobs`; every other launch passes none); a
|
|
310
|
+
* dynamic fee member's minimum is its liquidity over the divisor, and its tempo the default when all four tempo knobs
|
|
311
|
+
* are zero. A recapture member (`recapture.on`) binds the 30-word form, a member with every knob zero the config alone,
|
|
312
|
+
* any other the 22-word form. Like the Launcher it does not check the knobs; HookrRules refuses bad ones at bind
|
|
313
|
+
* (`checkRulesKnobs`).
|
|
314
|
+
*/
|
|
315
|
+
export function launcherRulesData(c, liquidity, opts = {}) {
|
|
316
|
+
const m = opts.knobs ?? {};
|
|
317
|
+
const k = {
|
|
318
|
+
minDynamicFeeLiquidity: 0n,
|
|
319
|
+
windowSeconds: m.windowSeconds ?? 0,
|
|
320
|
+
resetSeconds: m.resetSeconds ?? 0,
|
|
321
|
+
carryBps: m.carryBps ?? 0,
|
|
322
|
+
moveTicks: m.moveTicks ?? 0,
|
|
323
|
+
snipeCurve: m.snipeCurve ?? 0,
|
|
324
|
+
};
|
|
325
|
+
if (c.dynamicFeeSens !== 0) {
|
|
326
|
+
k.minDynamicFeeLiquidity = dynamicFeeMinLiquidity(liquidity, m.dynamicFeeLiquidityDivisor ?? 0);
|
|
327
|
+
if (k.windowSeconds === 0 && k.resetSeconds === 0 && k.carryBps === 0 && k.moveTicks === 0) {
|
|
328
|
+
k.windowSeconds = DYNAMIC_FEE_PARAMETERS.window;
|
|
329
|
+
k.resetSeconds = DYNAMIC_FEE_PARAMETERS.reset;
|
|
330
|
+
k.carryBps = DYNAMIC_FEE_PARAMETERS.carryBps;
|
|
331
|
+
k.moveTicks = DYNAMIC_FEE_PARAMETERS.moveTicks;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
if (opts.recapture?.on)
|
|
335
|
+
return encodeAbiParameters(RECAPTURE_RULES_TUPLE, [c, k, opts.recapture]);
|
|
336
|
+
if (!anyKnob(k))
|
|
337
|
+
return encodeRulesConfig(c);
|
|
338
|
+
return encodeAbiParameters(KNOBS_RULES_TUPLE, [c, k]);
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* The longest snipeDecaySeconds a pool binds with at `blockNumber` (HookrRules._validate, r31): MAX_SNIPE_DECAY_SECONDS,
|
|
342
|
+
* or the guard's length in seconds when that is longer, (guardEndBlock - blockNumber) x PARENT_BLOCK_SECONDS, at most
|
|
343
|
+
* 65,535. Without a block number only MAX_SNIPE_DECAY_SECONDS is certain.
|
|
344
|
+
*/
|
|
345
|
+
export function maxSnipeDecaySeconds(guardEndBlock, blockNumber) {
|
|
346
|
+
const guard = BigInt(guardEndBlock);
|
|
347
|
+
if (blockNumber === undefined || guard <= blockNumber)
|
|
348
|
+
return MAX_SNIPE_DECAY_SECONDS;
|
|
349
|
+
const length = (guard - blockNumber) * BigInt(PARENT_BLOCK_SECONDS);
|
|
350
|
+
const longest = length > 65535n ? 65535n : length;
|
|
351
|
+
return Number(longest) > MAX_SNIPE_DECAY_SECONDS ? Number(longest) : MAX_SNIPE_DECAY_SECONDS;
|
|
352
|
+
}
|
|
353
|
+
/** Whether a dynamic fee of `spanPips` (maxFeePips less the base fee) at `sens` and `shareBps` binds (HookrDynamicFee.withinReach). */
|
|
354
|
+
export function withinDynamicFeeReach(spanPips, sens, shareBps) {
|
|
355
|
+
const span = BigInt(spanPips);
|
|
356
|
+
const sens2 = BigInt(sens) * BigInt(sens);
|
|
357
|
+
const r = DYNAMIC_FEE_REACH;
|
|
358
|
+
return (span * sens2 <= r.maxReach && span * BigInt(shareBps) * (r.reserveScale - sens2) <= r.maxReserve * r.reserveScale);
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* The Anti-Snipe tax, in pips before clipping, of a guarded buy at `timestamp` in a pool bound at `launchedAt`
|
|
362
|
+
* (HookrRules._snipePips and _knobbedSnipe, r31): snipeTaxPips flat without a decay, otherwise falling from snipeTaxPips
|
|
363
|
+
* at launch to snipeFloorPips at snipeDecaySeconds along the pool's Snipe curve, every division rounded down. With
|
|
364
|
+
* d = snipeDecaySeconds, e the launch age, x = snipeTaxPips - snipeFloorPips and n = 8e / d: linear (the default),
|
|
365
|
+
* floor + x(d - e)/d; step, floor + x(8 - n)/8; front-loaded, what is left halves every eighth of the decay in a
|
|
366
|
+
* straight line between the halvings (the last eighth falls the rest of the way). The launch age is taken modulo 2^32
|
|
367
|
+
* and a clock behind the launch reads as the launch. The guard itself is counted in blocks (guardEndBlock).
|
|
368
|
+
*/
|
|
369
|
+
export function snipeTaxAt(c, launchedAt, timestamp, curve = SNIPE_CURVE.linear) {
|
|
370
|
+
int(curve, 8, "snipeCurve");
|
|
371
|
+
assert(curve <= SNIPE_CURVE.step, `Unknown Snipe curve ${curve}`);
|
|
372
|
+
const start = BigInt(c.snipeTaxPips);
|
|
373
|
+
const duration = BigInt(c.snipeDecaySeconds);
|
|
374
|
+
if (duration === 0n)
|
|
375
|
+
return Number(start);
|
|
376
|
+
let elapsed = (BigInt(timestamp) - BigInt(launchedAt)) & 0xffffffffn;
|
|
377
|
+
if (elapsed > 0xffffffffn >> 1n)
|
|
378
|
+
elapsed = 0n;
|
|
379
|
+
const floor = BigInt(c.snipeFloorPips);
|
|
380
|
+
if (elapsed >= duration)
|
|
381
|
+
return Number(floor);
|
|
382
|
+
if (curve === SNIPE_CURVE.linear)
|
|
383
|
+
return Number(floor + ((start - floor) * (duration - elapsed)) / duration);
|
|
384
|
+
const n = (elapsed * 8n) / duration;
|
|
385
|
+
if (curve === SNIPE_CURVE.step)
|
|
386
|
+
return Number(floor + ((start - floor) * (8n - n)) / 8n);
|
|
387
|
+
const h = (start - floor) >> n;
|
|
388
|
+
const l = n === 7n ? 0n : h >> 1n;
|
|
389
|
+
return Number(floor + l + ((h - l) * (duration - (elapsed * 8n - n * duration))) / duration);
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* The guarded per-parent-block buy cap at Anti-Snipe tax `snipePips` (HookrRules.settleSwap): maxBuyQuoteAmount
|
|
393
|
+
* without a decay; with one, maxBuyQuoteAmount * snipeTaxPips / snipePips, so a block that fills the cap pays the
|
|
394
|
+
* same tax at every point of the decay. `null` when there is no cap: a zero maxBuyQuoteAmount, or a zero tax.
|
|
395
|
+
*/
|
|
396
|
+
export function guardBuyLimit(c, snipePips) {
|
|
397
|
+
if (c.maxBuyQuoteAmount === 0n)
|
|
398
|
+
return null;
|
|
399
|
+
if (c.snipeDecaySeconds === 0)
|
|
400
|
+
return c.maxBuyQuoteAmount;
|
|
401
|
+
if (snipePips === 0)
|
|
402
|
+
return null;
|
|
403
|
+
return (c.maxBuyQuoteAmount * BigInt(c.snipeTaxPips)) / BigInt(snipePips);
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* HookrDynamicFee.averageRate: the average of min(1, (d / knee)^2), knee = 1 / (2 * sens), over the distances [a, b]
|
|
407
|
+
* from the reference, as a fraction of the dynamic fee span in WAD. Distances are in WAD; requires b > a.
|
|
408
|
+
*/
|
|
409
|
+
export function dynamicFeeAverageRate(a, b, sens) {
|
|
410
|
+
assert(b > a && a >= 0n && Number.isInteger(sens) && sens >= 1 && sens <= 10, "Invalid dynamic fee interval");
|
|
411
|
+
const s = BigInt(sens);
|
|
412
|
+
const knee = WAD / (2n * s);
|
|
413
|
+
if (a >= knee)
|
|
414
|
+
return WAD;
|
|
415
|
+
if (b <= knee)
|
|
416
|
+
return (4n * s * s * (a * a + a * b + b * b)) / (3n * WAD);
|
|
417
|
+
const area = (knee - (a * a * a) / (knee * knee)) / 3n + (b - knee);
|
|
418
|
+
const ratio = (area * WAD) / (b - a);
|
|
419
|
+
return ratio > WAD ? WAD : ratio;
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* HookrDynamicFee.distance: the signed distance of `sqrtPriceX96` from the reference `sqrtRefX96` in the direction of
|
|
423
|
+
* a swap, in WAD: sqrt(ref) / sqrt(p) - 1 for zeroForOne, sqrt(p) / sqrt(ref) - 1 otherwise.
|
|
424
|
+
*/
|
|
425
|
+
export function dynamicFeeDistance(sqrtRefX96, sqrtPriceX96, zeroForOne) {
|
|
426
|
+
return (zeroForOne ? (sqrtRefX96 * WAD) / sqrtPriceX96 : (sqrtPriceX96 * WAD) / sqrtRefX96) - WAD;
|
|
427
|
+
}
|
|
428
|
+
/** Dynamic fee pips before clipping for a move over the distances [a, b]: the span times the average rate. */
|
|
429
|
+
export function dynamicFeePips(c, baseLpFeePips, a, b) {
|
|
430
|
+
if (c.dynamicFeeSens === 0 || b <= a)
|
|
431
|
+
return 0;
|
|
432
|
+
const span = BigInt(c.maxFeePips) - BigInt(baseLpFeePips);
|
|
433
|
+
return Number((span * dynamicFeeAverageRate(a, b, c.dynamicFeeSens)) / WAD);
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* The protocol's share of a `bps` slice of a buy, in pips of the gross buyer spend, rounded up
|
|
437
|
+
* (HookrRules._protocolPips): ceil(bps x 100 x shareBps / 10,000). Exact whenever shareBps is a whole percent.
|
|
438
|
+
*/
|
|
439
|
+
export function protocolSharePips(bps, shareBps) {
|
|
440
|
+
return (BigInt(bps) * BigInt(shareBps) + 99n) / 100n;
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
443
|
+
* LP Rewards net of the protocol share, before the royalty, in pips: lpBps x 100 less its protocol share in pips,
|
|
444
|
+
* rounded up (HookrRules._buyParts, HookrFeeAdvisory._nativeCeiling, HookrSessionAdvisory._rulesCeiling).
|
|
445
|
+
*/
|
|
446
|
+
function lpNetPips(c) {
|
|
447
|
+
return BigInt(c.lpBps) * 100n - protocolSharePips(c.lpBps, c.protocolShareBps);
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* LP Rewards, quote take, net subject burn and the royalty's part of the take of a buy (HookrRules._buyParts). The
|
|
451
|
+
* protocol's share of LP Rewards and of Auto Burn is taken in pips, rounded up; the burn left after it is whole basis
|
|
452
|
+
* points, rounded down.
|
|
453
|
+
*/
|
|
454
|
+
function buyParts(c) {
|
|
455
|
+
const lpSlice = protocolSharePips(c.lpBps, c.protocolShareBps);
|
|
456
|
+
const lpNet = BigInt(c.lpBps) * 100n - lpSlice;
|
|
457
|
+
const royaltyPips = (lpNet * BigInt(c.royaltyBps)) / BPS;
|
|
458
|
+
const burnSlice = protocolSharePips(c.burnBps, c.protocolShareBps);
|
|
459
|
+
return {
|
|
460
|
+
lpReward: lpNet - royaltyPips,
|
|
461
|
+
take: lpSlice + royaltyPips + burnSlice,
|
|
462
|
+
burnBps: (BigInt(c.burnBps) * 100n - burnSlice) / 100n,
|
|
463
|
+
royaltyPips,
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* The royalty's part of a buy's quote take, in pips of the gross buyer spend (HookrRules._buyParts): LP Rewards net of
|
|
468
|
+
* the protocol share, times royaltyBps, rounded down. It is the creator's, not Hookr's: the Hookr minimum compares
|
|
469
|
+
* Hookr's rule-fee share alone with the minimum and adds the royalty on top. Sells pay no royalty.
|
|
470
|
+
*/
|
|
471
|
+
export function royaltyPips(c) {
|
|
472
|
+
return Number((lpNetPips(c) * BigInt(c.royaltyBps)) / BPS);
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* LP Rewards of a buy, in pips of the pool input: lpBps x 100 less the protocol share (in pips, rounded up), less
|
|
476
|
+
* the royalty (HookrRules._buyParts). A pool binds only when base + this is at most 600,000 pips.
|
|
477
|
+
*/
|
|
478
|
+
export function lpRewardPips(c) {
|
|
479
|
+
return Number(buyParts({ ...c, burnBps: 0 }).lpReward);
|
|
480
|
+
}
|
|
481
|
+
/**
|
|
482
|
+
* The smallest caps HookrRules accepts for `c` at base fee `baseLpFeePips` on a pool that freezes the Hookr minimum
|
|
483
|
+
* `minFeePips` (zero for a pool without one, `bindMinFeePips`): the worst-case total LP fee, quote take and subject
|
|
484
|
+
* burn over every swap (HookrRules._validate). The quote take is at least royalty + minimum, so a Hookr token's pool
|
|
485
|
+
* whose caps do not admit its minimum does not bind. These are the caps the Rules sees, after the Root has reserved the
|
|
486
|
+
* advisory's admitted maximum.
|
|
487
|
+
*/
|
|
488
|
+
export function rulesFeeCeilings(c, baseLpFeePips, minFeePips = 0) {
|
|
489
|
+
const base = int(baseLpFeePips, 24, "base fee");
|
|
490
|
+
const minimum = BigInt(minFee(minFeePips));
|
|
491
|
+
assert(BigInt(c.maxFeePips) >= base && c.maxFeePips <= MAX_LP_FEE_PIPS, "maxFeePips outside [base, 600000]");
|
|
492
|
+
const { lpReward, take, burnBps, royaltyPips } = buyParts(c);
|
|
493
|
+
assert(base + lpReward <= MAX_LP, "Base fee plus LP Rewards above 600000 pips");
|
|
494
|
+
const share = BigInt(c.protocolShareBps);
|
|
495
|
+
const span = BigInt(c.maxFeePips) - base;
|
|
496
|
+
const snipeTax = BigInt(c.snipeTaxPips);
|
|
497
|
+
const room = MAX_LP - base - lpReward;
|
|
498
|
+
const dynamicFee = min(span, room);
|
|
499
|
+
const snipe = min(snipeTax, room - dynamicFee);
|
|
500
|
+
let slice = (dynamicFee * share) / BPS + (snipe * share) / BPS;
|
|
501
|
+
let maximumLp = base + lpReward + dynamicFee + snipe - slice;
|
|
502
|
+
if (span + snipeTax > room && share !== 0n) {
|
|
503
|
+
// A clipped dynamic fee and Snipe round their shares separately: allow one pip less protocol share.
|
|
504
|
+
slice = ((dynamicFee + snipe) * share) / BPS;
|
|
505
|
+
maximumLp = base + lpReward + dynamicFee + snipe - (slice === 0n ? 0n : slice - 1n);
|
|
506
|
+
}
|
|
507
|
+
if (maximumLp < BigInt(c.maxFeePips))
|
|
508
|
+
maximumLp = BigInt(c.maxFeePips);
|
|
509
|
+
let maximumQuote = slice + take;
|
|
510
|
+
if ((span * share) / BPS > maximumQuote)
|
|
511
|
+
maximumQuote = (span * share) / BPS;
|
|
512
|
+
// The Hookr minimum replaces a smaller rule share (r25): a buy takes at most royalty + max(rule share, minimum), a
|
|
513
|
+
// sell max(dynamic fee share, minimum), so the bound is the larger of the rules' own and royalty + minimum.
|
|
514
|
+
if (royaltyPips + minimum > maximumQuote)
|
|
515
|
+
maximumQuote = royaltyPips + minimum;
|
|
516
|
+
return {
|
|
517
|
+
maxLpFeePips: Number(maximumLp),
|
|
518
|
+
maxQuoteTakePips: Number(maximumQuote),
|
|
519
|
+
maxSubjectTakeBps: Number(burnBps),
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
/**
|
|
523
|
+
* Largest native LP fee (base + Rules surcharge) HookrRules can quote with Anti-Snipe at `snipeTax`
|
|
524
|
+
* (HookrFeeAdvisory._nativeCeiling). The fee advisory's room on a HookrRoot pool is the pool's LP cap minus this.
|
|
525
|
+
*/
|
|
526
|
+
export function nativeLpCeiling(c, baseLpFeePips, snipeTax = 0) {
|
|
527
|
+
const base = BigInt(baseLpFeePips);
|
|
528
|
+
const share = BigInt(c.protocolShareBps);
|
|
529
|
+
const lpNet = lpNetPips(c);
|
|
530
|
+
const lpReward = lpNet - (lpNet * BigInt(c.royaltyBps)) / BPS;
|
|
531
|
+
if (base + lpReward >= MAX_LP)
|
|
532
|
+
return MAX_LP_FEE_PIPS;
|
|
533
|
+
const room = MAX_LP - base - lpReward;
|
|
534
|
+
const span = BigInt(c.maxFeePips) - base;
|
|
535
|
+
const dynamicFee = min(span, room);
|
|
536
|
+
const snipe = min(BigInt(snipeTax), room - dynamicFee);
|
|
537
|
+
let slice = (dynamicFee * share) / BPS + (snipe * share) / BPS;
|
|
538
|
+
if (span + BigInt(snipeTax) > room && share !== 0n) {
|
|
539
|
+
slice = ((dynamicFee + snipe) * share) / BPS;
|
|
540
|
+
if (slice !== 0n)
|
|
541
|
+
slice -= 1n;
|
|
542
|
+
}
|
|
543
|
+
const ceiling = base + lpReward + dynamicFee + snipe - slice;
|
|
544
|
+
return Number(ceiling < BigInt(c.maxFeePips) ? BigInt(c.maxFeePips) : ceiling);
|
|
545
|
+
}
|
|
546
|
+
function bps(value, max, label) {
|
|
547
|
+
assert(Number.isSafeInteger(value) && value >= 0 && value <= max, `${label} ${value} outside [0, ${max}]`);
|
|
548
|
+
return value;
|
|
549
|
+
}
|
|
550
|
+
/** A Hookr minimum a pool can freeze: 0 to 10,000 pips (HookrRules refuses a larger one at bind, InvalidConfig). */
|
|
551
|
+
function minFee(pips) {
|
|
552
|
+
return bps(pips, RULES_MAX_MIN_FEE_PIPS, "minFeePips");
|
|
553
|
+
}
|
|
554
|
+
/** Whether a runtime codehash is HookrToken's (`HOOKR_TOKEN_RUNTIME_KECCAK256`), the test HookrRules applies at bind. */
|
|
555
|
+
export function isHookrTokenRuntime(codeHash) {
|
|
556
|
+
return codeHash.toLowerCase() === HOOKR_TOKEN_RUNTIME_KECCAK256.toLowerCase();
|
|
557
|
+
}
|
|
558
|
+
/**
|
|
559
|
+
* HookrRules._earnsHookr (r31): whether a rule can pay Hookr at least a pip of some swap at the pool's
|
|
560
|
+
* protocolShareBps. Nothing does at a zero share; LP Rewards and Auto Burn do at any nonzero share (the Rules round
|
|
561
|
+
* their protocol slices up); the dynamic fee does when its whole span (maxFeePips less the base fee) times the share is
|
|
562
|
+
* at least 10,000, and the Anti-Snipe tax when snipeTaxPips times the share is (the Rules round both shares down).
|
|
563
|
+
*/
|
|
564
|
+
export function earnsHookr(rules, baseLpFeePips) {
|
|
565
|
+
const share = BigInt(rules.protocolShareBps);
|
|
566
|
+
if (share === 0n)
|
|
567
|
+
return false;
|
|
568
|
+
if (rules.lpBps !== 0 || rules.burnBps !== 0)
|
|
569
|
+
return true;
|
|
570
|
+
const base = BigInt(baseLpFeePips);
|
|
571
|
+
const span = BigInt(rules.maxFeePips) > base ? BigInt(rules.maxFeePips) - base : 0n;
|
|
572
|
+
return span * share >= BPS || BigInt(rules.snipeTaxPips) * share >= BPS;
|
|
573
|
+
}
|
|
574
|
+
/**
|
|
575
|
+
* The Hookr minimum a pool freezes at bind (HookrRules.bind, r31): the treasury's `feeTerms(integrator, false, id)`
|
|
576
|
+
* minimum (`decodeFeeTerms`) only on a Hookr token's pool that binds no arb recapture, whose rules earn Hookr no share
|
|
577
|
+
* (`earnsHookr`) and whose advisory is not Tax + Conversion at a nonzero share; zero on every other pool. Whatever
|
|
578
|
+
* pays Hookr a share replaces the minimum, never adds to it. `subject` is `"new"` for a launch that creates its token
|
|
579
|
+
* (HookrLauncher deploys a HookrToken, so it always is one) or the existing subject's runtime codehash (`eth_getCode`,
|
|
580
|
+
* then keccak256; a Hookr token exactly when it equals HOOKR_TOKEN_RUNTIME_KECCAK256). A treasury answer above 10,000
|
|
581
|
+
* pips is refused whatever follows, as the Rules refuse it. A Rules paying an address without code reads no terms and
|
|
582
|
+
* binds no minimum: pass 0 as `treasuryMinFeePips` for it.
|
|
583
|
+
*/
|
|
584
|
+
export function bindMinFeePips(subject, treasuryMinFeePips, terms) {
|
|
585
|
+
minFee(treasuryMinFeePips);
|
|
586
|
+
if (subject !== "new" && !isHookrTokenRuntime(subject))
|
|
587
|
+
return 0;
|
|
588
|
+
int(terms.baseLpFeePips, 24, "base fee");
|
|
589
|
+
if (terms.recapture || earnsHookr(terms.rules, terms.baseLpFeePips))
|
|
590
|
+
return 0;
|
|
591
|
+
const tax = terms.taxConversion;
|
|
592
|
+
if (tax && terms.rules.protocolShareBps !== 0 && tax.maxQuoteTakePips !== 0)
|
|
593
|
+
return 0;
|
|
594
|
+
return treasuryMinFeePips;
|
|
595
|
+
}
|
|
596
|
+
/**
|
|
597
|
+
* The minimum HookrTreasury.feeTerms answers for a pool (r25): the integrator's override when it has one
|
|
598
|
+
* (`overridePips` other than NO_MIN_FEE_OVERRIDE) and approved the pool's id, else the rate for its kind of pool (with
|
|
599
|
+
* arb recapture on, `recapturePips`; without, `plainPips`). An override never reaches a pool its integrator did not
|
|
600
|
+
* approve, so naming the integrator alone does not bind it. Offline mirror; read `feeTerms` on chain before signing.
|
|
601
|
+
*/
|
|
602
|
+
export function treasuryMinFeePips(t) {
|
|
603
|
+
const rate = minFee(t.recapture ? t.recapturePips : t.plainPips);
|
|
604
|
+
const override = t.overridePips ?? NO_MIN_FEE_OVERRIDE;
|
|
605
|
+
if (override === NO_MIN_FEE_OVERRIDE || !t.approved)
|
|
606
|
+
return rate;
|
|
607
|
+
return minFee(override);
|
|
608
|
+
}
|
|
609
|
+
/**
|
|
610
|
+
* The effective protocolShareBps floor HookrRules binds a pool at (HookrRules._feeTerms): the larger of its immutable
|
|
611
|
+
* minProtocolShareBps and the treasury's governed floor, both 0 to 5,000.
|
|
612
|
+
*/
|
|
613
|
+
export function effectiveShareFloor(minProtocolShareBps, ruleShareFloorBps = 0) {
|
|
614
|
+
return Math.max(bps(minProtocolShareBps, MAX_PROTOCOL_SHARE_BPS, "minProtocolShareBps"), bps(ruleShareFloorBps, MAX_RULE_SHARE_FLOOR_BPS, "ruleShareFloorBps"));
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* The rate a pool naming `integrator` freezes, from the treasury's list as read (`RulesContext.integrators`): zero for
|
|
618
|
+
* no integrator; refused when the integrator is not listed at 1 to 5,000. Undefined when the list was not given.
|
|
619
|
+
*/
|
|
620
|
+
export function listedIntegratorBps(integrator, integrators) {
|
|
621
|
+
const account = address(integrator, true);
|
|
622
|
+
if (account === ZERO)
|
|
623
|
+
return 0;
|
|
624
|
+
if (integrators === undefined)
|
|
625
|
+
return undefined;
|
|
626
|
+
const rate = integrators[account] ?? 0;
|
|
627
|
+
assert(Number.isSafeInteger(rate) && rate >= 1 && rate <= MAX_INTEGRATOR_BPS, `Integrator ${account} is not on the treasury's integrator list (HookrRules UnknownIntegrator)`);
|
|
628
|
+
return rate;
|
|
629
|
+
}
|
|
630
|
+
/**
|
|
631
|
+
* Splits Hookr's share of one swap's rule fees (the Snipe, dynamic fee, LP Rewards and Auto Burn, royalty already
|
|
632
|
+
* out) on a pool with an integrator frozen at `integratorBps` (HookrRules.settleSwap): the integrator gets
|
|
633
|
+
* share x rate / 10,000 rounded down as a Rules claim (`IntegratorPaid`), the protocol recipient the rest, the odd
|
|
634
|
+
* unit included (`FeesAllocated.protocol`). A pool without an integrator (rate 0) pays the protocol all of it.
|
|
635
|
+
*/
|
|
636
|
+
export function splitProtocolShare(share, integratorBps) {
|
|
637
|
+
uint(share);
|
|
638
|
+
const cut = (share * BigInt(bps(integratorBps, MAX_INTEGRATOR_BPS, "integratorBps"))) / BPS;
|
|
639
|
+
return { protocol: share - cut, integrator: cut };
|
|
640
|
+
}
|
|
641
|
+
/**
|
|
642
|
+
* A swap's HookrRules quote (HookrRules._quote, r25) on a pool bound with `c` at `baseLpFeePips` that froze the Hookr
|
|
643
|
+
* minimum `minFeePips`. `dynamicFeePips` is the dynamic fee before clipping (zero without one) and `snipePips` the
|
|
644
|
+
* Anti-Snipe tax (`snipeTaxAt`, only on a buy while the guard lasts). The dynamic fee is clipped to the room base and
|
|
645
|
+
* LP Rewards leave under 600,000 pips, the Snipe to what remains. Hookr's rule-fee share is its part of LP Rewards and
|
|
646
|
+
* Auto Burn (buys) and of the dynamic fee and the Snipe; the royalty is the creator's. The minimum is a top-up to the
|
|
647
|
+
* quote take only: `quoteTakePips` = royalty + max(ruleSharePips, minFeePips) and the LP fee surcharge is unchanged, so
|
|
648
|
+
* a pool without rules takes exactly the minimum and a pool whose rules give Hookr at least the minimum pays nothing
|
|
649
|
+
* more. Sells pay no LP Rewards, Auto Burn, royalty or Snipe, so a sell's share is its dynamic fee share alone. Since
|
|
650
|
+
* r31 only a pool whose rules earn Hookr nothing binds a minimum (`bindMinFeePips`), so there the top-up is the whole
|
|
651
|
+
* minimum, and no arb recapture pool binds one.
|
|
652
|
+
*/
|
|
653
|
+
export function rulesQuote(c, s) {
|
|
654
|
+
const base = int(s.baseLpFeePips, 24, "base fee");
|
|
655
|
+
const minimum = BigInt(minFee(s.minFeePips ?? 0));
|
|
656
|
+
let lpReward = 0n;
|
|
657
|
+
let take = 0n;
|
|
658
|
+
let burnBps = 0n;
|
|
659
|
+
let royalty = 0n;
|
|
660
|
+
if (s.isBuy)
|
|
661
|
+
({ lpReward, take, burnBps, royaltyPips: royalty } = buyParts(c));
|
|
662
|
+
assert(base + lpReward <= MAX_LP, "Base fee plus LP Rewards above 600000 pips");
|
|
663
|
+
const room = MAX_LP - base - lpReward;
|
|
664
|
+
let dynamicFee = BigInt(int(s.dynamicFeePips ?? 0, 24, "dynamic fee"));
|
|
665
|
+
let snipe = s.isBuy ? BigInt(int(s.snipePips ?? 0, 24, "Snipe")) : 0n;
|
|
666
|
+
if (dynamicFee > room)
|
|
667
|
+
dynamicFee = room;
|
|
668
|
+
if (snipe > room - dynamicFee)
|
|
669
|
+
snipe = room - dynamicFee;
|
|
670
|
+
const share = BigInt(c.protocolShareBps);
|
|
671
|
+
const protocolPips = (dynamicFee * share) / BPS + (snipe * share) / BPS;
|
|
672
|
+
take += protocolPips;
|
|
673
|
+
const ruleShare = take - royalty;
|
|
674
|
+
const topUp = ruleShare < minimum ? minimum - ruleShare : 0n;
|
|
675
|
+
return {
|
|
676
|
+
lpFeeSurchargePips: Number(lpReward + dynamicFee + snipe - protocolPips),
|
|
677
|
+
quoteTakePips: Number(take + topUp),
|
|
678
|
+
subjectBurnBps: Number(burnBps),
|
|
679
|
+
royaltyPips: Number(royalty),
|
|
680
|
+
ruleSharePips: Number(ruleShare),
|
|
681
|
+
topUpPips: Number(topUp),
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
/**
|
|
685
|
+
* The quote HookrRules.simulationQuote gives the root for its simulated swap on a dynamic fee pool (r26): the swap's
|
|
686
|
+
* quote (`rulesQuote`) without a dynamic fee, and on an exact-output sell a quote take of the larger of the largest
|
|
687
|
+
* dynamic fee share the sell can pay (the span, clipped to 600,000 pips less the base fee, times protocolShareBps) and
|
|
688
|
+
* that quote's take, which on a sell is the pool's Hookr minimum (zero without one). The real take of such a sell is
|
|
689
|
+
* max(its dynamic fee share, the minimum), so the simulation reaches at least as far as the real swap and no further
|
|
690
|
+
* than that bound; before r26 it reserved their sum and simulated a longer move. Every other shape is quoted without
|
|
691
|
+
* the dynamic fee.
|
|
692
|
+
*/
|
|
693
|
+
export function simulationQuote(c, s) {
|
|
694
|
+
const q = rulesQuote(c, {
|
|
695
|
+
baseLpFeePips: s.baseLpFeePips,
|
|
696
|
+
isBuy: s.isBuy,
|
|
697
|
+
snipePips: s.snipePips,
|
|
698
|
+
minFeePips: s.minFeePips,
|
|
699
|
+
});
|
|
700
|
+
if (s.isBuy || s.exactInput)
|
|
701
|
+
return q;
|
|
702
|
+
const base = BigInt(s.baseLpFeePips);
|
|
703
|
+
const span = BigInt(c.maxFeePips) - base;
|
|
704
|
+
const room = MAX_LP - base;
|
|
705
|
+
const largest = ((span > room ? room : span) * BigInt(c.protocolShareBps)) / BPS;
|
|
706
|
+
if (largest <= BigInt(q.quoteTakePips))
|
|
707
|
+
return q;
|
|
708
|
+
return { ...q, quoteTakePips: Number(largest) };
|
|
709
|
+
}
|
|
710
|
+
/**
|
|
711
|
+
* What one swap without an advisory pays the Rules, in the pool's quote (HookrRoot's settlement, then
|
|
712
|
+
* HookrRules.settleSwap): `rulesFee`, the royalty inside it (buys: quoteBasis x royalty pips / 1e6, at most the fee),
|
|
713
|
+
* Hookr's part (`hookr`, the rest: its rule-fee share with the minimum's top-up) and Hookr's part split with the
|
|
714
|
+
* pool's integrator (`splitProtocolShare`: the integrator's frozen rate rounded down, the protocol recipient the rest).
|
|
715
|
+
* `actualQuote` is the quote the pool swap took in (buys) or paid out (sells), `requested` the swap's specified
|
|
716
|
+
* amount. Buys are priced on the gross buyer spend, quoteBasis = actualQuote x 1e6 / (1e6 - take) (at most the
|
|
717
|
+
* specified input of an exact-input buy); an exact-input sell on the quote out; an exact-output sell reserves
|
|
718
|
+
* ceil(requested x take / (1e6 - take)) on top of the requested output, all of it the Rules' fee without an advisory.
|
|
719
|
+
*/
|
|
720
|
+
export function rulesSwapFees(s) {
|
|
721
|
+
const rate = BigInt(int(s.quoteTakePips, 24, "quote take"));
|
|
722
|
+
assert(rate < PIPS, "Quote take at or above 1000000 pips");
|
|
723
|
+
const actual = uint(s.actualQuote);
|
|
724
|
+
const requested = uint(s.requested);
|
|
725
|
+
let quoteBasis;
|
|
726
|
+
let rulesFee;
|
|
727
|
+
let royalty = 0n;
|
|
728
|
+
if (s.shape === "exactInputBuy" || s.shape === "exactOutputBuy") {
|
|
729
|
+
quoteBasis = (actual * PIPS) / (PIPS - rate);
|
|
730
|
+
if (s.shape === "exactInputBuy" && quoteBasis > requested)
|
|
731
|
+
quoteBasis = requested;
|
|
732
|
+
rulesFee = (quoteBasis * rate) / PIPS;
|
|
733
|
+
royalty = (quoteBasis * BigInt(int(s.royaltyPips, 24, "royalty"))) / PIPS;
|
|
734
|
+
if (royalty > rulesFee)
|
|
735
|
+
royalty = rulesFee;
|
|
736
|
+
}
|
|
737
|
+
else if (s.shape === "exactInputSell") {
|
|
738
|
+
quoteBasis = actual;
|
|
739
|
+
rulesFee = (quoteBasis * rate) / PIPS;
|
|
740
|
+
}
|
|
741
|
+
else {
|
|
742
|
+
quoteBasis = actual;
|
|
743
|
+
rulesFee = rate === 0n ? 0n : (requested * rate + (PIPS - rate) - 1n) / (PIPS - rate);
|
|
744
|
+
}
|
|
745
|
+
const hookr = rulesFee - royalty;
|
|
746
|
+
const split = splitProtocolShare(hookr, s.integratorBps ?? 0);
|
|
747
|
+
return { quoteBasis, rulesFee, royalty, hookr, ...split };
|
|
748
|
+
}
|
|
749
|
+
/**
|
|
750
|
+
* The profit one currency's push stands for (HookrLane._run, r28): the root reads it back from the push at the pool's
|
|
751
|
+
* frozen partner share, pushed x 10,000 / (10,000 - partnerBps) rounded down, and emits it as CorrectionSucceeded's
|
|
752
|
+
* `reportedProfit`. What the executor's call returns is not read. An executor that keeps floor(P x partnerBps /
|
|
753
|
+
* 10,000) of a profit P and pushes the rest is read back as P or P + 1.
|
|
754
|
+
*/
|
|
755
|
+
export function recaptureProfit(pushed, partnerBps) {
|
|
756
|
+
return (uint(pushed) * BPS) / (BPS - BigInt(bps(partnerBps, MAX_LANE_PARTNER_BPS, "partnerBps")));
|
|
757
|
+
}
|
|
758
|
+
/**
|
|
759
|
+
* One arb recapture's split of `amount` pushed in `currency` (HookrRecapture.settleRecapture): the protocol takes
|
|
760
|
+
* RECAPTURE_PROTOCOL_BPS (25% since r31) of the profit the push stands for (`reportedProfit`: the root's read-back,
|
|
761
|
+
* recaptureProfit(amount, partnerBps)), at most the push, on every pool alike and never split with an integrator; on
|
|
762
|
+
* a push in the pool's quote (`inQuote`) the King of the Pool pot takes potBps of the rest and the trader named on the
|
|
763
|
+
* after-phase traderBps of the rest's part its own move accounts for (min(amount, traderBasis) x rest / amount); the
|
|
764
|
+
* LPs take what is left, rounding dust included. A push in any other currency pays no trader or pot; a frame that
|
|
765
|
+
* pushed in several currencies splits each on its own. With WTH keeping 2,500 bps and the default traderBps of 2,500,
|
|
766
|
+
* a profit P splits 25% to the partner, 25% to the protocol, 12.5% to the trader and 37.5% to the LPs.
|
|
767
|
+
*/
|
|
768
|
+
export function recaptureSplit(p) {
|
|
769
|
+
assert(uint(p.amount) > 0n, "An arb recapture settles a nonzero push (HookrRecapture InvalidSettlement)");
|
|
770
|
+
const cut = (uint(p.reportedProfit) * BigInt(RECAPTURE_PROTOCOL_BPS)) / BPS;
|
|
771
|
+
const protocol = cut < p.amount ? cut : p.amount;
|
|
772
|
+
const rest = p.amount - protocol;
|
|
773
|
+
let trader = 0n;
|
|
774
|
+
let pot = 0n;
|
|
775
|
+
if (p.inQuote) {
|
|
776
|
+
if (address(p.trader, true) !== ZERO && uint(p.traderBasis) !== 0n) {
|
|
777
|
+
const part = p.traderBasis < p.amount ? p.traderBasis : p.amount;
|
|
778
|
+
trader = (((part * rest) / p.amount) * BigInt(bps(p.traderBps, 5_000, "traderBps"))) / BPS;
|
|
779
|
+
}
|
|
780
|
+
pot = (rest * BigInt(bps(p.potBps, 5_000, "potBps"))) / BPS;
|
|
781
|
+
}
|
|
782
|
+
return { protocol, trader, pot, lp: rest - trader - pot };
|
|
783
|
+
}
|
|
784
|
+
/**
|
|
785
|
+
* The largest King of the Pool maxPrizeBps a recapture pool binds with (HookrRecapture._validate): half of what the
|
|
786
|
+
* protocol keeps of LP Rewards and Auto Burn in basis points of a buy, each rounded down, where the kept share is
|
|
787
|
+
* protocolShareBps x (10,000 - integratorBps) / 10,000 rounded down. It is HookrRecapture.prizeBoundFor(lpBps, burnBps,
|
|
788
|
+
* protocolShareBps, integratorBps) (r26), read on chain with `encodePrizeBoundForCall`; for a pool naming an integrator,
|
|
789
|
+
* take the rate from `integration(id)` or `feeTerms`, never `prizeBound` on the pool's whole share.
|
|
790
|
+
*/
|
|
791
|
+
export function kingOfTheHillPrizeBound(lpBps, burnBps, protocolShareBps, integratorBps = 0) {
|
|
792
|
+
const kept = (BigInt(bps(protocolShareBps, MAX_PROTOCOL_SHARE_BPS, "protocolShareBps")) *
|
|
793
|
+
(BPS - BigInt(bps(integratorBps, MAX_INTEGRATOR_BPS, "integratorBps")))) /
|
|
794
|
+
BPS;
|
|
795
|
+
return (((BigInt(lpBps) * kept) / BPS + (BigInt(burnBps) * kept) / BPS) * BigInt(PRIZE_SHARE_OF_FEE_BPS)) / BPS;
|
|
796
|
+
}
|
|
797
|
+
/**
|
|
798
|
+
* Refuses a RulesConfig that HookrRules.bind would refuse, and returns its fee ceilings (`rulesFeeCeilings`).
|
|
799
|
+
* Mirrors HookrRules._validate, including the effective protocol share floor (`effectiveShareFloor`), the integrator
|
|
800
|
+
* list when the context carries it (`listedIntegratorBps`), the Anti-Snipe decay bounds and, on a pool that freezes the
|
|
801
|
+
* Hookr minimum (`RulesContext.minFeePips`), caps that admit royalty + minimum. A dynamic fee pool binds at any tick
|
|
802
|
+
* spacing. An integrator changes no fee ceiling: it is paid from Hookr's share after the take.
|
|
803
|
+
*/
|
|
804
|
+
export function checkRulesConfig(c, ctx) {
|
|
805
|
+
int(c.guardEndBlock, 40, "guardEndBlock");
|
|
806
|
+
for (const [k, bits] of [
|
|
807
|
+
["maxFeePips", 24],
|
|
808
|
+
["snipeTaxPips", 24],
|
|
809
|
+
["snipeFloorPips", 24],
|
|
810
|
+
["snipeDecaySeconds", 16],
|
|
811
|
+
["dynamicFeeSens", 16],
|
|
812
|
+
["burnBps", 16],
|
|
813
|
+
["lpBps", 16],
|
|
814
|
+
["potBps", 16],
|
|
815
|
+
["royaltyBps", 16],
|
|
816
|
+
["protocolShareBps", 16],
|
|
817
|
+
["potEveryNBuys", 32],
|
|
818
|
+
])
|
|
819
|
+
int(c[k], bits, k);
|
|
820
|
+
uint(c.maxBuyQuoteAmount, 128);
|
|
821
|
+
uint(c.potMinBuyQuote, 128);
|
|
822
|
+
const royaltyTo = address(c.royaltyTo, true);
|
|
823
|
+
listedIntegratorBps(c.integrator, ctx.integrators);
|
|
824
|
+
const integrator = address(c.integrator, true);
|
|
825
|
+
assert(integrator === ZERO || !(ctx.refusedIntegrators ?? []).some((a) => address(a, true) === integrator), `Integrator ${integrator} is an account of the Rules' own release that never claims (HookrRules UnknownIntegrator)`);
|
|
826
|
+
assert(c.potBps === 0 && c.potEveryNBuys === 0 && c.potMinBuyQuote === 0n, "Pot unavailable in this release");
|
|
827
|
+
assert(c.protocolShareBps >= effectiveShareFloor(ctx.minProtocolShareBps, ctx.ruleShareFloorBps), "Protocol share below the Rules minimum (the larger of minProtocolShareBps and the treasury's governed floor)");
|
|
828
|
+
assert(c.protocolShareBps <= MAX_PROTOCOL_SHARE_BPS, "Protocol share above 5000 bps");
|
|
829
|
+
assert(c.dynamicFeeSens <= 10, "Dynamic fee sensitivity above 10");
|
|
830
|
+
assert((c.dynamicFeeSens === 0) === (c.maxFeePips === ctx.baseLpFeePips), "Dynamic fees must be on exactly when maxFeePips is above base");
|
|
831
|
+
// r31: a decay may run past MAX_SNIPE_DECAY_SECONDS up to the guard's own length at bind (maxSnipeDecaySeconds).
|
|
832
|
+
assert(c.snipeDecaySeconds <= maxSnipeDecaySeconds(c.guardEndBlock, ctx.blockNumber), "Anti-Snipe decay above the longest the Rules accept: an hour, or the guard's length when it is longer");
|
|
833
|
+
// r31 (N09): a dynamic fee binds only within HookrDynamicFee's reach bound.
|
|
834
|
+
assert(withinDynamicFeeReach(c.maxFeePips - ctx.baseLpFeePips, c.dynamicFeeSens, c.protocolShareBps), "Dynamic fee past the reach bound: lower maxFeePips, the sensitivity or the protocol share");
|
|
835
|
+
assert(c.snipeDecaySeconds === 0 ? c.snipeFloorPips === 0 : c.snipeFloorPips < c.snipeTaxPips, "Anti-Snipe floor must be below the launch tax with a decay, and zero without one");
|
|
836
|
+
assert(ctx.baseLpFeePips + c.snipeTaxPips <= MAX_LP_FEE_PIPS, "Base fee plus Snipe above 600000 pips");
|
|
837
|
+
assert(c.burnBps + c.lpBps <= 1_000, "Burn plus LP Rewards above 1000 bps");
|
|
838
|
+
assert(c.royaltyBps <= 1_000, "Royalty above 1000 bps");
|
|
839
|
+
assert((c.royaltyBps === 0) === (royaltyTo === ZERO), "Royalty needs exactly one of rate and recipient");
|
|
840
|
+
assert(royaltyTo === ZERO || !(ctx.refusedRoyaltyRecipients ?? []).some((a) => address(a, true) === royaltyTo), `Royalty recipient ${royaltyTo} is an account of the Rules' own release that never claims (HookrRules InvalidConfig)`);
|
|
841
|
+
assert(c.royaltyBps === 0 || c.lpBps !== 0, "Royalty needs LP Rewards");
|
|
842
|
+
if (c.guardEndBlock === 0) {
|
|
843
|
+
assert(c.snipeTaxPips === 0 && c.maxBuyQuoteAmount === 0n && c.snipeDecaySeconds === 0, "Snipe, its decay and the buy limit need a launch guard");
|
|
844
|
+
}
|
|
845
|
+
else {
|
|
846
|
+
assert(ctx.liquidityOwner !== undefined && address(ctx.liquidityOwner) !== ZERO, "Launch guard needs a liquidity owner");
|
|
847
|
+
if (ctx.blockNumber !== undefined)
|
|
848
|
+
assert(BigInt(c.guardEndBlock) > ctx.blockNumber &&
|
|
849
|
+
BigInt(c.guardEndBlock) <= ctx.blockNumber + BigInt(MAX_GUARD_BLOCKS), "Launch guard outside the next 100000 blocks");
|
|
850
|
+
}
|
|
851
|
+
const ceilings = rulesFeeCeilings(c, ctx.baseLpFeePips, ctx.minFeePips ?? 0);
|
|
852
|
+
if (ctx.caps)
|
|
853
|
+
assert(ceilings.maxLpFeePips <= ctx.caps.maxLpFeePips &&
|
|
854
|
+
ceilings.maxQuoteTakePips <= ctx.caps.maxQuoteTakePips &&
|
|
855
|
+
ceilings.maxSubjectTakeBps <= ctx.caps.maxSubjectTakeBps, "Rules fee ceilings exceed the caps");
|
|
856
|
+
return ceilings;
|
|
857
|
+
}
|
|
858
|
+
function checkCaps(caps) {
|
|
859
|
+
int(caps.maxLpFeePips, 24, "maxLpFeePips");
|
|
860
|
+
int(caps.maxQuoteTakePips, 24, "maxQuoteTakePips");
|
|
861
|
+
int(caps.maxSubjectTakeBps, 16, "maxSubjectTakeBps");
|
|
862
|
+
assert(caps.maxLpFeePips <= MAX_LP_FEE_PIPS, "LP cap above 600000 pips");
|
|
863
|
+
assert(BigInt(caps.maxQuoteTakePips) < PIPS, "Quote take cap at or above 1000000 pips");
|
|
864
|
+
assert(caps.maxSubjectTakeBps <= 1_000, "Subject cap above 1000 bps");
|
|
865
|
+
}
|
|
866
|
+
/**
|
|
867
|
+
* The pool caps (PoolConfig.caps) of a pool whose Rules needs `rules` and whose advisory is admitted with
|
|
868
|
+
* `advisory` caps: the Rules ceilings plus the advisory's admitted maximum, so the Root's reservation leaves the
|
|
869
|
+
* Rules its full ceilings. Pass the result of `rulesFeeCeilings` (or `checkRulesConfig`) as `rules`.
|
|
870
|
+
*/
|
|
871
|
+
export function poolCapsFor(rules, baseLpFeePips, advisory) {
|
|
872
|
+
const caps = {
|
|
873
|
+
maxLpFeePips: rules.maxLpFeePips + (advisory?.maxLpFeePips ?? 0),
|
|
874
|
+
maxQuoteTakePips: rules.maxQuoteTakePips + (advisory?.maxQuoteTakePips ?? 0),
|
|
875
|
+
maxSubjectTakeBps: rules.maxSubjectTakeBps,
|
|
876
|
+
};
|
|
877
|
+
checkCaps(caps);
|
|
878
|
+
assert(baseLpFeePips <= caps.maxLpFeePips, "Base fee above the LP cap");
|
|
879
|
+
return caps;
|
|
880
|
+
}
|
|
881
|
+
/**
|
|
882
|
+
* The caps HookrRoot.initializePool hands the Rules: min(pool, Rules admission) per field, less the advisory's
|
|
883
|
+
* admitted maximum. Refuses what the Root refuses: pool caps out of range, or an advisory maximum above the room.
|
|
884
|
+
*/
|
|
885
|
+
export function rulesBindCaps(pool, rulesAdmission, advisoryAdmission) {
|
|
886
|
+
checkCaps(pool);
|
|
887
|
+
const limits = {
|
|
888
|
+
maxLpFeePips: Math.min(pool.maxLpFeePips, rulesAdmission.maxLpFeePips),
|
|
889
|
+
maxQuoteTakePips: Math.min(pool.maxQuoteTakePips, rulesAdmission.maxQuoteTakePips),
|
|
890
|
+
maxSubjectTakeBps: Math.min(pool.maxSubjectTakeBps, rulesAdmission.maxSubjectTakeBps),
|
|
891
|
+
};
|
|
892
|
+
if (!advisoryAdmission)
|
|
893
|
+
return limits;
|
|
894
|
+
assert(advisoryAdmission.maxLpFeePips <= limits.maxLpFeePips &&
|
|
895
|
+
advisoryAdmission.maxQuoteTakePips <= limits.maxQuoteTakePips, "Advisory admitted maximum above the pool room");
|
|
896
|
+
return {
|
|
897
|
+
maxLpFeePips: limits.maxLpFeePips - advisoryAdmission.maxLpFeePips,
|
|
898
|
+
maxQuoteTakePips: limits.maxQuoteTakePips - advisoryAdmission.maxQuoteTakePips,
|
|
899
|
+
maxSubjectTakeBps: limits.maxSubjectTakeBps,
|
|
900
|
+
};
|
|
901
|
+
}
|
|
902
|
+
const ZERO_ID = `0x${"0".repeat(64)}`;
|
|
903
|
+
/**
|
|
904
|
+
* `feeTerms(integrator, recapture, id)` calldata for the treasury a Rules pays (r25): its governed floor, `integrator`'s
|
|
905
|
+
* listed rate and the Hookr minimum of pool `id` (with arb recapture on when `recapture`; `integrator`'s override only
|
|
906
|
+
* when it approved `id`). It is exactly the read HookrRules makes at bind, with the pool's own id.
|
|
907
|
+
*/
|
|
908
|
+
export function encodeFeeTermsCall(integrator = ZERO, recapture = false, poolId = ZERO_ID) {
|
|
909
|
+
assert(/^0x[0-9a-fA-F]{64}$/.test(poolId), "Invalid pool id");
|
|
910
|
+
return encodeFunctionData({
|
|
911
|
+
abi: treasuryAbi,
|
|
912
|
+
functionName: "feeTerms",
|
|
913
|
+
args: [address(integrator, true), recapture, poolId],
|
|
914
|
+
});
|
|
915
|
+
}
|
|
916
|
+
/**
|
|
917
|
+
* Decodes a `feeTerms(integrator, recapture, id)` result: `ruleShareFloorBps` for `RulesContext.ruleShareFloorBps`,
|
|
918
|
+
* the integrator's rate (zero when it is not listed) for `RulesContext.integrators`, and the minimum the treasury
|
|
919
|
+
* gives the pool (`bindMinFeePips` applies the r31 rule for `RulesContext.minFeePips`). Refuses values no
|
|
920
|
+
* HookrTreasury holds, and a minimum above the 10,000 pips the Rules accept.
|
|
921
|
+
*/
|
|
922
|
+
export function decodeFeeTerms(data) {
|
|
923
|
+
const [floor, rate, minimum] = decodeFunctionResult({ abi: treasuryAbi, functionName: "feeTerms", data });
|
|
924
|
+
return {
|
|
925
|
+
ruleShareFloorBps: bps(Number(floor), MAX_RULE_SHARE_FLOOR_BPS, "ruleShareFloorBps"),
|
|
926
|
+
integratorBps: bps(Number(rate), MAX_INTEGRATOR_BPS, "integratorBps"),
|
|
927
|
+
minFeePips: minFee(Number(minimum)),
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
/** `minimumFee(poolId)` calldata for a HookrRules (served by its fallback): the Hookr minimum the pool froze. */
|
|
931
|
+
export function encodeMinimumFeeCall(poolId) {
|
|
932
|
+
return encodeFunctionData({ abi: recaptureAbi, functionName: "minimumFee", args: [poolId] });
|
|
933
|
+
}
|
|
934
|
+
/** Decodes a `minimumFee(poolId)` result: the pool's frozen minimum in pips, zero for a pool without one. */
|
|
935
|
+
export function decodeMinimumFee(data) {
|
|
936
|
+
return minFee(Number(decodeFunctionResult({ abi: recaptureAbi, functionName: "minimumFee", data })));
|
|
937
|
+
}
|
|
938
|
+
/**
|
|
939
|
+
* `approveMinFeePool(id, approved)` calldata for the treasury, sent by an integrator account itself: it approves (or
|
|
940
|
+
* withdraws) pool `id` binding that account's Hookr minimum override when the pool names it. Only the sender's own
|
|
941
|
+
* approvals change; an approval does nothing while the sender has no override, and it is read once, at bind. How Hookr
|
|
942
|
+
* runs chosen pools at a lower minimum: list an account it controls as an integrator, set that account's override
|
|
943
|
+
* through the treasury timelock, then have the account approve each chosen pool id before its launch (for a new
|
|
944
|
+
* token the id follows from `HookrLauncher.predictToken`, and nobody can initialize it first).
|
|
945
|
+
*/
|
|
946
|
+
export function encodeApproveMinFeePoolCall(poolId, approved) {
|
|
947
|
+
assert(/^0x[0-9a-fA-F]{64}$/.test(poolId), "Invalid pool id");
|
|
948
|
+
return encodeFunctionData({ abi: treasuryAbi, functionName: "approveMinFeePool", args: [poolId, approved] });
|
|
949
|
+
}
|
|
950
|
+
/** `integration(poolId)` calldata: the integrator and rate a bound pool froze. */
|
|
951
|
+
export function encodeIntegrationCall(poolId) {
|
|
952
|
+
return encodeFunctionData({ abi: rulesAbi, functionName: "integration", args: [poolId] });
|
|
953
|
+
}
|
|
954
|
+
/** Decodes an `integration(poolId)` result; zeros for a pool without an integrator. */
|
|
955
|
+
export function decodeIntegration(data) {
|
|
956
|
+
const [integrator, rate] = decodeFunctionResult({ abi: rulesAbi, functionName: "integration", data });
|
|
957
|
+
return {
|
|
958
|
+
integrator: integrator.toLowerCase(),
|
|
959
|
+
integratorBps: bps(Number(rate), MAX_INTEGRATOR_BPS, "integratorBps"),
|
|
960
|
+
};
|
|
961
|
+
}
|
|
962
|
+
/** `minProtocolShareBps()` calldata: the floor a HookrRules applies to every pool's protocol share. */
|
|
963
|
+
export function encodeMinProtocolShareCall() {
|
|
964
|
+
return encodeFunctionData({ abi: rulesAbi, functionName: "minProtocolShareBps" });
|
|
965
|
+
}
|
|
966
|
+
/**
|
|
967
|
+
* Decodes a `minProtocolShareBps()` result, the `ParamContext.minProtocolShareBps` of pools that bind to that Rules.
|
|
968
|
+
* Refuses a value no HookrRules can hold (its constructor caps it at 5,000 bps).
|
|
969
|
+
*/
|
|
970
|
+
export function decodeMinProtocolShareBps(data) {
|
|
971
|
+
const v = Number(decodeFunctionResult({ abi: rulesAbi, functionName: "minProtocolShareBps", data }));
|
|
972
|
+
assert(v <= MAX_PROTOCOL_SHARE_BPS, `minProtocolShareBps ${v} above ${MAX_PROTOCOL_SHARE_BPS}`);
|
|
973
|
+
return v;
|
|
974
|
+
}
|
|
975
|
+
/**
|
|
976
|
+
* `protocolShareFloor()` calldata for a HookrRules (r26, served by its HookrRecapture module through the fallback): the
|
|
977
|
+
* effective floor a pool binding now must meet, the larger of the Rules' minProtocolShareBps and the governed floor of
|
|
978
|
+
* the treasury they pay (zero governed floor for a recipient without code). One read in place of
|
|
979
|
+
* `encodeMinProtocolShareCall` plus `encodeFeeTermsCall`; it moves when the treasury's floor changes.
|
|
980
|
+
*/
|
|
981
|
+
export function encodeProtocolShareFloorCall() {
|
|
982
|
+
return encodeFunctionData({ abi: recaptureAbi, functionName: "protocolShareFloor" });
|
|
983
|
+
}
|
|
984
|
+
/**
|
|
985
|
+
* Decodes a `protocolShareFloor()` result: the least protocolShareBps a pool binds with now, for
|
|
986
|
+
* `ParamContext.minProtocolShareBps` (with `ruleShareFloorBps` left at zero, since it already includes the governed
|
|
987
|
+
* floor). Refuses a value above 5,000 bps, which neither floor can hold.
|
|
988
|
+
*/
|
|
989
|
+
export function decodeProtocolShareFloor(data) {
|
|
990
|
+
const v = Number(decodeFunctionResult({ abi: recaptureAbi, functionName: "protocolShareFloor", data }));
|
|
991
|
+
assert(v <= MAX_PROTOCOL_SHARE_BPS, `protocolShareFloor ${v} above ${MAX_PROTOCOL_SHARE_BPS}`);
|
|
992
|
+
return v;
|
|
993
|
+
}
|
|
994
|
+
/**
|
|
995
|
+
* `prizeBoundFor(lpBps, burnBps, protocolShareBps, integratorBps)` calldata for a HookrRules (r26, served by its
|
|
996
|
+
* HookrRecapture module): the widest King of the Pool maxPrizeBps the bind admits for a pool naming an integrator at
|
|
997
|
+
* that rate (0 for none). `kingOfTheHillPrizeBound` computes the same offline.
|
|
998
|
+
*/
|
|
999
|
+
export function encodePrizeBoundForCall(lpBps, burnBps, protocolShareBps, integratorBps = 0) {
|
|
1000
|
+
return encodeFunctionData({
|
|
1001
|
+
abi: recaptureAbi,
|
|
1002
|
+
functionName: "prizeBoundFor",
|
|
1003
|
+
args: [
|
|
1004
|
+
bps(lpBps, 65_535, "lpBps"),
|
|
1005
|
+
bps(burnBps, 65_535, "burnBps"),
|
|
1006
|
+
bps(protocolShareBps, 65_535, "protocolShareBps"),
|
|
1007
|
+
bps(integratorBps, MAX_INTEGRATOR_BPS, "integratorBps"),
|
|
1008
|
+
],
|
|
1009
|
+
});
|
|
1010
|
+
}
|
|
1011
|
+
/** Decodes a `prizeBoundFor(...)` result: the widest maxPrizeBps, in basis points of a buy. */
|
|
1012
|
+
export function decodePrizeBoundFor(data) {
|
|
1013
|
+
return decodeFunctionResult({ abi: recaptureAbi, functionName: "prizeBoundFor", data });
|
|
1014
|
+
}
|
|
1015
|
+
/** `configSchemaHash()` calldata for a HookrRules: its schema id, which must equal RULES_CONFIG_SCHEMA_HASH. */
|
|
1016
|
+
export function encodeConfigSchemaHashCall() {
|
|
1017
|
+
return encodeFunctionData({ abi: rulesAbi, functionName: "configSchemaHash" });
|
|
1018
|
+
}
|
|
1019
|
+
/**
|
|
1020
|
+
* Decodes a `configSchemaHash()` result and refuses any id but RULES_CONFIG_SCHEMA_HASH: a Rules of another layout
|
|
1021
|
+
* (the retired keccak256("hookr.rules.config") among them) is not the Rules this SDK encodes for.
|
|
1022
|
+
*/
|
|
1023
|
+
export function decodeConfigSchemaHash(data) {
|
|
1024
|
+
const id = decodeFunctionResult({ abi: rulesAbi, functionName: "configSchemaHash", data });
|
|
1025
|
+
assert(id.toLowerCase() === RULES_CONFIG_SCHEMA_HASH.toLowerCase(), `Rules schema id ${id} is not RULES_CONFIG_SCHEMA_HASH (the keccak256 of ${RULES_CONFIG_TYPE})` +
|
|
1026
|
+
(id.toLowerCase() === RETIRED_RULES_CONFIG_SCHEMA_HASH.toLowerCase()
|
|
1027
|
+
? "; it is the retired 15-word layout's"
|
|
1028
|
+
: ""));
|
|
1029
|
+
return id;
|
|
1030
|
+
}
|
|
1031
|
+
/**
|
|
1032
|
+
* The accounts HookrRules refuses as a pool's integrator whatever its treasury answers (r26, HookrRules._feeTerms),
|
|
1033
|
+
* lowercase, for `RulesContext.refusedIntegrators` / `ParamContext.refusedIntegrators`: the Rules, their trusted root,
|
|
1034
|
+
* their protocol recipient (the treasury), the PoolManager and their HookrRecapture module (`recaptureModule()`).
|
|
1035
|
+
* None of them ever claims what the Rules would credit it. The treasury also refuses, at proposal and acceptance, the
|
|
1036
|
+
* zero address, itself, the PoolManager and every admitted source.
|
|
1037
|
+
*/
|
|
1038
|
+
export function bindRefusedIntegrators(a) {
|
|
1039
|
+
return [a.rules, a.trustedRoot, a.protocolRecipient, a.poolManager, a.recaptureModule].map((x) => address(x));
|
|
1040
|
+
}
|
|
1041
|
+
/**
|
|
1042
|
+
* The accounts HookrRules refuses as a pool's royalty recipient (r31, HookrRules._validate), lowercase, for
|
|
1043
|
+
* `RulesContext.refusedRoyaltyRecipients` / `ParamContext.refusedRoyaltyRecipients`: the Rules, their trusted root, the
|
|
1044
|
+
* PoolManager and their HookrRecapture module (`recaptureModule()`), the integrator refusals less the protocol
|
|
1045
|
+
* recipient. None of them ever claims what settleSwap credits it.
|
|
1046
|
+
*/
|
|
1047
|
+
export function bindRefusedRoyaltyRecipients(a) {
|
|
1048
|
+
return [a.rules, a.trustedRoot, a.poolManager, a.recaptureModule].map((x) => address(x));
|
|
1049
|
+
}
|