@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.
@@ -0,0 +1,26 @@
1
+ {
2
+ "kit": 1,
3
+ "name": "hour",
4
+ "title": "first of the hour",
5
+ "contract": "HourMode",
6
+ "line": "The first buy of every hour wins from the pot.",
7
+ "how": [
8
+ "A share of every buy (2 % by default) fills the pot.",
9
+ "The first buy of at least the minimum size in each UTC hour gets 5 % of the pot.",
10
+ "A win is never more coins than the winning buy itself brings, and never goes to the creator."
11
+ ],
12
+ "share": "bps",
13
+ "settings": [
14
+ { "key": "bps", "type": "uint256", "label": "Share of every buy into the pot", "unit": "bps", "default": 200, "min": 100, "max": 300 },
15
+ { "key": "minPpm", "type": "uint256", "label": "Smallest buy that counts", "unit": "ppm", "default": 100, "min": 10, "max": 1000 }
16
+ ],
17
+ "actions": [],
18
+ "pokes": [],
19
+ "drand": false,
20
+ "custody": false,
21
+ "keeper": null,
22
+ "notes": [
23
+ "A bot can wait for the hour to turn and buy first. It wins at most what its own buy brings, and the pot only pays 5 % an hour."
24
+ ],
25
+ "author": { "wallet": "", "x": "", "telegram": "" }
26
+ }
@@ -0,0 +1,2 @@
1
+ forge-std/=lib/forge-std/src/
2
+ pickhooks/=lib/pickhooks/
@@ -0,0 +1,70 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.28;
3
+
4
+ import {ModeBase} from "pickhooks/ModeBase.sol";
5
+ import {Orders} from "pickhooks/IStickyMode.sol";
6
+
7
+ /// @title first of the hour: the mode kit's example, a whole mode in one file
8
+ /// @notice The first buy of at least the minimum size in each UTC hour gets 5 % of the game pot, at most what the buy
9
+ /// itself brings, never the creator. A share of every buy (2 % by default) fills the pot.
10
+ /// @dev A template to copy: settings with bounds (quote, _register), state per coin, one rule in _trade, an order back.
11
+ /// Everything a mode returns is checked by the coin: it can only pay out of the game pot, hold coins for at most 7
12
+ /// days, burn, or move held coins; it never touches sells or sends, and if it reverts, runs out of gas or answers
13
+ /// garbage, the coin skips it and the trade goes through.
14
+ contract HourMode is ModeBase {
15
+ using Orders for Orders.List;
16
+
17
+ uint256 public constant SHARE_BPS = 500; // of the pot for the first buy of the hour
18
+
19
+ struct Game {
20
+ uint16 bps; // share of every buy into the pot
21
+ uint32 minPpm; // the smallest buy that counts, millionths of the supply
22
+ uint32 hour; // the last hour that had its first buy
23
+ uint32 wins;
24
+ }
25
+
26
+ mapping(address => Game) public games;
27
+
28
+ event FirstOfTheHour(address indexed coin, address indexed buyer, uint32 hour, uint256 paid);
29
+
30
+ constructor(address factory_) ModeBase(factory_) {}
31
+
32
+ function name() external pure returns (string memory) {
33
+ return "first of the hour";
34
+ }
35
+
36
+ /// @notice Settings: abi.encode(bps, minPpm), zero for the default. bps 100 to 300 (default 200), minPpm 10 to 1000.
37
+ /// The factory asks quote() at launch: the share of buys this mode takes (the validator caps rules + game at 10 %).
38
+ function quote(address, bytes calldata data) external pure returns (uint16) {
39
+ (uint256 bps,) = _settings(data);
40
+ return uint16(bps);
41
+ }
42
+
43
+ function _settings(bytes calldata data) internal pure returns (uint256 bps, uint256 minPpm) {
44
+ if (data.length != 0) (bps, minPpm) = abi.decode(data, (uint256, uint256));
45
+ bps = _within(bps, 200, 100, 300, "bps");
46
+ minPpm = _within(minPpm, 100, 10, 1000, "min");
47
+ }
48
+
49
+ function _register(address coin, address, bytes calldata data) internal override {
50
+ (uint256 bps, uint256 minPpm) = _settings(data);
51
+ games[coin] = Game(uint16(bps), uint32(minPpm), 0, 0);
52
+ }
53
+
54
+ function _trade(address coin, Trade calldata t) internal override returns (uint256[] memory) {
55
+ if (t.kind != BUY) return Orders.none();
56
+ Game storage g = games[coin];
57
+ if (_ppm(t.amount) < g.minPpm) return Orders.none();
58
+ uint32 hour = uint32(block.timestamp / 1 hours);
59
+ if (hour == g.hour) return Orders.none();
60
+ g.hour = hour;
61
+ if (!_player(coin, t.who)) return Orders.none(); // the creator's buy takes the hour and wins nothing
62
+ uint256 paid = _min(t.pot * SHARE_BPS / 10_000, t.got);
63
+ if (paid == 0) return Orders.none();
64
+ g.wins++;
65
+ emit FirstOfTheHour(coin, t.who, hour, paid);
66
+ Orders.List memory l = Orders.list(1);
67
+ l.pay(t.who, paid);
68
+ return l.done();
69
+ }
70
+ }
@@ -0,0 +1,79 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.28;
3
+
4
+ import {ModeHarness} from "pickhooks/harness/ModeHarness.sol";
5
+ import {Live} from "pickhooks/harness/Live.sol";
6
+ import {HourMode} from "../src/HourMode.sol";
7
+
8
+ /// Runs on a BNB Chain fork against the live factory and the live coin code: `pickhooks-mode test`.
9
+ contract HourModeTest is ModeHarness {
10
+ function deployMode() internal override returns (address) {
11
+ return address(new HourMode(Live.FACTORY));
12
+ }
13
+
14
+ // ------------------------------------------------ the guardrails: the defaults and the edge of every setting
15
+
16
+ function test_guardrails_defaults() public {
17
+ guardrails("");
18
+ }
19
+
20
+ function test_guardrails_low() public {
21
+ guardrails(abi.encode(uint256(100), uint256(10)));
22
+ }
23
+
24
+ function test_guardrails_high() public {
25
+ guardrails(abi.encode(uint256(300), uint256(1000)));
26
+ }
27
+
28
+ function test_stress() public {
29
+ stress("", 60);
30
+ }
31
+
32
+ // ------------------------------------------------ every bound: quote() refuses what is outside it
33
+
34
+ function test_bounds() public {
35
+ expectLaunchRevert(abi.encode(uint256(99), uint256(0)));
36
+ expectLaunchRevert(abi.encode(uint256(301), uint256(0)));
37
+ expectLaunchRevert(abi.encode(uint256(0), uint256(9)));
38
+ expectLaunchRevert(abi.encode(uint256(0), uint256(1001)));
39
+ }
40
+
41
+ // ------------------------------------------------ the rule itself
42
+
43
+ function test_firstBuyOfTheHourWins() public {
44
+ launch("");
45
+ curveBuy(alice, bps(20)); // fills the pot and takes this hour (an empty pot pays nothing)
46
+ curveBuy(bob, bps(20)); // the same hour: nothing
47
+ wait(1 hours);
48
+ uint256 pot = coin.gamePot();
49
+ uint256 before = coin.balanceOf(carol);
50
+ curveBuy(carol, bps(20));
51
+ uint256 got = bps(20) * 9800 / 10_000; // 2 % of the buy goes into the pot
52
+ uint256 potWith = pot + bps(20) * 200 / 10_000;
53
+ assertEq(coin.balanceOf(carol) - before, got + potWith * 500 / 10_000, "5 % of the pot to the first buy of the hour");
54
+ }
55
+
56
+ function test_creatorTakesTheHourAndWinsNothing() public {
57
+ launch("");
58
+ curveBuy(alice, bps(50));
59
+ wait(1 hours);
60
+ uint256 pot = coin.gamePot();
61
+ curveBuy(dev, bps(10));
62
+ assertEq(coin.gamePot(), pot + bps(10) * 200 / 10_000, "the creator wins nothing");
63
+ uint256 b = coin.balanceOf(bob);
64
+ curveBuy(bob, bps(10)); // the hour is taken
65
+ assertEq(coin.balanceOf(bob) - b, bps(10) * 9800 / 10_000);
66
+ }
67
+
68
+ function test_smallBuysDoNotTakeTheHour() public {
69
+ launch("");
70
+ curveBuy(alice, bps(20));
71
+ wait(1 hours);
72
+ uint256 b = coin.balanceOf(bob);
73
+ curveBuy(bob, ppm(50)); // under the minimum of 100 millionths: no win, the hour stays open
74
+ assertEq(coin.balanceOf(bob) - b, ppm(50) * 9800 / 10_000);
75
+ uint256 c = coin.balanceOf(carol);
76
+ curveBuy(carol, ppm(200));
77
+ assertGt(coin.balanceOf(carol) - c, ppm(200) * 9800 / 10_000, "the first buy over the minimum wins");
78
+ }
79
+ }