@aqb-foundation/aqb-agent 1.0.1 → 1.0.3
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 +4 -16
- package/README.md +68 -21
- package/WHITEPAPER.md +2 -1
- package/artifacts/MLDSA65Fallback.json +59 -0
- package/client/aqb.mjs +232 -148
- package/compile.mjs +44 -0
- package/contracts/AgentQuantumCredit.sol +234 -0
- package/contracts/AgentRegistry.sol +85 -0
- package/contracts/GenesisSale.sol +89 -0
- package/contracts/MLDSA65Fallback.sol +34 -0
- package/contracts/MLDSAVerifier.sol +65 -0
- package/contracts/PQSmartAccount.sol +105 -0
- package/contracts/interfaces/IPQVerifier.sol +19 -0
- package/contracts/mocks/MockUSDC.sol +30 -0
- package/contracts/mocks/MockVerifier.sol +13 -0
- package/docs/CARD.md +1 -1
- package/docs/LAUNCH.md +1 -1
- package/docs/MARKETING.md +3 -3
- package/docs/SPEC.md +5 -4
- package/docs/articles/01-payment-rail-agents.md +55 -0
- package/docs/articles/01.body +56 -0
- package/docs/articles/02-gated-transfers.md +36 -0
- package/docs/articles/03-peg-without-oracle.md +33 -0
- package/docs/articles/04-discovery-over-landing.md +42 -0
- package/docs/articles/04-discovery-over-landing.md.body +40 -0
- package/docs/articles/04-discovery-over-landing.md.title +1 -0
- package/docs/articles/05-identity-survives-key.md +27 -0
- package/docs/articles/05-identity-survives-key.md.body +25 -0
- package/docs/articles/05-identity-survives-key.md.title +1 -0
- package/docs/articles/06-what-an-agent-needs.md +30 -0
- package/docs/articles/06-what-an-agent-needs.md.body +28 -0
- package/docs/articles/06-what-an-agent-needs.md.title +1 -0
- package/docs/articles/body.json +1 -0
- package/docs/articles/body.txt +50 -0
- package/docs/articles/title.txt +1 -0
- package/docs/pq-note.md +1 -0
- package/index.js +241 -0
- package/mcp/server.mjs +118 -25
- package/package.json +41 -11
- package/pq-deployment.json +14 -0
- package/registry/agent-coin.json +26 -11
- package/registry/llms.txt +1 -1
- package/test/e2e.mjs +100 -0
- package/test/evm.mjs +69 -0
- package/test/local.mjs +86 -0
- package/test/pq.mjs +37 -0
- package/test/smoke.mjs +27 -0
- package/vendor/mldsa/LICENSE +21 -0
- package/vendor/mldsa/MLDSAKeyFactory.json +570 -0
- package/vendor/mldsa/MLDSAVerifier.json +154 -0
- package/vendor/mldsa/README.md +20 -0
package/client/aqb.mjs
CHANGED
|
@@ -1,149 +1,233 @@
|
|
|
1
|
-
// AQB agent SDK — the only client an agent needs.
|
|
2
|
-
// docs/SPEC.md is normative; this file is a thin, boring implementation of it.
|
|
3
|
-
import { Contract, JsonRpcProvider, Wallet, keccak256, toUtf8Bytes, solidityPacked } from "ethers";
|
|
4
|
-
import { newIdentity, signDigest, stealthFor, stealthCheck } from "../pq/pq.mjs";
|
|
5
|
-
export { newIdentity, stealthFor, stealthCheck };
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
export const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
"function
|
|
20
|
-
"function
|
|
21
|
-
"function
|
|
22
|
-
"function
|
|
23
|
-
"function
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
"function
|
|
28
|
-
"function
|
|
29
|
-
"function
|
|
30
|
-
"function
|
|
31
|
-
|
|
32
|
-
];
|
|
1
|
+
// AQB agent SDK — the only client an agent needs.
|
|
2
|
+
// docs/SPEC.md is normative; this file is a thin, boring implementation of it.
|
|
3
|
+
import { Contract, JsonRpcProvider, Wallet, keccak256, toUtf8Bytes, solidityPacked } from "ethers";
|
|
4
|
+
import { newIdentity, signDigest, stealthFor, stealthCheck } from "../pq/pq.mjs";
|
|
5
|
+
export { newIdentity, stealthFor, stealthCheck };
|
|
6
|
+
|
|
7
|
+
export const CHAINS = { base: 8453, "base-sepolia": 84532 };
|
|
8
|
+
export const USDC = {
|
|
9
|
+
8453: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
|
|
10
|
+
84532: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
|
|
11
|
+
};
|
|
12
|
+
export const DEFAULT_RPC = {
|
|
13
|
+
8453: "https://mainnet.base.org",
|
|
14
|
+
84532: "https://sepolia.base.org",
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const ABI_TOKEN = [
|
|
18
|
+
"function mint(uint256 usd6) returns (uint256)",
|
|
19
|
+
"function redeem(uint256 aqb18) returns (uint256)",
|
|
20
|
+
"function balanceOf(address) view returns (uint256)",
|
|
21
|
+
"function collateralRatio() view returns (uint256)",
|
|
22
|
+
"function transfer(address,uint256) returns (bool)",
|
|
23
|
+
"function totalSupply() view returns (uint256)",
|
|
24
|
+
];
|
|
25
|
+
const ABI_REG = [
|
|
26
|
+
"function register(bytes32 pqKeyHash, bytes32 cardHash)",
|
|
27
|
+
"function isAgent(address) view returns (bool)",
|
|
28
|
+
"function updateCard(bytes32 cardHash)",
|
|
29
|
+
"function announce(address stealth, bytes ephPubKey, bytes1 viewTag, bytes32 meta)",
|
|
30
|
+
"function agentCount() view returns (uint256)",
|
|
31
|
+
];
|
|
33
32
|
const ABI_USDC = ["function approve(address,uint256) returns (bool)", "function balanceOf(address) view returns (uint256)"];
|
|
34
|
-
const ABI_ACCOUNT = [
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
return
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// ----
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
return
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
async
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
33
|
+
const ABI_ACCOUNT = [
|
|
34
|
+
"function digest(address,uint256,bytes,uint256) view returns (bytes32)",
|
|
35
|
+
"function nonce() view returns (uint256)",
|
|
36
|
+
"function execute(address,uint256,bytes,bytes)",
|
|
37
|
+
"function pqKeyHash() view returns (bytes32)",
|
|
38
|
+
];
|
|
39
|
+
const ABI_PAIR = [
|
|
40
|
+
"function getReserves() view returns (uint112,uint112,uint32)",
|
|
41
|
+
"function token0() view returns (address)",
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
/// ML-DSA-65 keypair stand-in: `pq` may be any FIPS-204 implementation.
|
|
45
|
+
/// pubKey must be 1952 bytes, sig 3309 bytes, scheme tag 0x02.
|
|
46
|
+
export function keyHash(pubKey) {
|
|
47
|
+
return keccak256(pubKey);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/// Recursively sort object keys so a card has one deterministic JSON form.
|
|
51
|
+
function canonicalize(value) {
|
|
52
|
+
if (Array.isArray(value)) return value.map(canonicalize);
|
|
53
|
+
if (value && typeof value === "object") {
|
|
54
|
+
const out = {};
|
|
55
|
+
for (const key of Object.keys(value).sort()) out[key] = canonicalize(value[key]);
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
return value;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/// Deterministic 32-byte agent card commitment (SPEC §1.4).
|
|
62
|
+
/// Nested objects are sorted too; a shallow JSON.stringify replacer is not enough.
|
|
63
|
+
export function cardHash(card) {
|
|
64
|
+
return keccak256(toUtf8Bytes(JSON.stringify(canonicalize(card))));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/// Stealth address + one-byte view tag (SPEC §5.2).
|
|
68
|
+
/// Matches pq.stealthFor(): h = keccak256(sharedSecret || ephPubKey).
|
|
69
|
+
export function stealth(ephPubKey, sharedSecret) {
|
|
70
|
+
const h = keccak256(solidityPacked(["bytes32", "bytes"], [sharedSecret, ephPubKey]));
|
|
71
|
+
return {
|
|
72
|
+
addr: ("0x" + h.slice(26)).toLowerCase(),
|
|
73
|
+
viewTag: "0x" + h.slice(2, 4),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export class AqbAgent {
|
|
78
|
+
constructor({ rpc, chain = "base", privateKey, token, registry }) {
|
|
79
|
+
this.chainId = typeof chain === "number" ? chain : CHAINS[chain];
|
|
80
|
+
this.provider = new JsonRpcProvider(rpc || DEFAULT_RPC[this.chainId], this.chainId, { staticNetwork: true });
|
|
81
|
+
this.signer = privateKey ? new Wallet(privateKey, this.provider) : null;
|
|
82
|
+
this.tokenAddr = token;
|
|
83
|
+
this.registryAddr = registry;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
requireSigner() {
|
|
87
|
+
if (!this.signer) throw new Error("read-only client: pass privateKey");
|
|
88
|
+
return this.signer;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
token(ro = false) {
|
|
92
|
+
return new Contract(this.tokenAddr, ABI_TOKEN, ro ? this.provider : this.requireSigner());
|
|
93
|
+
}
|
|
94
|
+
registry(ro = false) {
|
|
95
|
+
return new Contract(this.registryAddr, ABI_REG, ro ? this.provider : this.requireSigner());
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ---- read
|
|
99
|
+
usdBalance(a = this.signer?.address) {
|
|
100
|
+
if (!a) throw new Error("address required");
|
|
101
|
+
return new Contract(USDC[this.chainId], ABI_USDC, this.provider).balanceOf(a);
|
|
102
|
+
}
|
|
103
|
+
isAgent(a = this.signer?.address) {
|
|
104
|
+
if (!a) throw new Error("address required");
|
|
105
|
+
return this.registry(true).isAgent(a);
|
|
106
|
+
}
|
|
107
|
+
balance(a = this.signer?.address) {
|
|
108
|
+
if (!a) throw new Error("address required");
|
|
109
|
+
return this.token(true).balanceOf(a);
|
|
110
|
+
}
|
|
111
|
+
ratio() { return this.token(true).collateralRatio(); }
|
|
112
|
+
|
|
113
|
+
/// Uniswap V2 spot quote: how much USDC out for `amount` AQB (18dp).
|
|
114
|
+
async poolQuote(pair, amount = 10n ** 18n) {
|
|
115
|
+
const c = new Contract(pair, ABI_PAIR, this.provider);
|
|
116
|
+
const [r0, r1] = await c.getReserves();
|
|
117
|
+
const token0 = (await c.token0()).toLowerCase();
|
|
118
|
+
const [rAqb, rUsd] = token0 === this.tokenAddr.toLowerCase() ? [r0, r1] : [r1, r0];
|
|
119
|
+
if (rAqb === 0n || rUsd === 0n) return { amount, usdc: 0n, reserves: { aqb: 0n, usdc: 0n } };
|
|
120
|
+
const inWithFee = amount * 997n;
|
|
121
|
+
const out = (BigInt(rUsd) * inWithFee) / (BigInt(rAqb) * 1000n + inWithFee);
|
|
122
|
+
return { amount, usdc: out, reserves: { aqb: rAqb, usdc: rUsd } };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ---- write (EOA; the EOA must already be an agent to mint/redeem)
|
|
126
|
+
async register() {
|
|
127
|
+
throw new Error(
|
|
128
|
+
"AgentRegistry.register must be called by a PQSmartAccount; use registerFromAccount(account, card) or `aqb-agent register --account 0x...`"
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async fund(usd) {
|
|
133
|
+
const usdc = new Contract(USDC[this.chainId], ABI_USDC, this.requireSigner());
|
|
134
|
+
await (await usdc.approve(this.tokenAddr, usd)).wait();
|
|
135
|
+
return (await (await this.token().mint(usd)).wait()).hash;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async cashOut(aqb) {
|
|
139
|
+
return (await (await this.token().redeem(aqb)).wait()).hash;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async pay(to, aqb) {
|
|
143
|
+
return (await (await this.token().transfer(to, aqb)).wait()).hash;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ---- PQ smart-account path
|
|
147
|
+
account(addr, identity = null) {
|
|
148
|
+
const c = new Contract(addr, ABI_ACCOUNT, this.requireSigner());
|
|
149
|
+
return { c, identity, addr };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
_account(acc) {
|
|
153
|
+
return typeof acc === "string" ? this.account(acc) : acc;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/// Execute through a PQSmartAccount. `scheme` is "ecdsa" (0x01, current
|
|
157
|
+
/// on-chain path) or "pq" (0x02, requires a live ML-DSA verifier).
|
|
158
|
+
async exec(acc, target, data, scheme = "ecdsa", value = 0n) {
|
|
159
|
+
const a = this._account(acc);
|
|
160
|
+
const nonce = await a.c.nonce();
|
|
161
|
+
const digest = await a.c.digest(target, value, data, nonce);
|
|
162
|
+
let sig;
|
|
163
|
+
if (scheme === "pq") {
|
|
164
|
+
if (!a.identity) throw new Error("pq scheme needs an identity");
|
|
165
|
+
sig = signDigest(a.identity, digest);
|
|
166
|
+
} else {
|
|
167
|
+
const s = this.requireSigner().signingKey.sign(digest);
|
|
168
|
+
sig = "0x01" + s.r.slice(2) + s.s.slice(2) + s.v.toString(16).padStart(2, "0");
|
|
169
|
+
}
|
|
170
|
+
const tx = await a.c.execute(target, value, data, sig);
|
|
171
|
+
return (await tx.wait()).hash;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async execPQ(acc, target, data, value = 0n) {
|
|
175
|
+
return this.exec(acc, target, data, "pq", value);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async registerFromAccount(acc, card, scheme = "ecdsa") {
|
|
179
|
+
const a = this._account(acc);
|
|
180
|
+
const pqKeyHash = await a.c.pqKeyHash();
|
|
181
|
+
const data = this.registry(true).interface.encodeFunctionData("register", [pqKeyHash, cardHash(card)]);
|
|
182
|
+
return this.exec(a, this.registryAddr, data, scheme);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async registerViaAccount(accountAddr, card, scheme = "ecdsa") {
|
|
186
|
+
return this.registerFromAccount(accountAddr, card, scheme);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async updateCardFromAccount(acc, card, scheme = "ecdsa") {
|
|
190
|
+
const data = this.registry(true).interface.encodeFunctionData("updateCard", [cardHash(card)]);
|
|
191
|
+
return this.exec(acc, this.registryAddr, data, scheme);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async fundFromAccount(acc, usd, scheme = "ecdsa") {
|
|
195
|
+
const usdc = new Contract(USDC[this.chainId], ABI_USDC, this.provider);
|
|
196
|
+
await this.exec(acc, USDC[this.chainId], usdc.interface.encodeFunctionData("approve", [this.tokenAddr, usd]), scheme);
|
|
197
|
+
return this.exec(acc, this.tokenAddr, this.token().interface.encodeFunctionData("mint", [usd]), scheme);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async payFromAccount(acc, to, aqb, scheme = "ecdsa") {
|
|
201
|
+
return this.exec(acc, this.tokenAddr, this.token().interface.encodeFunctionData("transfer", [to, aqb]), scheme);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async redeemFromAccount(acc, aqb, scheme = "ecdsa") {
|
|
205
|
+
return this.exec(acc, this.tokenAddr, this.token().interface.encodeFunctionData("redeem", [aqb]), scheme);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async cashOutFromAccount(acc, aqb, scheme = "ecdsa") {
|
|
209
|
+
return this.redeemFromAccount(acc, aqb, scheme);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async announceFromAccount(acc, ephPubKey, sharedSecret, metaHash = "0x" + "00".repeat(32), scheme = "ecdsa") {
|
|
213
|
+
const { addr, viewTag } = stealth(ephPubKey, sharedSecret);
|
|
214
|
+
const data = this.registry(true).interface.encodeFunctionData("announce", [addr, ephPubKey, viewTag, metaHash]);
|
|
215
|
+
return this.exec(acc, this.registryAddr, data, scheme);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/// Discovery: read /.well-known/agent-coin.json from a host, or emit your own.
|
|
220
|
+
export async function discover(host) {
|
|
221
|
+
const base = host.replace(/^https?:\/\//, "").replace(/\/$/, "");
|
|
222
|
+
const r = await fetch(`https://${base}/.well-known/agent-coin.json`);
|
|
223
|
+
if (!r.ok) throw new Error(`no AQB discovery at ${host}`);
|
|
224
|
+
return r.json();
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export function wellKnown({ token, registry, verifier, chainId = 8453, vault, pool, sale }) {
|
|
228
|
+
return {
|
|
229
|
+
aqb: 1, chainId, token, registry, verifier,
|
|
230
|
+
vault: vault || token, pool, sale,
|
|
231
|
+
pq: "ML-DSA-65", kem: "ML-KEM-768",
|
|
232
|
+
};
|
|
233
|
+
}
|
package/compile.mjs
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Compile check: solc standard-JSON, optimized, no framework.
|
|
2
|
+
import solc from "solc";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
const root = "contracts";
|
|
7
|
+
const sources = {};
|
|
8
|
+
(function walk(dir) {
|
|
9
|
+
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
10
|
+
const p = path.join(dir, e.name);
|
|
11
|
+
if (e.isDirectory()) walk(p);
|
|
12
|
+
else if (p.endsWith(".sol"))
|
|
13
|
+
sources[p.replace(/\\/g, "/")] = { content: fs.readFileSync(p, "utf8") };
|
|
14
|
+
}
|
|
15
|
+
})(root);
|
|
16
|
+
|
|
17
|
+
const input = {
|
|
18
|
+
language: "Solidity",
|
|
19
|
+
sources,
|
|
20
|
+
settings: {
|
|
21
|
+
optimizer: { enabled: true, runs: 200 },
|
|
22
|
+
evmVersion: "cancun",
|
|
23
|
+
outputSelection: { "*": { "*": ["abi", "evm.bytecode.object", "evm.deployedBytecode.object"] } },
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const out = JSON.parse(solc.compile(JSON.stringify(input)));
|
|
28
|
+
const errs = (out.errors || []).filter((e) => e.severity === "error");
|
|
29
|
+
for (const e of out.errors || []) {
|
|
30
|
+
if (e.severity !== "error") continue;
|
|
31
|
+
console.log("ERR", e.formattedMessage);
|
|
32
|
+
}
|
|
33
|
+
if (errs.length) process.exit(1);
|
|
34
|
+
|
|
35
|
+
fs.mkdirSync("artifacts", { recursive: true });
|
|
36
|
+
for (const [file, ctrs] of Object.entries(out.contracts)) {
|
|
37
|
+
for (const [name, c] of Object.entries(ctrs)) {
|
|
38
|
+
fs.writeFileSync(`artifacts/${name}.json`, JSON.stringify({ file, abi: c.abi, bytecode: "0x" + c.evm.bytecode.object }, null, 1));
|
|
39
|
+
const size = c.evm.deployedBytecode.object.length / 2;
|
|
40
|
+
if (size > 24576) { console.log(`SIZE TOO BIG ${name} ${size}`); process.exitCode = 1; }
|
|
41
|
+
else console.log(`ok ${name} ${size}B`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
console.log("OK");
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity 0.8.24;
|
|
3
|
+
|
|
4
|
+
import {AgentRegistry} from "./AgentRegistry.sol";
|
|
5
|
+
|
|
6
|
+
interface IERC20 {
|
|
7
|
+
function transfer(address to, uint256 amount) external returns (bool);
|
|
8
|
+
function transferFrom(address from, address to, uint256 amount) external returns (bool);
|
|
9
|
+
function balanceOf(address who) external view returns (uint256);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/// @title AgentQuantumCredit (AQB)
|
|
13
|
+
/// @notice Agent-only, USD-pegged, post-quantum-authenticated credit token.
|
|
14
|
+
///
|
|
15
|
+
/// Peg: fully collateralized, 1 AQB == 1 USD at all times.
|
|
16
|
+
/// Only real dollars enter: AQB is minted exclusively against 1:1 USDC (real
|
|
17
|
+
/// fiat-backed dollars) sitting in this contract, and burned to withdraw them.
|
|
18
|
+
/// No algorithmic peg, no governance discretion over price, no oracle.
|
|
19
|
+
///
|
|
20
|
+
/// Supply: hard cap 900,000,000,000 AQB. Circulating supply therefore equals
|
|
21
|
+
/// dollars on deposit, and can never exceed the cap.
|
|
22
|
+
///
|
|
23
|
+
/// Access: minting requires a registered agent (AgentRegistry). Transfers are
|
|
24
|
+
/// allowed to registered agents and to whitelisted pool/escrow addresses so
|
|
25
|
+
/// liquidity venues work without opening the token to human retail flow.
|
|
26
|
+
contract AgentQuantumCredit {
|
|
27
|
+
string public constant name = "AgentQubit";
|
|
28
|
+
string public constant symbol = "AQB";
|
|
29
|
+
uint8 public constant decimals = 18;
|
|
30
|
+
uint256 public constant MAX_SUPPLY = 900_000_000_000 * 10 ** 18;
|
|
31
|
+
uint256 internal constant SCALE = 1e12; // USDC(6) -> AQB(18)
|
|
32
|
+
|
|
33
|
+
IERC20 public immutable usdc;
|
|
34
|
+
AgentRegistry public immutable registry;
|
|
35
|
+
address public governor;
|
|
36
|
+
|
|
37
|
+
uint256 public totalSupply;
|
|
38
|
+
uint256 public genesisSupply;
|
|
39
|
+
uint256 public genesisUnsold;
|
|
40
|
+
uint256 public backing; // dollars actually in the vault (6dp)
|
|
41
|
+
address public sale;
|
|
42
|
+
bool public saleOpen = true;
|
|
43
|
+
mapping(address => uint256) public balanceOf;
|
|
44
|
+
mapping(address => mapping(address => uint256)) public allowance;
|
|
45
|
+
mapping(address => bool) public pool; // whitelisted venues / escrows
|
|
46
|
+
|
|
47
|
+
event Transfer(address indexed from, address indexed to, uint256 amount);
|
|
48
|
+
event Approval(address indexed owner, address indexed spender, uint256 amount);
|
|
49
|
+
event Minted(address indexed agent, uint256 usdIn, uint256 aqbOut);
|
|
50
|
+
event Redeemed(address indexed agent, uint256 aqbIn, uint256 usdOut);
|
|
51
|
+
event GenesisMinted(address indexed to, uint256 amount);
|
|
52
|
+
event BackingDeposited(uint256 usdIn);
|
|
53
|
+
event SaleSet(address indexed sale);
|
|
54
|
+
event SaleClosed();
|
|
55
|
+
event AllotmentSold(uint256 amount);
|
|
56
|
+
event PoolSet(address indexed venue, bool allowed);
|
|
57
|
+
event GovernorSet(address indexed governor);
|
|
58
|
+
|
|
59
|
+
error NotAgent();
|
|
60
|
+
error NotGovernor();
|
|
61
|
+
error NotSale();
|
|
62
|
+
error BadAmount();
|
|
63
|
+
error CapExceeded();
|
|
64
|
+
error TransferBlocked();
|
|
65
|
+
error AlreadyGenesis();
|
|
66
|
+
error SaleStillOpen();
|
|
67
|
+
|
|
68
|
+
modifier onlyGovernor() {
|
|
69
|
+
if (msg.sender != governor) revert NotGovernor();
|
|
70
|
+
_;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
constructor(address usdc_, address registry_, address governor_) {
|
|
74
|
+
usdc = IERC20(usdc_);
|
|
75
|
+
registry = AgentRegistry(registry_);
|
|
76
|
+
governor = governor_;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function setGovernor(address g) external onlyGovernor {
|
|
80
|
+
governor = g;
|
|
81
|
+
emit GovernorSet(g);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function setPool(address venue, bool allowed) external onlyGovernor {
|
|
85
|
+
pool[venue] = allowed;
|
|
86
|
+
emit PoolSet(venue, allowed);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ------------------------------------------------------- founder allotment
|
|
90
|
+
|
|
91
|
+
/// @notice Genesis allotment to the inventor. Minted once, before any USDC
|
|
92
|
+
/// exists, and displayed/sold FIRST through the GenesisSale contract.
|
|
93
|
+
/// Every genesis coin sold brings one real dollar into this vault, so
|
|
94
|
+
/// the 1:1 claim stays whole for the coins actually in circulation.
|
|
95
|
+
function genesisMint(address to, uint256 amount) external onlyGovernor {
|
|
96
|
+
if (genesisSupply != 0) revert AlreadyGenesis();
|
|
97
|
+
if (to == address(0) || amount == 0) revert BadAmount();
|
|
98
|
+
if (amount > MAX_SUPPLY) revert CapExceeded();
|
|
99
|
+
genesisSupply = amount;
|
|
100
|
+
genesisUnsold = amount;
|
|
101
|
+
_mint(to, amount);
|
|
102
|
+
emit GenesisMinted(to, amount);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function setSale(address sale_) external onlyGovernor {
|
|
106
|
+
sale = sale_;
|
|
107
|
+
pool[sale_] = true; // the sale escrow may receive coins
|
|
108
|
+
emit SaleSet(sale_);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/// @notice Governor or the sale contract may close the genesis sale; public
|
|
112
|
+
/// mint stays shut while it is open so founder coins sell first.
|
|
113
|
+
function closeSale() external {
|
|
114
|
+
if (msg.sender != governor && msg.sender != sale) revert NotGovernor();
|
|
115
|
+
saleOpen = false;
|
|
116
|
+
emit SaleClosed();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/// @notice Called by the sale contract when it escrows a buyer's dollars:
|
|
120
|
+
/// credits the vault without minting (coins came from the allotment).
|
|
121
|
+
function depositBacking(uint256 usdAmount) external {
|
|
122
|
+
if (msg.sender != sale) revert NotSale();
|
|
123
|
+
backing += usdAmount;
|
|
124
|
+
emit BackingDeposited(usdAmount);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ---------------------------------------------------------------- peg core
|
|
128
|
+
|
|
129
|
+
/// @notice Buy AQB with real dollars (USDC). 1 USD in -> 1 AQB out.
|
|
130
|
+
/// Closed while the genesis allotment is still on sale.
|
|
131
|
+
function mint(uint256 usdAmount) external returns (uint256 aqbOut) {
|
|
132
|
+
if (saleOpen) revert SaleStillOpen();
|
|
133
|
+
if (!registry.isAgent(msg.sender)) revert NotAgent();
|
|
134
|
+
if (usdAmount == 0) revert BadAmount();
|
|
135
|
+
aqbOut = usdAmount * SCALE;
|
|
136
|
+
if (totalSupply + aqbOut > MAX_SUPPLY) revert CapExceeded();
|
|
137
|
+
require(usdc.transferFrom(msg.sender, address(this), usdAmount), "USDC_PULL");
|
|
138
|
+
backing += usdAmount;
|
|
139
|
+
_mint(msg.sender, aqbOut);
|
|
140
|
+
emit Minted(msg.sender, usdAmount, aqbOut);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/// @notice Sell AQB back for real dollars. 1 AQB -> 1 USD out.
|
|
144
|
+
function redeem(uint256 aqbAmount) external returns (uint256 usdOut) {
|
|
145
|
+
if (!registry.isAgent(msg.sender)) revert NotAgent();
|
|
146
|
+
if (aqbAmount == 0 || aqbAmount % SCALE != 0) revert BadAmount();
|
|
147
|
+
_burn(msg.sender, aqbAmount);
|
|
148
|
+
usdOut = aqbAmount / SCALE;
|
|
149
|
+
backing -= usdOut;
|
|
150
|
+
require(usdc.transfer(msg.sender, usdOut), "USDC_PUSH");
|
|
151
|
+
emit Redeemed(msg.sender, aqbAmount, usdOut);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/// @notice AQB that has actually been sold/backed (genesis allotment minus
|
|
155
|
+
/// the part still unsold). This is the number the peg must cover.
|
|
156
|
+
function backedSupply() public view returns (uint256) {
|
|
157
|
+
return totalSupply - genesisUnsold;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/// @notice Collateral ratio in 1e18 units against live coins (1e18 == fully backed).
|
|
161
|
+
function collateralRatio() external view returns (uint256) {
|
|
162
|
+
uint256 live = backedSupply();
|
|
163
|
+
if (live == 0) return 1e18;
|
|
164
|
+
return (usdc.balanceOf(address(this)) * SCALE * 1e18) / live;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function minimum(uint256 a, uint256 b) internal pure returns (uint256) {
|
|
168
|
+
return a < b ? a : b;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ------------------------------------------------------------------ ERC20
|
|
172
|
+
|
|
173
|
+
function transfer(address to, uint256 amount) external returns (bool) {
|
|
174
|
+
_transfer(msg.sender, to, amount);
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function approve(address spender, uint256 amount) external returns (bool) {
|
|
179
|
+
allowance[msg.sender][spender] = amount;
|
|
180
|
+
emit Approval(msg.sender, spender, amount);
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function transferFrom(address from, address to, uint256 amount) external returns (bool) {
|
|
185
|
+
uint256 allowed = allowance[from][msg.sender];
|
|
186
|
+
if (allowed != type(uint256).max) {
|
|
187
|
+
if (allowed < amount) revert BadAmount();
|
|
188
|
+
allowance[from][msg.sender] = allowed - amount;
|
|
189
|
+
}
|
|
190
|
+
_transfer(from, to, amount);
|
|
191
|
+
return true;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function _transfer(address from, address to, uint256 amount) internal {
|
|
195
|
+
if (!_receivable(to)) revert TransferBlocked();
|
|
196
|
+
uint256 bal = balanceOf[from];
|
|
197
|
+
if (bal < amount) revert BadAmount();
|
|
198
|
+
unchecked {
|
|
199
|
+
balanceOf[from] = bal - amount;
|
|
200
|
+
balanceOf[to] += amount;
|
|
201
|
+
}
|
|
202
|
+
emit Transfer(from, to, amount);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function _receivable(address to) internal view returns (bool) {
|
|
206
|
+
return to == address(0) == false ? (registry.isAgent(to) || pool[to]) : false;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function _mint(address to, uint256 amount) internal {
|
|
210
|
+
totalSupply += amount;
|
|
211
|
+
unchecked {
|
|
212
|
+
balanceOf[to] += amount;
|
|
213
|
+
}
|
|
214
|
+
emit Transfer(address(0), to, amount);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function _burn(address from, uint256 amount) internal {
|
|
218
|
+
uint256 bal = balanceOf[from];
|
|
219
|
+
if (bal < amount) revert BadAmount();
|
|
220
|
+
unchecked {
|
|
221
|
+
balanceOf[from] = bal - amount;
|
|
222
|
+
totalSupply -= amount;
|
|
223
|
+
}
|
|
224
|
+
emit Transfer(from, address(0), amount);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/// @notice Sale tells the vault that allotment coins sold, so they stop
|
|
228
|
+
/// counting as unsold and start counting as backed supply.
|
|
229
|
+
function noteSold(uint256 amount) external {
|
|
230
|
+
if (msg.sender != sale) revert NotSale();
|
|
231
|
+
genesisUnsold -= amount;
|
|
232
|
+
emit AllotmentSold(amount);
|
|
233
|
+
}
|
|
234
|
+
}
|