How the Akash Network Market Module Handles Bid Settlement and Lease Closure Events

The Akash Network market module processes bid settlement and lease closure through escrow-triggered hooks that cascade from group-level account closures to individual bid and lease state updates.

The akash-network/node repository implements a sophisticated market mechanism where the escrow module drives settlement logic by invoking specific hooks in the market module. When escrow accounts or payments close due to fund exhaustion or completion, the market module executes a deterministic sequence of state transitions to finalize bids and leases. This architecture ensures that financial finality in the escrow layer automatically propagates to the market layer’s order book and lease registry.

Settlement Architecture Overview

The bid settlement and lease closure flow operates through three coordinated steps triggered by escrow events:

  1. Escrow Account Closure (OnEscrowAccountClosed) – Handles group-level shutdowns when a deployment runs out of funds
  2. Escrow Payment Closure (OnEscrowPaymentClosed) – Handles individual bid settlement when a specific lease payment closes
  3. Keeper State Updates – Updates internal state machines for bids, orders, and leases while emitting corresponding events

This hook-based design decouples financial settlement from market state management while ensuring atomic consistency between the escrow and market modules.

Group-Level Closure via OnEscrowAccountClosed

When an escrow account closes—typically because a deployment has exhausted its funds—the escrow module invokes hooks.OnEscrowAccountClosed in x/market/hooks/hooks.go (lines 30–66).

The hook resolves the deployment ID, closes the active deployment, and iterates through all associated groups. For each group that can be closed, it executes:

func (h *hooks) OnEscrowAccountClosed(ctx sdk.Context, obj etypes.Account) error {
    // Resolve deployment ID from escrow account
    // Close deployment if still active
    // Iterate groups and call dkeeper.OnCloseGroup
    // Call mkeeper.OnGroupClosed for each group
}

The OnGroupClosed method in x/market/keeper/keeper.go (lines 78–109) then iterates through every order in the group. For each order, it runs processClose, which cascades to OnBidClosed and OnLeaseClosed for every bid attached to that order. This ensures that when a deployment’s escrow account drains, all associated market entities transition to closed states automatically.

Bid-Level Settlement via OnEscrowPaymentClosed

Individual bid settlement occurs when an escrow payment closes, signaling the end of a specific lease. The escrow module triggers hooks.OnEscrowPaymentClosed in x/market/hooks/hooks.go (lines 70–115), which implements the core settlement logic:

func (h *hooks) OnEscrowPaymentClosed(ctx sdk.Context, obj etypes.Payment) error {
    id, _ := mv1.LeaseIDFromPaymentID(obj.ID)
    bid   := h.mkeeper.GetBid(ctx, id.BidID())
    order := h.mkeeper.GetOrder(ctx, id.OrderID())
    lease := h.mkeeper.GetLease(ctx, id)

    // Close order
    if err := h.mkeeper.OnOrderClosed(ctx, order); err != nil { 
        return err 
    }

    // Close bid
    if err := h.mkeeper.OnBidClosed(ctx, bid); err != nil { 
        return err 
    }

    // Close lease with appropriate reason
    if obj.State.State == etypes.StateOverdrawn {
        err = h.mkeeper.OnLeaseClosed(ctx, lease,
            mv1.LeaseInsufficientFunds, 
            mv1.LeaseClosedReasonInsufficientFunds)
    } else {
        err = h.mkeeper.OnLeaseClosed(ctx, lease,
            mv1.LeaseClosed, 
            mv1.LeaseClosedReasonUnspecified)
    }
    return err
}

This method resolves the Lease ID from the payment ID, retrieves the associated bid, order, and lease objects, and orchestrates their closure. The closure reason depends on the escrow payment’s final state: StateOverdrawn triggers an insufficient-funds closure, while normal completion triggers a standard lease closure.

State Machine Updates in the Market Keeper

The market keeper in x/market/keeper/keeper.go implements three primary methods to handle the actual state transitions:

OnBidClosed

The OnBidClosed method (lines 100–123) finalizes bid settlement by:

  • Setting the bid state to BidClosed (unless already closed or lost)
  • Invoking the escrow keeper to close the bid’s escrow account
  • Emitting an EventBidClosed event for external consumers

OnOrderClosed

The OnOrderClosed method (lines 124–146) handles order finalization:

  • Updates the order state to OrderClosed if not already closed
  • Emits EventOrderClosed to notify subscribers of the order book change

OnLeaseClosed

The OnLeaseClosed method (lines 147–176) implements the final lease settlement logic:

  • Validates the lease is not already in a terminal state (closed or insufficient funds)
  • Updates the lease’s State, ClosedOn timestamp, and Reason fields
  • Persists the lease to update all internal indexes
  • Emits EventLeaseClosed with the appropriate closure reason

These keeper methods ensure that the market module’s internal state remains consistent with the escrow module’s financial finality, providing a complete audit trail through emitted events.

Practical Implementation Examples

Simulating Payment Closure in Tests

To verify that bid settlement triggers correctly when escrow payments close, you can simulate the hook invocation in test scenarios:

func TestPaymentCloseTriggersLeaseClosure(t *testing.T) {
    ctx := sdk.NewContext(...)

    // Create a lease in active state
    lease := createTestLease(ctx, keeper, bidID)

    // Simulate escrow payment close with overdrawn state
    payment := etypes.Payment{
        ID:    lease.ID.ToEscrowPaymentID(),
        State: etypes.State{State: etypes.StateOverdrawn},
    }

    // Invoke the market hook as the escrow module would
    err := marketHooks.OnEscrowPaymentClosed(ctx, payment)
    require.NoError(t, err)

    // Verify lease reflects insufficient-funds closure
    l, ok := keeper.GetLease(ctx, lease.ID)
    require.True(t, ok)
    assert.Equal(t, mv1.LeaseInsufficientFunds, l.State)
    assert.Equal(t, mv1.LeaseClosedReasonInsufficientFunds, l.Reason)
}

Direct Bid Closure Pattern

When implementing manual bid management or cleanup operations, use the keeper’s closure method directly:

func closeBid(ctx sdk.Context, k keeper.IKeeper, bidID mv1.BidID) error {
    bid, ok := k.GetBid(ctx, bidID)
    if !ok {
        return fmt.Errorf("bid %s not found", bidID)
    }
    // Updates state, closes escrow account, emits EventBidClosed
    return k.OnBidClosed(ctx, bid)
}

Summary

  • The escrow module drives market settlement through the OnEscrowAccountClosed and OnEscrowPaymentClosed hooks in x/market/hooks/hooks.go
  • Group-level closures cascade from deployment escrow accounts through orders to individual bids and leases via OnGroupClosed
  • Bid settlement resolves the lease from the payment ID and orchestrates closure of the order, bid, and lease with appropriate state reasons
  • The market keeper’s OnBidClosed, OnOrderClosed, and OnLeaseClosed methods enforce state consistency and emit EventBidClosed, EventOrderClosed, and EventLeaseClosed events
  • Closure reasons distinguish between normal completion (LeaseClosed) and fund exhaustion (LeaseInsufficientFunds) based on the escrow payment’s StateOverdrawn status

Frequently Asked Questions

What triggers bid settlement in the Akash market module?

Bid settlement triggers when the escrow module closes a payment associated with an active lease. The escrow module invokes OnEscrowPaymentClosed, which resolves the bid-to-lease relationship and calls the market keeper’s OnBidClosed method to finalize the state transition and emit closure events.

How does the market module distinguish between normal lease closure and insufficient funds closure?

The market module checks the escrow payment’s state within OnEscrowPaymentClosed. If obj.State.State == etypes.StateOverdrawn, the module calls OnLeaseClosed with LeaseInsufficientFunds and LeaseClosedReasonInsufficientFunds. Otherwise, it uses LeaseClosed with LeaseClosedReasonUnspecified, allowing external consumers to differentiate between natural completion and premature termination due to depleted funds.

Where is the group-level closure logic implemented when a deployment runs out of funds?

Group-level closure logic resides in x/market/hooks/hooks.go within the OnEscrowAccountClosed function (lines 30–66). This hook iterates through all groups associated with the closed escrow account and invokes OnGroupClosed in the market keeper, which subsequently processes every order and bid within those groups to ensure complete market cleanup.

What events are emitted during the bid settlement and lease closure process?

The market module emits three distinct events during settlement: EventBidClosed when OnBidClosed finalizes the bid state, EventOrderClosed when OnOrderClosed completes the order lifecycle, and EventLeaseClosed when OnLeaseClosed records the final lease disposition. These events provide a complete audit trail for block explorers and external monitoring systems tracking deployment lifecycles.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →