Skip to content

Add deposit policy enforcement and coin-based withdrawal constraints#35

Merged
outerlook merged 5 commits intodevelopfrom
finalize-policies-2
Mar 27, 2026
Merged

Add deposit policy enforcement and coin-based withdrawal constraints#35
outerlook merged 5 commits intodevelopfrom
finalize-policies-2

Conversation

@outerlook
Copy link
Copy Markdown
Member

@outerlook outerlook commented Mar 26, 2026

related to https://linear.app/usherlabs/issue/FIET-718/cex-broker-add-token-restrictions-to-withdrawdeposit-policies

Summary by CodeRabbit

  • New Features

    • Token-based withdrawal allowlists per rule with wildcard and case-insensitive matching.
    • Token-based deposit rules enforced for deposit-address fetching; missing/empty rules allow all.
    • Added example and backtest policy files including order/limit samples.
  • Documentation

    • Expanded policy docs with coin semantics, examples, rejection reasons, and troubleshooting.
  • Tests

    • Added/updated tests covering coin allowlist behavior, priority matching, and case-insensitivity.

Add optional `coins?: string[]` to `WithdrawRuleEntry` and create
new `DepositRuleEntry` type with exchange, network, and coins fields.
Update `PolicyConfig.deposit` from `Record<string, null>` to accept
an optional array of deposit rules.

Update Joi schema to validate the new coins arrays and deposit rule
structure. Extend `normalizePolicyConfig()` to uppercase coin values
in both withdraw and deposit rules.

Backward compatible: policies without coins field and empty deposit
objects remain valid.
Activate the previously-ignored ticker parameter to gate
withdrawals by the rule's coins list. After matching the
highest-priority withdraw rule by exchange/network, check
the ticker against rule.coins when present.

Behavior: coins absent, empty, or ["*"] allows any token.
Otherwise only listed coins pass; mismatches return an error
with the allowed set. No fallthrough to lower-priority rules.
Add getDepositRulePriority() mirroring the withdraw counterpart with
wildcard support and priority tiers (exact > partial > wildcard).

Rewrite validateDeposit() to check exchange, network, and coin against
policy deposit rules. Highest-priority rule wins with no fallthrough
on coin mismatch. Empty/absent rules preserve backward compatibility
by allowing all deposits.

Wire validation into the FetchDepositAddresses handler, rejecting
with PERMISSION_DENIED when policy is violated.

Replace stub tests with comprehensive suite covering coin filtering,
wildcard handling, priority conflicts, case insensitivity, and
backward-compatible empty policy cases.
Document withdraw `coins` support and the new deposit rule
structure in `POLICY.md`.

Refresh `policy.example.json` and `policy.backtest.json` to
show token-scoped withdraw rules and deposit rules that match
the current schema.

This keeps the primary policy reference and shipped examples
aligned with the latest validation behavior while leaving
production and staging policies untouched.
@outerlook outerlook self-assigned this Mar 26, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 26, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 8ae73445-0a53-475c-983f-6e127fc8cc04

📥 Commits

Reviewing files that changed from the base of the PR and between 4e00d28 and ea89a02.

📒 Files selected for processing (4)
  • src/helpers/index.ts
  • src/server.ts
  • src/types.ts
  • test/helpers.test.ts

Disabled knowledge base sources:

  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.


Walkthrough

The PR adds per-rule token/coin allowlists to withdraw and deposit policies, formalizes deposit.rules with exchange/network/coins matching and priority, enforces token checks in validateWithdraw/validateDeposit, and integrates deposit validation into the FetchDepositAddresses handler.

Changes

Cohort / File(s) Summary
Policy Documentation
POLICY.md
Documented per-rule coins for withdraw and new structured deposit.rule[] semantics (wildcard/omitted = allow-all, case-insensitive), updated examples and rejection reasons.
Policy Examples
policy/policy.backtest.json, policy/policy.example.json
Added JSON policy examples with withdraw, deposit, and order rule sets including per-rule coins, exchange/network scopes, and whitelist addresses.
Types
src/types.ts
Added DepositRuleEntry type; added optional coins?: string[] to WithdrawRuleEntry; changed PolicyConfig.deposit to { rule?: DepositRuleEntry[] }.
Policy Logic
src/helpers/index.ts
Normalized coins (trim+upper), added getDepositRulePriority() wildcard-aware matching, enforced coin allowlists in validateWithdraw() and implemented full validateDeposit() with rule matching, priority, and token checks.
Server Integration
src/server.ts
Call validateDeposit(...) inside Action.FetchDepositAddresses and return PERMISSION_DENIED when deposit validation fails, skipping address fetch.
Tests
test/helpers.test.ts
Expanded withdraw tests for coins behavior (wildcard, empty, case-insensitive, priority). Rewrote deposit tests to cover rule presence/absence, matching, coin allowlists, and priority semantics.

Sequence Diagram

sequenceDiagram
    actor Client
    participant Server
    participant PolicyValidator as Policy Validator
    participant RuleMatcher as Rule Matcher

    Client->>Server: FetchDepositAddresses(exchange, chain, symbol)
    Server->>PolicyValidator: validateDeposit(policy, exchange, network, ticker)
    PolicyValidator->>RuleMatcher: Find best-priority rule for (exchange, network)
    RuleMatcher-->>PolicyValidator: matched rule or null
    alt no deposit.rule OR matched rule allows all coins
        PolicyValidator-->>Server: { valid: true }
    else matched rule with coins list
        PolicyValidator->>PolicyValidator: normalize ticker & coins (UPPER/trim)
        alt ticker in coins
            PolicyValidator-->>Server: { valid: true }
        else
            PolicyValidator-->>Server: { valid: false, error: "token not allowed" }
        end
    else no matching rule
        PolicyValidator-->>Server: { valid: false, error: "exchange/network not allowed" }
    end
    alt validation.valid === true
        Server->>Server: Fetch addresses from exchange
        Server-->>Client: Address list
    else
        Server-->>Client: PERMISSION_DENIED + error message
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰
Coins on lists, wagging ears so bright,
Rules that sort tokens day and night,
Deposits checked, withdraws tallied true,
Wildcards hop open, priorities pursue,
A rabbit's stamp: policies approved with chew!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately reflects the main changes in the pull request: adding deposit policy enforcement and implementing coin-based constraints for both deposit and withdrawal rules across multiple files and test coverage.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch finalize-policies-2

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/helpers/index.ts (1)

378-398: Consider extracting common priority logic.

getDepositRulePriority and getWithdrawRulePriority (lines 356-376) have identical implementations. While duplication is acceptable for now, consider extracting to a shared helper if more rule types are added in the future.

♻️ Optional refactor to reduce duplication
+function getRulePriority(
+	rule: { exchange: string; network: string },
+	exchange: string,
+	network: string,
+): number {
+	const exchangeMatch = rule.exchange === exchange || rule.exchange === "*";
+	const networkMatch = rule.network === network || rule.network === "*";
+	if (!exchangeMatch || !networkMatch) {
+		return 0;
+	}
+	if (rule.exchange === exchange && rule.network === network) {
+		return 4;
+	}
+	if (rule.exchange === exchange && rule.network === "*") {
+		return 3;
+	}
+	if (rule.exchange === "*" && rule.network === network) {
+		return 2;
+	}
+	return 1;
+}
+
+const getWithdrawRulePriority = getRulePriority;
+const getDepositRulePriority = getRulePriority;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/helpers/index.ts` around lines 378 - 398, getDepositRulePriority and
getWithdrawRulePriority contain identical logic; extract the shared
matching/prioritization into a single helper (e.g., computeRulePriority or
getRulePriority) and have both getDepositRulePriority and
getWithdrawRulePriority delegate to it. Update the helper to accept (rule,
exchange, network) and return the same numeric priorities, keep existing
behavior for wildcards and exact matches, and replace duplicate bodies in both
functions with a call to the new helper to remove duplication while preserving
current semantics.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/helpers/index.ts`:
- Around line 378-398: getDepositRulePriority and getWithdrawRulePriority
contain identical logic; extract the shared matching/prioritization into a
single helper (e.g., computeRulePriority or getRulePriority) and have both
getDepositRulePriority and getWithdrawRulePriority delegate to it. Update the
helper to accept (rule, exchange, network) and return the same numeric
priorities, keep existing behavior for wildcards and exact matches, and replace
duplicate bodies in both functions with a call to the new helper to remove
duplication while preserving current semantics.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f32282ca-b125-42fb-af44-30b68b04b6d4

📥 Commits

Reviewing files that changed from the base of the PR and between 31db25a and 4e00d28.

📒 Files selected for processing (7)
  • POLICY.md
  • policy/policy.backtest.json
  • policy/policy.example.json
  • src/helpers/index.ts
  • src/server.ts
  • src/types.ts
  • test/helpers.test.ts
📜 Review details
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2026-01-29T17:09:55.684Z
Learnt from: victorshevtsov
Repo: usherlabs/cex-broker PR: 23
File: src/client.dev.ts:72-76
Timestamp: 2026-01-29T17:09:55.684Z
Learning: In the cex-broker codebase, Action.FetchAccountId must remain implemented in src/server.ts to support external consumers like fiet-prover that depend on fetching account IDs from CEX endpoints; removing or replacing this handler breaks backward compatibility.

Applied to files:

  • src/server.ts
🪛 LanguageTool
POLICY.md

[grammar] ~311-~311: Use a hyphen to join words.
Context: ...lidates the token against it. #### Rule matching priority Same priority scheme ...

(QB_NEW_EN_HYPHEN)


[style] ~412-~412: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ...ch (or wildcard-match) the request. - Withdraw token rejected: ensure the matched `w...

(ENGLISH_WORD_REPEAT_BEGINNING_RULE)

🔇 Additional comments (14)
policy/policy.example.json (1)

1-48: LGTM!

The example policy file is well-structured, demonstrates the new coins allowlist feature for both withdraw and deposit rules, and correctly shows wildcard usage for exchange matching. The structure aligns with the updated PolicyConfig type and documentation.

src/server.ts (1)

618-632: LGTM!

The deposit validation integration is correctly placed after payload parsing and before the actual deposit address fetch. The validation follows the same pattern as validateWithdraw in the Withdraw action, using PERMISSION_DENIED status for policy violations. The parameter mapping (cex → exchange, fetchDepositAddresses.chain → network, symbol → ticker) aligns with the validateDeposit function signature.

policy/policy.backtest.json (1)

1-65: LGTM!

The backtest policy file provides a comprehensive configuration with multiple exchange/market combinations for testing. It correctly demonstrates the wildcard exchange pattern and maintains structural consistency with the example policy and documented schema.

src/types.ts (2)

4-15: LGTM!

The type definitions correctly model the extended policy structure:

  • WithdrawRuleEntry.coins is optional for backward compatibility
  • DepositRuleEntry appropriately omits whitelist since deposits don't have destination addresses to validate
  • Both types share the same coins constraint pattern

31-33: LGTM!

Making deposit.rule optional (rule?: DepositRuleEntry[]) correctly enables backward compatibility — policies with "deposit": {} will allow all deposits, while policies with explicit rules will enforce them.

POLICY.md (2)

133-151: LGTM!

The withdraw.rule[].coins documentation clearly explains the optional token allowlist semantics, including wildcard behavior (["*"] or omitted means allow all), case-insensitive matching, and backward compatibility with rules written before coins was added.


289-394: LGTM!

The deposit policy documentation is comprehensive and well-structured. It correctly documents:

  • Backward compatibility ("deposit": {} allows all)
  • Rule matching priority (same scheme as withdraw)
  • Optional coins constraints with wildcard semantics
  • The important distinction that deposit validation gates FetchDepositAddresses only
test/helpers.test.ts (3)

145-157: LGTM!

Good backward compatibility test ensuring existing policies without coins field continue to work — any ticker is allowed when the field is absent.


239-401: LGTM!

Comprehensive test coverage for withdraw coin restrictions including:

  • Allowed/rejected ticker scenarios
  • Wildcard ["*"] and empty [] array semantics
  • Case-insensitive matching
  • Priority-based rule selection without fallthrough on coin mismatch

The error message assertions (toContain) ensure user-facing messages include the rejected ticker and allowed coins list.


691-821: LGTM!

Thorough test coverage for deposit validation including:

  • Coin allowlist enforcement
  • Backward compatibility with empty/missing rules
  • Wildcard and empty array semantics
  • Exchange/network rejection when rules exist but don't match
  • Priority-based rule selection
  • Case-insensitive matching across exchange, network, and coin
src/helpers/index.ts (4)

257-264: LGTM!

The Joi schemas correctly define coins as an optional array for both withdraw and deposit rules, maintaining backward compatibility with existing policies.


325-342: LGTM!

The normalization logic correctly handles optional fields:

  • coins is only normalized when present, avoiding the addition of empty arrays to rules that don't specify them
  • deposit.rule normalization is conditional, preserving the distinction between {} (no rules) and { rule: [] } (empty rules array)

438-448: LGTM!

The coin validation logic correctly handles all wildcard semantics:

  • coins undefined → allow all (backward compat)
  • coins: [] → allow all (empty array treated as no restriction)
  • coins: ["*"] → allow all (explicit wildcard)
  • coins: ["ETH", "USDT"] → only allow listed tokens

The case-insensitive comparison via tickerNorm is appropriate.


695-744: LGTM!

The validateDeposit implementation is correct and mirrors the validateWithdraw pattern appropriately:

  • Returns valid when no rules exist (lines 703-708) for backward compatibility
  • Uses priority-based rule selection (lines 714-722)
  • Enforces coin restrictions with the same wildcard semantics (lines 731-742)
  • Normalizes all inputs for case-insensitive matching (lines 710-712)

@outerlook outerlook changed the title draft Add deposit policy enforcement and coin-based withdrawal constraints Mar 26, 2026
@outerlook outerlook requested a review from rsoury March 26, 2026 17:35
Copy link
Copy Markdown
Member

@rsoury rsoury left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only consideration here is that a deposit rule is redundant.
If the CEX wallet address is exposed, there is no way to reject deposits (transfers) to that address.
Therefore, does not make sense operationally to concern with deposit gating.

However, withdrawal gating is definitely ok.

@rsoury
Copy link
Copy Markdown
Member

rsoury commented Mar 27, 2026

Also, please merge updates into develop generally, unless it's a hotfix.
There are release pipelines for NPM registry deployment too

@outerlook outerlook changed the base branch from master to develop March 27, 2026 11:52
@outerlook
Copy link
Copy Markdown
Member Author

@rsoury

Only consideration here is that a deposit rule is redundant.
If the CEX wallet address is exposed, there is no way to reject deposits (transfers) to that address.
Therefore, does not make sense operationally to concern with deposit gating.

However, withdrawal gating is definitely ok.

I thought a bit about it as well, and I agree that it might be confusing because, really, the CEX-Broker has no power to prohibit the user from depositing whatever he wants... but since the way to get an address also stipulates a network + token before this change, it ends up being one more line of defense against bad behaviors. I think about tokens that Binance supports or not (thinking about weth example). I imagine Binance also has this kind of guardrail on their fetch address method, which may become redundant here

Then it's more like a guardrail than an authorization in this case. If it's harmful for being confusing, I remove it, but if it's harmless, it could be ok

Copy link
Copy Markdown
Member

rsoury commented Mar 27, 2026

Fair — happy for you to merge in that case.

@outerlook outerlook merged commit 435da41 into develop Mar 27, 2026
2 of 3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants