@pickhooks/mode-kit 0.0.0-stage → 0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 pickhooks
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,125 @@
1
- # Temporary Holding Version
1
+ # @pickhooks/mode-kit
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Write a game for pickhooks coins. Earn on every coin that plays it.
4
+
5
+ A **mode** is the game inside a pickhooks coin on BNB Chain: combo, pay it forward, heist, revenge and the ones you
6
+ write. The coin calls its mode after every buy, sell and send, holders play it through the coin, and the coin carries
7
+ out what the mode answers under rules no mode can break. You write one Solidity contract, test it on a BNB Chain fork
8
+ against the live contracts, and send it in. pickhooks reviews it, deploys it and lists it in the launcher.
9
+
10
+ **What you earn: half of the pickhooks fee of every coin launched with your mode, in BNB, for as long as it trades.**
11
+ It comes out of the pickhooks side. The creator's share is not touched.
12
+
13
+ ```bash
14
+ npx @pickhooks/mode-kit init my-mode --contract MyMode --title "my mode"
15
+ cd my-mode
16
+ echo "BSC_RPC_URL=https://..." > .env # any BNB Chain RPC
17
+ npx @pickhooks/mode-kit test # your mode on the live factory, the live coin code, PancakeSwap's routers
18
+ npx @pickhooks/mode-kit pack # check, test, one file for the review
19
+ ```
20
+
21
+ You need Node 20+ and [Foundry](https://getfoundry.sh).
22
+
23
+ ## Why build here
24
+
25
+ - **A mode cannot break a coin.** The coin checks every order a mode gives. If a mode reverts, runs out of gas or
26
+ answers garbage, the coin skips it for that trade and the trade goes through. Sells and sends are never taxed.
27
+ - **Every coin trades everywhere from the first second.** GMGN, Binance Wallet, PancakeSwap, any router. Your game
28
+ reaches every trader, not only the ones on one site.
29
+ - **No fee to submit, no deploy cost.** pickhooks deploys your mode, verified on BscScan. Nobody holds a key to it:
30
+ a mode is fixed, like the coins that use it.
31
+ - **Tested against what is live.** The harness forks BNB Chain and lists your mode on the real factory, so your tests
32
+ run the exact coin code and pools your players will trade.
33
+ - **Do not write Solidity?** Tell us the mechanic and we build it with this kit. You are still its author and earn the
34
+ same share.
35
+
36
+ ## The project
37
+
38
+ ```
39
+ src/MyMode.sol the mode (it starts as "first of the hour", a whole mode in one file)
40
+ test/MyMode.t.sol its tests on a fork
41
+ mode.json what it is, its settings and actions, in the launcher's words
42
+ lib/pickhooks/ the kit: IStickyMode.sol, ModeBase.sol, harness/ (do not edit)
43
+ ```
44
+
45
+ ## Commands
46
+
47
+ Run them as `npx @pickhooks/mode-kit <command>`, or install the kit once (`npm i -g @pickhooks/mode-kit`) and run `pickhooks-mode <command>`.
48
+
49
+ | command | what it does |
50
+ | --- | --- |
51
+ | `pickhooks-mode init <dir>` | a Foundry project with a working mode, its tests and its mode.json |
52
+ | `pickhooks-mode check` | mode.json, the source and the built bytecode against the rules below |
53
+ | `pickhooks-mode test` | `forge test` on a fork (BSC_RPC_URL in .env) |
54
+ | `pickhooks-mode pack` | check, test, and `submission/<name>-<hash>.json` for the review |
55
+ | `pickhooks-mode encode settings bps=250` | the settings as the factory takes them |
56
+ | `pickhooks-mode encode action try coins=1000` | a holder's play |
57
+ | `pickhooks-mode brief` | a brief for a coding agent: paste it into Claude Code, Cursor or Codex with your idea |
58
+ | `pickhooks-mode addresses` | the live contracts |
59
+
60
+ ## How a mode works
61
+
62
+ Extend `ModeBase` and implement `name()`, `quote()` (the bounds of your settings and the share of every buy that goes
63
+ into the coin's game pot), `_register()`, `_trade()` (after every buy, sell and send) and, if players act, `_play()`
64
+ and `_poke()`. A mode never moves a coin: it answers with **orders** and the coin carries them out.
65
+
66
+ | order | what the coin does |
67
+ | --- | --- |
68
+ | `pay(to, amount)` | pays out of the game pot, at most the pot |
69
+ | `payHeld(to, amount, until)` | the same, into custody |
70
+ | `hold(who, to, amount, until, flags)` | a player's own free coins into custody, only in that player's own play |
71
+ | `release(who)` | custody back to the wallet |
72
+ | `burnHeld(who, amount)` | burns coins in custody |
73
+ | `moveHeld(who, to, amount, until)` | moves custody to another holder |
74
+ | `burnPot(amount)` | burns out of the pot |
75
+ | `burn(who, amount)` | burns a player's own free coins, only in that player's own play |
76
+ | `setUntil(who, until)` | moves the end of a custody |
77
+
78
+ At most 16 orders per answer. Custody ends by itself at most 7 days after it began. Randomness comes from drand
79
+ through the beacon on chain (`pickhooks-mode addresses`), never from block values.
80
+
81
+ ## What the coin holds whatever a mode does
82
+
83
+ - The mode gets 250,000 gas per trade; the harness wants every trade under 200,000.
84
+ - A failing mode is skipped for that trade and the trade stands. The harness fails on any skip.
85
+ - Sells and sends are never taxed. A buy gives at most 10 % to burns and pots together, the mode's share included.
86
+ - The pot fills only with the mode's share of buys and leaves only by orders: paid to holders or burned.
87
+ - The mode is fixed at launch, for good.
88
+
89
+ ## The harness
90
+
91
+ `ModeHarness` (lib/pickhooks/harness) forks BNB Chain, lists your mode on the live factory as its owner (on the fork
92
+ only), launches coins and trades them through PancakeSwap's V3 and V2 routers.
93
+
94
+ - `guardrails(settings)`: buys on every path (one by the creator), sends, sells, an hour apart. Sends and sells take
95
+ nothing, the coin never skipped the mode, the pot is backed, under 200,000 gas.
96
+ - `stress(settings, n)`: n trades of five wallets, changing sizes and times, the same checks.
97
+ - `expectLaunchRevert(settings)`: the factory refuses settings outside your bounds.
98
+ - `curveBuy`, `curveSell`, `routerBuy`, `routerSell`, `v2Buy`, `v2Sell`, `send`, `play`, `poke`, `wait`, `modeGas`.
99
+
100
+ ## What the review checks
101
+
102
+ `pickhooks-mode check` enforces the first part: no selfdestruct, delegatecall, payable, low level calls, coin transfers
103
+ or approvals, calls back into the coin, new contracts, owners, proxies or upgrades, no tx.origin, no block values as
104
+ randomness, runtime code under 24,576 bytes, name() equal to the title, notes that say what can be gamed. Then a person
105
+ reads every line against this list:
106
+
107
+ 1. Every setting has bounds; quote() reverts outside them and returns the share of buys.
108
+ 2. `_trade` stays well under 250,000 gas in its worst case; loops are bounded.
109
+ 3. Nothing in `_trade` can revert on a normal buy, sell or send.
110
+ 4. Payouts never go to the creator, a pool, the coin or the dead address, and never more than the trade or the pot
111
+ justifies.
112
+ 5. Custody always has an end the holder can see, never more than 7 days.
113
+ 6. A holder's free coins move only in that holder's own play.
114
+ 7. What can be gamed is said plainly in mode.json notes, with the cap that holds it.
115
+ 8. Randomness comes from drand through the beacon.
116
+ 9. The harness passes with the defaults and the edge of every setting, plus a test for every rule and action.
117
+ 10. Texts are short sentences, titles lower case, no dashes.
118
+
119
+ ## Submit
120
+
121
+ `pickhooks-mode pack` writes one JSON file with your sources, tests, mode.json and the results. Send it on
122
+ [pickhooks.com/modes/build](https://pickhooks.com/modes/build). No fee. When the mode passes, pickhooks deploys it,
123
+ verifies it on BscScan, lists it, and the wallet in `mode.json` starts earning.
124
+
125
+ MIT
@@ -0,0 +1,272 @@
1
+ #!/usr/bin/env node
2
+ // pickhooks-mode: write a mode for pickhooks coins, test it against the live contracts, pack it for review.
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ import crypto from "node:crypto";
6
+ import { spawnSync } from "node:child_process";
7
+ import { checkProject, sourceFiles } from "../lib/check.mjs";
8
+ import { encodeSettings, encodeAction, validate } from "../lib/modejson.mjs";
9
+ import { brief } from "../lib/brief.mjs";
10
+ import { LIVE } from "../lib/live.mjs";
11
+
12
+ const KIT = path.resolve(import.meta.dirname, "..");
13
+ const VERSION = JSON.parse(fs.readFileSync(path.join(KIT, "package.json"), "utf8")).version;
14
+
15
+ const HELP = `pickhooks-mode ${VERSION}: write a game for pickhooks coins (BNB Chain), test it, earn on every coin that uses it.
16
+
17
+ pickhooks-mode init <dir> [--contract MyMode] [--title "my mode"] [--name mymode] [--no-install]
18
+ a Foundry project with a whole working mode, its tests against the live contracts and its mode.json
19
+ pickhooks-mode check [dir] [--json]
20
+ mode.json, the source and the built bytecode against the rules every mode keeps
21
+ pickhooks-mode test [dir] [forge test args]
22
+ forge test on a BNB Chain fork (BSC_RPC_URL in .env): your mode on the live factory, the live coin code and
23
+ PancakeSwap's real routers
24
+ pickhooks-mode pack [dir] [--no-test]
25
+ check, test, and one file for the review: submission/<name>-<hash>.json
26
+ pickhooks-mode encode settings key=value ... the settings as the factory takes them
27
+ pickhooks-mode encode action <name> key=value ... a holder's play
28
+ pickhooks-mode encode poke <name> key=value ... a poke
29
+ pickhooks-mode brief
30
+ the brief for a coding agent: paste it into Claude Code, Cursor or Codex with your idea
31
+ pickhooks-mode addresses
32
+ the live contracts a mode plugs into
33
+
34
+ Submit: send the packed file to pickhooks (https://pickhooks.com/modes/build). No fee. pickhooks reviews it, deploys it
35
+ and lists it; the author wallet in mode.json earns half of the pickhooks fee of every coin launched with the mode.`;
36
+
37
+ function flags(argv) {
38
+ const pos = [];
39
+ const opt = {};
40
+ for (let i = 0; i < argv.length; i++) {
41
+ const a = argv[i];
42
+ if (a === "--") {
43
+ opt._rest = argv.slice(i + 1);
44
+ break;
45
+ }
46
+ if (a.startsWith("--")) {
47
+ const [k, v] = a.slice(2).split("=");
48
+ if (v !== undefined) opt[k] = v;
49
+ else if (argv[i + 1] !== undefined && !argv[i + 1].startsWith("--") && ["contract", "title", "name", "dir"].includes(k)) opt[k] = argv[++i];
50
+ else opt[k] = true;
51
+ } else pos.push(a);
52
+ }
53
+ return { pos, opt };
54
+ }
55
+
56
+ function die(msg, code = 1) {
57
+ console.error(msg);
58
+ process.exit(code);
59
+ }
60
+
61
+ function readEnv(dir) {
62
+ const env = {};
63
+ const f = path.join(dir, ".env");
64
+ if (!fs.existsSync(f)) return env;
65
+ for (const line of fs.readFileSync(f, "utf8").split(/\r?\n/)) {
66
+ const m = /^\s*([A-Z_][A-Z0-9_]*)\s*=\s*(.*?)\s*$/.exec(line);
67
+ if (m && m[2] !== "") env[m[1]] = m[2].replace(/^["']|["']$/g, "");
68
+ }
69
+ return env;
70
+ }
71
+
72
+ function copyDir(from, to, rename = (n) => n) {
73
+ fs.mkdirSync(to, { recursive: true });
74
+ for (const e of fs.readdirSync(from, { withFileTypes: true })) {
75
+ const src = path.join(from, e.name);
76
+ const dst = path.join(to, rename(e.name));
77
+ if (e.isDirectory()) copyDir(src, dst, rename);
78
+ else fs.copyFileSync(src, dst);
79
+ }
80
+ }
81
+
82
+ function forge() {
83
+ return process.env.FORGE || "forge";
84
+ }
85
+
86
+ function hasForge() {
87
+ const r = spawnSync(forge(), ["--version"], { encoding: "utf8", shell: process.platform === "win32" });
88
+ return r.status === 0;
89
+ }
90
+
91
+ // ------------------------------------------------------------------ init
92
+
93
+ function init(pos, opt) {
94
+ const dir = path.resolve(pos[0] ?? "my-mode");
95
+ if (fs.existsSync(dir) && fs.readdirSync(dir).length) die(`${dir} is not empty`);
96
+ const contract = opt.contract ?? "HourMode";
97
+ if (!/^[A-Z][A-Za-z0-9_]*$/.test(contract)) die("--contract must be a Solidity contract name, like MyMode");
98
+ const title = opt.title ?? (contract === "HourMode" ? "first of the hour" : contract.replace(/Mode$/, "").replace(/([a-z])([A-Z])/g, "$1 $2").toLowerCase());
99
+ const name = opt.name ?? (title.replace(/[^a-z0-9]/g, "").replace(/^[0-9]+/, "").slice(0, 24) || "mymode");
100
+
101
+ const swap = (s) => s.replaceAll("HourMode", contract).replaceAll('"first of the hour"', JSON.stringify(title)).replaceAll("first of the hour:", `${title}:`);
102
+ copyDir(path.join(KIT, "template"), dir, (n) => (n === "gitignore" ? ".gitignore" : n === "env.example" ? ".env.example" : n.replace("HourMode", contract)));
103
+ for (const f of [path.join(dir, "src", `${contract}.sol`), path.join(dir, "test", `${contract}.t.sol`)]) fs.writeFileSync(f, swap(fs.readFileSync(f, "utf8")));
104
+ const mj = JSON.parse(fs.readFileSync(path.join(dir, "mode.json"), "utf8"));
105
+ Object.assign(mj, { name, title, contract });
106
+ fs.writeFileSync(path.join(dir, "mode.json"), JSON.stringify(mj, null, 2) + "\n");
107
+ copyDir(path.join(KIT, "contracts"), path.join(dir, "lib", "pickhooks"));
108
+ fs.copyFileSync(path.join(KIT, "schema", "mode.schema.json"), path.join(dir, "mode.schema.json"));
109
+
110
+ console.log(`${dir}\n src/${contract}.sol a whole mode (first of the hour), rename and rewrite it\n test/${contract}.t.sol its tests on a BNB Chain fork against the live factory\n mode.json what it is, in the launcher's words\n lib/pickhooks/ the kit (do not edit)`);
111
+ if (opt["no-install"]) {
112
+ console.log(`\nforge-std: cd ${pos[0] ?? "my-mode"} && forge install foundry-rs/forge-std --no-git`);
113
+ } else if (hasForge()) {
114
+ const r = spawnSync(forge(), ["install", "foundry-rs/forge-std", "--no-git"], { cwd: dir, stdio: "inherit", shell: process.platform === "win32" });
115
+ if (r.status !== 0) console.log(`\nforge-std did not install; run it yourself: forge install foundry-rs/forge-std --no-git`);
116
+ } else {
117
+ console.log("\nFoundry is not installed: https://getfoundry.sh, then: forge install foundry-rs/forge-std --no-git");
118
+ }
119
+ console.log(`\nNext: put a BNB Chain RPC into .env (BSC_RPC_URL=...), then npx @pickhooks/mode-kit test`);
120
+ }
121
+
122
+ // ------------------------------------------------------------------ check
123
+
124
+ function printCheck(r) {
125
+ for (const e of r.errors) console.log(`FAIL ${e}`);
126
+ for (const w of r.warnings) console.log(`WARN ${w}`);
127
+ if (r.facts.runtimeBytes) console.log(`OK ${r.facts.runtimeBytes} bytes of runtime code (limit 24,576)`);
128
+ console.log(r.ok ? `OK ${r.mode?.title ?? "the mode"}: no errors${r.warnings.length ? `, ${r.warnings.length} warnings for the review` : ""}` : `STOP ${r.errors.length} errors`);
129
+ }
130
+
131
+ function check(pos, opt) {
132
+ const dir = path.resolve(pos[0] ?? ".");
133
+ const r = checkProject(dir, { strict: !!opt.strict });
134
+ if (opt.json) console.log(JSON.stringify({ ok: r.ok, errors: r.errors, warnings: r.warnings, facts: r.facts }, null, 2));
135
+ else printCheck(r);
136
+ process.exitCode = r.ok ? 0 : 1;
137
+ return r;
138
+ }
139
+
140
+ // ------------------------------------------------------------------ test
141
+
142
+ function runTests(dir, extra = [], quiet = false) {
143
+ if (!hasForge()) die("Foundry is not installed (https://getfoundry.sh). Set FORGE to its path if it is not on PATH.");
144
+ const env = { ...process.env, ...readEnv(dir) };
145
+ if (!env.FORK_RPC && !env.BSC_RPC_URL) die("Put a BNB Chain RPC into .env: BSC_RPC_URL=https://...");
146
+ env.FOUNDRY_PROFILE ??= "fork";
147
+ const r = spawnSync(forge(), ["test", ...extra], { cwd: dir, env, encoding: "utf8", shell: process.platform === "win32", maxBuffer: 64 << 20 });
148
+ const out = (r.stdout ?? "") + (r.stderr ?? "");
149
+ if (!quiet) process.stdout.write(out);
150
+ const m = /(\d+) tests passed, (\d+) failed, (\d+) skipped/.exec(out);
151
+ return { ok: r.status === 0, passed: m ? Number(m[1]) : null, failed: m ? Number(m[2]) : null, skipped: m ? Number(m[3]) : null, tail: out.split("\n").slice(-40).join("\n") };
152
+ }
153
+
154
+ function test(pos, opt) {
155
+ const dir = path.resolve(pos[0] ?? ".");
156
+ const r = runTests(dir, opt._rest ?? pos.slice(1));
157
+ process.exitCode = r.ok ? 0 : 1;
158
+ }
159
+
160
+ // ------------------------------------------------------------------ pack
161
+
162
+ function pack(pos, opt) {
163
+ const dir = path.resolve(pos[0] ?? ".");
164
+ let t = null;
165
+ if (!opt["no-test"]) {
166
+ console.log("testing on a BNB Chain fork ...");
167
+ t = runTests(dir, [], true);
168
+ console.log(t.ok ? `OK tests: ${t.passed} passed` : `FAIL tests:\n${t.tail}`);
169
+ }
170
+ const r = checkProject(dir, { strict: true });
171
+ printCheck(r);
172
+ if (!r.ok) die("Fix the errors above, then pack again.");
173
+ if (t && !t.ok) die("The tests fail. Fix them, then pack again.");
174
+
175
+ const files = {};
176
+ const add = (f) => (files[path.relative(dir, f).replace(/\\/g, "/")] = fs.readFileSync(f, "utf8"));
177
+ for (const f of sourceFiles(dir)) add(f);
178
+ for (const f of fs.existsSync(path.join(dir, "test")) ? fs.readdirSync(path.join(dir, "test"), { recursive: true }).map((x) => path.join(dir, "test", x)).filter((x) => x.endsWith(".sol")) : []) add(f);
179
+ for (const f of ["mode.json", "foundry.toml", "remappings.txt"]) if (fs.existsSync(path.join(dir, f))) add(path.join(dir, f));
180
+ const kitFiles = {};
181
+ for (const f of ["IStickyMode.sol", "ModeBase.sol"]) {
182
+ const p = path.join(dir, "lib", "pickhooks", f);
183
+ if (fs.existsSync(p)) kitFiles[f] = crypto.createHash("sha256").update(fs.readFileSync(p)).digest("hex");
184
+ }
185
+ const bundle = {
186
+ kind: "pickhooks mode submission",
187
+ kit: VERSION,
188
+ mode: r.mode,
189
+ check: { errors: r.errors, warnings: r.warnings, facts: r.facts },
190
+ test: t ? { ran: true, ok: t.ok, passed: t.passed, failed: t.failed } : { ran: false },
191
+ kitFiles,
192
+ files,
193
+ };
194
+ const body = JSON.stringify(bundle, null, 2);
195
+ const id = crypto.createHash("sha256").update(body).digest("hex").slice(0, 12);
196
+ fs.mkdirSync(path.join(dir, "submission"), { recursive: true });
197
+ const out = path.join(dir, "submission", `${r.mode.name}-${id}.json`);
198
+ fs.writeFileSync(out, body + "\n");
199
+ console.log(`\n${out}\nSend this file to pickhooks: https://pickhooks.com/modes/build (no fee). The author share goes to ${r.mode.author.wallet}.`);
200
+ }
201
+
202
+ // ------------------------------------------------------------------ encode
203
+
204
+ function kv(list) {
205
+ const o = {};
206
+ for (const a of list) {
207
+ const i = a.indexOf("=");
208
+ if (i < 1) die(`expected key=value, got ${a}`);
209
+ o[a.slice(0, i)] = a.slice(i + 1);
210
+ }
211
+ return o;
212
+ }
213
+
214
+ function encodeCmd(pos, opt) {
215
+ const dir = path.resolve(opt.dir ?? ".");
216
+ const f = path.join(dir, "mode.json");
217
+ if (!fs.existsSync(f)) die(`no mode.json in ${dir}`);
218
+ const m = JSON.parse(fs.readFileSync(f, "utf8"));
219
+ const v = validate(m);
220
+ if (v.errors.length) die(`mode.json has errors, run pickhooks-mode check:\n${v.errors.join("\n")}`);
221
+ const [what, ...rest] = pos;
222
+ try {
223
+ if (what === "settings") console.log(encodeSettings(m, kv(rest)));
224
+ else if (what === "action" || what === "poke") {
225
+ const [name, ...args] = rest;
226
+ if (!name) die(`which ${what}? ${(m[what === "poke" ? "pokes" : "actions"] ?? []).map((a) => a.name).join(", ") || "(none)"}`);
227
+ console.log(encodeAction(m, name, kv(args), what === "poke" ? "pokes" : "actions"));
228
+ } else die("encode settings | action <name> | poke <name>");
229
+ } catch (e) {
230
+ die(e.message);
231
+ }
232
+ }
233
+
234
+ // ------------------------------------------------------------------ main
235
+
236
+ const [cmd, ...argv] = process.argv.slice(2);
237
+ const { pos, opt } = flags(argv);
238
+ switch (cmd) {
239
+ case "init":
240
+ init(pos, opt);
241
+ break;
242
+ case "check":
243
+ check(pos, opt);
244
+ break;
245
+ case "test":
246
+ test(pos, opt);
247
+ break;
248
+ case "pack":
249
+ pack(pos, opt);
250
+ break;
251
+ case "encode":
252
+ encodeCmd(pos, opt);
253
+ break;
254
+ case "brief":
255
+ console.log(brief());
256
+ break;
257
+ case "addresses":
258
+ console.log(JSON.stringify(LIVE, null, 2));
259
+ break;
260
+ case undefined:
261
+ case "help":
262
+ case "--help":
263
+ case "-h":
264
+ console.log(HELP);
265
+ break;
266
+ case "--version":
267
+ case "-v":
268
+ console.log(VERSION);
269
+ break;
270
+ default:
271
+ die(`unknown command ${cmd}\n\n${HELP}`);
272
+ }
@@ -0,0 +1,175 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.28;
3
+
4
+ /// @title the mode of a sticky 3 coin
5
+ /// @notice A sticky 3 coin carries one mode, fixed at launch. The coin calls it on every buy, sell and send, and passes
6
+ /// on what holders and the keeper ask it to do (`play`, `poke`). A mode keeps its own state (a combo counter,
7
+ /// a queue, a round) and answers with orders. It never moves a coin itself: the coin checks every
8
+ /// order against its guardrails and carries it out (see StickyToken3 and Orders).
9
+ /// One mode contract serves every coin that uses it; it tells the coins apart by the caller.
10
+ interface IStickyMode {
11
+ /// @notice What the coin passes on every trade. `who` is the trader: the receiver of a buy or the sender of a sell
12
+ /// or send, or the transaction's signer when that address is a contract (a router that forwards the coins).
13
+ struct Trade {
14
+ uint8 kind; // BUY 1, SELL 2, SEND 4
15
+ address from;
16
+ address to;
17
+ address who;
18
+ uint256 amount; // coins that left `from`
19
+ uint256 got; // coins that reached `to` (a buy: after the takes, with the rows' drop)
20
+ uint256 dev; // coins with the DEV mark that went into a pool or the pot in this trade
21
+ uint256 sniper; // the same for the SNIPER mark
22
+ uint256 pot; // the game pot after this trade's take
23
+ uint256 buys; // buys since launch, this one included
24
+ }
25
+
26
+ /// @notice The share of every buy (basis points) that goes into the game pot for these settings. Reverts when the
27
+ /// settings are out of bounds. Called by the factory before the coin exists.
28
+ function quote(address creator, bytes calldata data) external view returns (uint16 gameBps);
29
+
30
+ /// @notice The factory registers a new coin with its settings, before the coin can trade.
31
+ function register(address coin, address creator, bytes calldata data) external;
32
+
33
+ /// @notice Called by the coin after every buy, sell and send, with a fixed gas budget. A mode that fails here only
34
+ /// loses its effect on this trade: the trade goes through.
35
+ function onTrade(Trade calldata t) external returns (uint256[] memory orders);
36
+
37
+ /// @notice A holder's action, passed on by the coin (`coin.play(data)`). `player` is the caller of the coin.
38
+ function onPlay(address player, bytes calldata data) external returns (uint256[] memory orders);
39
+
40
+ /// @notice Anyone's nudge, passed on by the coin (`coin.poke(data)`): settle a round, open with drand, end a game.
41
+ function onPoke(address caller, bytes calldata data) external returns (uint256[] memory orders);
42
+
43
+ /// @notice The mode's name, as the launcher and the coin page show it.
44
+ function name() external view returns (string memory);
45
+ }
46
+
47
+ /// @notice What a mode reads of its coins.
48
+ interface IStickyCoin3 {
49
+ function balanceOf(address) external view returns (uint256);
50
+ function totalSupply() external view returns (uint256);
51
+ function creator() external view returns (address);
52
+ function pool() external view returns (address);
53
+ function pool2() external view returns (address);
54
+ function start() external view returns (uint40);
55
+ function gamePot() external view returns (uint128);
56
+ function heldOf(address who) external view returns (uint256 amount, uint256 dev, uint40 until, uint40 since);
57
+ function aged(address who, uint256 before, uint8 skip) external view returns (uint256);
58
+ function play(bytes calldata data) external;
59
+ function poke(bytes calldata data) external;
60
+ function transfer(address to, uint256 amount) external returns (bool);
61
+ function transferFrom(address from, address to, uint256 amount) external returns (bool);
62
+ function approve(address spender, uint256 amount) external returns (bool);
63
+ }
64
+
65
+ /// @title the orders a mode can give
66
+ /// @notice An order is three words: [kind | amount << 8 | until << 136 | flags << 176], [who], [to]. The coin reads a
67
+ /// mode's answer as a plain uint256[] of such triples and carries out at most MAX orders. What each order may
68
+ /// do is decided by the coin, not by the mode:
69
+ /// - PAY: coins out of the game pot to `to`, as a fresh lot; into `to`'s custody until `until` when it is set.
70
+ /// Never more than the pot. Coins paid to the creator carry the DEV mark.
71
+ /// - HOLD: `amount` of `who`'s free coins into the custody of `to` (usually `who`) until `until`. Only in a
72
+ /// play, and only the player's own coins. The coins leave by the send rules of the table. Flag NODEV: only
73
+ /// coins without the DEV mark.
74
+ /// - RELEASE: all of `who`'s custody back into `who`'s wallet, with the lots as they went in.
75
+ /// - BURN_HELD: coins out of `who`'s custody to the dead address.
76
+ /// - MOVE_HELD: coins out of `who`'s custody to `to`: into `to`'s wallet, or into `to`'s custody when `until`
77
+ /// is set. The lots keep their time, marks and lock (like a send).
78
+ /// - BURN_POT: coins out of the game pot to the dead address.
79
+ /// - BURN: `amount` of `who`'s free coins to the dead address. Only in a play, only the player's own coins.
80
+ /// - UNTIL: sets when `who`'s custody ends.
81
+ /// Custody always ends by itself: the coin caps `until` at 7 days after the custody began (14 days for custody
82
+ /// made in the launch block), and after `until` anyone can call `coin.releaseHeld(who)`.
83
+ library Orders {
84
+ uint8 internal constant PAY = 1;
85
+ uint8 internal constant HOLD = 2;
86
+ uint8 internal constant RELEASE = 3;
87
+ uint8 internal constant BURN_HELD = 4;
88
+ uint8 internal constant MOVE_HELD = 5;
89
+ uint8 internal constant BURN_POT = 6;
90
+ uint8 internal constant BURN = 7;
91
+ uint8 internal constant UNTIL = 8;
92
+
93
+ /// HOLD: take only coins without the DEV mark
94
+ uint8 internal constant NODEV = 1;
95
+
96
+ uint256 internal constant MAX = 16;
97
+
98
+ struct List {
99
+ uint256[] w;
100
+ uint256 n; // words used
101
+ }
102
+
103
+ function list(uint256 orders) internal pure returns (List memory l) {
104
+ l.w = new uint256[](orders * 3);
105
+ }
106
+
107
+ function add(List memory l, uint8 kind, address who, address to, uint256 amount, uint256 until, uint8 flags)
108
+ internal
109
+ pure
110
+ {
111
+ if (l.n + 3 > l.w.length) {
112
+ // grow (a mode rarely needs more than it asked for)
113
+ uint256[] memory w = new uint256[](l.w.length * 2 + 3);
114
+ for (uint256 i; i < l.n; i++) {
115
+ w[i] = l.w[i];
116
+ }
117
+ l.w = w;
118
+ }
119
+ if (amount > type(uint128).max) amount = type(uint128).max;
120
+ if (until > type(uint40).max) until = type(uint40).max;
121
+ l.w[l.n] = uint256(kind) | (amount << 8) | (until << 136) | (uint256(flags) << 176);
122
+ l.w[l.n + 1] = uint256(uint160(who));
123
+ l.w[l.n + 2] = uint256(uint160(to));
124
+ l.n += 3;
125
+ }
126
+
127
+ function pay(List memory l, address to, uint256 amount) internal pure {
128
+ if (amount != 0) add(l, PAY, address(0), to, amount, 0, 0);
129
+ }
130
+
131
+ function payHeld(List memory l, address to, uint256 amount, uint256 until) internal pure {
132
+ if (amount != 0) add(l, PAY, address(0), to, amount, until, 0);
133
+ }
134
+
135
+ function hold(List memory l, address who, address to, uint256 amount, uint256 until, uint8 flags) internal pure {
136
+ add(l, HOLD, who, to, amount, until, flags);
137
+ }
138
+
139
+ function release(List memory l, address who) internal pure {
140
+ add(l, RELEASE, who, address(0), 0, 0, 0);
141
+ }
142
+
143
+ function burnHeld(List memory l, address who, uint256 amount) internal pure {
144
+ if (amount != 0) add(l, BURN_HELD, who, address(0), amount, 0, 0);
145
+ }
146
+
147
+ function moveHeld(List memory l, address who, address to, uint256 amount, uint256 until) internal pure {
148
+ if (amount != 0) add(l, MOVE_HELD, who, to, amount, until, 0);
149
+ }
150
+
151
+ function burnPot(List memory l, uint256 amount) internal pure {
152
+ if (amount != 0) add(l, BURN_POT, address(0), address(0), amount, 0, 0);
153
+ }
154
+
155
+ function burn(List memory l, address who, uint256 amount) internal pure {
156
+ add(l, BURN, who, address(0), amount, 0, 0);
157
+ }
158
+
159
+ function setUntil(List memory l, address who, uint256 t) internal pure {
160
+ add(l, UNTIL, who, address(0), 0, t, 0);
161
+ }
162
+
163
+ /// @notice The finished list: the words used, nothing behind them.
164
+ function done(List memory l) internal pure returns (uint256[] memory w) {
165
+ w = l.w;
166
+ uint256 n = l.n;
167
+ assembly ("memory-safe") {
168
+ mstore(w, n)
169
+ }
170
+ }
171
+
172
+ function none() internal pure returns (uint256[] memory) {
173
+ return new uint256[](0);
174
+ }
175
+ }