Contract architecture

Garden uses Hashed Time Lock Contracts (HTLCs) to implement atomic swap functionality on Solana. The program manages the lifecycle of a swap through four main operations using Program Derived Addresses (PDAs) for state management:

Core functions

Initiate

The initiate function creates a new HTLC by transferring SOL to a PDA vault. It requires:
  • Secret hash: SHA256 hash of a preimage known only to the initiator.
  • Timelock: Number of slots after which refund is possible (1 slot ≈ 400ms).
  • Redeemer: Public key authorized to claim the funds.
  • Amount: Quantity of native SOL in lamports (1 SOL = 1,000,000,000 lamports).
pub fn initiate(
    ctx: Context<Initiate>,
    amount_lamports: u64,
    expires_in_slots: u64,
    redeemer: Pubkey,
    secret_hash: [u8; 32],
) -> Result<()>
Uses PDAs as deterministic vaults, ensuring each swap with the same initiator and secret hash is unique and prevents replay attacks.

Redeem

The redeem function allows the redeemer to claim the locked SOL by providing the secret that hashes to the stored secret hash.
pub fn redeem(
    ctx: Context<Redeem>, 
    secret: [u8; 32]
) -> Result<()>
The secret must hash to the exact value stored during initiation. Once revealed, this secret enables the counterparty to claim funds on the other chain. No signature required — anyone can execute if they know the secret.

Refund

The refund function allows the initiator to reclaim their SOL after the timelock has expired and the redeemer has not claimed the funds.
pub fn refund(ctx: Context<Refund>) -> Result<()>
Uses slot-based timing which provides more granular control than Bitcoin’s block-based system. Each slot represents approximately 400ms.

Instant refund

The instant refund function provides a way for the redeemer to consent to canceling the swap before the timelock expires.
pub fn instant_refund(ctx: Context<InstantRefund>) -> Result<()>
Requires the redeemer’s signature to prevent unauthorized instant refunds. This ensures mutual consent before the settlement window expires.

Solana-specific features

PDA state management

The program uses a PDA (Program Derived Address) to store swap state, derived from deterministic seeds:
#[account]
#[derive(InitSpace)]
pub struct SwapAccount {
    amount_lamports: u64,    // SOL amount in base units
    expiry_slot: u64,        // Absolute slot for expiry
    initiator: Pubkey,       // Initiator's public key
    redeemer: Pubkey,        // Redeemer's public key  
    secret_hash: [u8; 32],   // SHA256 hash of secret
}

Account constraints

Anchor provides compile-time account validation and runtime constraints:
#[account(
    init,
    payer = initiator,
    seeds = [b"swap_account", initiator.key().as_ref(), &secret_hash],
    bump,
    space = ANCHOR_DISCRIMINATOR + SwapAccount::INIT_SPACE,
)]
pub swap_account: Account<'info, SwapAccount>,
The seed structure ensures that each swap is uniquely identified by the initiator and secret hash, preventing duplicate swaps until completion.

Event logging

The program emits events for each state transition to enable efficient off-chain monitoring:
#[event]
pub struct Initiated {
    pub swap_amount: u64,
    pub expires_in_slots: u64,
    pub initiator: Pubkey,
    pub redeemer: Pubkey,
    pub secret_hash: [u8; 32],
}

#[event]
pub struct Redeemed {
    pub initiator: Pubkey,
    pub secret: [u8; 32],
}

Rent management

Solana’s rent system is automatically handled through Anchor’s close attribute:
#[account(mut, close = initiator)]
pub swap_account: Account<'info, SwapAccount>,
When a swap completes (redeem/refund), the PDA is closed and rent is automatically refunded to the initiator, ensuring no SOL is permanently locked.

Error handling

Custom error types provide clear feedback for failed operations:
#[error_code]
pub enum SwapError {
    #[msg("The provided initiator is not the original initiator of this swap")]
    InvalidInitiator,
    #[msg("The provided redeemer is not the original redeemer of this swap")]  
    InvalidRedeemer,
    #[msg("The provided secret does not correspond to the secret hash of this swap")]
    InvalidSecret,
    #[msg("Attempt to perform a refund before expiry time")]
    RefundBeforeExpiry,
}