5.1 Trade State Machine

All three implementations share the same fundamental state machine for trades:

RequestCreated RequestAccepted EscrowFunded FiatDeposited EscrowReleased RequestCanceled RequestExpired EscrowRefunded Disputed DisputeResolved accept fund confirm fiat release cancel expire refund dispute resolve

5.2 State Definitions

CosmWasm Trade State

#[cw_serde]
pub enum TradeState {
    RequestCreated,
    RequestAccepted,
    RequestCanceled,
    RequestExpired,
    EscrowFunded,
    EscrowRefunded,
    FiatDeposited,
    EscrowReleased,
    EscrowDisputed,
    SettledForMaker,
    SettledForTaker,
}

#[cw_serde]
pub struct Trade {
    pub id: u64,
    pub offer_id: u64,
    pub buyer: Addr,
    pub seller: Addr,
    pub state: TradeState,
    pub amount: Uint128,
    pub fiat_amount: Uint128,
    pub denom: String,
    pub fiat_currency: FiatCurrency,
    pub arbitrator: Option<Addr>,
    pub state_history: Vec<StateChange>,
    pub created_at: Timestamp,
    pub expires_at: Timestamp,
}

// State history for audit trail
#[cw_serde]
pub struct StateChange {
    pub from: TradeState,
    pub to: TradeState,
    pub actor: Addr,
    pub timestamp: Timestamp,
}

Solidity Trade State

enum TradeState {
    RequestCreated,
    RequestAccepted,
    RequestCanceled,
    RequestExpired,
    EscrowFunded,
    EscrowRefunded,
    FiatDeposited,
    EscrowReleased,
    Disputed,
    DisputeResolved
}

struct TradeData {
    uint256 id;
    uint256 offerId;
    address buyer;
    address seller;
    TradeState state;
    uint256 amount;
    uint256 fiatAmount;
    address token;
    string fiatCurrency;
    address arbitrator;
    uint256 createdAt;
    uint256 expiresAt;
}

// State history via events
event TradeStateChanged(
    uint256 indexed tradeId,
    TradeState fromState,
    TradeState toState,
    address actor,
    uint256 timestamp
);

Solana Trade State

#[derive(AnchorSerialize, AnchorDeserialize, Clone, PartialEq)]
pub enum TradeState {
    RequestCreated,
    RequestAccepted,
    RequestCanceled,
    RequestExpired,
    EscrowFunded,
    EscrowRefunded,
    FiatDeposited,
    EscrowReleased,
    Disputed,
    DisputeResolved,
}

#[account]
pub struct Trade {
    pub id: u64,
    pub offer_id: u64,
    pub buyer: Pubkey,
    pub seller: Pubkey,
    pub state: TradeState,
    pub amount: u64,
    pub fiat_amount: u64,
    pub token_mint: Pubkey,
    pub fiat_currency: [u8; 3],
    pub arbitrator: Option<Pubkey>,
    pub created_at: i64,
    pub expires_at: i64,
    pub buyer_contact: String,
    pub seller_contact: String,
    pub bump: u8,
}

// Events via Anchor emit!
#[event]
pub struct TradeStateChanged {
    pub trade_id: u64,
    pub from_state: TradeState,
    pub to_state: TradeState,
    pub actor: Pubkey,
    pub timestamp: i64,
}

5.3 Enforcing Valid Transitions

The state machine must enforce valid transitions to prevent invalid states:

CosmWasm Transitions

impl TradeState {
    pub fn can_transition_to(
        &self,
        next: &TradeState
    ) -> bool {
        use TradeState::*;
        match (self, next) {
            // From RequestCreated
            (RequestCreated, RequestAccepted) => true,
            (RequestCreated, RequestCanceled) => true,
            (RequestCreated, RequestExpired) => true,

            // From RequestAccepted
            (RequestAccepted, EscrowFunded) => true,
            (RequestAccepted, RequestCanceled) => true,

            // From EscrowFunded
            (EscrowFunded, FiatDeposited) => true,
            (EscrowFunded, EscrowRefunded) => true,

            // From FiatDeposited
            (FiatDeposited, EscrowReleased) => true,
            (FiatDeposited, EscrowDisputed) => true,

            // From Disputed
            (EscrowDisputed, SettledForMaker) => true,
            (EscrowDisputed, SettledForTaker) => true,

            _ => false,
        }
    }
}

// Usage in handler
fn transition_state(
    trade: &mut Trade,
    new_state: TradeState,
    actor: &Addr,
    time: Timestamp,
) -> Result<(), ContractError> {
    if !trade.state.can_transition_to(&new_state) {
        return Err(ContractError::InvalidTransition {
            from: trade.state.clone(),
            to: new_state,
        });
    }

    trade.state_history.push(StateChange {
        from: trade.state.clone(),
        to: new_state.clone(),
        actor: actor.clone(),
        timestamp: time,
    });

    trade.state = new_state;
    Ok(())
}

Solidity Transitions

contract Trade {
    // Modifier for valid transitions
    modifier validTransition(
        uint256 tradeId,
        TradeState expectedCurrent,
        TradeState newState
    ) {
        TradeData storage trade = trades[tradeId];
        require(
            trade.state == expectedCurrent,
            "Invalid current state"
        );
        require(
            _isValidTransition(expectedCurrent, newState),
            "Invalid state transition"
        );
        _;
    }

    function _isValidTransition(
        TradeState from,
        TradeState to
    ) internal pure returns (bool) {
        if (from == TradeState.RequestCreated) {
            return to == TradeState.RequestAccepted ||
                   to == TradeState.RequestCanceled ||
                   to == TradeState.RequestExpired;
        }
        if (from == TradeState.RequestAccepted) {
            return to == TradeState.EscrowFunded ||
                   to == TradeState.RequestCanceled;
        }
        if (from == TradeState.EscrowFunded) {
            return to == TradeState.FiatDeposited ||
                   to == TradeState.EscrowRefunded;
        }
        if (from == TradeState.FiatDeposited) {
            return to == TradeState.EscrowReleased ||
                   to == TradeState.Disputed;
        }
        if (from == TradeState.Disputed) {
            return to == TradeState.DisputeResolved;
        }
        return false;
    }

    function _setState(
        uint256 tradeId,
        TradeState newState
    ) internal {
        TradeState oldState = trades[tradeId].state;
        trades[tradeId].state = newState;

        emit TradeStateChanged(
            tradeId, oldState, newState,
            msg.sender, block.timestamp
        );
    }
}

Solana Transitions

impl TradeState {
    pub fn can_transition_to(
        &self,
        next: &TradeState
    ) -> bool {
        use TradeState::*;
        matches!(
            (self, next),
            (RequestCreated, RequestAccepted) |
            (RequestCreated, RequestCanceled) |
            (RequestCreated, RequestExpired) |
            (RequestAccepted, EscrowFunded) |
            (RequestAccepted, RequestCanceled) |
            (EscrowFunded, FiatDeposited) |
            (EscrowFunded, EscrowRefunded) |
            (FiatDeposited, EscrowReleased) |
            (FiatDeposited, Disputed) |
            (Disputed, DisputeResolved)
        )
    }
}

// Constraint in account validation
#[derive(Accounts)]
pub struct FundEscrow<'info> {
    #[account(
        mut,
        constraint = trade.state == TradeState::RequestAccepted
            @ TradeError::InvalidState
    )]
    pub trade: Account<'info, Trade>,
    // ...
}

// Helper function
pub fn transition_to(
    trade: &mut Trade,
    new_state: TradeState,
    clock: &Clock,
) -> Result<()> {
    require!(
        trade.state.can_transition_to(&new_state),
        TradeError::InvalidTransition
    );

    let old_state = trade.state.clone();
    trade.state = new_state.clone();
    trade.updated_at = clock.unix_timestamp;

    emit!(TradeStateChanged {
        trade_id: trade.id,
        from_state: old_state,
        to_state: new_state,
        actor: trade.buyer, // or appropriate
        timestamp: clock.unix_timestamp,
    });

    Ok(())
}

5.4 Dispute Resolution

When a trade is disputed, an arbitrator resolves it:

CosmWasm Dispute

pub fn execute_dispute(
    deps: DepsMut,
    env: Env,
    info: MessageInfo,
    trade_id: u64,
) -> Result<Response, ContractError> {
    let mut trade = TRADES.load(deps.storage, trade_id)?;

    // Only after fiat deposited
    if trade.state != TradeState::FiatDeposited {
        return Err(ContractError::InvalidState {});
    }

    // Only buyer or seller can dispute
    if info.sender != trade.buyer && info.sender != trade.seller {
        return Err(ContractError::Unauthorized {});
    }

    // Assign random arbitrator
    let arbitrator = select_arbitrator(
        deps.as_ref(),
        &trade.fiat_currency,
        &env.block,
    )?;

    trade.arbitrator = Some(arbitrator.clone());
    trade.state = TradeState::EscrowDisputed;
    trade.dispute_initiated_at = Some(env.block.time);

    TRADES.save(deps.storage, trade_id, &trade)?;

    Ok(Response::new()
        .add_attribute("action", "dispute")
        .add_attribute("arbitrator", arbitrator))
}

pub fn execute_settle_dispute(
    deps: DepsMut,
    info: MessageInfo,
    trade_id: u64,
    winner: DisputeWinner,
) -> Result<Response, ContractError> {
    let mut trade = TRADES.load(deps.storage, trade_id)?;

    // Only arbitrator can settle
    if trade.arbitrator != Some(info.sender.clone()) {
        return Err(ContractError::NotArbitrator {});
    }

    // Determine recipient
    let recipient = match winner {
        DisputeWinner::Maker => &trade.seller,
        DisputeWinner::Taker => &trade.buyer,
    };

    // Calculate arbitrator fee
    let arb_fee = trade.escrow_amount * arb_fee_pct;
    let winner_amount = trade.escrow_amount - arb_fee;

    // Send to winner and arbitrator
    let msgs = vec![
        bank_send(recipient, winner_amount, &trade.denom),
        bank_send(&info.sender, arb_fee, &trade.denom),
    ];

    trade.state = match winner {
        DisputeWinner::Maker => TradeState::SettledForMaker,
        DisputeWinner::Taker => TradeState::SettledForTaker,
    };

    Ok(Response::new().add_messages(msgs))
}

Solidity Dispute

struct DisputeInfo {
    uint256 tradeId;
    address initiator;
    uint256 initiatedAt;
    address arbitrator;
    string buyerEvidence;
    string sellerEvidence;
    address winner;
    uint256 resolvedAt;
}

mapping(uint256 => DisputeInfo) public disputes;

function initiateDispute(
    uint256 tradeId,
    string calldata evidence
) external {
    TradeData storage trade = trades[tradeId];

    require(
        trade.state == TradeState.FiatDeposited,
        "Invalid state"
    );
    require(
        msg.sender == trade.buyer ||
        msg.sender == trade.seller,
        "Not trade party"
    );

    // Assign arbitrator
    address arbitrator = arbitratorManager
        .assignArbitrator(trade.fiatCurrency);

    disputes[tradeId] = DisputeInfo({
        tradeId: tradeId,
        initiator: msg.sender,
        initiatedAt: block.timestamp,
        arbitrator: arbitrator,
        buyerEvidence: msg.sender == trade.buyer
            ? evidence : "",
        sellerEvidence: msg.sender == trade.seller
            ? evidence : "",
        winner: address(0),
        resolvedAt: 0
    });

    trade.state = TradeState.Disputed;
    emit DisputeInitiated(tradeId, msg.sender, arbitrator);
}

function resolveDispute(
    uint256 tradeId,
    address winner
) external nonReentrant {
    DisputeInfo storage dispute = disputes[tradeId];
    TradeData storage trade = trades[tradeId];

    require(
        msg.sender == dispute.arbitrator,
        "Not arbitrator"
    );
    require(
        winner == trade.buyer || winner == trade.seller,
        "Invalid winner"
    );

    // Calculate and distribute
    uint256 total = escrowBalances[tradeId];
    uint256 arbFee = (total * hub.arbitratorFeeBps) / 10000;
    uint256 winnerAmount = total - arbFee;

    escrowBalances[tradeId] = 0;
    dispute.winner = winner;
    dispute.resolvedAt = block.timestamp;
    trade.state = TradeState.DisputeResolved;

    IERC20(trade.token).safeTransfer(winner, winnerAmount);
    IERC20(trade.token).safeTransfer(msg.sender, arbFee);
}

Solana Dispute

#[derive(Accounts)]
pub struct InitiateDispute<'info> {
    #[account(
        constraint = initiator.key() == trade.buyer ||
                     initiator.key() == trade.seller
    )]
    pub initiator: Signer<'info>,

    #[account(
        mut,
        constraint = trade.state == TradeState::FiatDeposited
    )]
    pub trade: Account<'info, Trade>,

    #[account(
        seeds = [b"arbitrator_registry"],
        bump
    )]
    pub arbitrator_registry: Account<'info, ArbitratorRegistry>,
}

pub fn initiate_dispute(
    ctx: Context<InitiateDispute>
) -> Result<()> {
    let trade = &mut ctx.accounts.trade;
    let clock = Clock::get()?;

    // Select arbitrator randomly
    let arbitrator = select_arbitrator(
        &ctx.accounts.arbitrator_registry,
        &trade.fiat_currency,
        clock.slot,
    )?;

    trade.arbitrator = Some(arbitrator);
    trade.state = TradeState::Disputed;
    trade.dispute_initiated_at = Some(clock.unix_timestamp);

    emit!(DisputeInitiated {
        trade_id: trade.id,
        initiator: ctx.accounts.initiator.key(),
        arbitrator,
    });

    Ok(())
}

#[derive(Accounts)]
pub struct ResolveDispute<'info> {
    #[account(
        constraint = arbitrator.key() == trade.arbitrator.unwrap()
            @ TradeError::NotArbitrator
    )]
    pub arbitrator: Signer<'info>,

    #[account(
        mut,
        constraint = trade.state == TradeState::Disputed
    )]
    pub trade: Account<'info, Trade>,

    // Winner's token account
    #[account(mut)]
    pub winner_token_account: Account<'info, TokenAccount>,

    // Arbitrator's token account for fee
    #[account(mut)]
    pub arbitrator_token_account: Account<'info, TokenAccount>,

    // Vault accounts...
}

Knowledge Check

Test Your Understanding

1. Why is state history tracking important in a trading protocol?

2. When can a dispute be initiated in the trade lifecycle?

3. How does Solidity track state change history?