Skip to content

Commit 435da41

Browse files
authored
Merge pull request #35 from usherlabs/finalize-policies-2
Add deposit policy enforcement and coin-based withdrawal constraints
2 parents 1d20e30 + ea89a02 commit 435da41

7 files changed

Lines changed: 671 additions & 30 deletions

File tree

POLICY.md

Lines changed: 135 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,16 @@
22

33
This broker uses a JSON policy file to restrict:
44

5-
- **Withdrawals**: permitted exchanges, networks, and destination address whitelist
5+
- **Withdrawals**: permitted exchanges, networks, tokens, and destination address whitelist
6+
- **Deposits**: permitted exchanges, networks, and tokens (gates deposit address fetching)
67
- **Orders**: permitted exchanges/pairs, plus optional directional conversion limits
78

89
The policy is loaded at startup. If you start the broker with a **policy file path**, the file is watched and policies are reloaded when the file changes.
910

1011
## Quick start
1112

12-
- **Example policy**: `policy/policy.json`
13+
- **Example policy**: `policy/policy.example.json`
14+
- **Backtest policy**: `policy/policy.backtest.json`
1315
- **CLI usage**:
1416

1517
```bash
@@ -23,7 +25,7 @@ Top-level keys (all required):
2325
| Key | Type | Description |
2426
|-----|------|-------------|
2527
| `withdraw` | `{ rule: WithdrawRuleEntry[] }` | Withdrawal restrictions (at least one rule entry required) |
26-
| `deposit` | `{}` | Placeholder object — not enforced today |
28+
| `deposit` | `{ rule?: DepositRuleEntry[] }` | Deposit restrictions (optional; omitting `rule` or using `{}` allows all) |
2729
| `order` | `{ rule: OrderRule }` | Order/conversion restrictions |
2830

2931
Canonical type: `PolicyConfig` in `src/types.ts`.
@@ -32,10 +34,14 @@ Canonical type: `PolicyConfig` in `src/types.ts`.
3234
{
3335
"withdraw": {
3436
"rule": [
35-
{ "exchange": "BINANCE", "network": "ARBITRUM", "whitelist": [] }
37+
{ "exchange": "BINANCE", "network": "ARBITRUM", "coins": ["ETH", "USDT"], "whitelist": ["0x..."] }
38+
]
39+
},
40+
"deposit": {
41+
"rule": [
42+
{ "exchange": "BINANCE", "network": "ARBITRUM", "coins": ["ETH", "USDT"] }
3643
]
3744
},
38-
"deposit": {},
3945
"order": { "rule": { "markets": [], "limits": [] } }
4046
}
4147
```
@@ -124,6 +130,26 @@ Accepted values:
124130

125131
---
126132

133+
### `withdraw.rule[].coins`
134+
135+
| | |
136+
|---|---|
137+
| **Type** | `string[]` |
138+
| **Required** | No |
139+
| **Normalisation** | Each entry is trimmed, uppercased |
140+
141+
Optional array of CEX ticker symbols that are allowed for withdrawal under this rule.
142+
143+
Accepted values:
144+
145+
- **An array of ticker symbols** — e.g. `["ETH", "USDT", "USDC", "ARB"]`. Only these tokens may be withdrawn when this rule matches.
146+
- **`["*"]`** — wildcard; allows any token (same as omitting the field).
147+
- **Omitted / not present** — allows any token (backward compatible with rules written before `coins` was added).
148+
149+
Matching is **case-insensitive** — both the policy value and the request ticker are uppercased before comparison.
150+
151+
---
152+
127153
### Full withdraw example
128154

129155
```json
@@ -133,6 +159,7 @@ Accepted values:
133159
{
134160
"exchange": "BINANCE",
135161
"network": "ARBITRUM",
162+
"coins": ["ETH", "USDT", "USDC", "ARB"],
136163
"whitelist": ["0x9d467fa9062b6e9b1a46e26007ad82db116c67cb"]
137164
},
138165
{
@@ -143,6 +170,7 @@ Accepted values:
143170
{
144171
"exchange": "*",
145172
"network": "BEP20",
173+
"coins": ["BNB", "USDT"],
146174
"whitelist": ["0x9d467fa9062b6e9b1a46e26007ad82db116c67cb"]
147175
},
148176
{
@@ -155,12 +183,13 @@ Accepted values:
155183
}
156184
```
157185

158-
In this example, a BINANCE + ARBITRUM withdraw uses the first rule (priority 4). A BINANCE + SOL withdraw falls to the second rule (priority 3). A KRAKEN + BEP20 withdraw uses the third rule (priority 2). Everything else hits the global catch-all (priority 1).
186+
In this example, a BINANCE + ARBITRUM withdraw uses the first rule (priority 4) and only allows ETH, USDT, USDC, and ARB. A BINANCE + SOL withdraw falls to the second rule (priority 3), which has no `coins` restriction — any token is allowed. A KRAKEN + BEP20 withdraw uses the third rule (priority 2) and is restricted to BNB and USDT. Everything else hits the global catch-all (priority 1), which also allows any token.
159187

160188
Common rejection reasons:
161189

162190
- no matching exchange + network entry
163191
- address not whitelisted
192+
- token not in `coins` for the matched rule
164193

165194
---
166195

@@ -261,12 +290,107 @@ Example:
261290

262291
| | |
263292
|---|---|
264-
| **Type** | `{}` (empty object) |
293+
| **Type** | `{ rule?: DepositRuleEntry[] }` |
294+
| **Required** | Yes (the `deposit` key must be present) |
295+
296+
Deposit validation gates the `FetchDepositAddresses` action — a request to fetch a deposit address is rejected if the policy does not permit deposits for the given exchange, network, and token. The actual deposit confirmation is not gated by the policy (only the address fetch is).
297+
298+
### Backward compatibility
299+
300+
- **`"deposit": {}`** — no `rule` key or empty rule array: **all deposits are allowed**. This is backward compatible with policies written before deposit rules were added.
301+
- **`"deposit": { "rule": [...] }`** — only deposits matching a rule are allowed.
302+
303+
---
304+
305+
### `deposit.rule: DepositRuleEntry[]`
306+
307+
**Optional.** When omitted or empty, all deposits are permitted.
308+
309+
Each entry scopes deposit permissions to an `exchange` + `network` combination, optionally restricted to specific tokens. When a `FetchDepositAddresses` request arrives, the broker finds the highest-priority matching rule and validates the token against it.
310+
311+
#### Rule matching priority
312+
313+
Same priority scheme as withdraw rules:
314+
315+
| Priority | `exchange` | `network` | Description |
316+
|----------|-----------|-----------|-------------|
317+
| 4 (highest) | exact match | exact match | Fully specific rule |
318+
| 3 | exact match | `"*"` | Exchange-specific, any network |
319+
| 2 | `"*"` | exact match | Network-specific, any exchange |
320+
| 1 (lowest) | `"*"` | `"*"` | Global catch-all |
321+
322+
If rules are present but no entry matches, the request is rejected.
323+
324+
---
325+
326+
### `deposit.rule[].exchange`
327+
328+
| | |
329+
|---|---|
330+
| **Type** | `string` |
265331
| **Required** | Yes |
332+
| **Normalisation** | Trimmed, uppercased |
266333

267-
`deposit` is required in the current policy schema, but it is **not enforced** today.
334+
Same as `withdraw.rule[].exchange` — an exchange identifier (e.g. `"BINANCE"`) or `"*"` for wildcard.
335+
336+
---
337+
338+
### `deposit.rule[].network`
339+
340+
| | |
341+
|---|---|
342+
| **Type** | `string` |
343+
| **Required** | Yes |
344+
| **Normalisation** | Trimmed, uppercased |
345+
346+
Same as `withdraw.rule[].network` — a network/chain identifier (e.g. `"ARBITRUM"`) or `"*"` for wildcard.
347+
348+
---
349+
350+
### `deposit.rule[].coins`
351+
352+
| | |
353+
|---|---|
354+
| **Type** | `string[]` |
355+
| **Required** | No |
356+
| **Normalisation** | Each entry is trimmed, uppercased |
357+
358+
Optional array of CEX ticker symbols allowed for deposit under this rule. Behaves identically to `withdraw.rule[].coins`:
359+
360+
- **An array of ticker symbols** — e.g. `["ETH", "USDT"]`. Only these tokens may be deposited.
361+
- **`["*"]`** — wildcard; allows any token (same as omitting the field).
362+
- **Omitted / not present** — allows any token.
363+
364+
Matching is **case-insensitive**.
365+
366+
---
367+
368+
### Full deposit example
369+
370+
```json
371+
{
372+
"deposit": {
373+
"rule": [
374+
{
375+
"exchange": "BINANCE",
376+
"network": "ARBITRUM",
377+
"coins": ["ETH", "USDT", "USDC", "ARB"]
378+
},
379+
{
380+
"exchange": "*",
381+
"network": "*"
382+
}
383+
]
384+
}
385+
}
386+
```
387+
388+
In this example, a BINANCE + ARBITRUM deposit address request is matched by the first rule (priority 4) and is restricted to ETH, USDT, USDC, and ARB. Any other exchange/network combination hits the catch-all rule, which allows all tokens.
389+
390+
Common rejection reasons:
268391

269-
At present, deposits are effectively always allowed; this field is reserved for future deposit rule support. Set it to `{}`.
392+
- deposit rules are present but no matching exchange + network entry
393+
- token not in `coins` for the matched rule
270394

271395
---
272396

@@ -285,5 +409,7 @@ The broker validates the policy JSON against a Joi schema when loading it.
285409

286410
- **Withdraw address rejected**: ensure the address is in the matching `withdraw.rule[].whitelist` entry (lowercase recommended).
287411
- **Withdraw exchange/network rejected**: ensure there is a `withdraw.rule[]` entry whose `exchange` and `network` match (or wildcard-match) the request.
412+
- **Withdraw token rejected**: ensure the matched `withdraw.rule[].coins` includes the token ticker (or omit `coins` to allow all).
413+
- **Deposit address fetch rejected**: ensure `deposit.rule` contains a matching entry for the exchange + network + token, or use `"deposit": {}` to allow all.
288414
- **Order rejected (market)**: ensure `order.rule.markets` contains a matching pattern for the exchange + pair.
289415
- **Order rejected (limits)**: if `limits` is non-empty, ensure there's an entry for the exact `from``to` direction and the amount is within bounds.

policy/policy.backtest.json

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
{
2+
"withdraw": {
3+
"rule": [
4+
{
5+
"exchange": "BINANCE",
6+
"network": "BEP20",
7+
"coins": ["BNB", "USDT", "USDC", "ETH"],
8+
"whitelist": ["0x9d467fa9062b6e9b1a46e26007ad82db116c67cb"]
9+
},
10+
{
11+
"exchange": "BINANCE",
12+
"network": "ARBITRUM",
13+
"coins": ["ETH", "USDT", "USDC", "ARB"],
14+
"whitelist": ["0x9d467fa9062b6e9b1a46e26007ad82db116c67cb"]
15+
},
16+
{
17+
"exchange": "*",
18+
"network": "ARBITRUM",
19+
"coins": ["ETH", "USDT", "USDC", "ARB"],
20+
"whitelist": ["0x9d467fa9062b6e9b1a46e26007ad82db116c67cb"]
21+
}
22+
]
23+
},
24+
"deposit": {
25+
"rule": [
26+
{
27+
"exchange": "BINANCE",
28+
"network": "BEP20",
29+
"coins": ["BNB", "USDT", "USDC", "ETH"]
30+
},
31+
{
32+
"exchange": "BINANCE",
33+
"network": "ARBITRUM",
34+
"coins": ["ETH", "USDT", "USDC", "ARB"]
35+
},
36+
{
37+
"exchange": "*",
38+
"network": "ARBITRUM",
39+
"coins": ["ETH", "USDT", "USDC", "ARB"]
40+
}
41+
]
42+
},
43+
"order": {
44+
"rule": {
45+
"markets": [
46+
"BINANCE:ARB/USDT",
47+
"BYBIT:ARB/USDC",
48+
"UPBIT:ETH/USDC",
49+
"BINANCE:ETH/USDT",
50+
"BINANCE:BTC/ETH",
51+
"BINANCE:BTC/USDC",
52+
"KRAKEN:ETH/USDT",
53+
"KRAKEN:ETH/USDC"
54+
],
55+
"limits": [
56+
{ "from": "USDT", "to": "ETH", "min": 1, "max": 100000 },
57+
{ "from": "ETH", "to": "USDT", "min": 0.5, "max": 5 },
58+
{ "from": "ARB", "to": "USDC", "min": 1, "max": 1000 },
59+
{ "from": "USDC", "to": "ARB", "min": 1, "max": 10000 },
60+
{ "from": "ARB", "to": "USDT", "min": 1, "max": 1000 },
61+
{ "from": "USDT", "to": "ARB", "min": 1, "max": 10000 }
62+
]
63+
}
64+
}
65+
}

policy/policy.example.json

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
{
2+
"withdraw": {
3+
"rule": [
4+
{
5+
"exchange": "BINANCE",
6+
"network": "ARBITRUM",
7+
"coins": ["ETH", "USDT", "USDC", "ARB"],
8+
"whitelist": ["0x9d467fa9062b6e9b1a46e26007ad82db116c67cb"]
9+
},
10+
{
11+
"exchange": "BINANCE",
12+
"network": "BEP20",
13+
"coins": ["BNB", "USDT", "USDC"],
14+
"whitelist": ["0x9d467fa9062b6e9b1a46e26007ad82db116c67cb"]
15+
},
16+
{
17+
"exchange": "*",
18+
"network": "ARBITRUM",
19+
"whitelist": ["0x9d467fa9062b6e9b1a46e26007ad82db116c67cb"]
20+
}
21+
]
22+
},
23+
"deposit": {
24+
"rule": [
25+
{
26+
"exchange": "BINANCE",
27+
"network": "ARBITRUM",
28+
"coins": ["ETH", "USDT", "USDC", "ARB"]
29+
},
30+
{
31+
"exchange": "BINANCE",
32+
"network": "BEP20",
33+
"coins": ["BNB", "USDT", "USDC"]
34+
}
35+
]
36+
},
37+
"order": {
38+
"rule": {
39+
"markets": ["BINANCE:ARB/USDT", "BINANCE:ETH/USDT", "BINANCE:BTC/USDC"],
40+
"limits": [
41+
{ "from": "USDT", "to": "ETH", "min": 1, "max": 100000 },
42+
{ "from": "ETH", "to": "USDT", "min": 0.5, "max": 5 },
43+
{ "from": "ARB", "to": "USDT", "min": 1, "max": 1000 },
44+
{ "from": "USDT", "to": "ARB", "min": 1, "max": 10000 }
45+
]
46+
}
47+
}
48+
}

0 commit comments

Comments
 (0)