You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
-**Architecture:**`WDK` class manages a collection of `WalletManager` instances and a `PolicyEngine` that intercepts write-facing operations on every account returned from `getAccount` / `getAccountByPath`.
50
+
51
+
## Policy Engine
52
+
- Source lives under `src/policy/`. Public surface is the `PolicyViolationError` and `PolicyConfigurationError` classes plus the `Policy*` / `SimulationResult` typedefs re-exported from `index.js`. Everything else under `src/policy/` is internal.
53
+
- The engine wraps account write methods and protocol getters at `getAccount` time. When any policy applies to an account ("governed"), the proxy wraps **every** method in `OPERATIONS` that exists on the underlying account — not just the ones referenced by registered rules. Combined with the default-deny evaluator (below) this closes sibling-method bypasses (e.g. a "cap transfer" policy cannot be sidestepped by `sendTransaction({ to: token, data: ERC-20-transfer-calldata })`, `approve` to an attacker, off-chain Permit via `signTypedData`, or ERC-7702 `delegate`).
54
+
-**Default-deny on governed accounts.**`policy-evaluator.evaluate()` returns BLOCK with `reason: 'no-applicable-rule'` whenever an operation reaches it and no rule addresses that operation. To opt into permissive semantics on a specific account, register a wildcard ALLOW baseline (`operation: '*', action: 'ALLOW'`) and layer specific DENYs on top. Accounts with no registered policies are not governed and skip the proxy entirely (zero cost).
55
+
-**Operation name = canonical IWalletAccount method name.** The engine wraps `account[op]` by string lookup, so every entry in `OPERATIONS` (`src/policy/constants.js`) must match the exact method name on `@tetherto/wdk-wallet`'s base `IWalletAccount` or a registered protocol class. A divergence silently no-ops the policy (registers fine, never fires) — which is worse than a hard error. When adding a new wallet method that emits a signature or moves value (e.g., `signTransaction`, `sign`), add it to `OPERATIONS` and the `PolicyOperation` typedef. Conversely, do not add aspirational entries that no wallet implements; the schema accepts them, but they produce silently-broken policies.
56
+
- Two scopes only: `project` (with optional wallet restriction declared via the policy's `wallet` field) and `account` (with required `wallet` field and a per-account `accounts` list of paths and/or integer indexes). The `wallet` value is the same string passed to `registerWallet` — an opaque consumer-chosen key, not a blockchain identifier. `wallet` accepts a single string or a non-empty array of strings; omitting it on a project-scope policy applies it across every registered wallet. DENY wins across scopes; account-scope `ALLOW` rules can short-circuit project-scope DENYs via `override_broader_scope: true`.
57
+
- Index-form `accounts` entries match the index passed to `wdk.getAccount(wallet, index)`. They do not match accounts retrieved via `getAccountByPath` because the wallet manager has no synchronous path → index resolver. Path-form entries match either retrieval style.
58
+
- Nested-call escape works via a `Proxy` (see `src/policy/policy-account-proxy.js`), not an async-context marker. The engine returns a Proxy that exposes enforced versions of write methods; the proxy's `get` trap binds non-enforced methods to the underlying account, so internal `this.method()` calls and protocol code that holds a direct reference to the account naturally bypass enforcement. The policy engine itself does not mutate the account (the WDK's `_registerProtocols` step does install `registerProtocol` / `getXProtocol` helpers before the proxy is built — separate, pre-existing concern). The same code path runs on every JavaScript runtime that supports `Proxy` — no `AsyncLocalStorage`, no runtime detection, no Bare-specific fallback.
59
+
- Enforcement applies to the **surface of the proxy** only. Underscore-prefixed fields (e.g. `protocol._account`) reach the raw account and bypass enforcement by design — they are private by convention and not part of the supported surface. The same is true for protocol methods that internally call account operations via their stored `this._account` (e.g. `bridge.bridge(...)` calling `this._account.sendTransaction(...)`); this is the documented nested-call escape.
60
+
-`proxy.registerProtocol(...)` is special: the underlying helper returns the raw account, so the `get` trap rewrites the return value to be the proxy itself. Without this rewrite, `proxy.registerProtocol(...).sendTransaction(...)` would skip enforcement.
61
+
- Each condition is raced against `conditionTimeoutMs` (default 30s, settable via `RegisterPolicyOptions`). A condition that throws or times out is fail-closed for DENY rules (treated as matched → block) and fail-open-as-no-match for ALLOW rules.
62
+
- Conditions are user-supplied functions in Phase 1. The engine accepts `state` and `onSuccess` rule fields for Phase 2 (engine-managed state + post-execution hooks) but ignores them at runtime; `state` is deep-cloned at registration so callers can't mutate engine state post-registration.
63
+
- Tests live in `tests/wdk-policy.test.js` and exercise the engine exclusively through the public WDK API.
Copy file name to clipboardExpand all lines: README.md
+65Lines changed: 65 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -57,9 +57,74 @@ wdk.dispose()
57
57
-**Multi-Chain Operations**: Coordinate balances, fee lookups, and transaction flows across registered chains
58
58
-**Protocol Registration Support**: Attach swap, bridge, lending, fiat, and swidge protocols to registered blockchains
59
59
-**Middleware Hooks**: Intercept account derivation with custom middleware
60
+
-**Transaction Policies**: Local policy engine that intercepts write-facing operations and enforces user-defined ALLOW/DENY rules at project (global or wallet-bound) and account scopes — with simulation, nested-call handling, and structured `PolicyViolationError`s
60
61
-**Seed Utilities**: Generate and validate BIP-39 seed phrases
61
62
-**Selective Disposal**: Dispose specific registered wallets or clear the full WDK instance
62
63
64
+
## Transaction Policies
65
+
66
+
Register policies on a `WDK` instance to gate write-facing operations on every wallet account. Each registered rule can `ALLOW` or `DENY` an attempted operation based on a condition function; matching `DENY`s throw a `PolicyViolationError` before the underlying method runs.
Policies have two scopes — `project` and `account`. A project-scope policy applies globally by default, or only to the wallets named in its `wallet` field (`wallet: 'ethereum'` or `wallet: ['ethereum', 'ton']`). The `wallet` value is the same string passed to `registerWallet`. It might be a chain name like `"ethereum"`, but it could equally be `"treasury-cold"` or any label the consumer chose; the engine treats it as an opaque key. An account-scope policy must declare a `wallet` and targets specific accounts within it, identified by either derivation path (`accounts: ["0'/0/0"]`) or integer index (`accounts: [0, 1]`) — index entries match accounts retrieved via `wdk.getAccount(wallet, index)`; path entries match either retrieval style. Evaluation is narrowest-first with `DENY` winning across scopes. Account-scope `ALLOW` rules can opt into `override_broader_scope: true` to short-circuit broader policies for explicit exceptions (e.g., treasury accounts). Conditions can be sync or async and may carry user-owned state via closures. Templates (`@tetherto/wdk-policy-templates`) and a portal UI for editing policies are coming in later phases.
102
+
103
+
### Default-deny semantics
104
+
105
+
The engine is **default-deny on governed accounts**. As soon as any policy applies to an account, the engine wraps every method in `OPERATIONS` (the set of write-facing and signing primitives — `sendTransaction`, `signTransaction`, `transfer`, `approve`, `sign`, `signTypedData`, `signAuthorization`, `delegate`, `revokeDelegation`, and protocol methods like `swap`, `bridge`, `swidge`, etc.) on that account. Any call to a wrapped method whose operation is not addressed by an `ALLOW` rule throws `PolicyViolationError` with `reason: 'no-applicable-rule'`.
106
+
107
+
This is intentional: a "cap transfer at $100" policy must not be sidesteppable by `sendTransaction({ to: token, data: <ERC-20 transfer calldata> })`, `approve(spender, MAX)`, an off-chain `signTypedData` Permit, or an ERC-7702 `delegate` to an attacker contract. The engine closes those bypasses by treating any unaddressed money-movement op on a governed account as DENY.
108
+
109
+
If you want permissive semantics on a specific account (allow anything that isn't explicitly denied), register a wildcard ALLOW rule as a baseline and layer specific DENYs on top:
Accounts that have **no** registered policies are not governed — the proxy is not applied, and method calls go straight to the underlying account at zero cost.
123
+
124
+
The engine wraps accounts through an ES `Proxy` so internal SDK code that uses `this.method()` naturally bypasses enforcement — nested-call escape (e.g. `bridge` internally calling `sendTransaction`) works without any async-context tracking. The same code path runs on every JavaScript runtime that supports `Proxy`, including Bare.
125
+
126
+
Policy enforcement applies to the **surface of the proxy** returned by `getAccount` / `getAccountByPath`. Reaching for underscore-prefixed fields (e.g. `protocol._account`) bypasses enforcement by design — treat them as private. The same applies to account-level operations invoked from inside a protocol's own methods (e.g. `bridge.bridge(...)` internally calling `this._account.sendTransaction(...)`), which is the documented nested-call escape; it lets protocols use the account they were constructed with without re-entering the engine on every internal step.
127
+
63
128
## Compatibility
64
129
65
130
-**WDK Wallet Modules** including EVM, Solana, TON, TRON, and Bitcoin integrations
0 commit comments