Context
After the borrow PR #3 review consolidation, the three action provider domains (LendProvider, SwapProvider, BorrowProvider) have drifted in how they shape per-action validation, allowlist resolution, and config matching. Borrow is now the most refined (post-hoist allowlist resolution commit); lend and swap haven't been brought up to parity.
This issue tracks the larger pass: pull shared logic up into BaseActionProvider where it belongs, extract per-domain validations.ts modules, and make naming/shape parallel across all three domains.
Current state
| Concern |
Lend |
Swap |
Borrow (post-cleanup) |
| Validations module |
inline + scattered helpers in lend/utils/markets.ts |
fully inline in SwapProvider.ts |
borrow/core/validations.ts (6 functions) |
| Protected method name |
validateMarketAllowed (renamed for parity) |
validateMarketAllowed |
validateMarketAllowed ✅ |
| Method scope |
bundles chain check + allowlist |
allowlist + blocklist only |
allowlist + blocklist only |
| Strict lookup-and-return helper |
none (returns undefined) |
resolveMarketConfig (returns undefined) |
requireAllowlistedMarketConfig (throws) |
| Allowlist matcher helper |
lendMarketIdMatches (extracted) |
inline closure in findMatchingConfig (SwapProvider.ts:493) |
marketIdMatches (extracted) |
| Tampered-config protection on write path |
n/a |
n/a |
resolved in resolveTrustedBaseParams (write hooks receive allowlist-trusted config) |
| Walletaddress validation |
inline AddressRequiredError |
validateNotZeroAddress inline |
validateBorrowWalletAddress (deduped 2-step check) |
| Quote validation helpers |
n/a (no quotes) |
inline (validateQuoteExpiration) |
validateQuoteAction + validateQuoteNotExpired |
Proposed consolidation
1. Per-domain validations.ts modules
lend/core/validations.ts
swap/core/validations.ts
Move existing scattered helpers in. Mirror borrow's pattern — pure functions taking config rather than this.
2. Lift to BaseActionProvider
BaseActionProvider<TConfig, TSettings> is the right place for cross-domain shared protected methods. Candidates:
validateMarketAllowed(...) — every domain has this with the same intent (allowlist + blocklist). The args differ (marketId vs assetIn/assetOut/chainId vs BorrowMarketConfig), so we'd need either a generic shape or domain-specific overrides. Likely a generic `validateMarketAllowed(market, matcher)` accepting a per-domain matcher.
requireAllowlistedMarketConfig(...) — same pattern; strict lookup that throws.
validateWalletAddress(walletAddress) — agnostic; move to utils/validation.ts if not already, drop the per-domain wrappers (borrow has `validateBorrowWalletAddress` — could become `validateNonZeroWalletAddress` shared).
3. Per-domain matcher helpers
lendMarketIdMatches (already extracted)
marketIdMatches (borrow, already extracted)
- New:
swapMarketPairMatches(assetIn, assetOut, chainId, pair) — extract from inline closure in `SwapProvider.ts:493`.
4. Naming / scope alignment
- Lend's `validateMarketAllowed` currently bundles chain check + allowlist. Borrow and swap keep them separate. Decide a single convention and apply.
- Swap's `resolveMarketConfig` returns `undefined`. Borrow's `requireAllowlistedMarketConfig` throws. Add a consistent pair (`findX` returns, `requireX` throws) per domain.
5. Tampered-config protection
- Lend write paths (deposit/withdraw) currently trust caller-provided `asset` metadata. Audit whether a parallel `resolveTrustedBaseParams` is warranted.
- Swap is less exposed because the router doesn't take a marketParams hash — the asset addresses go directly into Permit2 / router calldata. Still worth a one-pass audit.
Out of scope
- Behavior changes beyond what naming/structure consolidation surfaces.
- Cross-domain abstraction over market-id shape (lend uses
address, swap uses asset pair, borrow uses marketId hash) — these are genuinely different and shouldn't be forced together.
Acceptance
- All three domains have a `/core/validations.ts` module.
- Naming for shared concepts is identical across domains where the shape allows.
- Any logic identical across all three domains lives on `BaseActionProvider`.
- No behavioral regressions; existing tests still pass.
Related history
The borrow consolidation that motivated this issue landed in branch `kevin/borrow-pr3` between commits `0f1e980c` (validations module) and `d0b71326` (lend rename for parity).
Context
After the borrow PR #3 review consolidation, the three action provider domains (
LendProvider,SwapProvider,BorrowProvider) have drifted in how they shape per-action validation, allowlist resolution, and config matching. Borrow is now the most refined (post-hoist allowlist resolutioncommit); lend and swap haven't been brought up to parity.This issue tracks the larger pass: pull shared logic up into
BaseActionProviderwhere it belongs, extract per-domainvalidations.tsmodules, and make naming/shape parallel across all three domains.Current state
lend/utils/markets.tsSwapProvider.tsborrow/core/validations.ts(6 functions)validateMarketAllowed(renamed for parity)validateMarketAllowedvalidateMarketAllowed✅undefined)resolveMarketConfig(returnsundefined)requireAllowlistedMarketConfig(throws)lendMarketIdMatches(extracted)findMatchingConfig(SwapProvider.ts:493)marketIdMatches(extracted)resolveTrustedBaseParams(write hooks receive allowlist-trusted config)AddressRequiredErrorvalidateNotZeroAddressinlinevalidateBorrowWalletAddress(deduped 2-step check)validateQuoteExpiration)validateQuoteAction+validateQuoteNotExpiredProposed consolidation
1. Per-domain
validations.tsmoduleslend/core/validations.tsswap/core/validations.tsMove existing scattered helpers in. Mirror borrow's pattern — pure functions taking config rather than
this.2. Lift to
BaseActionProviderBaseActionProvider<TConfig, TSettings>is the right place for cross-domain shared protected methods. Candidates:validateMarketAllowed(...)— every domain has this with the same intent (allowlist + blocklist). The args differ (marketIdvsassetIn/assetOut/chainIdvsBorrowMarketConfig), so we'd need either a generic shape or domain-specific overrides. Likely a generic `validateMarketAllowed(market, matcher)` accepting a per-domain matcher.requireAllowlistedMarketConfig(...)— same pattern; strict lookup that throws.validateWalletAddress(walletAddress)— agnostic; move toutils/validation.tsif not already, drop the per-domain wrappers (borrow has `validateBorrowWalletAddress` — could become `validateNonZeroWalletAddress` shared).3. Per-domain matcher helpers
lendMarketIdMatches(already extracted)marketIdMatches(borrow, already extracted)swapMarketPairMatches(assetIn, assetOut, chainId, pair)— extract from inline closure in `SwapProvider.ts:493`.4. Naming / scope alignment
5. Tampered-config protection
Out of scope
address, swap uses asset pair, borrow usesmarketIdhash) — these are genuinely different and shouldn't be forced together.Acceptance
Related history
The borrow consolidation that motivated this issue landed in branch `kevin/borrow-pr3` between commits `0f1e980c` (validations module) and `d0b71326` (lend rename for parity).