Skip to content

feat(denario): onboard Denario — partner wallet, Gold/Silver Polygon assets, and permanent referral alias#4209

Open
joshuakrueger-dfx wants to merge 9 commits into
DFXswiss:developfrom
joshuakrueger-dfx:agent/add-denario-permanent-ref
Open

feat(denario): onboard Denario — partner wallet, Gold/Silver Polygon assets, and permanent referral alias#4209
joshuakrueger-dfx wants to merge 9 commits into
DFXswiss:developfrom
joshuakrueger-dfx:agent/add-denario-permanent-ref

Conversation

@joshuakrueger-dfx

@joshuakrueger-dfx joshuakrueger-dfx commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator

Overview

Bundles the full Denario onboarding into one PR:

  1. Partner wallet — add Denario to the wallet table (analogous to existing partner wallets, e.g. Cake Wallet).
  2. Assets — add the two Denario precious-metal tokens on Polygon (DGC, DSC) as inert, list-only assets.
  3. Referral alias — permanent denario alias on the ref-keys setting (original scope of this PR).
  4. Pay-in hardening (from review) — the register pipeline only maps assets that have a price rule, so unpriced tokens can no longer produce stuck pay-ins; plus an append-only migration audit store that makes the data migrations ownership-safe and reversible without destructive overwrites.

1. Partner wallet

Migration 1784994200000-AddDenarioWalletAndAssets.js inserts a Denario row into wallet. Only name/displayName are set; every compliance/behaviour column takes its conservative DB default (isKycClient=false, autoTradeApproval=false, usesDummyAddresses=false, displayFraudWarning=false, amlRules='0', buySpecificIbanEnabled=false) — kept conservative on purpose so the partner cannot bypass any check by default. Idempotent on name, reversible via the audit store (see §4): down() only removes rows this migration provably created.

The partial unique index on wallet.name = 'Denario' (declared on the entity) is created in all environments (CREATE UNIQUE INDEX IF NOT EXISTS), so migration-driven databases stay in sync with the entity schema and migration:generate stays clean. down() intentionally does not drop it — it is entity schema and carries no data.

Needs DFX confirmation: the partner's actual KYC/AML setup (whether isKycClient / amlRules should differ from the safe defaults). Adjust in a follow-up if required.

2. Assets (Polygon ERC-20)

Added in the same migration and mirrored into migration/seed/asset.csv (ids 413/414), values verified on-chain (decimals()/symbol()/name()):

Name Symbol Contract Decimals
Denario Gold Coin DGC 0xf7e2…042f 8
Denario Silver Coin DSC 0x5d4e…c7e7 8

Contract addresses independently verified against Denario's official token pages. Backing: 1 DGC = 1 oz gold, 1 DSC = 1 oz silver (999.9), vaulted in Switzerland.

Both are inert, list-only: type=Token, blockchain=Polygon, category=Public, no priceRuleId, and every trade/payment flag falseisActive=false. Consequences:

  • Excluded from the hourly price job (no priceRuleId).
  • Not buyable/sellable/payable — no cron/observable/liquidity path picks them up.
  • No automatic liquidity-management mechanism to buy or sell these tokens; any purchase or sale is handled manually, as required.
  • financialType is intentionally left null (no precious-metal type exists yet) — set it when trading is enabled.

Pay-in behaviour (changed in this PR, see §5): inbound DGC/DSC transfers to DFX deposit addresses are not mapped to these assets while no price rule is attached. They are persisted as terminal FAILED crypto inputs without asset assignment and trigger a monitoring alert, instead of entering the former endless retry loop. Once a price rule is attached, new deposits register and process normally.

Reference txs: DGC · DSC

Deliberately left empty (correct for an inert asset — not guessed)

  • financialType: no precious-metal type exists in the repo (values in use: Other/USD/EUR/BTC/CHF/DEPS/FPS). It drives fee-matching (fee.entity) and balance grouping (log-job) — introducing a new bucket is a tax/reporting classification decision for DFX/DAS, so left null.
  • approxPriceUsd/Chf/Eur: would hard-code a decaying gold/silver snapshot; only read for active/custody/traded assets.
  • sortOrder: display-order only (e.g. dEURO/SAND also leave it empty).

Follow-ups when trading is enabled (verified inputs, need DFX/DAS decision)

  • Pricing is available (not on CoinGecko, but): DIA on-chain price feed for DSC, and a QuickSwap DSC/USDC pool on Polygon (0x6722e2405a3b6ee5bf862112a143cf7228ee6c18). Denario also publishes Proof-of-Reserve oracles (DSC PoR 0xb507a787cda02d6086f101d6067e9c35d58f1fc5). Attach a PriceRule (DEX/oracle source) then flip the buy/sell flags.
  • Set financialType per token once the metal asset class is decided.
  • Confirm partner-wallet compliance fields (isKycClient/amlRules).
  • Out of scope but available: DGC/DSC also exist on Plume, Soneium and BNB (BinanceSmartChain) — this PR lists Polygon only, as requested.

3. Referral alias (original scope)

  • add a permanent denario alias to the existing ref-keys setting (migration 1784994100000-AddDenarioPermanentRef.js, prd-only)
  • resolve the target referral code from the Denario organization account pinned by its immutable user_data.id = 408808 (name-based resolution was dropped after review — an editable organizationName would be hijackable)
  • preserve all existing aliases; the migration is reversible and idempotent via the audit store (§4)
  • fail closed on a missing, inactive, ambiguous, malformed, or conflicting target/configuration
  • fix /v1/app?code=<alias> to persist the resolved referral code instead of the unresolved alias, matching /v1/app/:app (includes a prototype-pollution guard on the alias lookup)

Stable link after deployment

  • Production: https://api.dfx.swiss/v1/app/services?code=denario (generic redirect: /v1/app?code=denario)
  • Development: the migration is prd-gated, so dev has no denario alias unless the ref-keys setting is set manually there.

Deployment precondition (prd)

user_data.id 408808 must be an active organization account with exactly one distinct active referral code across its active users. The migration intentionally aborts (and thereby blocks the boot migration run) if this invariant is not met — see the Ops checklist below, which must be confirmed before merge. It also refuses to overwrite an existing denario alias that points elsewhere.


4. Migration audit store

1784994000000-CreateMigrationAuditStore.js (runs in all environments) creates two append-only tables (migration_audit_lock, migration_audit_event) with DB triggers rejecting UPDATE/DELETE/TRUNCATE, check constraints, and an apply↔rollback self-FK (ON DELETE RESTRICT). The data migrations write a before→after event before mutating (same migration transaction — TypeORM default transaction: 'all'), and their down() paths only revert state whose full snapshot still matches what up() recorded: pre-existing rows, admin re-points and post-deploy changes are detected and left intact, double-down() and re-apply are guarded. Rollbacks append events instead of deleting audit rows.


5. Pay-in hardening (review round 1 finding, generalised)

Register strategies previously mapped inbound transfers against all blockchain assets; a token with chainId but no priceRule then produced a CREATED crypto input that the minute jobs re-validated forever (price lookup throws → endless retry + error log). Now:

  • All 10 multi-asset register strategies (alchemy/evm base ×2 call sites, citrea, cardano, zano, solana, tron, binance-pay, kucoin-pay, icp) use the new AssetService.getPayInAssets() (priceRule IS NOT NULL, own cache key). getAllBlockchainAssets and its balance/monitoring/asset-API callers are intentionally unchanged.
  • An inbound transfer of an unpriced token resolves to asset: nullCryptoInput is persisted FAILED (terminal, no retry loop) and triggers an ERROR_MONITORING notification so the funds on the deposit address are visible to Ops.
  • updateFailedPayments filters unpriced assets in SQL and logs a warning in its fail-closed in-loop guard.
  • 1784994300000-LinkOndoPriceRule.js (prd-only) links the existing Ethereum/ONDO asset (buyable/sellable since its seed, but priceless → every deposit stuck) to the CoinGecko ondo-finance price rule, fail-closed on missing/ambiguous rule or asset.

Declared behaviour change for existing data: the seed lists further assets with chainId but no price rule (Ethereum/BGB, Ethereum/FOX, Ethereum/EURC, Arbitrum/EURC, DFI on Ethereum/Arbitrum/BSC). New deposits of these tokens are no longer mapped to the asset; they persist as FAILED without asset assignment (previously: stuck CREATED with asset). Existing stuck CREATED rows are not migrated — see Ops checklist. Native-coin strategies (bitcoin/lightning/monero/firo) keep their dedicated coin getters without a price-rule filter; Firo/FIRO has no price rule in the seed, so the old stuck-CREATED mode still applies there — follow-up candidate, out of scope here.


Ops checklist (before merge / deploy — read-only)

All four migrations run at boot (migrationsRun); any failed precondition on prd is a crash-loop, by design (fail closed). Verify on prd:

  1. Referral target: SELECT COUNT(DISTINCT u.ref) FROM "user" u WHERE u."userDataId" = 408808 AND u.status = 'Active' AND u.ref IS NOT NULL; → must be exactly 1, and user_data 408808 is the intended active Denario organization account.
  2. ONDO price rule: exactly one row matching the rule referenced in 1784994300000-LinkOndoPriceRule.js (CoinGecko / ondo-finance), and exactly one Ethereum/ONDO asset with priceRuleId IS NULL (or already linked to that rule — then the migration no-ops).
  3. Wallet state: at most one wallet row with name = 'Denario', and if present, only conservative defaults (otherwise the migration fails closed).
  4. Stranded pay-ins: SELECT ci.id, a."uniqueName", ci.created FROM crypto_input ci JOIN asset a ON a.id = ci."assetId" WHERE ci.status = 'Created' AND a."priceRuleId" IS NULL; — ONDO rows will be processed retroactively once the price rule is linked (buy-crypto/AML/forwarding of old deposits — confirm this is wanted); rows of other unpriced assets keep retrying every minute until a price rule is linked or they are reconciled manually.

Review rounds

  • Round 1 (TaprootFreak, 3× P1): stuck pay-in pipeline for unpriced assets, destructive ref-keys overwrite, non-owned down() deletes — all three addressed (§4/§5) with regression tests (repoint, pre-existing-row before up(), two apply/rollback cycles).
  • Round 2: prd-gating of the partner migrations, referral target pinned to user_data.id instead of name.
  • Round 3: migration timestamps re-issued above the current develop head (audit constants and spec references updated in lockstep), wallet unique index created in all environments (schema-drift fix, down() no longer drops it), monitoring alert for failed unassigned pay-ins, warn-log in the updateFailedPayments guard, PR body corrected (pay-in note, deployment precondition, seed ids).

Tests & checks

  • Migration specs run the real up()/down() SQL against pg-mem (simplified schemas; pg-mem cannot execute the audit-store triggers — their guarantees are enforced by the DDL itself and reviewed, not covered by tests). Ownership scenarios covered: pre-existing rows created before up(), admin repoint, double-down(), re-apply, two full cycles.
  • Register-strategy spec drives the real AlchemyStrategy + CryptoInput.create + updateFailedPayments for the priced happy path, the unpriced FAILED path, and the new monitoring alert.
  • Full local gate: targeted Jest suites, npm run type-check, npm run lint, npm run format:check.

@joshuakrueger-dfx joshuakrueger-dfx changed the title feat(referral): add permanent Denario referral alias feat(denario): onboard Denario — partner wallet, Gold/Silver Polygon assets, and permanent referral alias Jul 15, 2026
@joshuakrueger-dfx

Copy link
Copy Markdown
Collaborator Author

Completed a full-diff conformance and logic review (1 round, 0 findings) before marking ready.

Verified:

  • Wallet + asset migration is idempotent (NOT EXISTS guards on name/uniqueName) and reversible; mirrors the merged AddOlkyFrozenAsset precedent (migration + loc-only seed row).
  • DGC/DSC contract addresses and decimals (8) confirmed on-chain and against Denario's official token pages; 1 DGC = 1 oz gold, 1 DSC = 1 oz silver.
  • Assets are inert/list-only (no priceRuleId, all trade flags false → isActive=false): excluded from the price job and every buy/sell/liquidity path — no automatic liquidity mechanism, manual only, as intended.
  • Partner wallet added with conservative DB defaults (compliance fields flagged for confirmation); not mirrored into the wallet seed (partners are prod-only).
  • Referral alias unchanged and still fail-closed/reversible.
  • All CI checks green.

@joshuakrueger-dfx
joshuakrueger-dfx marked this pull request as ready for review July 15, 2026 09:32

@TaprootFreak TaprootFreak left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I found three blocking correctness/data-safety issues. The asset rows currently activate pay-in detection without providing a processable pricing/manual path, and both migrations can delete or overwrite state they did not create. Please address the inline findings before merge.

("name", "uniqueName", "type", "blockchain", "category", "dexName", "chainId", "decimals", "description",
"buyable", "sellable", "cardBuyable", "cardSellable", "instantBuyable", "instantSellable",
"paymentEnabled", "refEnabled", "refundEnabled", "ikna", "personalIbanEnabled", "comingSoon")
SELECT 'DGC', 'Polygon/DGC', 'Token', 'Polygon', 'Public', 'DGC', '0xf7e2d612f1a0ce09ce9fc6fc0b59c7fd5b75042f', 8, 'Denario Gold Coin',

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P1] Prevent inert DGC/DSC deposits from getting stuck in the pay-in pipeline. Supplying chainId makes Alchemy map inbound Polygon transfers to these assets because the register strategy loads all blockchain assets, regardless of isActive. The registration flow then calls validateInput(), which requires an asset→CHF price; with no priceRuleId, pricing throws and the catch leaves the pay-in in CREATED, so the minute job retries and logs it indefinitely. This contradicts the PR's claim that recognition/forwarding is harmless. Either provide the complete priced/manual processing path or explicitly exclude these inert assets from pay-in registration, and add a DGC/DSC deposit regression test.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed. The register strategies now load getPayInAssets() (filters priceRule IS NOT NULL) instead of getAllBlockchainAssets(), so DGC/DSC (no price rule) are never mapped by Alchemy: an inbound Polygon transfer resolves to asset: nullCryptoInput.create sets FAILED, so it never enters the CREATED retry loop. updateFailedPayments got a matching SQL filter (asset: { priceRule: Not(IsNull()) }) plus a fail-closed in-loop guard, so the payment-quote cron can't resurrect an unpriced input either.

The swap was applied to every multi-asset register strategy (alchemy/evm base, citrea, cardano, zano, solana, tron, binance-pay, kucoin-pay, icp); native coin strategies keep their dedicated priced getters, and the balance/monitoring/asset-API callers of getAllBlockchainAssets are intentionally left unchanged (getPayInAssets uses its own cache key). Regression coverage in alchemy.strategy.spec.ts drives the real strategy + CryptoInput.create + updateFailedPayments for both the priced-ONDO happy path and the unpriced-token FAILED path.

This also surfaced a latent ONDO issue (chainId + buyable/sellable but no price rule → same stuck-pay-in) — fixed via the LinkOndoPriceRule migration and the seed row.

const value = JSON.stringify(refKeys);

if (row) {
await queryRunner.query(`UPDATE "setting" SET "value" = $1, "updated" = NOW() WHERE "key" = $2`, [

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P1] Preserve and audit the previous ref-keys state before overwriting it. Production already has this setting, so this replaces the serialized value without a durable before→after record, contrary to CONTRIBUTING.md's critical “Auditable mutations — no destructive overwrites” rule. Rollback ownership is also unsafe: if denario already pointed to the same ref, up() returns without changing anything, but down() still deletes that pre-existing alias; it also deletes a value changed after deployment. Persist an immutable prior-state/audit record before the update (fail closed if it cannot be written) and make down() restore only the change actually owned by this migration.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed. Added an append-only migration_audit_* store (immutability triggers rejecting UPDATE/DELETE/TRUNCATE, Apply/Rollback events tied by a composite self-FK with ON DELETE RESTRICT). up() writes the before→after event before mutating the ref-keys setting, in the same batch transaction (TypeORM default transaction: 'all', verified in config.ts), so a crash between the two rolls both back — no fail-open window.

down() now removes the alias only while it still equals the value we set (ownedRef); an admin re-point or re-add after deployment is detected and left intact (skippedAliasRepointed / skippedAliasMissing), and it restores the exact prior state (null vs pruned object) based on the recorded existed flag. Reapply after a rollback fails closed rather than clobbering a later change. Covered by add-denario-permanent-ref.migration.spec.ts, including the repoint and reapply cases.

* @param {QueryRunner} queryRunner
*/
async down(queryRunner) {
await queryRunner.query(`DELETE FROM "asset" WHERE "uniqueName" IN ('Polygon/DGC', 'Polygon/DSC')`);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P1] Do not delete rows that up() may not have created. Each up() insert is guarded by NOT EXISTS, but down() unconditionally deletes every matching asset and wallet (and wallet.name is not unique). In a pre-seeded environment or after migration-history recovery, an up→down cycle therefore removes pre-existing data. Either fail closed when matching rows already exist or persist exact migration ownership/prior state and revert only that state; add an up→down test covering the pre-existing-row case.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed. down() reads the ownership snapshot recorded by the audit store and deletes only rows whose full snapshot is still unchanged (assertOwnedRowIsUnchanged); pre-existing or reused rows are never touched, and it fails closed on an ambiguous/altered row. It also refuses to delete the wallet if any user.walletId still references it, and records the exact rollback (deleted asset/wallet ids) before the destructive writes. Double-down() is guarded (if (!applyAudit) return runs before the non-IF EXISTS DROP INDEX). Up→down and the pre-existing-row case are covered by add-denario-wallet-and-assets.migration.spec.ts with two apply/rollback cycles.

@joshuakrueger-dfx

Copy link
Copy Markdown
Collaborator Author

Thanks — addressed all three findings in 5f4f986.

1. Inert DGC/DSC deposits stuck in the pay-in pipeline. Pay-in register strategies now recognize only priced assets via a new AssetService.getPayInAssets (SQL filter priceRule IS NOT NULL, scoped to the pay-in path with its own cache key, so balances/monitoring/asset-API are untouched). An unpriced token now maps to asset = nullCryptoInput is created as FAILED → never re-selected by getNewPayIns, so no CREATED retry loop and no per-minute logging. Applied to all register strategies (not just Alchemy), since the same map-by-chainId-then-price pattern was latent on every chain (e.g. Ethereum/ONDO). Regression test added: an unpriced token is excluded from recognition.

2. ref-keys overwrite without a durable before→after record. AddDenarioPermanentRef now writes an immutable before-image (ref-keys.backup.<migration-id> = { existed, previous, ownedRef }) before the update; the migration is transactional, so a failed backup insert rolls back the ref-keys change (fail closed). down() restores from the backup and removes the alias only while it still carries the value this migration set — an admin re-point or a later-added alias is preserved.

3. down() deleting rows it may not have created. AddDenarioWalletAndAssets.down() now removes the assets only while still inert (priceRuleId IS NULL AND NOT buyable AND NOT sellable) and the wallet only while ownerId IS NULL and no user references it. Anything activated or linked afterwards is left in place (roll-forward). Added an up→down migration spec covering the pre-existing/activated-row case.

All local gates green (type-check, lint, 23 tests across the 3 new/updated specs).

Copy link
Copy Markdown
Collaborator

Follow-up review of 5f4f986: the targeted fixes improve the PR, but two blocking issues remain, plus one broader pay-in regression risk.

  1. [P1] AddDenarioWalletAndAssets.down() still deletes rows that up() may not have created.
    The new predicates describe the rows' current state; they do not establish migration ownership. If inert DGC/DSC rows or an unused Denario wallet already exist, up() skips them via NOT EXISTS, but down() still deletes them. The wallet query can also delete multiple rows because wallet.name is not unique. Additionally, assets activated only through cardBuyable, cardSellable, instantBuyable, instantSellable, paymentEnabled, or refEnabled still match the delete. Please either fail closed on pre-existing rows or persist the exact IDs created by this migration. The migration spec should pre-insert the rows before up() and verify that up() -> down() preserves them; the current test only activates rows created by up().

  2. [P1] The supposedly immutable ref-keys audit record is destroyed by down().
    Writing the before-image before the snapshot update fixes the forward path, but down() unconditionally deletes ref-keys.backup.1784037000000 after updating—or deciding not to update—the snapshot. After rollback, the database can no longer reconstruct which alias value was removed, the before→after transition, or whether rollback skipped the change because an admin had re-pointed it. This still conflicts with CONTRIBUTING's critical append-only/reconstructible mutation rule. Keep the audit row as superseded/rolled back, or write a separate immutable rollback event before changing the snapshot; do not delete the only history row.

  3. [P2] Guard the global pay-in filter against active assets without a linked price rule.
    Switching every register strategy to getPayInAssets() stops the unpriced-token retry loop, but the fresh seed already contains an active counterexample: Ethereum/ONDO is buyable/sellable while its priceRuleId is empty, even though price rule 60 exists. A legitimate ONDO sell deposit is therefore mapped to an unknown asset and saved as FAILED. Please link ONDO to its rule and add a seed/config invariant asserting that every active pay-in/sell asset is priced. The current AssetService test only verifies the mocked list/filter; it does not exercise a register strategy, the resulting CryptoInput status, or the no-retry behavior.

The referral controller change itself looks correct, and the DGC/DSC retry-loop cause is addressed. I reran the four targeted suites locally: 29 tests passed. The remaining blockers are test gaps and ownership/audit semantics, not failing CI.

@joshuakrueger-dfx
joshuakrueger-dfx force-pushed the agent/add-denario-permanent-ref branch from 655c372 to 35d3c29 Compare July 21, 2026 10:01
@joshuakrueger-dfx

Copy link
Copy Markdown
Collaborator Author

All three P1 findings are addressed (see the inline replies), and the branch has been rebased onto the current develop.

Conflict resolution worth flagging:

  • develop had meanwhile taken asset ids 411/412 (Frick EUR/CHF). The Denario DGC/DSC rows were renumbered to 413/414 in the seed CSV. The migration inserts by uniqueName (not by id), so there is no PK coupling — the renumber is seed-only.
  • crypto-input.entity.spec.ts and payin.service.ts merged with develop's parallel pay-in changes; PayInService gained a new NotificationService dependency on develop, so the new alchemy.strategy.spec.ts construction was updated to match.

Local gate before push: prettier, eslint, tsc --noEmit, and the affected suites all green.

One pre-deploy note (not a code issue): routing the register strategies through getPayInAssets means a currently-active-but-unpriced asset on those chains would now register a deposit as FAILED instead of looping forever in CREATED. That is the intended fix, but worth a quick check against real prod asset data before deploy.

@joshuakrueger-dfx

Copy link
Copy Markdown
Collaborator Author

Added one dependency commit chore(deps): override tar to >=7.5.19 (GHSA-23hp-3jrh-7fpw) — cherry-picked verbatim from the existing chore/override-tar-critical-advisory branch (original authorship preserved). This is unrelated to the Denario change: the critical came in from develop's lockfile during the rebase (review/npm-audit gates on critical), and this PR itself touches no dependency manifest. npm audit now reports 0 critical (tar → 7.5.20). It can be dropped from this branch once the same override lands on develop.

- pay-in register strategies recognize only priced assets (new AssetService.getPayInAssets); an unpriced token is no longer mapped into the register flow, where validateInput/pricing throws and would loop the pay-in in CREATED forever
- AddDenarioPermanentRef: persist an immutable before-image of ref-keys before overwriting (fail closed in the migration transaction) and revert only the alias this migration set
- AddDenarioWalletAndAssets: down() removes only still-inert assets and the wallet only while it has no owner or users
- specs: unpriced-asset exclusion from recognition, ref-keys audit/ownership, wallet/asset up->down and ownership preservation
@joshuakrueger-dfx
joshuakrueger-dfx force-pushed the agent/add-denario-permanent-ref branch from 70f8f32 to ca99196 Compare July 22, 2026 07:29
… account id

Guard up() and down() of the three Denario data migrations
(AddDenarioPermanentRef, AddDenarioWalletAndAssets, LinkOndoPriceRule) with
ENVIRONMENT === 'prd', matching the AddBankFrickCustodyAssets/ActivateBankFrick
pattern. Without the guard these boot-blocking migrations throw on dev/loc/CI,
where the Denario organization account and ONDO price rule do not exist, and
reject DataSource init. The audit-store migration stays unguarded (pure DDL).

Resolve the Denario referral target by the immutable prod user_data.id instead
of the user-editable organizationName, so a user who completes Organization KYC
under the name "Denario" cannot hijack the referral routing. The referral code
itself is still read live; accountType/status/ambiguity guards remain as
defense-in-depth. Specs force ENVIRONMENT=prd and prove the id pin excludes
same-named impostor accounts.
…tion lint

The `[DENARIO_USER_DATA_ID]` array literal tripped the migration-psql-check
guard, whose MSSQL bracket-quoting detector flags any `[identifier]`. Use
Array.of(...) — the single-element param idiom already used elsewhere in this
migration — which is equivalent and avoids the false positive.
@joshuakrueger-dfx

Copy link
Copy Markdown
Collaborator Author

Rebased onto current develop to resolve the conflicts with the native-first exact-amounts ledger changes (#4288) — merged the getPayInAssets narrowing and the amountBaseUnits capture in the Solana/Zano register strategies.

Two rounds of independent review (conformity, logic, and a focused security/failure-mode pass) surfaced and fixed two issues:

  1. Boot-blocking migrations. The three Denario data migrations (AddDenarioPermanentRef, AddDenarioWalletAndAssets, LinkOndoPriceRule) threw when their prod prerequisites (the Denario organization account / the ONDO price rule) were absent, which would reject DataSource init on dev/loc/CI where those don't exist. Guarded up()/down() with ENVIRONMENT === 'prd', matching the AddBankFrickCustodyAssets / ActivateBankFrick pattern; the audit-store migration stays unguarded (pure idempotent DDL). Specs force ENVIRONMENT=prd.

  2. Referral-target hardening. The permanent denario alias resolved its target via the user-editable organizationName, which a user completing Organization KYC under the name "Denario" could hijack. Pinned it to the immutable Denario user_data.id instead; accountType/status/ambiguity guards remain as defense-in-depth. Note for deploy: this id must belong to the active Denario organization account (fail-closed otherwise).

All checks green. Follow-ups tracked separately: recommendation-skip and server-side referral→wallet coupling.

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