Volta Soroban Smart Contract Integration Guide
This guide provides comprehensive documentation for integrating with the Volta multi-signature governance smart contract deployed on Soroban (Stellar's smart contract platform).
Table of Contents
- Overview
- Contract Architecture
- Common Flows
- Installation
- Contract Methods
- TypeScript/JavaScript Examples
- Golang Examples
- Error Handling
- Events
- Best Practices
Tutorials & Guides
New to Volta? Start here:
- Getting Started - Deploy and initialize your first Volta contract
- Creating Your First Proposal - Step-by-step guide to creating and voting on proposals
- dApp Integration - Integrate Volta into your application with TypeScript examples
- Troubleshooting - Common errors and solutions
Overview
The Volta contract is a multi-signature governance contract that enables:
- Multi-owner configuration: Multiple owners with configurable voting thresholds
- Proposal system: Owners can create proposals for various actions
- Voting mechanism: Owners vote on proposals with configurable thresholds
- Contract invocation: Ability to call other contracts with proper authorization
- Upgrade mechanism: Contract upgrade capabilities
📦 Deployment
WASM Hash:
ce84b965f3fdbf4ff9ea4c28813a7a30d6dd65c69d0d1bc19834d907a5e0d27bUse this hash to verify contract integrity when deploying or interacting with the contract.
Contract Architecture
Key Concepts
- Owners: Addresses that can create proposals and vote
- Threshold: Minimum number of "Yes" votes required to approve a proposal
- Proposals: Actions that require owner consensus before execution
- Proposal Types: Config, Invoke, Upgrade
Proposal Lifecycle
- Pending: Proposal created, awaiting votes
- Approved: Threshold reached, ready for execution
- Executed: Proposal executed successfully
- Rejected: Approval becomes mathematically impossible
- Revoked: Creator revokes the proposal
Limitations
⚠️ Authorization for Sub-Calls
When the invoked contract needs to call other contracts that require the Volta contract's authorization, you must provide the appropriate
auth_entrieswhen callinginvoke(). The contract will use these entries to authorize itself for the sub-calls. Without proper auth entries, sub-contract invocations requiring authorization will fail.
Common Flows
The diagrams below show the four runtime flows owners and integrators encounter most often. Each lane labeled Owner X is a separate signing key calling the contract; the Volta contract lane represents on-ledger contract execution. Events the contract emits at each step are shown as inline notes — see the Events section for how to subscribe to them.
Config / Upgrade Proposal Lifecycle
propose() creates either a Config proposal (changes owners / threshold) or an Upgrade proposal (replaces the contract WASM by hash). Both share the same voting mechanics: the proposal sits in Pending until Yes votes reach the configured threshold, at which point the contract auto-executes the change. Executing a Config or Upgrade proposal invalidates all other pending proposals. Config execution additionally emits a cfg_set event with the new configuration; Upgrade execution emits no extra event beyond exec_prop.
sequenceDiagram
actor A as Owner A (proposer)
actor B as Owner B
actor C as Owner C
participant V as Volta contract
A->>V: propose(Config { owners, threshold })
V-->>A: Proposal { id, status: Pending, votes: {} }
Note over V: emit new_prop
B->>V: vote(id, Yes)
V-->>B: Proposal { status: Pending, votes: {B: Yes} }
Note over V: emit vote, pend_prop
C->>V: vote(id, Yes)
Note over V: threshold met, auto-execute<br/>emit vote, exec_prop<br/>plus cfg_set for Config, no extra event for Upgrade
V-->>C: Proposal { status: Executed }
Invoke Proposal (with auto-vote and sub-call)
invoke() differs from propose() in two ways. First, the caller's vote is auto-counted as Yes, so only threshold - 1 additional votes are needed. Second, when the proposal is approved, the contract authorizes itself using the supplied auth_entries before calling the target function. Without those entries, sub-calls that require the Volta contract's authorization will fail.
sequenceDiagram
actor A as Owner A (caller)
actor B as Owner B
participant V as Volta contract
participant T as Target contract
A->>V: invoke(target, fn, args, auth_entries)
Note over V: creates Invoke proposal<br/>auto-counts A's vote as Yes<br/>emit new_prop
V-->>A: ()
B->>V: vote(id, Yes)
Note over V: threshold met, execute<br/>authorize self with auth_entries
V->>T: fn(args)
T-->>V: result
Note over V: emit vote, exec_prop, inv_ok
V-->>B: Proposal { status: Executed }
Revoke Proposal
Only the proposal creator can revoke a pending proposal — other owners attempting to revoke get the NotCaller error. Other owners who object should vote No instead. Once a proposal is executed or rejected it can no longer be revoked.
sequenceDiagram
actor A as Owner A (creator)
actor B as Owner B
participant V as Volta contract
A->>V: propose(...)
V-->>A: Proposal { id, status: Pending }
B->>V: revoke_proposal(id)
V-->>B: Error: NotCaller
Note right of B: only the creator can revoke
A->>V: revoke_proposal(id)
Note over V: emit rev_prop
V-->>A: ()
Rejection (Mathematically Impossible Approval)
The contract auto-rejects a proposal as soon as approval becomes mathematically unreachable — that is, when Yes votes + remaining unvoted owners < threshold — without waiting for all owners to vote. The example below uses three owners with a threshold of two; once two No votes are cast, the third owner could only contribute one more Yes, so the threshold of two is unreachable.
sequenceDiagram
actor A as Owner A (proposer)
actor B as Owner B
actor C as Owner C
participant V as Volta contract
Note over V: 3 owners, threshold = 2
A->>V: propose(...)
V-->>A: Proposal { status: Pending }
B->>V: vote(id, No)
V-->>B: Proposal { status: Pending, votes: {B: No} }
Note over V: emit vote, pend_prop
C->>V: vote(id, No)
Note over V: max possible Yes (0 + 1) below threshold (2)<br/>auto-reject<br/>emit vote, rej_prop
V-->>C: Proposal { status: Rejected }
Installation
TypeScript/JavaScript
Golang
Contract Methods
version() -> u32
Returns the contract version number.
Returns:
- u32: Contract version
get_config() -> ConfigInput
Retrieves the current contract configuration.
Returns:
- ConfigInput: Object containing:
- owners: Vec<Address>: List of owner addresses
- threshold: u32: Minimum votes required for approval
Errors:
- NotInitialized: Contract has not been initialized
propose(owner: Address, proposal: ProposalInput) -> Proposal
Creates a new proposal. Only owners can create proposals.
Parameters:
- owner: Address: The address of the owner creating the proposal (must match caller)
- proposal: ProposalInput: The proposal to create (see Proposal Types below)
Returns:
- Proposal: The created proposal object
Proposal Types:
-
Config: Change contract configuration (owners/threshold)
-
Upgrade: Upgrade the contract
Note: Invoke proposals cannot be directly created via propose(). They are created when owners call invoke().
Errors:
- NotOwner: Caller is not an owner
- InvokeNotAllowed: Attempted to propose an Invoke proposal directly
- InvalidOwners: Invalid owner configuration (duplicates or < 2 owners)
- InvalidThreshold: Threshold out of valid range
- NoConfigChanges: Config proposal identical to current config
- InvalidUpgrade: Upgrade hash is invalid (all zeros or all 0xFF)
vote(owner: Address, proposal_id: u64, vote: VoteType) -> Proposal
Votes on a proposal. Only owners can vote.
Parameters:
- owner: Address: The voting owner's address (must match caller)
- proposal_id: u64: The ID of the proposal to vote on
- vote: VoteType: The vote type (Yes, No, or Abstain)
Returns:
- Proposal: Updated proposal object
Vote Types:
- VoteType::Yes: Approve the proposal
- VoteType::No: Reject the proposal
- VoteType::Abstain: Neutral vote (doesn't count toward approval/rejection)
Voting Logic:
- Proposal is Approved when Yes votes >= threshold
- Proposal is Rejected when approval becomes mathematically impossible
- Proposal remains Pending otherwise
- Approved proposals are automatically executed
Errors:
- NotOwner: Caller is not an owner
- ProposalNotFound: Proposal doesn't exist
- ProposalNotPending: Proposal is not in pending status
- VoterAlreadyVoted: Owner has already voted on this proposal
invoke(caller: Address, contract: Address, fn_name: Symbol, args: Vec<Val>, auth_entries: Vec<InvokerContractAuthEntry>) -> ()
Creates an Invoke proposal to call a function on another contract. Only owners can call this method.
Parameters:
- caller: Address: The owner's address (must match caller)
- contract: Address: The contract address to invoke
- fn_name: Symbol: The function name to call
- args: Vec<Val>: Arguments to pass to the function
- auth_entries: Vec<InvokerContractAuthEntry>: Authorization entries for sub-contract calls
Behavior:
- Creates an Invoke proposal with the caller's vote automatically set to Yes
- Requires (threshold - 1) additional Yes votes to execute
- When approved, the contract authorizes itself using the provided auth_entries before invoking
Returns:
- (): Success (no return value)
Errors:
- NotOwner: Caller is not an owner
- AddressNotOnLedger: Target contract address doesn't exist on ledger
- InvalidFunctionName: Function name is empty
get_proposal(proposal_id: u64) -> Proposal
Retrieves a proposal by its ID.
Parameters:
- proposal_id: u64: The ID of the proposal to retrieve
Returns:
- Proposal: The proposal object
Errors:
- ProposalNotFound: Proposal doesn't exist
- ProposalNotPending: Proposal was created before the last config change (implicitly invalidated)
revoke_proposal(caller: Address, proposal_id: u64) -> ()
Revokes a proposal. Only the proposal creator can revoke pending proposals.
Parameters:
- caller: Address: The caller's address (must match caller)
- proposal_id: u64: The ID of the proposal to revoke
Returns:
- (): Success
Errors:
- NotCaller: Caller is not the proposal creator
- ProposalNotPending: Proposal is not in pending status
- ProposalNotFound: Proposal doesn't exist
TypeScript/JavaScript Examples
Setup
import {
Contract,
Networks,
SorobanRpc,
Address,
nativeToScVal,
scValToNative,
xdr,
} from '@stellar/stellar-sdk';
import { SorobanRpc as SorobanRpcType } from '@stellar/stellar-sdk';
// Initialize RPC client
const rpcUrl = 'https://soroban-testnet.stellar.org';
const rpc = new SorobanRpc.Server(rpcUrl, {
allowHttp: rpcUrl.startsWith('http://'),
});
// Contract address (replace with deployed contract address)
const contractAddress = 'C...'; // Your contract address
// Helper to create contract instance
function getContract(contractId: string): Contract {
return new Contract(contractId);
}
Get Contract Version
async function getVersion(): Promise<number> {
const contract = getContract(contractAddress);
const result = await rpc.getContractData(
contractAddress,
xdr.ScVal.scvLedgerKeyContractInstance()
);
const response = await rpc.invokeContract({
contractAddress,
method: 'version',
args: [],
});
return scValToNative(response.result.retval);
}
Get Configuration
interface ConfigInput {
owners: string[];
threshold: number;
}
async function getConfig(): Promise<ConfigInput> {
const contract = getContract(contractAddress);
const response = await rpc.invokeContract({
contractAddress,
method: 'get_config',
args: [],
});
const config = scValToNative(response.result.retval);
return {
owners: config.owners.map((addr: any) => addr.toString()),
threshold: config.threshold,
};
}
Create a Config Proposal
import { Keypair } from '@stellar/stellar-sdk';
async function proposeConfig(
ownerKeypair: Keypair,
newOwners: string[],
newThreshold: number
): Promise<any> {
const contract = getContract(contractAddress);
const ownerAddress = Address.fromString(ownerKeypair.publicKey());
// Build proposal input
const proposalInput = {
tag: 'Config',
values: [
{
owners: newOwners.map(addr => Address.fromString(addr).toScVal()),
threshold: nativeToScVal(newThreshold, 'u32'),
},
],
};
// Build transaction
const sourceAccount = await rpc.getAccount(ownerKeypair.publicKey());
const tx = new TransactionBuilder(sourceAccount, {
fee: '100',
networkPassphrase: Networks.TESTNET,
})
.addOperation(
contract.call('propose', ownerAddress.toScVal(), proposalInput)
)
.setTimeout(30)
.build();
tx.sign(ownerKeypair);
// Send transaction
const response = await rpc.sendTransaction(tx);
const result = await rpc.getTransaction(response.hash);
return scValToNative(result.returnValue);
}
Vote on a Proposal
enum VoteType {
Abstain = 0,
Yes = 1,
No = 2,
}
async function vote(
ownerKeypair: Keypair,
proposalId: number,
voteType: VoteType
): Promise<any> {
const contract = getContract(contractAddress);
const ownerAddress = Address.fromString(ownerKeypair.publicKey());
const sourceAccount = await rpc.getAccount(ownerKeypair.publicKey());
const tx = new TransactionBuilder(sourceAccount, {
fee: '100',
networkPassphrase: Networks.TESTNET,
})
.addOperation(
contract.call(
'vote',
ownerAddress.toScVal(),
nativeToScVal(proposalId, 'u64'),
nativeToScVal(voteType, 'u32')
)
)
.setTimeout(30)
.build();
tx.sign(ownerKeypair);
const response = await rpc.sendTransaction(tx);
const result = await rpc.getTransaction(response.hash);
return scValToNative(result.returnValue);
}
Create Invoke Proposal
async function createInvokeProposal(
ownerKeypair: Keypair,
targetContract: string,
functionName: string,
args: any[],
authEntries: xdr.SorobanAuthorizationEntry[] = []
): Promise<void> {
const contract = getContract(contractAddress);
const ownerAddress = Address.fromString(ownerKeypair.publicKey());
const targetAddress = Address.fromString(targetContract);
const sourceAccount = await rpc.getAccount(ownerKeypair.publicKey());
const tx = new TransactionBuilder(sourceAccount, {
fee: '100',
networkPassphrase: Networks.TESTNET,
})
.addOperation(
contract.call(
'invoke',
ownerAddress.toScVal(),
targetAddress.toScVal(),
xdr.ScVal.scvSymbol(functionName),
xdr.ScVal.scvVec(args.map(arg => nativeToScVal(arg))),
xdr.ScVal.scvVec(authEntries)
)
)
.setTimeout(30)
.build();
tx.sign(ownerKeypair);
await rpc.sendTransaction(tx);
}
Revoke a Proposal
async function revokeProposal(
callerKeypair: Keypair,
proposalId: number
): Promise<void> {
const contract = getContract(contractAddress);
const callerAddress = Address.fromString(callerKeypair.publicKey());
const sourceAccount = await rpc.getAccount(callerKeypair.publicKey());
const tx = new TransactionBuilder(sourceAccount, {
fee: '100',
networkPassphrase: Networks.TESTNET,
})
.addOperation(
contract.call(
'revoke_proposal',
callerAddress.toScVal(),
nativeToScVal(proposalId, 'u64')
)
)
.setTimeout(30)
.build();
tx.sign(callerKeypair);
await rpc.sendTransaction(tx);
}
Golang Examples
Setup
package main
import (
"github.com/stellar/go/clients/horizonclient"
"github.com/stellar/go/keypair"
"github.com/stellar/go/network"
"github.com/stellar/go/txnbuild"
"github.com/stellar/go/xdr"
)
const (
contractAddress = "C..." // Your contract address
testnetRPC = "https://soroban-testnet.stellar.org"
)
func getClient() *horizonclient.Client {
return &horizonclient.Client{
HorizonURL: testnetRPC,
}
}
Get Contract Version
func getVersion(sourceAccount txnbuild.Account) (uint32, error) {
client := getClient()
// Build invoke contract operation
op := &txnbuild.InvokeHostFunction{
HostFunction: xdr.HostFunction{
Type: xdr.HostFunctionTypeHostFunctionTypeInvokeContract,
InvokeContract: &xdr.InvokeContractArgs{
ContractAddress: contractAddress,
FunctionName: "version",
Args: []xdr.ScVal{},
},
},
}
tx, err := txnbuild.NewTransaction(
txnbuild.TransactionParams{
SourceAccount: sourceAccount,
IncrementSequenceNum: true,
BaseFee: txnbuild.MinBaseFee,
Timebounds: txnbuild.NewInfiniteTimeout(),
Operations: []txnbuild.Operation{op},
},
)
if err != nil {
return 0, err
}
// Sign and submit transaction
// ... (transaction signing and submission logic)
// Parse result
// ... (result parsing logic)
return version, nil
}
Get Configuration
type ConfigInput struct {
Owners []string `json:"owners"`
Threshold uint32 `json:"threshold"`
}
func getConfig(sourceAccount txnbuild.Account) (*ConfigInput, error) {
client := getClient()
op := &txnbuild.InvokeHostFunction{
HostFunction: xdr.HostFunction{
Type: xdr.HostFunctionTypeHostFunctionTypeInvokeContract,
InvokeContract: &xdr.InvokeContractArgs{
ContractAddress: contractAddress,
FunctionName: "get_config",
Args: []xdr.ScVal{},
},
},
}
tx, err := txnbuild.NewTransaction(
txnbuild.TransactionParams{
SourceAccount: sourceAccount,
IncrementSequenceNum: true,
BaseFee: txnbuild.MinBaseFee,
Timebounds: txnbuild.NewInfiniteTimeout(),
Operations: []txnbuild.Operation{op},
},
)
if err != nil {
return nil, err
}
// Sign, submit, and parse result
// ... (transaction handling logic)
return &ConfigInput{
Owners: owners,
Threshold: threshold,
}, nil
}
Create a Config Proposal
func proposeConfig(
sourceAccount txnbuild.Account,
ownerKP *keypair.Full,
newOwners []string,
newThreshold uint32,
) (*Proposal, error) {
// Convert owner addresses to ScVal
ownerScVals := make([]xdr.ScVal, len(newOwners))
for i, owner := range newOwners {
addr, err := xdr.AddressToScVal(owner)
if err != nil {
return nil, err
}
ownerScVals[i] = addr
}
// Build proposal input
configInput := xdr.ScVal{
Type: xdr.ScValTypeScvVec,
Vec: &xdr.ScVec{
Elements: []xdr.ScVal{
{
Type: xdr.ScValTypeScvVec,
Vec: &xdr.ScVec{Elements: ownerScVals},
},
{
Type: xdr.ScValTypeScvU32,
U32: &newThreshold,
},
},
},
}
proposalInput := xdr.ScVal{
Type: xdr.ScValTypeScvEnum,
Enum: &xdr.ScValEnum{
Type: 0, // Config variant
Values: []xdr.ScVal{configInput},
},
}
ownerAddr, err := xdr.AddressToScVal(ownerKP.Address())
if err != nil {
return nil, err
}
op := &txnbuild.InvokeHostFunction{
HostFunction: xdr.HostFunction{
Type: xdr.HostFunctionTypeHostFunctionTypeInvokeContract,
InvokeContract: &xdr.InvokeContractArgs{
ContractAddress: contractAddress,
FunctionName: "propose",
Args: []xdr.ScVal{
ownerAddr,
proposalInput,
},
},
},
}
tx, err := txnbuild.NewTransaction(
txnbuild.TransactionParams{
SourceAccount: sourceAccount,
IncrementSequenceNum: true,
BaseFee: txnbuild.MinBaseFee,
Timebounds: txnbuild.NewInfiniteTimeout(),
Operations: []txnbuild.Operation{op},
},
)
if err != nil {
return nil, err
}
tx, err = tx.Sign(network.TestNetworkPassphrase, ownerKP)
if err != nil {
return nil, err
}
// Submit transaction and parse result
// ... (transaction submission and parsing logic)
return proposal, nil
}
Vote on a Proposal
const (
VoteTypeAbstain = 0
VoteTypeYes = 1
VoteTypeNo = 2
)
func vote(
sourceAccount txnbuild.Account,
ownerKP *keypair.Full,
proposalId uint64,
voteType uint32,
) (*Proposal, error) {
ownerAddr, err := xdr.AddressToScVal(ownerKP.Address())
if err != nil {
return nil, err
}
proposalIdScVal := xdr.ScVal{
Type: xdr.ScValTypeScvU64,
U64: &proposalId,
}
voteTypeScVal := xdr.ScVal{
Type: xdr.ScValTypeScvU32,
U32: &voteType,
}
op := &txnbuild.InvokeHostFunction{
HostFunction: xdr.HostFunction{
Type: xdr.HostFunctionTypeHostFunctionTypeInvokeContract,
InvokeContract: &xdr.InvokeContractArgs{
ContractAddress: contractAddress,
FunctionName: "vote",
Args: []xdr.ScVal{
ownerAddr,
proposalIdScVal,
voteTypeScVal,
},
},
},
}
tx, err := txnbuild.NewTransaction(
txnbuild.TransactionParams{
SourceAccount: sourceAccount,
IncrementSequenceNum: true,
BaseFee: txnbuild.MinBaseFee,
Timebounds: txnbuild.NewInfiniteTimeout(),
Operations: []txnbuild.Operation{op},
},
)
if err != nil {
return nil, err
}
tx, err = tx.Sign(network.TestNetworkPassphrase, ownerKP)
if err != nil {
return nil, err
}
// Submit transaction and parse result
// ... (transaction submission and parsing logic)
return proposal, nil
}
Create Invoke Proposal
func createInvokeProposal(
sourceAccount txnbuild.Account,
ownerKP *keypair.Full,
targetContract string,
functionName string,
args []xdr.ScVal,
authEntries []xdr.SorobanAuthorizationEntry,
) error {
ownerAddr, err := xdr.AddressToScVal(ownerKP.Address())
if err != nil {
return err
}
targetAddr, err := xdr.AddressToScVal(targetContract)
if err != nil {
return err
}
fnNameScVal := xdr.ScVal{
Type: xdr.ScValTypeScvSymbol,
Symbol: &xdr.ScSymbol(functionName),
}
argsVec := xdr.ScVal{
Type: xdr.ScValTypeScvVec,
Vec: &xdr.ScVec{Elements: args},
}
// Convert auth entries to ScVal
authEntriesVec := xdr.ScVal{
Type: xdr.ScValTypeScvVec,
Vec: &xdr.ScVec{Elements: []xdr.ScVal{}}, // Populate as needed
}
op := &txnbuild.InvokeHostFunction{
HostFunction: xdr.HostFunction{
Type: xdr.HostFunctionTypeHostFunctionTypeInvokeContract,
InvokeContract: &xdr.InvokeContractArgs{
ContractAddress: contractAddress,
FunctionName: "invoke",
Args: []xdr.ScVal{
ownerAddr,
targetAddr,
fnNameScVal,
argsVec,
authEntriesVec,
},
},
},
}
tx, err := txnbuild.NewTransaction(
txnbuild.TransactionParams{
SourceAccount: sourceAccount,
IncrementSequenceNum: true,
BaseFee: txnbuild.MinBaseFee,
Timebounds: txnbuild.NewInfiniteTimeout(),
Operations: []txnbuild.Operation{op},
},
)
if err != nil {
return err
}
tx, err = tx.Sign(network.TestNetworkPassphrase, ownerKP)
if err != nil {
return err
}
// Submit transaction
// ... (transaction submission logic)
return nil
}
Revoke a Proposal
func revokeProposal(
sourceAccount txnbuild.Account,
callerKP *keypair.Full,
proposalId uint64,
) error {
callerAddr, err := xdr.AddressToScVal(callerKP.Address())
if err != nil {
return err
}
proposalIdScVal := xdr.ScVal{
Type: xdr.ScValTypeScvU64,
U64: &proposalId,
}
op := &txnbuild.InvokeHostFunction{
HostFunction: xdr.HostFunction{
Type: xdr.HostFunctionTypeHostFunctionTypeInvokeContract,
InvokeContract: &xdr.InvokeContractArgs{
ContractAddress: contractAddress,
FunctionName: "revoke_proposal",
Args: []xdr.ScVal{
callerAddr,
proposalIdScVal,
},
},
},
}
tx, err := txnbuild.NewTransaction(
txnbuild.TransactionParams{
SourceAccount: sourceAccount,
IncrementSequenceNum: true,
BaseFee: txnbuild.MinBaseFee,
Timebounds: txnbuild.NewInfiniteTimeout(),
Operations: []txnbuild.Operation{op},
},
)
if err != nil {
return err
}
tx, err = tx.Sign(network.TestNetworkPassphrase, callerKP)
if err != nil {
return err
}
// Submit transaction
// ... (transaction submission logic)
return nil
}
Error Handling
The contract uses the following error codes:
| Code | Name | Description |
|---|---|---|
| 2 | NotInitialized |
Contract has not been initialized |
| 5 | InvalidOwners |
Invalid owner configuration (duplicates or < 2 owners) |
| 6 | InvalidThreshold |
Threshold out of valid range |
| 8 | NotOwner |
Caller is not an owner |
| 9 | ProposalNotFound |
Proposal doesn't exist |
| 10 | VoterAlreadyVoted |
Owner has already voted |
| 11 | ProposalNotPending |
Proposal is not in pending status |
| 12 | InvokeError |
Contract invocation failed |
| 14 | NoConfigChanges |
Config proposal identical to current config |
| 15 | NotCaller |
Caller is not the proposal creator |
| 19 | InvokeNotAllowed |
Cannot directly propose Invoke proposals |
| 22 | InvalidUpgrade |
Upgrade hash is invalid |
| 23 | AddressNotOnLedger |
Target contract address doesn't exist on ledger |
| 24 | InvalidFunctionName |
Function name is empty |
Events
The contract emits the following events:
new_prop: Emitted when a new proposal is createdpend_prop: Emitted when a proposal remains pending after votingrej_prop: Emitted when a proposal is rejectedexec_prop: Emitted when a proposal is executedrev_prop: Emitted when a proposal is revokedcfg_set: Emitted when configuration is updatedinv_ok: Emitted when a contract invocation succeedsinv_err: Emitted when invoke result conversion failsvote: Emitted when a vote is cast
Listening to Events
TypeScript:
// Subscribe to contract events
const eventFilter = {
contractIds: [contractAddress],
};
const eventStream = rpc.subscribe({
filter: eventFilter,
onmessage: (event) => {
console.log('Event received:', event);
// Parse event data
},
});
Golang:
// Use Horizon client to fetch events
// Events are stored in transaction results
// Query transactions for the contract address to retrieve events
Best Practices
- Always check proposal status before voting or executing
- Validate thresholds ensure they're between 2 and owner count
- Handle errors gracefully check error codes and provide user feedback
- Monitor events for proposal lifecycle changes
- Test thoroughly before deploying to mainnet