Smart Contract

The Términa Escrow smart contract is written in Rust using the Odra framework, providing type-safe, gas-efficient escrow operations on the Casper blockchain.

Architecture

The contract is designed as a single, self-contained module that manages the entire escrow lifecycle:

#[odra::module]
pub struct Escrow {
    // Invoice data
    invoice: Var<Invoice>,

    // Escrow state
    state: Var<EscrowState>,
    balance: Var<U512>,

    // Dispute info
    dispute_reason: Var<Option<String>>,
}

Data Structures

#[derive(OdraType)]
pub struct Invoice {
    pub id: String,
    pub description: String,
    pub amount: U512,
    pub issuer_address: Address,
    pub payer_address: Address,
    pub arbiter_address: Option<Address>,
    pub created_at: u64,
    pub due_date: Option<u64>,
}

#[derive(OdraType)]
pub enum EscrowState {
    Draft,
    Accepted,
    Funded,
    Released,
    Cancelled,
    Disputed,
}

Entry Points

The contract exposes the following callable methods:

init

Initializes a new escrow contract with invoice details.

#[odra(init)]
pub fn init(&mut self, config: EscrowConfig) {
    // Only callable once during deployment
    // Sets up invoice, initial state = Draft
}

accept

Payer accepts the escrow terms.

pub fn accept(&mut self) {
    // Caller must be payer
    // State must be Draft
    // Transitions to Accepted
}

fund

Payer deposits funds into the escrow.

#[odra(payable)]
pub fn fund(&mut self) {
    // Caller must be payer
    // State must be Accepted
    // Amount must be >= invoice.amount
    // Transitions to Funded
}

release

Payer releases funds to the issuer.

pub fn release(&mut self) {
    // Caller must be payer
    // State must be Funded
    // Transfers balance to issuer
    // Transitions to Released
}

cancel

Cancel the escrow and return any funds.

pub fn cancel(&mut self) {
    // Callable by issuer (in Draft) or both (in Accepted)
    // If Funded, only via dispute resolution
    // Returns funds to payer if any
    // Transitions to Cancelled
}

dispute

Raise a dispute when funded.

pub fn dispute(&mut self, reason: String) {
    // Caller must be issuer or payer
    // State must be Funded
    // Stores dispute reason
    // Transitions to Disputed
}

resolve_dispute

Arbiter resolves the dispute.

pub fn resolve_dispute(&mut self, release_to_issuer: bool) {
    // Caller must be arbiter
    // State must be Disputed
    // If true: releases to issuer
    // If false: refunds to payer
}

View Methods

pub fn get_state(&self) -> EscrowState;
pub fn get_invoice(&self) -> Invoice;
pub fn get_balance(&self) -> U512;
pub fn get_dispute_reason(&self) -> Option<String>;

Events

The contract emits events for all state changes, enabling off-chain indexing and notifications:

EventDataEmitted When
EscrowCreatedinvoice_id, issuer, payer, amountContract deployed
EscrowAcceptedinvoice_id, payerPayer accepts
FundsDepositedinvoice_id, amountPayer funds escrow
FundsReleasedinvoice_id, recipient, amountFunds released to issuer
EscrowCancelledinvoice_id, cancelled_byEscrow cancelled
DisputeRaisedinvoice_id, raised_by, reasonDispute initiated
DisputeResolvedinvoice_id, resolved_by, outcomeArbiter resolves

Gas Costs

Estimated gas costs on Casper testnet:

  • Deploy: ~5-10 CSPR
  • Accept: ~1 CSPR
  • Fund: ~1 CSPR + transfer amount
  • Release: ~1 CSPR
  • Cancel/Dispute: ~1 CSPR

Security Considerations

  • Access Control: Each method validates the caller against stored addresses
  • State Guards: Actions are only allowed in valid states
  • Reentrancy: State updated before external calls
  • Integer Overflow: Using Odra's safe math operations