Votes trait is not bound to the token contract type

Aperta
#930 0 commenti 0 reazioni 1 assegnatario Vedi su GitHub

I maintainer di solito rispondono entro 1 giorno

@ozgunozerk ci sta già lavorando.

Dal 9/11/2026.

  • #962 di @ninglinLiu — chiusa senza merge

Valutazione

Difficoltà
5/5
Tempo stimato
Più di una settimana
Idoneità per principianti
35/100
Tipo di issue
Refactoring
Chiarezza
Abbastanza chiara
Stato di attività
Attiva
Stack tecnologico
rust
Ambito
backend

Direzione di ricerca

Start with packages/governance/src/votes/mod.rs and the fungible and non-fungible composition modules named in the issue; compare their trait bounds and contract-type markers. The issue outlines two incomplete approaches and says to address this during the planned extension restructure, but does not settle a complete design. Done means preventing the unbounded token implementation while preserving the intended reusable votes interface and validating the cross-contract call behavior.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Descrizione

Description

Every token extension trait is tied to the contract type through its bound. For example, FungibleAllowList: FungibleToken<ContractType: AllowListContractType> and NonFungibleEnumerable: NonFungibleToken<ContractType: EnumerableContractType>. Implementing one of these traits without the matching entry in the Compose<(...)> list does not compile, and both composition modules (fungible/extensions/combinations/mod.rs, non_fungible/extensions/combinations/mod.rs) document this guarantee.

Votes is the exception. It is declared in stellar-governance (packages/governance/src/votes/mod.rs) with no supertrait and no bound. So the following compiles:

#[contractimpl(contracttrait)]
impl FungibleToken for MyToken {
    type ContractType = Compose<(AllowList,)>; // no FungibleVotes
}

#[contractimpl(contracttrait)]
impl Votes for MyToken {}

Transfers, mints and burns never call transfer_voting_units, so no checkpoint is ever written. get_votes and get_votes_at_checkpoint return 0 for every account, and delegate succeeds while moving nothing. The contract type is fixed at compile time, so this holds for the whole life of the contract.

This was raised as L-03 in the latest audit.

The bound cannot simply be added to Votes: stellar-tokens depends on stellar-governance, so governance cannot name FungibleToken, NonFungibleToken, or a tokens-side marker trait without a dependency cycle.

Potential solutions

A. Move the trait to tokens, keep only storage in governance
  • Governance keeps the votes storage functions, errors (4100 block) and events, but no longer declares a Votes trait. A small client-only interface stays so the Governor can still call get_votes_at_checkpoint on the token.
  • Tokens defines FungibleVotes: FungibleToken<ContractType: VotesContractType> and NonFungibleVotes: NonFungibleToken<ContractType: VotesContractType>, with default bodies calling the governance storage functions.
  • The contract types are renamed to Votes (matching AllowList, Enumerable, ...), and VotesContractType is implemented by Votes and the existing *Votes combined types.

Contract author:

type ContractType = Compose<(AllowList, Votes)>;

#[contractimpl(contracttrait)]
impl FungibleVotes for MyToken {}

Advantages

  • Token contracts can only get the votes functions through a bounded trait, so the L-03 scenario fails to compile.
  • The "rejected by that trait's bound" claim in both composition modules holds without exceptions.
  • Votes follows the same naming and structure as the other extensions.

Disadvantages

  • Contracts that are not tokens (staking contracts, custom voting-power sources) lose the ready-made trait. They have to write the votes functions themselves on top of the storage functions.
  • Breaking change: impl Votes for X {} becomes impl FungibleVotes for X {}, and the FungibleVotes / NonFungibleVotes contract types are renamed.
B. Body-less interface in governance, bounded traits in tokens
  • Governance replaces the #[contracttrait] Votes with a body-less #[contractclient(name = "VotesClient")] pub trait VotesInterface that declares all six votes functions (the counterpart of Solidity's IVotes). This follows the existing Upgradeable / ClaimIssuer pattern. Storage, errors and events are unchanged.
  • Tokens adds the same bounded FungibleVotes / NonFungibleVotes traits, contract type rename and VotesContractType marker as in A.

Advantages

  • Governance still publishes the full votes interface that any voting source should expose, so non-token contracts keep a reference to implement against.
  • governor/storage.rs only changes an import.
  • An empty impl VotesInterface for X {} no longer compiles, since there are no default bodies.

Disadvantages

  • It does not close the gap for tokens. A token can still implement VotesInterface directly, writing bodies that forward to the governance storage functions, without Votes in its Compose list. The result is the same silent-zero behavior as today, just with more code to write.
  • The function signatures live in two places (governance interface and tokens traits), with no compile-time check that they match. A mismatch only shows up as a failed cross-contract call at runtime, so it needs a test that calls a FungibleVotes token through VotesClient.
  • Same breaking change for token authors as A.

Summary

Neither option is complete: A gives full type-safety for tokens but drops the reusable trait for non-token contracts, and B keeps that reusability but leaves the unbounded path open for tokens.

Reminder for the restructure

We plan to make some extensions reusable across fungible and non-fungible by moving their business logic (storage functions) to a shared location (e.g. contract-utils), while keeping the traits per token type. When that restructure happens, this issue should be addressed as part of it, since votes is the same shape of problem: shared storage logic, with per-token-type traits that carry the contract type bound.

Lingua principale
Rust
Stelle
97
Fork
75
Merge medio
1g 1h
PR unite (30g)
8

Preparare l'ambiente

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Altre issue di OpenZeppelin/stellar-contracts

Tutte le issue di OpenZeppelin/stellar-contracts

Issue simili

Altre issue su Rust

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.