@forgezero/runtime 0.1.14 → 0.1.16

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 (44) hide show
  1. package/README.md +27 -388
  2. package/dist/jobs.d.ts +5 -6
  3. package/dist/notify-templates.js +1 -1
  4. package/dist/schema-typebox.js +116 -7
  5. package/dist/schema.d.ts +14 -1
  6. package/dist/schema.js +116 -7
  7. package/package.json +3 -62
  8. package/contracts/foundry.toml +0 -9
  9. package/contracts/src/ColdVault.sol +0 -206
  10. package/contracts/src/DepositFactory.sol +0 -202
  11. package/contracts/src/DepositProxy.sol +0 -72
  12. package/contracts/src/IERC20.sol +0 -7
  13. package/contracts/src/MockTokens.sol +0 -32
  14. package/contracts/src/SafeTransferLib.sol +0 -31
  15. package/contracts/test/Custody.t.sol +0 -361
  16. package/contracts/test/Vectors.t.sol +0 -45
  17. package/dist/compliance.d.ts +0 -172
  18. package/dist/compliance.js +0 -168
  19. package/dist/finance/chain-addresses.d.ts +0 -130
  20. package/dist/finance/chain-addresses.js +0 -462
  21. package/dist/finance/chain-deposits.d.ts +0 -193
  22. package/dist/finance/chain-deposits.js +0 -600
  23. package/dist/finance/chain-reconcile.d.ts +0 -112
  24. package/dist/finance/chain-reconcile.js +0 -76
  25. package/dist/finance/chain-withdrawals.d.ts +0 -223
  26. package/dist/finance/chain-withdrawals.js +0 -635
  27. package/dist/finance/chain.d.ts +0 -116
  28. package/dist/finance/chain.js +0 -316
  29. package/dist/finance/commission.d.ts +0 -155
  30. package/dist/finance/commission.js +0 -423
  31. package/dist/finance/custody.d.ts +0 -68
  32. package/dist/finance/custody.js +0 -107
  33. package/dist/finance/derive.d.ts +0 -115
  34. package/dist/finance/derive.js +0 -116
  35. package/dist/finance/ledger.d.ts +0 -227
  36. package/dist/finance/ledger.js +0 -313
  37. package/dist/finance/market.d.ts +0 -209
  38. package/dist/finance/market.js +0 -112
  39. package/dist/finance/rates.d.ts +0 -178
  40. package/dist/finance/rates.js +0 -292
  41. package/dist/finance/transfers.d.ts +0 -153
  42. package/dist/finance/transfers.js +0 -292
  43. package/dist/finance/venues.d.ts +0 -190
  44. package/dist/finance/venues.js +0 -251
@@ -1,361 +0,0 @@
1
- // SPDX-License-Identifier: MIT
2
- pragma solidity ^0.8.24;
3
-
4
- import { DepositFactory } from '../src/DepositFactory.sol';
5
- import { DepositProxy } from '../src/DepositProxy.sol';
6
- import { ColdVault } from '../src/ColdVault.sol';
7
- import { MockToken, MockNoReturnToken, MockFeeToken } from '../src/MockTokens.sol';
8
-
9
- /** Foundry cheatcodes, declared inline — this repository does not use submodules. */
10
- interface Vm {
11
- function prank(address) external;
12
- function expectRevert(bytes4) external;
13
- function expectRevert(bytes calldata) external;
14
- function expectRevert() external;
15
- function deal(address, uint256) external;
16
- function addr(uint256) external pure returns (address);
17
- function sign(uint256, bytes32) external pure returns (uint8, bytes32, bytes32);
18
- function warp(uint256) external;
19
- }
20
-
21
- contract CustodyTest {
22
- Vm constant vm = Vm(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D);
23
-
24
- DepositFactory factory;
25
- MockToken token;
26
- address owner = address(this);
27
- address hot = address(0x8074);
28
-
29
- function setUp() public {
30
- factory = new DepositFactory(owner);
31
- token = new MockToken();
32
- }
33
-
34
- /* ================================================== deposit factory === */
35
-
36
- function test_addressIsDerivedWithNoChainWrite() public {
37
- // A million deposit addresses cost nothing to derive. This is the whole
38
- // advantage over generating and funding a key per address.
39
- bytes32 salt = keccak256('anything');
40
- address derived = factory.computeAddress(salt);
41
- require(derived != address(0), 'derived');
42
- require(derived.code.length == 0, 'nothing deployed yet');
43
- require(factory.computeAddress(salt) == derived, 'stable');
44
- }
45
-
46
- function test_offChainDerivationMatchesTheContract() public {
47
- // The formula a platform runs off-chain, computed here from the published
48
- // init-code hash. If these ever disagree, deposits go to an address the
49
- // platform is not watching.
50
- bytes32 salt = keccak256('user:42');
51
- address expected = address(
52
- uint160(uint256(keccak256(abi.encodePacked(
53
- bytes1(0xff), address(factory), salt, factory.PROXY_INIT_CODE_HASH()
54
- ))))
55
- );
56
- require(factory.computeAddress(salt) == expected, 'formula matches');
57
- }
58
-
59
- function test_theSaltIsOpaque_userOrInvoiceBothWork() public {
60
- // The runtime never learns what a salt means. A per-user address and a
61
- // per-invoice address are the same primitive with a different convention,
62
- // and encoding either here would force it on every platform.
63
- bytes32 perUser = keccak256(abi.encodePacked('app:user:', uint256(7)));
64
- bytes32 perInvoice = keccak256(abi.encodePacked('app:invoice:', uint256(7)));
65
- require(factory.computeAddress(perUser) != factory.computeAddress(perInvoice), 'distinct');
66
- }
67
-
68
- function test_sweepNeedsNoApprovalAndNoGasAtTheDepositAddress() public {
69
- // The property that makes this design work at all. The deposit address has
70
- // no key, no native balance and has never signed anything.
71
- bytes32 salt = keccak256('user:1');
72
- address deposit = factory.computeAddress(salt);
73
- token.mint(deposit, 500);
74
- require(deposit.balance == 0, 'no gas at the deposit address');
75
-
76
- bytes32[] memory salts = new bytes32[](1);
77
- salts[0] = salt;
78
- factory.sweepTo(salts, address(token), hot);
79
-
80
- require(token.balanceOf(deposit) == 0, 'collected');
81
- require(token.balanceOf(hot) == 500, 'delivered');
82
- }
83
-
84
- function test_theProxyIsDeployedLazilyAndOnlyOnce() public {
85
- bytes32 salt = keccak256('user:2');
86
- address deposit = factory.computeAddress(salt);
87
- token.mint(deposit, 100);
88
-
89
- bytes32[] memory salts = new bytes32[](1);
90
- salts[0] = salt;
91
- factory.sweepTo(salts, address(token), hot);
92
- require(deposit.code.length > 0, 'deployed on first sweep');
93
-
94
- token.mint(deposit, 100);
95
- factory.sweepTo(salts, address(token), hot);
96
- require(token.balanceOf(hot) == 200, 'second sweep works');
97
- }
98
-
99
- function test_onlyTheFactoryCanSweepAProxy() public {
100
- bytes32 salt = keccak256('user:3');
101
- address deposit = factory.computeAddress(salt);
102
- token.mint(deposit, 100);
103
- bytes32[] memory salts = new bytes32[](1);
104
- salts[0] = salt;
105
- factory.sweepTo(salts, address(token), hot);
106
-
107
- // Deployed now — and inert to everybody else.
108
- vm.prank(address(0xBAD));
109
- vm.expectRevert(DepositProxy.OnlyFactory.selector);
110
- DepositProxy(payable(deposit)).sweep(address(token), address(0xBAD));
111
- }
112
-
113
- function test_manyAddressesConsolidateInOneTransaction() public {
114
- bytes32[] memory salts = new bytes32[](3);
115
- for (uint256 i = 0; i < 3; i++) {
116
- salts[i] = keccak256(abi.encodePacked('user:', i));
117
- token.mint(factory.computeAddress(salts[i]), 100);
118
- }
119
- factory.sweepTo(salts, address(token), hot);
120
- require(token.balanceOf(hot) == 300, 'all three');
121
- }
122
-
123
- function test_anAlreadyEmptyAddressDoesNotUndoTheBatch() public {
124
- bytes32[] memory salts = new bytes32[](2);
125
- salts[0] = keccak256('empty');
126
- salts[1] = keccak256('funded');
127
- token.mint(factory.computeAddress(salts[1]), 250);
128
- factory.sweepTo(salts, address(token), hot);
129
- require(token.balanceOf(hot) == 250, 'funded one still swept');
130
- }
131
-
132
- function test_nativeCoinSweeps() public {
133
- bytes32 salt = keccak256('native');
134
- address deposit = factory.computeAddress(salt);
135
- vm.deal(deposit, 3 ether);
136
- bytes32[] memory salts = new bytes32[](1);
137
- salts[0] = salt;
138
- factory.sweepTo(salts, address(0), hot);
139
- require(hot.balance == 3 ether, 'native delivered');
140
- }
141
-
142
- function test_usdtStyleTokenSweeps() public {
143
- // Returns nothing from transfer. A bool-typed call reverts on decode.
144
- MockNoReturnToken usdt = new MockNoReturnToken();
145
- bytes32 salt = keccak256('usdt');
146
- usdt.mint(factory.computeAddress(salt), 900);
147
- bytes32[] memory salts = new bytes32[](1);
148
- salts[0] = salt;
149
- factory.sweepTo(salts, address(usdt), hot);
150
- require(usdt.balanceOf(hot) == 900, 'usdt swept');
151
- }
152
-
153
- function test_feeOnTransferMovesWhatActuallyArrives() public {
154
- // The proxy sweeps its measured balance, so the platform never credits a
155
- // figure the token did not deliver.
156
- MockFeeToken fee = new MockFeeToken();
157
- bytes32 salt = keccak256('fee');
158
- fee.mint(factory.computeAddress(salt), 1000);
159
- bytes32[] memory salts = new bytes32[](1);
160
- salts[0] = salt;
161
- factory.sweepTo(salts, address(fee), hot);
162
- // 1% on the way to the factory, 1% again on the way out.
163
- require(fee.balanceOf(hot) < 1000, 'fee was taken');
164
- require(fee.balanceOf(hot) > 970, 'and only the fee');
165
- }
166
-
167
- function test_aStrangerCannotSweep() public {
168
- bytes32[] memory salts = new bytes32[](0);
169
- vm.prank(address(0xBAD));
170
- vm.expectRevert(DepositFactory.NotOwner.selector);
171
- factory.sweepTo(salts, address(token), hot);
172
- }
173
- }
174
-
175
- contract VaultTest {
176
- Vm constant vm = Vm(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D);
177
-
178
- ColdVault vault;
179
- MockToken token;
180
-
181
- uint256 k1 = 0xA11CE;
182
- uint256 k2 = 0xB0B;
183
- uint256 k3 = 0xCA401;
184
- address c1;
185
- address c2;
186
- address c3;
187
- address operator = address(0x0FF);
188
- address payee = address(0xFEE);
189
-
190
- function setUp() public {
191
- c1 = vm.addr(k1);
192
- c2 = vm.addr(k2);
193
- c3 = vm.addr(k3);
194
- address[] memory custodians = new address[](3);
195
- // Sorted, because signatures must arrive in ascending signer order.
196
- (custodians[0], custodians[1], custodians[2]) = _sorted(c1, c2, c3);
197
- vault = new ColdVault(custodians, 2);
198
- token = new MockToken();
199
- token.mint(address(vault), 1_000_000);
200
- }
201
-
202
- function _sorted(address a, address b, address c) private pure returns (address, address, address) {
203
- address t;
204
- if (a > b) { t = a; a = b; b = t; }
205
- if (b > c) { t = b; b = c; c = t; }
206
- if (a > b) { t = a; a = b; b = t; }
207
- return (a, b, c);
208
- }
209
-
210
- function _sign(uint256 key, bytes32 digest) private pure returns (bytes memory) {
211
- (uint8 v, bytes32 r, bytes32 s) = vm.sign(key, digest);
212
- return abi.encodePacked(r, s, v);
213
- }
214
-
215
- /** Two signatures, ordered by signer address as the vault requires. */
216
- function _quorum(bytes32 digest) private view returns (bytes[] memory out) {
217
- out = new bytes[](2);
218
- uint256[3] memory keys = [k1, k2, k3];
219
- address[3] memory addrs = [c1, c2, c3];
220
- // Pick the two lowest addresses so the ordering rule is satisfied.
221
- uint256 lowest = 0;
222
- for (uint256 i = 1; i < 3; i++) if (addrs[i] < addrs[lowest]) lowest = i;
223
- uint256 second = lowest == 0 ? 1 : 0;
224
- for (uint256 i = 0; i < 3; i++) {
225
- if (i != lowest && addrs[i] < addrs[second]) second = i;
226
- }
227
- if (second == lowest) second = (lowest + 1) % 3;
228
- out[0] = _sign(keys[lowest], digest);
229
- out[1] = _sign(keys[second], digest);
230
- if (addrs[lowest] > addrs[second]) {
231
- bytes memory tmp = out[0]; out[0] = out[1]; out[1] = tmp;
232
- }
233
- }
234
-
235
- /* ======================================================= cold vault === */
236
-
237
- function test_quorumMovesFunds() public {
238
- bytes32 digest = vault.digestFor(address(token), payee, 5000);
239
- vault.withdraw(address(token), payee, 5000, _quorum(digest));
240
- require(token.balanceOf(payee) == 5000, 'paid');
241
- }
242
-
243
- function test_oneSignatureIsNotAQuorum() public {
244
- bytes32 digest = vault.digestFor(address(token), payee, 5000);
245
- bytes[] memory one = new bytes[](1);
246
- one[0] = _sign(k1, digest);
247
- vm.expectRevert(abi.encodeWithSelector(ColdVault.NotEnoughSignatures.selector, 1, 2));
248
- vault.withdraw(address(token), payee, 5000, one);
249
- }
250
-
251
- function test_theSameCustodianTwiceIsNotTwoCustodians() public {
252
- // Without the ascending-order rule, one custodian submits M copies of
253
- // their own approval and an M-of-N vault is a 1-of-N vault.
254
- bytes32 digest = vault.digestFor(address(token), payee, 5000);
255
- bytes[] memory doubled = new bytes[](2);
256
- doubled[0] = _sign(k1, digest);
257
- doubled[1] = _sign(k1, digest);
258
- vm.expectRevert(ColdVault.SignaturesOutOfOrder.selector);
259
- vault.withdraw(address(token), payee, 5000, doubled);
260
- }
261
-
262
- function test_aStrangersSignatureIsRejected() public {
263
- bytes32 digest = vault.digestFor(address(token), payee, 5000);
264
- bytes[] memory sigs = new bytes[](2);
265
- uint256 outsider = 0xDEAD;
266
- address outsiderAddr = vm.addr(outsider);
267
- // Ordered so the failure is "not a custodian", not the ordering rule.
268
- if (outsiderAddr < c1) {
269
- sigs[0] = _sign(outsider, digest);
270
- sigs[1] = _sign(k1, digest);
271
- } else {
272
- sigs[0] = _sign(k1, digest);
273
- sigs[1] = _sign(outsider, digest);
274
- }
275
- vm.expectRevert(abi.encodeWithSelector(ColdVault.NotACustodian.selector, outsiderAddr));
276
- vault.withdraw(address(token), payee, 5000, sigs);
277
- }
278
-
279
- function test_aSpentApprovalCannotBeReplayed() public {
280
- // The nonce is what makes a gathered signature spendable once. Without it,
281
- // a set of approvals collected today drains the vault forever.
282
- bytes32 digest = vault.digestFor(address(token), payee, 5000);
283
- bytes[] memory sigs = _quorum(digest);
284
- vault.withdraw(address(token), payee, 5000, sigs);
285
-
286
- // The nonce moved, so the same bytes now recover to SOME other address --
287
- // whichever one, it is not a custodian, and the vault refuses.
288
- uint256 before = token.balanceOf(payee);
289
- vm.expectRevert();
290
- vault.withdraw(address(token), payee, 5000, sigs);
291
- require(token.balanceOf(payee) == before, 'nothing moved twice');
292
- }
293
-
294
- function test_signaturesDoNotTransferToADifferentAmount() public {
295
- // The amount is inside what was signed, so a submitter cannot inflate it.
296
- bytes32 digest = vault.digestFor(address(token), payee, 5000);
297
- bytes[] memory sigs = _quorum(digest);
298
- vm.expectRevert();
299
- vault.withdraw(address(token), payee, 999_999, sigs);
300
- require(token.balanceOf(payee) == 0, 'nothing moved');
301
- }
302
-
303
- function test_signaturesDoNotTransferToADifferentDestination() public {
304
- bytes32 digest = vault.digestFor(address(token), payee, 5000);
305
- bytes[] memory sigs = _quorum(digest);
306
- vm.expectRevert();
307
- vault.withdraw(address(token), address(0xBAD), 5000, sigs);
308
- require(token.balanceOf(address(0xBAD)) == 0, 'nothing moved');
309
- }
310
-
311
- function test_signaturesFromAnotherVaultAreVoidHere() public {
312
- // `verifyingContract` is in the domain, so approvals gathered for one
313
- // deployment cannot be pointed at another.
314
- address[] memory custodians = new address[](3);
315
- (custodians[0], custodians[1], custodians[2]) = _sorted(c1, c2, c3);
316
- ColdVault other = new ColdVault(custodians, 2);
317
- token.mint(address(other), 10_000);
318
-
319
- bytes32 otherDigest = other.digestFor(address(token), payee, 5000);
320
- bytes[] memory sigs = _quorum(otherDigest);
321
- vm.expectRevert();
322
- vault.withdraw(address(token), payee, 5000, sigs);
323
- require(token.balanceOf(payee) == 0, 'nothing moved');
324
- }
325
-
326
- function test_aThresholdOfOneIsRefused() public {
327
- address[] memory custodians = new address[](3);
328
- (custodians[0], custodians[1], custodians[2]) = _sorted(c1, c2, c3);
329
- vm.expectRevert(ColdVault.BadThreshold.selector);
330
- new ColdVault(custodians, 1);
331
- }
332
-
333
- function test_aThresholdAboveTheSetIsRefused() public {
334
- // A vault nobody can ever open is not more secure, it is destroyed.
335
- address[] memory custodians = new address[](2);
336
- custodians[0] = c1 < c2 ? c1 : c2;
337
- custodians[1] = c1 < c2 ? c2 : c1;
338
- vm.expectRevert(ColdVault.BadThreshold.selector);
339
- new ColdVault(custodians, 3);
340
- }
341
-
342
- function test_nobodyCanRotateCustodiansWithoutTheQuorum() public {
343
- address[] memory next = new address[](2);
344
- next[0] = address(0x1);
345
- next[1] = address(0x2);
346
- vm.expectRevert(ColdVault.OnlySelf.selector);
347
- vault.setCustodians(next, 2);
348
- }
349
-
350
- /* ------------------------------------------------------------------------
351
- * There is no hot vault, deliberately.
352
- *
353
- * A hot wallet is an ADDRESS, not a contract. The factory already holds float
354
- * and already sweeps to any destination, so a separate contract to hold
355
- * working capital adds a deployment, an upgrade path and a second set of
356
- * permissions without adding a property neither of these already has.
357
- *
358
- * Operational funds sit at whatever address the platform sweeps to; the cold
359
- * vault holds what M-of-N custodians guard. Two things, each doing one job.
360
- * ---------------------------------------------------------------------- */
361
- }
@@ -1,45 +0,0 @@
1
- // SPDX-License-Identifier: MIT
2
- pragma solidity ^0.8.24;
3
-
4
- import { DepositFactory } from '../src/DepositFactory.sol';
5
- import { ColdVault } from '../src/ColdVault.sol';
6
-
7
- /**
8
- * Emit values the TypeScript client must reproduce.
9
- *
10
- * The client mirrors two pure contract functions. Checking it against itself
11
- * proves nothing — a formula wrong the same way every time is deterministic and
12
- * still points a platform at an address the factory cannot sweep.
13
- */
14
- contract VectorsTest {
15
- function test_emitVectors() public {
16
- // A fixed owner so the factory address is stable across runs.
17
- DepositFactory factory = new DepositFactory(address(0xF00D));
18
-
19
- bytes32 salt = keccak256(abi.encodePacked('myapp:user:', 'u1'));
20
-
21
- address[] memory custodians = new address[](3);
22
- custodians[0] = address(0x1111111111111111111111111111111111111111);
23
- custodians[1] = address(0x2222222222222222222222222222222222222222);
24
- custodians[2] = address(0x3333333333333333333333333333333333333333);
25
- ColdVault vault = new ColdVault(custodians, 2);
26
-
27
- bytes32 digest = vault.digestFor(
28
- address(0xdAC17F958D2ee523a2206206994597C13D831ec7),
29
- address(0x000000000000000000000000000000000000dEaD),
30
- 1_000_000
31
- );
32
-
33
- // Read from the trace; the TS test asserts against these.
34
- require(factory.computeAddress(salt) != address(0), 'derived');
35
- emit Vector('factory', abi.encode(address(factory)));
36
- emit Vector('proxyInitCodeHash', abi.encode(factory.PROXY_INIT_CODE_HASH()));
37
- emit Vector('salt', abi.encode(salt));
38
- emit Vector('depositAddress', abi.encode(factory.computeAddress(salt)));
39
- emit Vector('vault', abi.encode(address(vault)));
40
- emit Vector('chainId', abi.encode(block.chainid));
41
- emit Vector('withdrawDigest', abi.encode(digest));
42
- }
43
-
44
- event Vector(string name, bytes value);
45
- }
@@ -1,172 +0,0 @@
1
- /**
2
- * Screening — the stage that already has a seat.
3
- *
4
- * `@forgezero/runtime/finance/transfers` reserved position zero and has been running a
5
- * pass-everything placeholder there since before this file existed. That
6
- * ordering is the whole reason this is a drop-in: nothing about deposits or
7
- * withdrawals changes, and the audit trail already records the stage running.
8
- *
9
- * ## Screening is a DECISION RECORD, not a boolean
10
- *
11
- * The value of compliance work six months later is being able to say why a
12
- * transfer was allowed — not just that it was. A screen that returns true or
13
- * false gives an auditor nothing, so every check produces a verdict with the
14
- * rules that fired, the risk it scored and the list version it was screened
15
- * against. That record is the deliverable; the refusal is a side effect.
16
- *
17
- * ## Fail CLOSED on an unavailable list, and say so
18
- *
19
- * A sanctions list that cannot be reached is not "no hits". Treating an
20
- * unreachable provider as a pass is how sanctioned money moves during an
21
- * outage, and it is invisible afterwards because the trail says allowed. So an
22
- * unavailable list refuses with a retryable status — the transfer waits rather
23
- * than proceeding unscreened.
24
- *
25
- * ## What this deliberately does NOT do
26
- *
27
- * No list is bundled. A sanctions list embedded in a package is out of date the
28
- * day it publishes, and being out of date is the only failure mode that
29
- * matters. The provider is injected, and `staticList` exists for tests and for
30
- * an operator's own denylist — never as the primary source.
31
- */
32
- export declare class ComplianceError extends Error {
33
- readonly code: 'LIST_UNAVAILABLE' | 'BAD_SUBJECT';
34
- constructor(code: 'LIST_UNAVAILABLE' | 'BAD_SUBJECT', message: string);
35
- }
36
- export declare const RISK_LEVELS: readonly ["low", "medium", "high", "prohibited"];
37
- export type RiskLevel = (typeof RISK_LEVELS)[number];
38
- export declare const VERIFICATION_TIERS: readonly ["none", "basic", "verified", "enhanced"];
39
- export type VerificationTier = (typeof VERIFICATION_TIERS)[number];
40
- export interface Subject {
41
- /** The account being screened. */
42
- owner: string;
43
- /** Counterparty address, for a transfer. */
44
- address?: string;
45
- network?: string;
46
- /** Value in USD, so a threshold rule can fire. */
47
- usdValue?: number;
48
- direction?: 'deposit' | 'withdrawal';
49
- /** How far the account has verified. Drives the tier rules. */
50
- tier?: VerificationTier;
51
- /** Anything a rule wants — country, name, date of birth. */
52
- attributes?: Record<string, string>;
53
- }
54
- export interface ListEntry {
55
- /** Address, name or identifier this entry matches. */
56
- value: string;
57
- kind: 'address' | 'name' | 'country';
58
- /** Which list it came from — OFAC, an internal denylist, a chain analytics feed. */
59
- source: string;
60
- reason?: string;
61
- }
62
- export interface ScreeningList {
63
- /** Bumped whenever the list content changes. Recorded on every verdict. */
64
- readonly version: string;
65
- /** Throws `ComplianceError('LIST_UNAVAILABLE')` rather than returning empty. */
66
- match(subject: Subject): Promise<ListEntry[]>;
67
- }
68
- export interface RuleHit {
69
- rule: string;
70
- risk: RiskLevel;
71
- detail: string;
72
- }
73
- export interface Rule {
74
- name: string;
75
- /** Returns a hit, or undefined when the rule does not fire. */
76
- check(subject: Subject): RuleHit | undefined | Promise<RuleHit | undefined>;
77
- }
78
- /**
79
- * A transfer above a threshold for the tier the account has reached.
80
- *
81
- * Tiered rather than one global limit, because the whole point of verification
82
- * is that it raises what an account may move. A single threshold means either
83
- * verified accounts are throttled or unverified ones are not.
84
- */
85
- export declare const tierLimitRule: (limits: Partial<Record<VerificationTier, number>>) => Rule;
86
- /** A jurisdiction the platform will not serve. */
87
- export declare const countryRule: (prohibited: readonly string[]) => Rule;
88
- /**
89
- * A deposit from an address that has never been seen, above a threshold.
90
- *
91
- * Weak on its own and useful in combination — it is the kind of signal that
92
- * raises a transfer to review rather than refusing it, which is why it scores
93
- * `medium` and not higher.
94
- */
95
- export declare const newCounterpartyRule: (args: {
96
- aboveUsd: number;
97
- isKnown: (address: string) => boolean | Promise<boolean>;
98
- }) => Rule;
99
- export interface Verdict {
100
- decision: 'allow' | 'review' | 'refuse';
101
- risk: RiskLevel;
102
- hits: RuleHit[];
103
- listMatches: ListEntry[];
104
- /** Which list version this was screened against. The auditable part. */
105
- listVersion: string;
106
- screenedAtMs: number;
107
- subject: Subject;
108
- }
109
- export interface ScreenOptions {
110
- list?: ScreeningList;
111
- rules?: readonly Rule[];
112
- /** At or above this, the transfer is held for a human rather than refused. */
113
- reviewAt?: RiskLevel;
114
- now?: () => number;
115
- }
116
- /**
117
- * Screen a subject and produce a verdict.
118
- *
119
- * A list match is always `prohibited` — that is what a sanctions list means,
120
- * and softening it to "high risk, review it" is the decision nobody should be
121
- * able to make quietly in a config file. Rules can only ever raise the level
122
- * arrived at, never lower it.
123
- */
124
- export declare function screen(subject: Subject, options?: ScreenOptions): Promise<Verdict>;
125
- export interface StageOptions extends ScreenOptions {
126
- /** Called for every verdict, allowed or not. The case record. */
127
- record?: (verdict: Verdict) => void | Promise<void>;
128
- /** Whether a `review` verdict has already been approved by a human. */
129
- isApproved?: (subject: Subject) => boolean | Promise<boolean>;
130
- }
131
- /**
132
- * The screening stage, shaped for `@forgezero/runtime/finance/transfers`.
133
- *
134
- * Drops into the seat that was reserved at order zero. Nothing else changes —
135
- * which was the entire point of building the pipeline before the policy.
136
- *
137
- * EVERY verdict is recorded, including allowances. A trail of refusals answers
138
- * "what did we stop" and not "what did we decide", and the second question is
139
- * the one an auditor asks.
140
- */
141
- export declare function screeningStage(options?: StageOptions): {
142
- name: string;
143
- order: number;
144
- run(transfer: {
145
- owner: string;
146
- direction: "deposit" | "withdrawal";
147
- address?: string;
148
- network?: string;
149
- amount: {
150
- units: bigint;
151
- asset: string;
152
- };
153
- context?: Record<string, unknown>;
154
- }): Promise<{
155
- screenedAtMs: number;
156
- risk: "low" | "medium" | "high" | "prohibited";
157
- listVersion: string;
158
- }>;
159
- };
160
- /**
161
- * A list held in memory.
162
- *
163
- * For tests, and for an operator's own denylist alongside a real feed — never
164
- * as the primary source. A sanctions list bundled into a package is out of date
165
- * the day it publishes, and being out of date is the only failure mode that
166
- * matters here.
167
- */
168
- export declare function staticList(entries: readonly ListEntry[], version?: string): ScreeningList;
169
- /** Combine several lists. Any one being unavailable fails the whole screen. */
170
- export declare function combineLists(...lists: readonly ScreeningList[]): ScreeningList;
171
- /** A list that is not configured yet. Refuses, so nothing runs unscreened by accident. */
172
- export declare const unavailableList: (reason: string) => ScreeningList;