Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions STABILITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Stability Guarantees

## ContractError Discriminant Values

The `ContractError` enum in `contracts/anonvote/src/errors.rs` defines error codes
returned by the AnonVote Soroban contract. Each variant has a fixed `#[repr(u32)]`
discriminant.

**These discriminant values must never change after the contract is deployed.**
Consumers that parse error codes from Stellar transaction results depend on the
numeric value, not the variant name. Changing a value is a breaking change.

| Variant | Value | Description |
|------------------------|-------|------------------------------------------|
| AlreadyInitialized | 1 | initialize called after admin is set |
| Unauthorized | 2 | caller is not the admin |
| BallotAlreadyExists | 3 | record_ballot called with existing hash |
| BallotNotFound | 4 | write op with unregistered ballot |
| BallotAlreadyFinalised | 5 | record_result after result is set |
| InvalidBallotIdHash | 6 | ballot_id_hash not valid 64-char hex |
| InvalidResultHash | 7 | result_hash not valid 64-char hex |
| InvalidAdminAddress | 8 | new admin address is zero or same |
| CounterOverflow | 9 | token/vote counter exceeds u32::MAX |
| BallotExpired | 10 | operation after ballot ledger expiry |

## Storage Key Stability

Storage keys used for instance and persistent storage are derived from the
`DataKey` enum variants. Variant names must not be changed or reordered after
deployment.

## Event Topics

Events published by the contract use fixed topic symbols. Changing topic
symbols is a breaking change for off-chain event indexers.
17 changes: 17 additions & 0 deletions contracts/anonvote/src/errors.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
use soroban_sdk::contracterror;

#[contracterror]
#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
#[repr(u32)]
pub enum ContractError {
AlreadyInitialized = 1,
Unauthorized = 2,
BallotAlreadyExists = 3,
BallotNotFound = 4,
BallotAlreadyFinalised = 5,
InvalidBallotIdHash = 6,
InvalidResultHash = 7,
InvalidAdminAddress = 8,
CounterOverflow = 9,
BallotExpired = 10,
}
12 changes: 9 additions & 3 deletions contracts/anonvote/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ impl AnonVoteContract {
/// Initializes the contract. Governance starts as 1-of-1 with the admin as
/// the sole approver, so deployments can explicitly configure M-of-N next.
pub fn initialize(env: Env, admin: Address) -> Result<(), ContractError> {
if env.storage().instance().has(&DataKey::Admin) {
if env.storage().instance().has(&DataKey::Initialized) {
return Err(ContractError::AlreadyInitialized);
}

Expand Down Expand Up @@ -567,6 +567,7 @@ impl AnonVoteContract {
caller: Address,
ballot_id_hash: String,
) -> Result<(), ContractError> {
validate_hex_hash(&env, &ballot_id_hash, ContractError::InvalidBallotIdHash)?;
caller.require_auth();
Self::require_not_paused(&env)?;
Self::require_admin(&env, &caller)?;
Expand Down Expand Up @@ -595,6 +596,7 @@ impl AnonVoteContract {
caller: Address,
ballot_id_hash: String,
) -> Result<(), ContractError> {
validate_hex_hash(&env, &ballot_id_hash, ContractError::InvalidBallotIdHash)?;
caller.require_auth();
Self::require_not_paused(&env)?;
Self::require_admin(&env, &caller)?;
Expand Down Expand Up @@ -623,6 +625,7 @@ impl AnonVoteContract {
caller: Address,
ballot_id_hash: String,
) -> Result<(), ContractError> {
validate_hex_hash(&env, &result_hash, ContractError::InvalidResultHash)?;
caller.require_auth();
Self::require_admin(&env, &caller)?;
Self::require_ballot_metadata(&env, &ballot_id_hash)?;
Expand Down Expand Up @@ -1104,9 +1107,9 @@ impl AnonVoteContract {
.storage()
.instance()
.get(&DataKey::Admin)
.ok_or(ContractError::NotInitialized)?;
.ok_or(ContractError::Unauthorized)?;
if *caller != admin {
return Err(ContractError::AdminUnauthorized);
return Err(ContractError::Unauthorized);
}
Ok(())
}
Expand Down Expand Up @@ -1950,3 +1953,6 @@ mod tests {
assert!(!client.is_consistent(&phantom));
}
}

#[cfg(test)]
mod test;
Loading
Loading