Skip to content

feat: add vote snapshot checkpointing, vault reentrancy guard, oracle failover, and standardized TTL configs (#793, #794, #795, #796) - #797

Merged
Jaydbrown merged 1 commit into
conduit-protocol:mainfrom
blurbeast:feat/resolve-793-794-795-796
Oct 18, 2026
Merged

Jaydbrown merged 1 commit into
conduit-protocol:mainfrom
blurbeast:feat/resolve-793-794-795-796

Conversation

@blurbeast

Copy link
Copy Markdown
Contributor
## 1. Proposal Vote Tally Snapshotting at Snapshot Block (Closes #793)

### Problem
Previously, the governance contract tallied votes dynamically as voters called `cast_vote` without anchoring

voting weights to the ledger sequence at which a proposal was created. In a token-weighted or account-delegated
governance model, evaluating voting power at vote time rather than at a fixed proposal snapshot ledger exposes
the protocol to vote-multiplication attacks: a single participant can cast a vote from one wallet, transfer their
governance tokens or weight to a secondary wallet after the proposal is already active, and vote a second time on
the exact same proposal to artificially inflate support or quorum.

### Solution
We introduced historical balance checkpointing and per-proposal snapshot ledger anchoring in

contracts/governor/src/storage.rs and wired it into contracts/governor/src/lib.rs:
- Snapshot Ledger Anchoring: Extended Proposal with snapshot_ledger and added
DataKey::ProposalSnapshotLedger(u64) and DataKey::ProposalVoteSnapshot(u64) (VoteTallySnapshot). When
propose is invoked, the current ledger sequence (env.ledger().sequence()) and total governance supply are
immutably recorded for that proposal.
- Historical Balance Checkpoints: Added BalanceCheckpoint { ledger, weight } stored in
DataKey::VoteCheckpoints(Address) along with checkpoint_balance and get_prior_votes. Checkpoints record an
account's voting weight at a specific ledger sequence.
- Snapshot-Time Weight Lookup & Double-Vote Prevention: During cast_vote / cast_vote_with_rationale,
the governor verifies that the voter has not already voted on the proposal via DataKey::VoterReceipt(proposal_id, voter) (reverting with Error::AlreadyVoted if so) and resolves the voter's weight strictly from checkpoints
recorded at or before proposal.snapshot_ledger. Any tokens or weight acquired after proposal.snapshot_ledger
resolve to zero weight for that proposal and are rejected with Error::InsufficientVotingWeight.

---

## 2. Reentrancy Protection Macro Across All External Token Transfers (Closes #794)

### Problem
`TokenVault` interacts with external SEP-0041 token contracts across multiple deposit, withdrawal, batch payout,

multi-token, and emergency migration entry points. While standard Stellar asset contracts do not re-enter caller
contracts, custom or arbitrary SEP-0041 token implementations can execute hooks during transfer calls that re-
invoke the vault before post-transfer balance assertions, accounting updates, or state cleanups are finalized.

### Solution
We implemented a lightweight transient reentrancy guard in `contracts/token-vault/src/lib.rs`, 

contracts/token-vault/src/storage.rs, and contracts/token-vault/src/errors.rs:
- Transient Reentrancy Guard Macro: Defined non_reentrant! (and alias reentrancy_guard!) in
contracts/token-vault/src/lib.rs, backed by Soroban temporary storage (DataKey::ReentrancyLock). Before any
external token invocation begins, the macro checks whether DataKey::ReentrancyLock is set; if active, the call
immediately aborts with Error::ReentrancyDetected. Otherwise, it sets the lock, executes the transfer block,
and removes the lock from temporary storage upon completion.
- Comprehensive Transfer Coverage: Wrapped every external token transfer flow in TokenVault (deposit,
batch_deposit, withdraw_payouts covering withdraw and withdraw_batch, deposit_token, withdraw_token,
and execute_emergency_migration) with non_reentrant!.

---

## 3. Fallback Secondary Oracle Feed with Automatic Failover (Closes #795)

### Problem
Streams and payout calculations that depend on external price feeds face a single point of failure when relying

on a single primary oracle provider. If the primary oracle node or contract becomes unresponsive, uninitialized,
or serves stale data beyond the configured max_staleness window, downstream stream settlements and fiat payout
conversions halt until manual administrative intervention occurs.

### Solution
We added redundant oracle feed storage and automatic failover resolution in `contracts/oracle/src/lib.rs`:
- **Primary & Fallback Feed Storage**: Added `DataKey::PrimaryOracle` and `DataKey::FallbackOracle` in instance

storage, along with admin-gated configuration entry points (set_oracle_feeds, set_primary_oracle,
set_fallback_oracle) and read accessors (primary_oracle, fallback_oracle).
- Automatic Failover Resolution: Implemented query_oracle_feed and resolve_oracle_with_failover,
exposed via get_price_with_fallback and integrated into get_twap_price and latest_price. The oracle queries
primary_oracle first (supporting both local feeder submissions and external oracle contract cross-invocations
via try_invoke_contract) and validates freshness against max_staleness. If the primary feed is missing,
unresponsive, or stale, execution seamlessly fails over to fallback_oracle.

---

## 4. Standardized TTL Configuration Constants for Testnet and Mainnet (Closes #796)

### Problem
Storage time-to-live (TTL) renewal parameters across the workspace previously relied on scattered or aliased

ledger constants without distinguishing between Testnet and Mainnet ledger retention profiles, and some per-
contract ttl.rs modules still performed inline extend_ttl calls instead of delegating to drip-common.
Inconsistent TTL thresholds risk premature state archival on Mainnet or unnecessary rent overhead on Testnet.

### Solution
We centralized all ledger TTL constants and network configurations in `contracts/common/src/ttl.rs`:
- **Centralized Constants**: Defined `MIN_TTL_LEDGERS` and `EXTEND_TTL_LEDGERS` in `contracts/common/src/ttl.

rs(and re-exported them at the root ofdrip-common), preserving TTL_THRESHOLDandTTL_EXTEND_TOas aliases for backward compatibility. - **Testnet & Mainnet Profiles**: AddedTtlConfig, TESTNET_MIN_TTL_LEDGERS (17_280ledgers ≈ 1 day),TESTNET_EXTEND_TTL_LEDGERS (34_560ledgers ≈ 2 days),MAINNET_MIN_TTL_LEDGERS (100_000ledgers ≈ 5.8 days),MAINNET_EXTEND_TTL_LEDGERS (200_000ledgers ≈ 11.6 days), plusttl::testnetandttl::mainnet modules and Cargo feature flags (testnet, mainnet). - **Unified Delegation**: Updated contracts/batch-processor/src/ttl.rsandcontracts/factory/src/ttl.rsto delegate directly todrip_common::ttl`.

---

closes #793
closes #794
closes #795
closes #796

…uit-protocol#795, and conduit-protocol#796

- governor: add proposal vote tally snapshotting and balance checkpoints at snapshot ledger (Closes conduit-protocol#793)
- token-vault: add transient reentrancy protection macro across all external token transfers (Closes conduit-protocol#794)
- oracle: support fallback secondary oracle feed with automatic failover if primary feed fails (Closes conduit-protocol#795)
- common: centralize MIN_TTL_LEDGERS, EXTEND_TTL_LEDGERS, and testnet/mainnet TTL config constants (Closes conduit-protocol#796)
@blurbeast
blurbeast requested a review from Jaydbrown as a code owner October 18, 2026 08:12
@drips-wave

drips-wave Bot commented Oct 18, 2026

Copy link
Copy Markdown

@blurbeast Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants