Skip to Content
ContractsAMACI Contract

AMACI Contract

AMACI (Anonymous MACI) contract is the core of each voting round, handling user signup, vote message storage, proof verification, and result publication.

Core Functions

The AMACI contract provides four main functions:

Registration Configuration

When creating an AMACI round, two configuration enums must be provided:

VoiceCreditMode

Defines how voting power is allocated to users:

ModeDescriptionUse Case
Unified { amount }All users receive the same fixed amount of voice creditsStandard AMACI rounds
DynamicEach user’s voice credits equal the amount provided in their signed certificateOracle-gated rounds with variable weights
// Unified mode: everyone gets 100 voice credits voiceCreditMode: { unified: { amount: "100" } } // Dynamic mode: voice credits come from oracle certificate voiceCreditMode: "dynamic"

RegistrationModeConfig

Defines how users register and how access is controlled. This single enum replaces the old whitelist, oracle_whitelist_pubkey, pre_deactivate_root, and pre_deactivate_coordinator fields:

ModeAccess ControlDescription
SignUpWithStaticWhitelist { whitelist }Pre-defined address listUsers call SignUp directly; only whitelisted addresses are allowed
SignUpWithOracle { oracle_pubkey }Backend signatureUsers call SignUp with an oracle-signed certificate
PrePopulated { pre_deactivate_root, pre_deactivate_coordinator }ZK proofUsers must use PreAddNewKey; direct SignUp is disabled

deactivate_enabled

An optional boolean flag (default: false) that enables the deactivate → AddNewKey flow for dynamic key rotation within the round. When false, only Signup and PreAddNewKey (in PrePopulated mode) are available.

// Create round with configuration await client.createAMaciRound({ operator: selectedOperator, maxVoter: 100, voteOptionMap: ["Option A", "Option B", "Option C"], voiceCreditMode: { unified: { amount: "100" } }, registrationMode: { sign_up_with_static_whitelist: { whitelist: { users: [ { addr: "dora1abc..." }, { addr: "dora1def..." }, ] } } }, deactivateEnabled: false, // ...other params });

User Registration Methods

AMACI supports three registration methods, allowing users to choose based on privacy needs.

1. Signup (Standard Registration)

The simplest and fastest registration method, suitable for scenarios where you don’t mind the Operator knowing your identity.

Signup Message

ExecuteMsg::Signup { pubkey: PubKey, // User's MACI public key certificate: Option<String>, // Oracle certificate (required for SignUpWithOracle mode) amount: Option<Uint256>, // Voice credit amount (required for Dynamic VC mode with SignUpWithOracle) }

Signup Flow

Signup Code Example

// 1. Generate MACI keypair // Derive EdDSA-Poseidon keypair from dora address const maciKeypair = await client.genKeypairFromSign({ signer: wallet, address }); // 2. Signup (whitelist addresses only) await client.signup({ signer: wallet, address: userAddress, contractAddress: amaciAddress, maciKeypair });

Registration Mode Determines Signup Behavior:

The behavior of SignUp depends on the round’s RegistrationModeConfig:

  • SignUpWithStaticWhitelist: Only pre-listed addresses can signup. No certificate or amount needed.
  • SignUpWithOracle: Any address can attempt signup but must provide a backend-signed certificate. With Dynamic voice credit mode, amount must also be provided (it is included in the certificate’s signature).
  • PrePopulated: SignUp is disabled — users must use PreAddNewKey instead.
// SignUpWithStaticWhitelist mode: simple signup await client.signup({ signer: wallet, address: userAddress, contractAddress: amaciAddress, maciKeypair }); // SignUpWithOracle + Dynamic VC mode: provide certificate and amount await client.signup({ signer: wallet, address: userAddress, contractAddress: amaciAddress, maciKeypair, certificate: oracleCertificate, // base64-encoded oracle signature amount: "150" // voice credits (included in the signed payload) });

Signup Privacy Level

Privacy Level: Low

Operator Can See:

  • Wallet address (through on-chain signup transaction)
  • MACI public key
  • State Index
  • Vote content (after decryption)

Risks:

  • Operator can fully correlate: wallet address → pubkey → State Index → vote content
  • Potential for targeted bribery or retaliation

Suitable For:

  • Don’t mind Operator knowing identity
  • Quick and simple voting
  • Trusted Operator

2. Add-new-key (Dynamic Key Change)

Uses zero-knowledge proofs to create anonymous identity that Operator cannot link to original user.

Add-new-key Message

ExecuteMsg::AddNewKey { pubkey: PubKey, // New user public key nullifier: Uint256, // Anti-replay identifier d: [Uint256; 4], // [d1[0], d1[1], d2[0], d2[1]] groth16_proof: Groth16ProofType, // ZK proof }

Complete Flow

Old User Deactivate

// Old user submits deactivate message await client.deactivate({ signer: oldWallet, address: oldAddress, contractAddress, maciKeypair: oldKeypair }); // This generates a special vote message: // - voIdx = 0 // - newVotes = 0 // - newPubKey = [0, 0] (indicates last message)

Operator Processes Deactivate

// Operator processes all deactivate messages // Generates deactivate tree // Each deactivate leaf contains: // [c1[0], c1[1], c2[0], c2[1], sharedKeyHash] // Operator submits ProcessDeactivate await contract.execute({ process_deactivate: { size: deactivateMessages.length, new_deactivate_commitment: deactivateCommitment, new_deactivate_root: deactivateRoot, groth16_proof: processDeactivateProof } });

New User Generates ZK Proof

// Use VoterClient to generate payload import { VoterClient } from '@dorafactory/maci-sdk'; const voterClient = new VoterClient({ network: 'testnet', secretKey: oldPrivateKeyHex // Old user's private key }); // Get deactivate data const deactivates = await client.fetchAllDeactivateLogs(contractAddress); // Generate add-new-key payload const payload = await voterClient.buildAddNewKeyPayload({ stateTreeDepth: 10, operatorPubkey: operatorPubkey, deactivates: deactivates, wasmFile, // addNewKey.wasm zkeyFile // addNewKey.zkey }); // payload contains: // { // proof: { a, b, c }, // Groth16 proof // d: [d1_0, d1_1, d2_0, d2_1], // Re-randomized values // nullifier: "0x..." // Anti-replay // }

Submit Add-new-key

// Submit with new wallet (important!) const newKeypair = genKeypair(); await client.addNewKey({ signer: newWallet, // New wallet contractAddress, d: payload.d, proof: payload.proof, nullifier: payload.nullifier, newMaciKeypair: newKeypair, // New MACI keypair fee: 'auto' });

Contract Verification Flow

pub fn execute_add_new_key( deps: DepsMut, env: Env, pubkey: PubKey, nullifier: Uint256, d: [Uint256; 4], groth16_proof: Groth16ProofType, ) -> Result<Response, ContractError> { // 1. Check voting period check_voting_time(env, voting_time)?; // 2. Check if nullifier already used if NULLIFIERS.has(deps.storage, nullifier.to_be_bytes()) { return Err(ContractError::NewKeyExist {}); } NULLIFIERS.save(deps.storage, nullifier.to_be_bytes(), &true)?; // 3. Build public inputs let input = [ DNODES.load(...)?, // deactivate root COORDINATORHASH.load(...)?, // operator pubkey hash nullifier, d[0], d[1], d[2], d[3] ]; let input_hash = hash_256(input) % SNARK_SCALAR_FIELD; // 4. Verify ZK proof let is_valid = groth16_verify(&proof, &[input_hash])?; if !is_valid { return Err(ContractError::InvalidProof { step: "AddNewKey".to_string() }); } // 5. Create new State Leaf (includes d1, d2) let state_leaf = StateLeaf { pub_key: pubkey, voice_credit_balance: voice_credit_amount, vote_option_tree_root: Uint256::zero(), nonce: Uint256::zero(), }.hash_new_key_state_leaf(d); // 6. Assign State Index let state_index = num_sign_ups; state_enqueue(&mut deps, state_leaf)?; num_sign_ups += 1; // 7. Save mapping (pubkey → state index) SIGNUPED.save(deps.storage, &pubkey_bytes, &state_index)?; Ok(Response::new() .add_attribute("action", "add_new_key") .add_attribute("state_idx", state_index.to_string())) }

ZK Circuit Description

Add-new-key uses a specialized ZK circuit to verify user identity:

template AddNewKey(stateTreeDepth) { // Public inputs signal input deactivateRoot; // Deactivate tree root signal input coordPubKey[2]; // Operator public key signal input nullifier; // Anti-replay signal input d1[2]; // Re-randomized values signal input d2[2]; // Private inputs (critical! not public) signal input oldPrivateKey; // Old user private key signal input deactivateIndex; // Position in tree signal input deactivateLeaf; // Leaf value signal input c1[2]; // Original encrypted values signal input c2[2]; signal input randomVal; // Random value for randomization signal input deactivateLeafPathElements[...]; // Merkle path // Verification steps: // 1. Verify nullifier = hash(oldPrivateKey, constant) // 2. Calculate sharedKeyHash = hash(ecdh(oldPrivKey, coordPubKey)) // 3. Verify deactivateLeaf = hash(c1, c2, sharedKeyHash) // 4. Verify leaf is in deactivateRoot tree (using deactivateIndex and path) // 5. Verify d1, d2 = rerandomize(c1, c2, randomVal, coordPubKey) }

Add-new-key Privacy Level

Privacy Level: Highest

Operator Can Only See:

  • New wallet address (possibly brand new)
  • New MACI public key
  • nullifier (just a hash)
  • d1, d2 (re-randomized values)
  • ZK proof (verified valid)

Operator Cannot Determine:

  • Which position in deactivate tree the new user is
  • Which wallet address corresponds to
  • Which old identity is associated with
  • What oldPrivateKey is

Anonymity Set:

  • Anonymity set size = number of entries in deactivate tree
  • Example: 100 deactivates → Operator knows “one of 100 people”

3. Pre-add-new-key (Pre-configured Key Change)

Uses pre-configured deactivate root to create anonymous identity immediately.

Pre-add-new-key Message

ExecuteMsg::PreAddNewKey { pubkey: PubKey, // New user public key nullifier: Uint256, // Anti-replay identifier d: [Uint256; 4], // [d1[0], d1[1], d2[0], d2[1]] groth16_proof: Groth16ProofType, // ZK proof }

Differences from Add-new-key

FeatureAdd-new-keyPre-add-new-key
Deactivate SourceDynamically generated during votingPre-configured at Round creation
CoordinatorCurrent OperatorPre-configured Coordinator
Wait TimeNeed to wait for ProcessDeactivateAvailable immediately
FlexibilityHigh (real-time)Medium (requires pre-configuration)

Contract Verification Differences

// add-new-key uses dynamically generated root input[0] = DNODES.load(deps.storage, ...)?; // Dynamic input[1] = COORDINATORHASH.load(deps.storage)?; // Current operator // pre-add-new-key uses pre-configured root input[0] = PRE_DEACTIVATE_ROOT.load(deps.storage)?; // Pre-configured input[1] = PRE_DEACTIVATE_COORDINATOR_HASH.load(...)?; // May be different

Use Cases

Suitable For:

  • Round creation already knows anonymous voting is needed
  • Pre-prepared set of deactivate data (e.g., platform distributes keys to users)
  • Want users to anonymously participate immediately without waiting

Round Configuration Example:

// Configure when creating Round using PrePopulated mode await client.createAMaciRound({ // ... other parameters registrationMode: { pre_populated: { pre_deactivate_root: preDeactivateData.root, pre_deactivate_coordinator: { x: preCoordPubkey[0].toString(), y: preCoordPubkey[1].toString() } } }, voiceCreditMode: { unified: { amount: "100" } }, deactivateEnabled: false // PrePopulated mode does not need live deactivation });

Pre-add-new-key Privacy Level

Privacy Level: Highest

Same privacy protection as add-new-key, but without waiting.


4. Process Deactivate (AMACI-specific)

Operator processes deactivate messages to generate deactivate tree for add-new-key.

Process Deactivate Message

ExecuteMsg::ProcessDeactivate { size: u64, // Number of messages processed new_deactivate_commitment: Uint256, // New deactivate commitment new_deactivate_root: Uint256, // New deactivate tree root groth16_proof: Groth16ProofType, // ZK proof }

Workflow

1. Collect Deactivate Messages

// User submits deactivate await client.deactivate({ signer: wallet, contractAddress, maciKeypair }); // Generated as special vote message: // { // nonce: current_nonce, // stateIdx: user_state_idx, // voIdx: 0, // Special marker // newVotes: 0, // Special marker // newPubKey: [0, 0], // Indicates last message // signature: ... // }

2. Operator Processes Deactivate

// Operator decrypts deactivate messages const deactivateCommands = deactivateMessages.map(msg => { const sharedKey = ecdh(operatorPrivKey, msg.encPubKey); const command = poseidonDecrypt(msg.data, sharedKey); return command; }); // Generate deactivate tree const deactivateLeaves = []; for (const cmd of deactivateCommands) { const stateIdx = cmd.stateIdx; const userPubKey = stateTree.getLeaf(stateIdx).pubKey; // Generate encrypted deactivation flag const sharedKey = ecdh(operatorPrivKey, userPubKey); const sharedKeyHash = poseidon(sharedKey); const randomVal = deterministicRandom(operatorPrivKey, stateIdx); const { c1, c2 } = encryptDeactivateFlag( true, // Mark as deactivated operatorPubKey, randomVal ); // Build leaf const leaf = [c1[0], c1[1], c2[0], c2[1], sharedKeyHash]; deactivateLeaves.push(leaf); } // Build Merkle tree const deactivateTree = new Tree(5, stateTreeDepth + 2, 0n); deactivateTree.initLeaves(deactivateLeaves.map(l => poseidon(l))); const deactivateRoot = deactivateTree.root;

3. Generate and Submit Proof

// Generate ProcessDeactivate proof const { proof } = await groth16.fullProve( processDeactivateInput, wasmFile, zkeyFile ); // Submit to contract await contract.execute({ process_deactivate: { size: deactivateMessages.length, new_deactivate_commitment: deactivateCommitment, new_deactivate_root: deactivateRoot, groth16_proof: proof } });

Process Deactivate Circuit

template ProcessDeactivateMessages(stateTreeDepth, batchSize) { // Public inputs signal input newDeactivateRoot; signal input coordPubKey[2]; signal input batchStartHash; signal input batchEndHash; signal input currentDeactivateCommitment; signal input newDeactivateCommitment; // Private inputs signal input coordPrivKey; // Operator private key signal input msgs[batchSize][7]; // Encrypted messages signal input encPubKeys[batchSize][2]; // Encryption public keys signal input currentStateLeaves[batchSize][10]; // Current state signal input c1[batchSize][2]; // Generated encrypted values signal input c2[batchSize][2]; // Verification steps: // 1. Decrypt all deactivate messages // 2. Verify each message signature // 3. Generate deactivate entry (c1, c2, hash) for each user // 4. Build new deactivate tree // 5. Verify newDeactivateRoot is correct }

Contract Verification

pub fn execute_process_deactivate( deps: DepsMut, size: u64, new_deactivate_commitment: Uint256, new_deactivate_root: Uint256, groth16_proof: Groth16ProofType, ) -> Result<Response, ContractError> { // 1. Check state // 2. Build public inputs let input = [ new_deactivate_root, coord_pubkey_hash, batch_start_hash, batch_end_hash, current_deactivate_commitment, new_deactivate_commitment, current_state_root ]; // 3. Verify ZK proof let is_valid = groth16_verify(&proof, &[input_hash])?; if !is_valid { return Err(ContractError::InvalidProof { step: "ProcessDeactivate".to_string() }); } // 4. Update deactivate root DNODES.save(deps.storage, key, &new_deactivate_root)?; CURRENT_DEACTIVATE_COMMITMENT.save(..., &new_deactivate_commitment)?; // 5. Update processing count PROCESSED_DMSG_COUNT.save(deps.storage, &new_count)?; Ok(Response::new()) }

Purpose of Process Deactivate

Provides Anonymity Set for Add-new-key:

Without ProcessDeactivate: → Cannot use add-new-key → Can only use signup (low privacy) With ProcessDeactivate: → Generates deactivate tree → Users can use add-new-key → Anonymity set size = number of deactivate messages

Timing:

  • Can be processed any time during voting period
  • Usually processed after enough deactivate messages accumulate
  • Recommendation: At least 50-100 deactivates before processing (larger anonymity set)

Voting Messages (PublishMessage)

After registration (regardless of method), users can submit encrypted voting messages.

PublishMessage Message

PublishMessage accepts messages in batch — a single call can submit one or more encrypted votes:

ExecuteMsg::PublishMessage { messages: Vec<MessageData>, // Batch of encrypted vote messages enc_pub_keys: Vec<PubKey>, // Corresponding ephemeral encryption public keys } pub struct MessageData { pub data: [Uint256; 10], // 10 encrypted fields }

Each messages[i] must have a corresponding enc_pub_keys[i]. A per-message fee of 10 DORA is charged for each message submitted.

Message Format

Each encrypted message contains exactly 10 Uint256 fields:

interface EncryptedMessage { data: [ bigint, // [0] packaged: nonce(32b)|stateIdx(32b)|voIdx(32b)|newVotes(96b)|pollId(32b) bigint, // [1] newPubKey.x bigint, // [2] newPubKey.y bigint, // [3] salt bigint, // [4] signature.R8.x bigint, // [5] signature.R8.y bigint, // [6] signature.S bigint, // [7] Poseidon encryption output[0] bigint, // [8] Poseidon encryption output[1] bigint, // [9] Poseidon encryption output[2] ]; }

Command array layout: Fields [0]–[6] are the plaintext command elements. The packaged field at [0] encodes five sub-fields using fixed-width bit positions (see bit layout in Message Packing). Fields [7]–[9] are the Poseidon ciphertext output. Each message is submitted together with its ephemeral enc_pub_key.

Voting Flow

Voting Example

// Get round info (includes coordinator pubkey) const roundInfo = await client.getRoundInfo({ contractAddress: amaciAddress }); await client.vote({ signer: wallet, address: userAddress, contractAddress: amaciAddress, selectedOptions: [ { idx: 0, vc: 5 }, // 5 votes for option 0 { idx: 1, vc: 3 }, // 3 votes for option 1 ], operatorCoordPubKey: [ BigInt(roundInfo.coordinatorPubkeyX), BigInt(roundInfo.coordinatorPubkeyY) ], maciKeypair: keypair });

Message Storage

Messages are stored in contract in order:

// Message queue pub const MESSAGES: Item<Vec<Message>> = Item::new("messages"); // Add message fn publish_message( deps: DepsMut, message: MessageData ) -> Result<Response, ContractError> { let mut messages = MESSAGES.load(deps.storage)?; messages.push(Message { msg_type: Uint256::from(1u128), // 1 = voting message data: message.data, }); MESSAGES.save(deps.storage, &messages)?; Ok(Response::new() .add_attribute("action", "publish_message") .add_attribute("message_id", messages.len().to_string())) }

Multiple Votes

Users can call PublishMessage multiple times, later messages override earlier ones:

// First vote await vote({ options: [{ idx: 0, vc: 5 }], nonce: 0 }); // Change mind, revote await vote({ options: [{ idx: 1, vc: 5 }], nonce: 1 }); // Change mind again await vote({ options: [{ idx: 2, vc: 5 }], nonce: 2 }); // During processing, only last vote (option 2) is valid

Message Processing (ProcessMessages)

Operator submits zero-knowledge proofs to process voting messages.

ProcessMessages Message

ExecuteMsg::ProcessMessages { new_state_commitment: Uint256, // New state root groth16_proof: Groth16ProofType, // Groth16 proof }

Processing Flow

Proof Verification

fn process_messages( deps: DepsMut, new_state_commitment: Uint256, proof: Groth16ProofType ) -> Result<Response, ContractError> { // 1. Check state let round_info = ROUND_INFO.load(deps.storage)?; if round_info.status != RoundStatus::Processing { return Err(ContractError::InvalidRoundStatus {}); } // 2. Construct public inputs let public_inputs = vec![ coordinator_pub_key_x, coordinator_pub_key_y, message_root, current_state_root, new_state_commitment, // ... other public inputs ]; // 3. Verify Groth16 proof let is_valid = verify_groth16_proof( proof, public_inputs, verification_key )?; if !is_valid { return Err(ContractError::ProofVerificationFailed {}); } // 4. Update state root STATE_COMMITMENT.save(deps.storage, &new_state_commitment)?; Ok(Response::new() .add_attribute("action", "process_messages") .add_attribute("new_state_root", new_state_commitment.to_string())) }

Result Tallying (ProcessTally)

Operator submits tally proof to publish results.

ProcessTally Message

ExecuteMsg::ProcessTally { new_tally_commitment: Uint256, // Tally result commitment groth16_proof: Groth16ProofType, // Groth16 proof }

Tallying Flow

Result Publication

fn process_tally( deps: DepsMut, new_tally_commitment: Uint256, proof: Groth16ProofType ) -> Result<Response, ContractError> { // 1. Verify proof let is_valid = verify_groth16_proof( proof, public_inputs, tally_verification_key )?; if !is_valid { return Err(ContractError::ProofVerificationFailed {}); } // 2. Save tally results TALLY_COMMITMENT.save(deps.storage, &new_tally_commitment)?; // 3. Update status let mut round_info = ROUND_INFO.load(deps.storage)?; round_info.status = RoundStatus::Tallied; ROUND_INFO.save(deps.storage, &round_info)?; Ok(Response::new() .add_attribute("action", "process_tally") .add_attribute("tally_commitment", new_tally_commitment.to_string())) }

Query Functions

Round Information

QueryMsg::GetRoundInfo {} QueryMsg::GetPeriod {} QueryMsg::GetNumSignUp {} QueryMsg::GetVotingTime {}

Poll ID

Each AMACI instance is assigned a unique poll_id by the Registry at creation time:

QueryMsg::GetPollId {} // Returns: u64

Registration Configuration

// Get the current registration and voice credit configuration QueryMsg::GetRegistrationConfig {} // Returns: RegistrationConfigInfo { // deactivate_enabled: bool, // voice_credit_mode: VoiceCreditMode, // registration_mode: RegistrationMode, // } // Get whether deactivate feature is enabled QueryMsg::GetDeactivateEnabled {} // Returns: bool

Registration Status (replaces IsWhiteList / WhiteBalanceOf)

A unified query that returns registration eligibility and voice credit balance, adapting to the active registration mode:

QueryMsg::QueryRegistrationStatus { sender: Option<Addr>, // For SignUpWithStaticWhitelist mode pubkey: Option<PubKey>, // For SignUpWithOracle / PrePopulated mode certificate: Option<String>,// For SignUpWithOracle mode amount: Option<Uint256>, // For SignUpWithOracle + Dynamic VC mode } // Returns: RegistrationStatus { // can_sign_up: bool, // is_register: bool, // balance: Uint256, // }
// Check registration eligibility (whitelist mode) const status = await client.queryRegistrationStatus({ contractAddress: amaciAddress, address: userAddress }); // Check registration eligibility (oracle mode with dynamic VC) const status = await client.queryRegistrationStatus({ contractAddress: amaciAddress, pubkey: { x: pubkeyX, y: pubkeyY }, certificate: oracleCert, amount: "150" }); console.log(status.can_sign_up); // Whether user can register console.log(status.is_register); // Whether already registered console.log(status.balance); // Voice credits if registered

Message Queries

QueryMsg::GetMsgChainLength {} // Total number of vote messages QueryMsg::GetAllResult {} // Final tally for a single option index QueryMsg::GetAllResults {} // Final tally results for all options (Vec<Uint256>)

State Queries

QueryMsg::GetCurrentStateCommitment {} QueryMsg::GetCurrentDeactivateCommitment {} QueryMsg::Signuped { pubkey: PubKey } // Returns state index if registered QueryMsg::GetStateIdxInc { address: Addr } QueryMsg::GetVoiceCreditBalance { index: String } QueryMsg::MaxVoteOptions {} QueryMsg::QueryCircuitType {} QueryMsg::QueryOracleWhitelistConfig {}

Round Status

AMACI contract has the following states:

pub enum PeriodStatus { Pending, // Contract instantiated, waiting for voting start time Processing, // Voting period ended; operator is processing messages Tallying, // Message processing complete; operator is tallying votes Ended, // Tally submitted and verified; round is complete }

Note: The Voting state was removed. The contract determines whether the voting window is open by comparing the current block time against the configured start_time and end_time. There is no explicit state transition into “Voting”.

State Transitions

UpdateRegistrationConfig

After a round is created but before voting starts, the organizer can update registration settings:

ExecuteMsg::UpdateRegistrationConfig { config: RegistrationConfigUpdate { deactivate_enabled: Option<bool>, // Toggle deactivate feature voice_credit_mode: Option<VoiceCreditMode>, // Only changeable when num_signups == 0 registration_mode: Option<RegistrationModeConfig>, // Only changeable when num_signups == 0 } }

This replaces the old SetWhitelists message.

Security Features

Time Verification

fn ensure_voting_period( env: &Env, voting_time: &VotingTime ) -> Result<(), ContractError> { let current_time = env.block.time.seconds(); if current_time < voting_time.start_time { return Err(ContractError::VotingNotStarted {}); } if current_time > voting_time.end_time { return Err(ContractError::VotingEnded {}); } Ok(()) }

Access Control

// Only Coordinator can process messages fn ensure_coordinator( sender: &Addr, coordinator: &Addr ) -> Result<(), ContractError> { if sender != coordinator { return Err(ContractError::Unauthorized {}); } Ok(()) }

Reentrancy Protection

// Use state lock to prevent reentrancy fn process_messages_with_lock( deps: DepsMut, // ... parameters ) -> Result<Response, ContractError> { // Check lock let is_locked = PROCESSING_LOCK.may_load(deps.storage)?.unwrap_or(false); if is_locked { return Err(ContractError::AlreadyProcessing {}); } // Set lock PROCESSING_LOCK.save(deps.storage, &true)?; // Processing logic let result = process_messages_internal(deps, ...); // Release lock PROCESSING_LOCK.save(deps.storage, &false)?; result }

Registration Method Selection Recommendations

Choose the appropriate registration method based on your privacy needs:

Low Privacy Scenarios

Use Signup:

  • Internal community voting
  • Non-sensitive decisions
  • Trusted Operator

High Privacy Scenarios

Use Add-new-key:

  • Large fund allocation
  • Sensitive topic voting
  • Potentially controversial decisions
  • Don’t fully trust Operator

Use Pre-add-new-key:

  • Need quick anonymity
  • Round already pre-configured
  • Emergency voting scenarios

Mixed Usage

In the same Round, different users can use different registration methods:

// User A: Use signup (fast) await userA.signup({...}); // User B: Use add-new-key (anonymous) await userB.addNewKey({...}); // User C: Use pre-add-new-key (fast and anonymous) await userC.rawPreAddNewKey({...}); // All can vote normally await userA.vote({...}); await userB.vote({...}); await userC.vote({...});

Next Steps

After understanding all AMACI contract registration methods, you can:

Last updated on