@oracle-agent/oracle 0.1.0 → 0.2.1

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.
Files changed (36) hide show
  1. package/README.md +37 -10
  2. package/SECURITY.md +11 -11
  3. package/SETUP.md +76 -16
  4. package/bin/desk-server.mjs +3 -1
  5. package/docs/architecture.md +10 -9
  6. package/package.json +13 -7
  7. package/protocols/templates/safe-erc20/SECURITY.md +16 -0
  8. package/protocols/templates/safe-erc20/foundry.toml +12 -0
  9. package/protocols/templates/safe-erc20/remappings.txt +2 -0
  10. package/protocols/templates/safe-erc20/src/SafeERC20.sol +53 -0
  11. package/protocols/templates/safe-erc20/test/SafeERC20.t.sol +72 -0
  12. package/public/oracle-splash/index.html +648 -598
  13. package/public/oracle-splash/mesh-graph.png +0 -0
  14. package/public/oracle-splash/mesh-graph.svg +216 -0
  15. package/scripts/protocol-template-gate.mjs +15 -0
  16. package/skills/oracle-protocol-builder/SKILL.md +45 -24
  17. package/src/data/catalog.mjs +9 -0
  18. package/src/data/desk-data.mjs +24 -0
  19. package/src/data/providers/bitcoin-esplora.mjs +2 -16
  20. package/src/data/providers/cowswap.mjs +9 -96
  21. package/src/data/providers/evm-rpc.mjs +19 -0
  22. package/src/data/providers/hl-assets.mjs +165 -0
  23. package/src/data/providers/hl-info.mjs +7 -1
  24. package/src/data/providers/hl-outcome.mjs +61 -0
  25. package/src/data/providers/hl-perps.mjs +40 -35
  26. package/src/data/providers/magiceden-sol.mjs +7 -6
  27. package/src/data/providers/poly-clob.mjs +460 -0
  28. package/src/data/providers/satflow.mjs +5 -4
  29. package/src/data/providers/solana-rpc.mjs +9 -0
  30. package/src/data/providers/uniswap-v3.mjs +3 -2
  31. package/src/index.mjs +1 -0
  32. package/src/onboarding/agent-keys.mjs +4 -0
  33. package/src/prepare-envelope.mjs +108 -0
  34. package/src/protocol-templates/gate.mjs +179 -0
  35. package/src/protocol-templates/prepare-deploy.mjs +81 -0
  36. package/src/public-control/bundler-client.mjs +3 -18
package/README.md CHANGED
@@ -11,13 +11,26 @@ open-source agent runtime from [Nous Research](https://nousresearch.com).
11
11
  Oracle itself makes **no model calls and needs no API key**. It is the tool layer
12
12
  an agent drives. Bring whatever model your Hermes is already using.
13
13
 
14
- The **public package is prepare-only**: it never takes a private key and never
15
- broadcasts. Owner-local signer modules may exist in a private tree for operator
16
- deployments; they are excluded from the npm artifact and from every public
17
- entrypoint.
14
+ This package is **prepare-only**: it never takes a private key and never
15
+ broadcasts. For agentic trading with **your** keys on **your** machine, install
16
+ the companion [`@oracle-agent/operator`](https://github.com/demi-hl/oracle-operator)
17
+ alongside it — same split we run ourselves.
18
18
 
19
19
  ---
20
20
 
21
+ ## Install
22
+
23
+ ```bash
24
+ # prepare-only (safe default)
25
+ npm i @oracle-agent/oracle
26
+
27
+ # full self-host lane (keys stay on YOUR machine) — same model we run
28
+ npm i @oracle-agent/oracle @oracle-agent/operator
29
+ ```
30
+
31
+ See the operator [SETUP](https://github.com/demi-hl/oracle-operator/blob/main/SETUP.md)
32
+ for local key vault + Hyperliquid sign-and-submit.
33
+
21
34
  ## What Oracle is
22
35
 
23
36
  Most "AI crypto agent" projects give a model a hot wallet and hope. Oracle does
@@ -78,9 +91,9 @@ Three planes, and the boundary between them is mechanically enforced:
78
91
  - **Policy plane** (public) — destination allowlists, slippage guards, route and
79
92
  vault attestations, grant schema. Holds no keys; constrains what a signer may
80
93
  be asked to do.
81
- - **Exec plane** (owner-local source only) - signing and broadcast modules exist
82
- for private operator deployments, but are excluded from every public package
83
- entrypoint and from the npm artifact.
94
+ - **Exec plane** (optional companion) - signing and broadcast live in
95
+ [`@oracle-agent/operator`](https://github.com/demi-hl/oracle-operator) on
96
+ your machine with your key. Not in this package or its npm artifact.
84
97
 
85
98
  `test/custody-boundary.test.mjs` walks the import graph and fails if any public
86
99
  module reaches wallet key material or a house signer. The split is a test, not a
@@ -146,7 +159,7 @@ allowlisted the wrong contract.
146
159
  | Intents | CoW Protocol |
147
160
  | Perps / lending / yield | GMX v2, Morpho, Pendle |
148
161
  | Bridges | Across, Hop, Relay |
149
- | Venues | Hyperliquid (incl. HyperCore staking), Polymarket |
162
+ | Venues | Hyperliquid (core perps + HIP-3 builder dexs + HIP-4 outcomes), Polymarket (read + local CLOB prepare) |
150
163
  | NFT | OpenSea, Satflow, Magic Eden (Solana) |
151
164
 
152
165
  Every provider and every scanner capability declares an honest tier —
@@ -194,8 +207,11 @@ capability-labeled per chain; unconfigured venues stay unavailable instead of fa
194
207
 
195
208
  ## Install
196
209
 
197
- The npm package is intentionally unpublished while release hardening is in
198
- progress. Install from source:
210
+ ```bash
211
+ npm install @oracle-agent/oracle # Node >= 20.19.0
212
+ ```
213
+
214
+ Or from source, if you want to run the suite:
199
215
 
200
216
  ```bash
201
217
  git clone https://github.com/demi-hl/oracle.git
@@ -382,3 +398,14 @@ oracle-data-mcp
382
398
 
383
399
  The code is open source. The **Oracle** name and marks are reserved; please
384
400
  don't imply endorsement by an official deployment when shipping a fork.
401
+
402
+ ### Operator hardening (0.4+)
403
+
404
+ Prefer the local signer daemon so the agent never sees the vault passphrase:
405
+
406
+ ```bash
407
+ npx oracle-signer # has ORACLE_VAULT_PASSPHRASE
408
+ # agent: ORACLE_SIGNER_URL + ORACLE_SIGNER_TOKEN only
409
+ ```
410
+
411
+ See [oracle-operator SETUP](https://github.com/demi-hl/oracle-operator/blob/main/SETUP.md).
package/SECURITY.md CHANGED
@@ -70,20 +70,20 @@ A security claim that isn't enforced by a test is just a comment.
70
70
 
71
71
  ## Residual operator notes
72
72
 
73
- These are not public-package custody holes. They are operator / release-process
74
- constraints that still apply after the public boundary hardening:
75
-
76
- 1. **Owner-local signer modules may exist in the git tree** (`hl-exec`,
77
- `key-vault`, `oracle-vault`) and are excluded from public entrypoints and the
78
- npm artifact. Do not restore them to the public package for compatibility.
79
- Before making the repository public, keep that exclusion intentional or move
80
- the modules to a private operator repo.
73
+ These are not public-package custody holes. They are operator constraints that
74
+ still apply after the public boundary hardening:
75
+
76
+ 1. **Owner-local signer modules live in the public companion**
77
+ [`@oracle-agent/operator`](https://github.com/demi-hl/oracle-operator)
78
+ (`hl-exec`, `key-vault`, `oracle-vault`). Keys stay on the operator's
79
+ machine. They must not be restored into this prepare-only package or its
80
+ npm artifact. CI fails if a pack of *this* repo includes those paths.
81
81
  2. **Prepare helpers that validate executable routes require an attestation
82
82
  secret** (`requireSigned`) so they cannot be used as a softer pre-broadcast
83
83
  gate than `enforceTxPolicy`.
84
84
  3. **Daily spend ledger locks are pid + TTL based.** A live owner holds the
85
85
  lock; a dead pid or expired lock is reclaimed. Override TTL with
86
86
  `ORACLE_LEDGER_LOCK_TTL_MS` only if you know you need a longer window.
87
- 4. **GitHub branch protection, code scanning, and vulnerability alerts** are
88
- process controls outside this package. Enable them on `main` before treating
89
- a public release as fully hardened operations.
87
+ 4. **GitHub process controls** (branch protection, CodeQL, secret scanning,
88
+ Dependabot) are enabled on this public repo and are independent of the
89
+ package boundary tests.
package/SETUP.md CHANGED
@@ -5,8 +5,11 @@ a wallet private key. Prepared actions are signed in the user's wallet.
5
5
 
6
6
  ## Install
7
7
 
8
- The npm package is intentionally unpublished while release hardening is in
9
- progress. Install the source checkout with Node 20.19 or newer:
8
+ ```bash
9
+ npm install @oracle-agent/oracle # Node >= 20.19.0
10
+ ```
11
+
12
+ Or from source, if you want to run the suite:
10
13
 
11
14
  ```bash
12
15
  git clone https://github.com/demi-hl/oracle.git
@@ -22,7 +25,7 @@ Oracle has two distribution boundaries and they are separate on purpose:
22
25
  | Plane | What it does | Needs a key? |
23
26
  |---|---|---|
24
27
  | **Read / prepare** | Market data, quotes, and *unsigned* transactions or EIP-712 actions | No, mostly |
25
- | **Owner-local source lane** | Optional private operator signing code, excluded from npm and public entrypoints | Yes |
28
+ | **Owner-local source lane** | Optional companion `@oracle-agent/operator` keys on YOUR machine | Yes |
26
29
 
27
30
  A prepared object is inert. It becomes real only when something signs it. You
28
31
  choose whether that something is your browser wallet or separately operated
@@ -129,8 +132,11 @@ Keeps the secret out of the process environment.
129
132
 
130
133
  ### 3. An encrypted vault (recommended)
131
134
 
135
+ Install the companion and use its CLI (not shipped in this package):
136
+
132
137
  ```bash
133
- node bin/oracle-vault.mjs encrypt ~/.config/oracle/keys/hl.key
138
+ npm i -g @oracle-agent/operator # or npm i -D in your project
139
+ oracle-vault encrypt ~/.config/oracle/keys/hl.key
134
140
  # writes hl.key.vault.json (scrypt N=2^17 + AES-256-GCM)
135
141
  # moves hl.key -> hl.key.plaintext.bak
136
142
 
@@ -138,23 +144,22 @@ export ORACLE_VAULT_PASSPHRASE="..." # or be prompted
138
144
  export HL_KEY_FILE=~/.config/oracle/keys/hl.key.vault.json
139
145
 
140
146
  # verify it unlocks, THEN destroy the plaintext backup
141
- node bin/oracle-vault.mjs inspect ~/.config/oracle/keys/hl.key.vault.json
147
+ oracle-vault inspect ~/.config/oracle/keys/hl.key.vault.json
142
148
  shred -u ~/.config/oracle/keys/hl.key.plaintext.bak
143
149
  ```
144
150
 
145
- Oracle detects a vault file and decrypts it in memory. The key is never written
146
- back to disk, never logged, and never included in any returned object — there
147
- is a test asserting exactly that.
151
+ The operator vault decrypts in memory only. The key is never written back to
152
+ disk, never logged, and never included in any returned object.
148
153
 
149
154
  **What the vault does and does not protect.** It defends against a key file read
150
155
  by something that is not you: a synced backup, a stolen laptop at rest, an
151
156
  overly-broad `chmod`, a leaked tarball. It does **not** defend against malware
152
- running as your user while Oracle is unlocked. Nothing file-based does. If you
153
- need that, use a hardware wallet and the prepare-only path.
157
+ running as your user while the vault is unlocked. Nothing file-based does. If
158
+ you need that, use a hardware wallet and the prepare-only path.
154
159
 
155
160
  ```bash
156
- node bin/oracle-vault.mjs rekey <vault> # rotate the passphrase
157
- node bin/oracle-vault.mjs inspect <vault> # show KDF params, no secret
161
+ oracle-vault rekey <vault> # rotate the passphrase
162
+ oracle-vault inspect <vault> # show KDF params, no secret
158
163
  ```
159
164
 
160
165
  ---
@@ -178,10 +183,10 @@ const order = await data.call("hl-perps", "prepareOrder", {
178
183
 
179
184
  ### B. Owner-local execution
180
185
 
181
- The public `data.call()` facade refuses `hl-exec`. Private operator deployments
182
- must wire the source-only signer behind their own authenticated execution
183
- boundary. It is intentionally not exported or documented as a public package
184
- API.
186
+ The public `data.call()` facade refuses `hl-exec`. Operator signing lives in
187
+ [`@oracle-agent/operator`](https://github.com/demi-hl/oracle-operator) on your
188
+ machine with your key. Import `hlSignAndSubmit` from that package never from
189
+ this one. Do not expose it on a public HTTP port.
185
190
 
186
191
  The public prepare path hard-caps slippage at 100 bps, bounds leverage by the
187
192
  asset maximum, rounds sizes and prices to venue precision, and makes brackets
@@ -233,3 +238,58 @@ routing is what makes running several practical instead of theoretical.
233
238
 
234
239
  Everything still works fine with a single provider. It is just better with a
235
240
  router that knows which brain to use for which job.
241
+
242
+ ### Operator hardening (0.4+)
243
+
244
+ Prefer the local signer daemon so the agent never sees the vault passphrase:
245
+
246
+ ```bash
247
+ npx oracle-signer # has ORACLE_VAULT_PASSPHRASE
248
+ # agent: ORACLE_SIGNER_URL + ORACLE_SIGNER_TOKEN only
249
+ ```
250
+
251
+ See [oracle-operator SETUP](https://github.com/demi-hl/oracle-operator/blob/main/SETUP.md).
252
+
253
+ ## Local HIP-3 / HIP-4 / Polymarket (no VPS)
254
+
255
+ ### Hyperliquid HIP-3 (builder perps)
256
+ ```js
257
+ const prep = await data.call("hl-perps", "prepareOrder", {
258
+ coin: "xyz:TSLA", side: "buy", type: "limit", price: "250", size: "1",
259
+ });
260
+ await hlSignAndSubmit(prep, { keyFile, userInitiated: true });
261
+ // or signViaDaemon({ surface: "hl", prepared: prep, userInitiated: true })
262
+ ```
263
+
264
+ ### Hyperliquid HIP-4 (outcomes)
265
+ ```js
266
+ const prep = await data.call("hl-outcome", "prepareOrder", {
267
+ outcome: 969, side: "yes", // or coin: "#9690"
268
+ side: "buy", type: "limit", price: "0.42", size: "20",
269
+ });
270
+ await hlSignAndSubmit(prep, { keyFile, userInitiated: true });
271
+ ```
272
+
273
+ ### Polymarket (local CLOB)
274
+ ```js
275
+ const prep = await data.call("poly-clob", "prepareOrder", {
276
+ tokenId, side: "BUY", price: 0.45, size: 10,
277
+ maker: eoa, signer: eoa, tickSize: "0.01",
278
+ });
279
+ // signer process needs POLYMARKET_API_KEY/SECRET/PASSPHRASE + key
280
+ await polySignAndSubmit(prep, { keyFile, userInitiated: true });
281
+ // or signViaDaemon({ surface: "poly", prepared: prep, userInitiated: true })
282
+ ```
283
+
284
+ ## Protocol templates (builder)
285
+
286
+ Gated Foundry templates under `protocols/templates/`.
287
+
288
+ ```bash
289
+ # requires Foundry (forge)
290
+ cd protocols/templates/safe-erc20 && forge install && forge test
291
+ npm run protocol:gate -- safe-erc20
292
+ ```
293
+
294
+ `prepareTemplateDeploy` will not stamp an unsigned deploy unless `forge test` passes.
295
+ **Not a paid security-firm audit** — see each template `SECURITY.md`.
@@ -421,7 +421,9 @@ const server = createServer(async (req, res) => {
421
421
  if (e instanceof HttpError) {
422
422
  return send(res, e.status, { error: e.code, message: e.message });
423
423
  }
424
- return send(res, 500, { error: String(e.message || e) });
424
+ // Never echo raw Error objects or stacks to the client.
425
+ console.error("desk-server internal error:", e && e.stack ? e.stack : e);
426
+ return send(res, 500, { error: "internal-error" });
425
427
  }
426
428
  });
427
429
 
@@ -22,8 +22,9 @@ Three planes. The boundary between them is enforced by a test, not a convention.
22
22
  x no import may cross
23
23
  |
24
24
  +-------------------------------------------+
25
- | PRIVATE (not published) |
26
- | keystore, house signer, exec-server |
25
+ | OWNER-LOCAL (companion package) |
26
+ | @oracle-agent/operator |
27
+ | key-vault / hl-exec / oracle-vault |
27
28
  +-------------------------------------------+
28
29
  ```
29
30
 
@@ -41,10 +42,10 @@ That is why the boundary is a test (`test/custody-boundary.test.mjs`) rather tha
41
42
  paragraph in a README. It walks the import graph from every shipped entrypoint and
42
43
  fails on:
43
44
 
44
- - a private module present in the tree
45
45
  - any public module importing signer or key material
46
- - a dangling local import (extraction left something behind)
46
+ - a dangling local import that reaches a signer from a shipped entrypoint
47
47
  - a secret-shaped literal anywhere
48
+ - a packed npm artifact that still contains signer/vault modules
48
49
 
49
50
  ## Data plane
50
51
 
@@ -127,9 +128,9 @@ No lane pins a model. `profile.json` carries a capability *class*
127
128
  (`strong-reasoner`, `fast-tool-caller`), so the installer never writes a vendor into
128
129
  a user's config.
129
130
 
130
- ## What is not here
131
+ ## What is not in the public package
131
132
 
132
- The exec plane: keystore, house signer, local-signer service, exec-server,
133
- capability minting, and the venue adapters that hold signing authority. That is
134
- operator infrastructure. Its absence is the point a self-hosted Oracle has nothing
135
- to steal.
133
+ House custody, an always-on exec server, and capability minting are not shipped.
134
+ Owner-local signer modules (`key-vault`, `hl-exec`, `oracle-vault`) live in the
135
+ companion package `@oracle-agent/operator` on the user's machine. This prepare
136
+ package has nothing to steal: it never takes a key and never broadcasts.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@oracle-agent/oracle",
3
- "version": "0.1.0",
4
- "description": "Oracle: prepare-only multichain agent control plane. Policy-bounded intents for a user-signed wallet. Self-custody by default the public package never takes your key. Built for Hermes; no model key required.",
3
+ "version": "0.2.1",
4
+ "description": "Oracle: prepare-only multichain agent control plane. Policy-bounded intents for a user-signed wallet. Self-custody by default \u2014 the public package never takes your key. Built for Hermes; no model key required.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
7
7
  "publishConfig": {
@@ -40,7 +40,8 @@
40
40
  "./chains": "./src/chains.mjs",
41
41
  "./scanner": "./src/scanner/index.mjs",
42
42
  "./router": "./src/router/index.mjs",
43
- "./nft-gas-war": "./src/nft-gas-war-guard.mjs"
43
+ "./nft-gas-war": "./src/nft-gas-war-guard.mjs",
44
+ "./prepare-envelope": "./src/prepare-envelope.mjs"
44
45
  },
45
46
  "scripts": {
46
47
  "test": "ORACLE_TEST_ISOLATE_SECRETS=1 ORACLE_ROUTE_ATTESTATION_SECRET=oracle-unit-test-secret node --test test/*.test.mjs",
@@ -58,7 +59,9 @@
58
59
  "route": "node bin/oracle-route.mjs",
59
60
  "scan:secrets": "node scripts/secret-scan.mjs",
60
61
  "test:count": "node scripts/check-test-count.mjs",
61
- "test:count:update": "node scripts/check-test-count.mjs --update"
62
+ "test:count:update": "node scripts/check-test-count.mjs --update",
63
+ "test:protocol-templates": "RUN_PROTOCOL_TEMPLATE_TESTS=1 node --test test/protocol-templates.test.mjs",
64
+ "protocol:gate": "node scripts/protocol-template-gate.mjs"
62
65
  },
63
66
  "dependencies": {
64
67
  "@msgpack/msgpack": "^3.1.3",
@@ -83,8 +86,11 @@
83
86
  "docs/",
84
87
  "examples/",
85
88
  "scripts/",
86
- "!bin/oracle-vault.mjs",
87
- "!src/key-vault.mjs",
88
- "!src/data/providers/hl-exec.mjs"
89
+ "protocols/templates/**/src/**",
90
+ "protocols/templates/**/test/**",
91
+ "protocols/templates/**/foundry.toml",
92
+ "protocols/templates/**/remappings.txt",
93
+ "protocols/templates/**/SECURITY.md",
94
+ "src/protocol-templates/**"
89
95
  ]
90
96
  }
@@ -0,0 +1,16 @@
1
+ # SafeERC20 template security notice
2
+
3
+ **Not a paid security-firm audit.**
4
+
5
+ This template is:
6
+ - fixed-supply ERC20
7
+ - Ownable2Step + pause
8
+ - no tax / blacklist / upgrade proxy
9
+ - covered by Foundry unit tests in `test/`
10
+
11
+ Before mainnet capital:
12
+ 1. Keep or expand Foundry tests green
13
+ 2. Run static analysis (Slither/Aderyn) if available
14
+ 3. Book an independent Solidity auditor for any real TVL
15
+
16
+ Oracle only prepares unsigned deploys after the JS gate confirms `forge test` passes.
@@ -0,0 +1,12 @@
1
+ [profile.default]
2
+ src = "src"
3
+ out = "out"
4
+ libs = ["lib"]
5
+ solc = "0.8.24"
6
+ optimizer = true
7
+ optimizer_runs = 200
8
+ ffi = false
9
+ fs_permissions = [{ access = "read", path = "./" }]
10
+
11
+ [fmt]
12
+ line_length = 100
@@ -0,0 +1,2 @@
1
+ @openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/
2
+ forge-std/=lib/forge-std/src/
@@ -0,0 +1,53 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.24;
3
+
4
+ import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
5
+ import {ERC20Burnable} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol";
6
+ import {ERC20Pausable} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Pausable.sol";
7
+ import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
8
+ import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol";
9
+
10
+ /**
11
+ * @title SafeERC20
12
+ * @notice Opinionated fixed-supply ERC20 for Oracle protocol-builder v1.
13
+ *
14
+ * Security posture (templates, NOT a firm audit):
15
+ * - Fixed supply minted once to `initialHolder` in constructor (no hidden mint).
16
+ * - Ownable2Step ownership (no single-tx accidental transfer).
17
+ * - Pausable emergency stop (owner only).
18
+ * - No transfer tax, no blacklist, no max-tx, no trading cooldown.
19
+ * - No upgradeability / no proxy.
20
+ * - Burn optional by holders only (ERC20Burnable).
21
+ *
22
+ * This is a reviewed starter template with Foundry tests. It is NOT a paid
23
+ * security-firm audit. Do not put mainnet TVL on custom forks without one.
24
+ */
25
+ contract SafeERC20 is ERC20, ERC20Burnable, ERC20Pausable, Ownable2Step {
26
+ constructor(
27
+ string memory name_,
28
+ string memory symbol_,
29
+ uint256 initialSupply,
30
+ address initialHolder,
31
+ address initialOwner
32
+ ) ERC20(name_, symbol_) Ownable(initialOwner) {
33
+ require(initialHolder != address(0), "SafeERC20: holder zero");
34
+ require(initialOwner != address(0), "SafeERC20: owner zero");
35
+ require(initialSupply > 0, "SafeERC20: supply zero");
36
+ _mint(initialHolder, initialSupply);
37
+ }
38
+
39
+ function pause() external onlyOwner {
40
+ _pause();
41
+ }
42
+
43
+ function unpause() external onlyOwner {
44
+ _unpause();
45
+ }
46
+
47
+ function _update(address from, address to, uint256 value)
48
+ internal
49
+ override(ERC20, ERC20Pausable)
50
+ {
51
+ super._update(from, to, value);
52
+ }
53
+ }
@@ -0,0 +1,72 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.24;
3
+
4
+ import {Test} from "forge-std/Test.sol";
5
+ import {SafeERC20} from "../src/SafeERC20.sol";
6
+
7
+ contract SafeERC20Test is Test {
8
+ SafeERC20 internal token;
9
+ address internal owner = makeAddr("owner");
10
+ address internal holder = makeAddr("holder");
11
+ address internal alice = makeAddr("alice");
12
+ address internal bob = makeAddr("bob");
13
+
14
+ uint256 internal constant SUPPLY = 1_000_000 ether;
15
+
16
+ function setUp() public {
17
+ token = new SafeERC20("Safe", "SAFE", SUPPLY, holder, owner);
18
+ }
19
+
20
+ function test_fixedSupply_noHiddenMint() public view {
21
+ assertEq(token.totalSupply(), SUPPLY);
22
+ assertEq(token.balanceOf(holder), SUPPLY);
23
+ assertEq(token.balanceOf(owner), 0);
24
+ }
25
+
26
+ function test_transfer_noTax() public {
27
+ vm.prank(holder);
28
+ token.transfer(alice, 100 ether);
29
+ assertEq(token.balanceOf(alice), 100 ether);
30
+ assertEq(token.balanceOf(holder), SUPPLY - 100 ether);
31
+ }
32
+
33
+ function test_pause_blocksTransfers() public {
34
+ vm.prank(owner);
35
+ token.pause();
36
+ vm.prank(holder);
37
+ vm.expectRevert();
38
+ token.transfer(alice, 1 ether);
39
+ }
40
+
41
+ function test_onlyOwner_canPause() public {
42
+ vm.prank(alice);
43
+ vm.expectRevert();
44
+ token.pause();
45
+ }
46
+
47
+ function test_ownable2Step_requiresAccept() public {
48
+ vm.prank(owner);
49
+ token.transferOwnership(alice);
50
+ // still owner until accept
51
+ assertEq(token.owner(), owner);
52
+ vm.prank(alice);
53
+ token.acceptOwnership();
54
+ assertEq(token.owner(), alice);
55
+ }
56
+
57
+ function test_burn_reducesSupply() public {
58
+ vm.prank(holder);
59
+ token.burn(10 ether);
60
+ assertEq(token.totalSupply(), SUPPLY - 10 ether);
61
+ }
62
+
63
+ function test_rejectZeroHolder() public {
64
+ vm.expectRevert(bytes("SafeERC20: holder zero"));
65
+ new SafeERC20("X", "X", SUPPLY, address(0), owner);
66
+ }
67
+
68
+ function test_rejectZeroSupply() public {
69
+ vm.expectRevert(bytes("SafeERC20: supply zero"));
70
+ new SafeERC20("X", "X", 0, holder, owner);
71
+ }
72
+ }