@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.
- package/README.md +37 -10
- package/SECURITY.md +11 -11
- package/SETUP.md +76 -16
- package/bin/desk-server.mjs +3 -1
- package/docs/architecture.md +10 -9
- package/package.json +13 -7
- package/protocols/templates/safe-erc20/SECURITY.md +16 -0
- package/protocols/templates/safe-erc20/foundry.toml +12 -0
- package/protocols/templates/safe-erc20/remappings.txt +2 -0
- package/protocols/templates/safe-erc20/src/SafeERC20.sol +53 -0
- package/protocols/templates/safe-erc20/test/SafeERC20.t.sol +72 -0
- package/public/oracle-splash/index.html +648 -598
- package/public/oracle-splash/mesh-graph.png +0 -0
- package/public/oracle-splash/mesh-graph.svg +216 -0
- package/scripts/protocol-template-gate.mjs +15 -0
- package/skills/oracle-protocol-builder/SKILL.md +45 -24
- package/src/data/catalog.mjs +9 -0
- package/src/data/desk-data.mjs +24 -0
- package/src/data/providers/bitcoin-esplora.mjs +2 -16
- package/src/data/providers/cowswap.mjs +9 -96
- package/src/data/providers/evm-rpc.mjs +19 -0
- package/src/data/providers/hl-assets.mjs +165 -0
- package/src/data/providers/hl-info.mjs +7 -1
- package/src/data/providers/hl-outcome.mjs +61 -0
- package/src/data/providers/hl-perps.mjs +40 -35
- package/src/data/providers/magiceden-sol.mjs +7 -6
- package/src/data/providers/poly-clob.mjs +460 -0
- package/src/data/providers/satflow.mjs +5 -4
- package/src/data/providers/solana-rpc.mjs +9 -0
- package/src/data/providers/uniswap-v3.mjs +3 -2
- package/src/index.mjs +1 -0
- package/src/onboarding/agent-keys.mjs +4 -0
- package/src/prepare-envelope.mjs +108 -0
- package/src/protocol-templates/gate.mjs +179 -0
- package/src/protocol-templates/prepare-deploy.mjs +81 -0
- 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
|
-
|
|
15
|
-
broadcasts.
|
|
16
|
-
|
|
17
|
-
|
|
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** (
|
|
82
|
-
|
|
83
|
-
|
|
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 (
|
|
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
|
-
|
|
198
|
-
|
|
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
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
1. **Owner-local signer modules
|
|
77
|
-
`
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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,
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
9
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
146
|
-
|
|
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
|
|
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
|
-
|
|
157
|
-
|
|
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`.
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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`.
|
package/bin/desk-server.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
|
package/docs/architecture.md
CHANGED
|
@@ -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
|
-
|
|
|
26
|
-
|
|
|
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
|
|
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
|
|
131
|
+
## What is not in the public package
|
|
131
132
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
|
4
|
-
"description": "Oracle: prepare-only multichain agent control plane. Policy-bounded intents for a user-signed wallet. Self-custody by default
|
|
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
|
-
"
|
|
87
|
-
"
|
|
88
|
-
"
|
|
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,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
|
+
}
|