@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 +21 -0
- package/README.md +124 -2
- package/bin/pickhooks-mode.mjs +272 -0
- package/contracts/IStickyMode.sol +175 -0
- package/contracts/ModeBase.sol +124 -0
- package/contracts/harness/Live.sol +80 -0
- package/contracts/harness/ModeHarness.sol +322 -0
- package/lib/abi.mjs +117 -0
- package/lib/brief.mjs +122 -0
- package/lib/check.mjs +158 -0
- package/lib/index.mjs +6 -0
- package/lib/live.mjs +14 -0
- package/lib/modejson.mjs +176 -0
- package/package.json +39 -4
- package/schema/mode.schema.json +80 -0
- package/template/env.example +2 -0
- package/template/foundry.toml +15 -0
- package/template/gitignore +5 -0
- package/template/mode.json +26 -0
- package/template/remappings.txt +2 -0
- package/template/src/HourMode.sol +70 -0
- package/template/test/HourMode.t.sol +79 -0
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
|
-
#
|
|
1
|
+
# @pickhooks/mode-kit
|
|
2
2
|
|
|
3
|
-
|
|
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
|
+
}
|