From de50b65d012204712443873fc2c39579c5f46eb4 Mon Sep 17 00:00:00 2001 From: hpmaxi <358059+hpmaxi@users.noreply.github.com> Date: Wed, 19 Aug 2026 14:31:52 -0300 Subject: [PATCH] docs: record design decisions and closed questions as adrs --- ...composition-via-explicit-leaf-overrides.md | 30 ++++++++++++++++ ...always-open-deposit-gates-receiver-only.md | 25 ++++++++++++++ ...ull-name-symbols-role-manager-read-only.md | 34 +++++++++++++++++++ ...uthorities-are-native-stellar-multisigs.md | 21 ++++++++++++ ...-version-pins-no-mid-milestone-upgrades.md | 21 ++++++++++++ ...ream-policy-and-authorized-674-fallback.md | 34 +++++++++++++++++++ docs/adr/0007-apache-2-0-license.md | 20 +++++++++++ docs/adr/0008-lp-counter-removed.md | 20 +++++++++++ 8 files changed, 205 insertions(+) create mode 100644 docs/adr/0001-composition-via-explicit-leaf-overrides.md create mode 100644 docs/adr/0002-exits-always-open-deposit-gates-receiver-only.md create mode 100644 docs/adr/0003-roles-full-name-symbols-role-manager-read-only.md create mode 100644 docs/adr/0004-authorities-are-native-stellar-multisigs.md create mode 100644 docs/adr/0005-exact-version-pins-no-mid-milestone-upgrades.md create mode 100644 docs/adr/0006-upstream-policy-and-authorized-674-fallback.md create mode 100644 docs/adr/0007-apache-2-0-license.md create mode 100644 docs/adr/0008-lp-counter-removed.md diff --git a/docs/adr/0001-composition-via-explicit-leaf-overrides.md b/docs/adr/0001-composition-via-explicit-leaf-overrides.md new file mode 100644 index 0000000..c0c4d15 --- /dev/null +++ b/docs/adr/0001-composition-via-explicit-leaf-overrides.md @@ -0,0 +1,30 @@ +# 1. Composition via explicit leaf overrides + +Status: accepted + +## Context + +OpenZeppelin stellar-contracts routes token behavior through a single +`type ContractType` slot, making flavors mutually exclusive: `Vault` and +`AllowList` cannot compose as types +([#560](https://github.com/OpenZeppelin/stellar-contracts/issues/560)). Upstream +fixes are in flux (`Compose`, +[PR #821](https://github.com/OpenZeppelin/stellar-contracts/pull/821), open and +unaudited). The same convergence exists across Rust chains: cw20-base's +documented extension pattern is explicit delegation from the final contract, not +type-level composition. + +## Decision + +`ContractType = Vault` stays intact so share math remains 100% OpenZeppelin. +`AllowList` is reused as a storage module, and every gate is written in this +contract's own entrypoint overrides. This is the design, not a stopgap: adopting +a future upstream composition mechanism is a governance option to be evaluated +on its merits, never a scheduled migration. + +## Consequences + +- The storage layout stays upstream-compatible either way. +- Every gated entrypoint needs an explicit test (the check is code we own). +- The earlier plan to "delete manual overrides when #560 ships audited" is void; + #560 is tracked only as a potential simplification. diff --git a/docs/adr/0002-exits-always-open-deposit-gates-receiver-only.md b/docs/adr/0002-exits-always-open-deposit-gates-receiver-only.md new file mode 100644 index 0000000..15f8ef4 --- /dev/null +++ b/docs/adr/0002-exits-always-open-deposit-gates-receiver-only.md @@ -0,0 +1,25 @@ +# 2. Exits always open; deposits gate the receiver only + +Status: accepted + +## Context + +The vault enforces a post-KYC allowlist, but a compliance vault that can trap +funds is worse than no vault. The open question was whether the deposit payer +must also be allowlisted, or only the receiver of the shares. + +## Decision + +`withdraw`/`redeem` (and any future redemption-claim path) are never +allowlist-gated and never pausable: a de-listed holder can always leave. +`deposit`/`mint` gate the receiver only. The payer is not checked — USDC carries +its own compliance on the asset side; the regulated asset here is the share, and +shares can only land in allowlisted hands. + +## Consequences + +- Third-party funding (custodian or treasury paying for an allowlisted investor) + is accepted behavior. +- Pause blocks entries and transfers only. +- Every change to an exit path must preserve the invariant, and tests assert + that exits work for de-listed holders and while paused. diff --git a/docs/adr/0003-roles-full-name-symbols-role-manager-read-only.md b/docs/adr/0003-roles-full-name-symbols-role-manager-read-only.md new file mode 100644 index 0000000..0150a29 --- /dev/null +++ b/docs/adr/0003-roles-full-name-symbols-role-manager-read-only.md @@ -0,0 +1,34 @@ +# 3. Five full-name roles; Role Manager is read-only, execution in /admin + +Status: accepted + +## Context + +Access control moves from `Ownable` to `stellar-access` AccessControl with five +roles. `symbol_short!` caps at 9 characters, so `governance`, `compliance` and +`attestation` cannot be compile-time constants. The roadmap assumed OpenZeppelin +Role Manager as the full governance UI. The spike +([#7](https://github.com/BootNodeDev/open-rwa-vault/issues/7)) verified it +against a live testnet contract: detection, role listing and admin transfers +work; `grant_role`/`revoke_role` fail against OZ ≥0.6.0 (argument-order bug we +reported upstream: +[openzeppelin-adapters#72](https://github.com/OpenZeppelin/openzeppelin-adapters/issues/72)), +its execution path has no Stellar multisig support, and the hosted instance is +testnet-only. Roles are discovered from indexed grant events, so a role never +granted is invisible to the tool. + +## Decision + +Roles are declared with runtime `Symbol::new` and full names — `governance`, +`compliance`, `attestation`, `treasury`, `guardian` — matching the pattern OZ's +own generator uses. All five are granted at deploy time. Role Manager is scoped +to visualization, audit and history; role mutations and pause execute through +the admin panel's signing-harness forms (compose → sign → share → submit), which +the multisig authorities require anyway. + +## Consequences + +- The deploy script grants every role so tooling renders them from day one. +- The /admin surface gains grant/revoke forms; the runbook documents Role + Manager as the verification step after each operation. +- Self-hosting Role Manager is a mainnet-track task, not needed for testnet. diff --git a/docs/adr/0004-authorities-are-native-stellar-multisigs.md b/docs/adr/0004-authorities-are-native-stellar-multisigs.md new file mode 100644 index 0000000..2ea6e48 --- /dev/null +++ b/docs/adr/0004-authorities-are-native-stellar-multisigs.md @@ -0,0 +1,21 @@ +# 4. Authorities are native Stellar multisig accounts + +Status: accepted + +## Context + +Separation of duties requires each privileged role to be held by a multi-party +authority. Stellar accounts support signers, weights and thresholds natively +(`SetOptions`), proven in the current testnet deployment. + +## Decision + +Each authority is a native Stellar multisig G-account. Contracts check only the +account's authorization; no multisig logic is reimplemented on-chain. + +## Consequences + +- Any execution surface must support multi-signature collection. The signing + harness does; Role Manager does not (see ADR-0003). +- The deploy tooling creates and configures the authority accounts as part of a + reproducible setup. diff --git a/docs/adr/0005-exact-version-pins-no-mid-milestone-upgrades.md b/docs/adr/0005-exact-version-pins-no-mid-milestone-upgrades.md new file mode 100644 index 0000000..4589fc1 --- /dev/null +++ b/docs/adr/0005-exact-version-pins-no-mid-milestone-upgrades.md @@ -0,0 +1,21 @@ +# 5. Exact version pins; upgrades never mid-milestone + +Status: accepted + +## Context + +The OZ 0.7.x line has a published audit; soroban-sdk 27.x is incompatible with +OZ 0.7.2 (coupled to sdk 26). A dependency bump mid-milestone invalidates tested +behavior at the worst possible time. + +## Decision + +Exact pins: `stellar-*` = 0.7.2, `soroban-sdk` 26.x. Dependency upgrades are +never performed mid-milestone; each migration is its own issue in the following +milestone. + +## Consequences + +- Dependabot needs a cargo ecosystem entry plus an ignore rule for soroban-sdk + major bumps. +- A compatible OZ release triggers a migration issue, not a hot swap. diff --git a/docs/adr/0006-upstream-policy-and-authorized-674-fallback.md b/docs/adr/0006-upstream-policy-and-authorized-674-fallback.md new file mode 100644 index 0000000..a79ff17 --- /dev/null +++ b/docs/adr/0006-upstream-policy-and-authorized-674-fallback.md @@ -0,0 +1,34 @@ +# 6. Upstream policy, and the authorized fallback for #674 + +Status: accepted + +## Context + +Gaps found in OZ primitives so far: +[#560](https://github.com/OpenZeppelin/stellar-contracts/issues/560) +(composition), +[#674](https://github.com/OpenZeppelin/stellar-contracts/issues/674) +(conversions ignore `total_assets` overrides — blocks attested-NAV accounting), +and +[openzeppelin-adapters#72](https://github.com/OpenZeppelin/openzeppelin-adapters/issues/72) +(Role Manager grant/revoke, found and filed by us). Waiting on upstream +responses would put third parties on our critical path. The roadmap also +prohibited rewriting conversion math without a formal decision — this ADR is +that decision, made in advance. + +## Decision + +Every gap gets an upstream comment or PR with this repo as evidence, plus a +local workaround marked `// WORKAROUND(OZ#nnn): — delete when ` +(greppable; a workaround without this marker is a process bug). Upstream never +sits on the critical path. Specifically for #674: if the fix has not shipped in +an audited release when attested-NAV accounting lands, a fork-minimal wrapper is +pre-authorized — it may override only the conversion entrypoints, must delegate +every other line to the audited crate, must carry a parity test against OZ +behavior at zero off-chain value, and must carry the deletion trigger. + +## Consequences + +- Workarounds are enumerable (`grep WORKAROUND`), each with its exit condition. +- The #674 wrapper is the only sanctioned hand-rolled money math, bounded and + deletable. diff --git a/docs/adr/0007-apache-2-0-license.md b/docs/adr/0007-apache-2-0-license.md new file mode 100644 index 0000000..797b456 --- /dev/null +++ b/docs/adr/0007-apache-2-0-license.md @@ -0,0 +1,20 @@ +# 7. Apache-2.0 license + +Status: accepted + +## Context + +The reference base ships BUSL-1.1, which is source-available but not +OSI-approved. The Stellar Community Fund requires a permissive OSI-approved +license for its awards. BootNode owns the reference base, so relicensing the +code that moves across is possible. + +## Decision + +Apache-2.0, LICENSE file at the repo root. + +## Consequences + +- SCF-compatible, aligned with the OZ/Stellar ecosystem, includes an explicit + patent grant. +- Code migrated from the demo base is relicensed on the way in. diff --git a/docs/adr/0008-lp-counter-removed.md b/docs/adr/0008-lp-counter-removed.md new file mode 100644 index 0000000..0366971 --- /dev/null +++ b/docs/adr/0008-lp-counter-removed.md @@ -0,0 +1,20 @@ +# 8. LP counter removed from the contract + +Status: accepted + +## Context + +The reference base kept an on-chain counter of liquidity providers. No product +requirement consumes it; on-chain counters add state that every entry/exit must +touch, creating footprint contention under concurrency. + +## Decision + +Removed. If the dashboard needs the figure, it is reconstructed off-chain from +events. + +## Consequences + +- One less write per deposit/withdraw and one less contention point. +- An on-chain holder-limit requirement, if it ever appears, is a new feature + with its own design — not this counter revived.