6. Smart Contract Specification

6.1 Exhibition Contract

The Exhibition contract manages the registry of circulating exhibitions. Critically, no transfer function exists—exhibitions are permanently bound to their creating artist.

use cosmwasm_std::{
    entry_point, to_json_binary, Binary, Deps, DepsMut, Env, 
    MessageInfo, Response, StdError, StdResult, Addr, Timestamp, Uint128,
};
use cw_storage_plus::{Item, Map};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

// ============================================================================
// STATE DEFINITIONS
// ============================================================================

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct Config {
    pub admin: Addr,
    pub license_contract: Addr,
    pub governance_contract: Addr,
    pub next_exhibition_id: u64,
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct Exhibition {
    pub id: u64,
    pub artist: Addr,
    pub curator: Addr,
    pub title: String,
    pub metadata_uri: String,        // IPFS hash
    pub content_hash: String,        // SHA-256 of primary media
    pub created_at: Timestamp,
    pub total_licenses: u64,
    pub active_licenses: u64,
    pub lifetime_revenue: Uint128,
    pub status: ExhibitionStatus,
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub enum ExhibitionStatus {
    Draft,
    Active,
    Paused,      // Artist can pause licensing temporarily
    Archived,    // No new licenses, existing honored
}

pub const CONFIG: Item<Config> = Item::new("config");
pub const EXHIBITIONS: Map<u64, Exhibition> = Map::new("exhibitions");
pub const ARTIST_EXHIBITIONS: Map<&Addr, Vec<u64>> = Map::new("artist_exhibitions");
pub const CURATOR_EXHIBITIONS: Map<&Addr, Vec<u64>> = Map::new("curator_exhibitions");

// ============================================================================
// INSTANTIATE
// ============================================================================

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct InstantiateMsg {
    pub license_contract: String,
    pub governance_contract: String,
}

#[entry_point]
pub fn instantiate(
    deps: DepsMut,
    _env: Env,
    info: MessageInfo,
    msg: InstantiateMsg,
) -> StdResult<Response> {
    let config = Config {
        admin: info.sender.clone(),
        license_contract: deps.api.addr_validate(&msg.license_contract)?,
        governance_contract: deps.api.addr_validate(&msg.governance_contract)?,
        next_exhibition_id: 1,
    };
    CONFIG.save(deps.storage, &config)?;
    
    Ok(Response::new()
        .add_attribute("method", "instantiate")
        .add_attribute("admin", info.sender))
}

// ============================================================================
// EXECUTE MESSAGES
// ============================================================================

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ExecuteMsg {
    // Artist creates new exhibition
    CreateExhibition {
        title: String,
        curator: String,
        metadata_uri: String,
        content_hash: String,
    },
    // Artist updates metadata (not content hash - immutable)
    UpdateMetadata {
        exhibition_id: u64,
        metadata_uri: String,
    },
    // Artist controls availability
    SetStatus {
        exhibition_id: u64,
        status: ExhibitionStatus,
    },
    // Called by License contract to update stats
    RecordLicense {
        exhibition_id: u64,
        fee_amount: Uint128,
    },
    // Called by License contract when license completes
    CompleteLicense {
        exhibition_id: u64,
    },
    // NOTE: No Transfer message - by design
    // NOTE: No Burn message - exhibitions are permanent records
}

#[entry_point]
pub fn execute(
    deps: DepsMut,
    env: Env,
    info: MessageInfo,
    msg: ExecuteMsg,
) -> Result<Response, ContractError> {
    match msg {
        ExecuteMsg::CreateExhibition { title, curator, metadata_uri, content_hash } => {
            execute_create_exhibition(deps, env, info, title, curator, metadata_uri, content_hash)
        }
        ExecuteMsg::UpdateMetadata { exhibition_id, metadata_uri } => {
            execute_update_metadata(deps, info, exhibition_id, metadata_uri)
        }
        ExecuteMsg::SetStatus { exhibition_id, status } => {
            execute_set_status(deps, info, exhibition_id, status)
        }
        ExecuteMsg::RecordLicense { exhibition_id, fee_amount } => {
            execute_record_license(deps, info, exhibition_id, fee_amount)
        }
        ExecuteMsg::CompleteLicense { exhibition_id } => {
            execute_complete_license(deps, info, exhibition_id)
        }
    }
}

fn execute_create_exhibition(
    deps: DepsMut,
    env: Env,
    info: MessageInfo,
    title: String,
    curator: String,
    metadata_uri: String,
    content_hash: String,
) -> Result<Response, ContractError> {
    let mut config = CONFIG.load(deps.storage)?;
    let curator_addr = deps.api.addr_validate(&curator)?;
    
    let exhibition = Exhibition {
        id: config.next_exhibition_id,
        artist: info.sender.clone(),
        curator: curator_addr.clone(),
        title: title.clone(),
        metadata_uri,
        content_hash,
        created_at: env.block.time,
        total_licenses: 0,
        active_licenses: 0,
        lifetime_revenue: Uint128::zero(),
        status: ExhibitionStatus::Draft,
    };
    
    EXHIBITIONS.save(deps.storage, config.next_exhibition_id, &exhibition)?;
    
    // Update artist index
    let mut artist_exh = ARTIST_EXHIBITIONS
        .may_load(deps.storage, &info.sender)?
        .unwrap_or_default();
    artist_exh.push(config.next_exhibition_id);
    ARTIST_EXHIBITIONS.save(deps.storage, &info.sender, &artist_exh)?;
    
    // Update curator index
    let mut curator_exh = CURATOR_EXHIBITIONS
        .may_load(deps.storage, &curator_addr)?
        .unwrap_or_default();
    curator_exh.push(config.next_exhibition_id);
    CURATOR_EXHIBITIONS.save(deps.storage, &curator_addr, &curator_exh)?;
    
    config.next_exhibition_id += 1;
    CONFIG.save(deps.storage, &config)?;
    
    Ok(Response::new()
        .add_attribute("method", "create_exhibition")
        .add_attribute("exhibition_id", exhibition.id.to_string())
        .add_attribute("artist", info.sender)
        .add_attribute("title", title))
}

fn execute_update_metadata(
    deps: DepsMut,
    info: MessageInfo,
    exhibition_id: u64,
    metadata_uri: String,
) -> Result<Response, ContractError> {
    let mut exhibition = EXHIBITIONS.load(deps.storage, exhibition_id)?;
    
    // Only artist can update metadata
    if info.sender != exhibition.artist {
        return Err(ContractError::Unauthorized {});
    }
    
    exhibition.metadata_uri = metadata_uri;
    EXHIBITIONS.save(deps.storage, exhibition_id, &exhibition)?;
    
    Ok(Response::new()
        .add_attribute("method", "update_metadata")
        .add_attribute("exhibition_id", exhibition_id.to_string()))
}

fn execute_set_status(
    deps: DepsMut,
    info: MessageInfo,
    exhibition_id: u64,
    status: ExhibitionStatus,
) -> Result<Response, ContractError> {
    let mut exhibition = EXHIBITIONS.load(deps.storage, exhibition_id)?;
    
    // Only artist can change status
    if info.sender != exhibition.artist {
        return Err(ContractError::Unauthorized {});
    }
    
    exhibition.status = status.clone();
    EXHIBITIONS.save(deps.storage, exhibition_id, &exhibition)?;
    
    Ok(Response::new()
        .add_attribute("method", "set_status")
        .add_attribute("exhibition_id", exhibition_id.to_string())
        .add_attribute("status", format!("{:?}", status)))
}

fn execute_record_license(
    deps: DepsMut,
    info: MessageInfo,
    exhibition_id: u64,
    fee_amount: Uint128,
) -> Result<Response, ContractError> {
    let config = CONFIG.load(deps.storage)?;
    
    // Only license contract can record
    if info.sender != config.license_contract {
        return Err(ContractError::Unauthorized {});
    }
    
    let mut exhibition = EXHIBITIONS.load(deps.storage, exhibition_id)?;
    exhibition.total_licenses += 1;
    exhibition.active_licenses += 1;
    exhibition.lifetime_revenue += fee_amount;
    EXHIBITIONS.save(deps.storage, exhibition_id, &exhibition)?;
    
    Ok(Response::new()
        .add_attribute("method", "record_license")
        .add_attribute("exhibition_id", exhibition_id.to_string()))
}

fn execute_complete_license(
    deps: DepsMut,
    info: MessageInfo,
    exhibition_id: u64,
) -> Result<Response, ContractError> {
    let config = CONFIG.load(deps.storage)?;
    
    if info.sender != config.license_contract {
        return Err(ContractError::Unauthorized {});
    }
    
    let mut exhibition = EXHIBITIONS.load(deps.storage, exhibition_id)?;
    exhibition.active_licenses = exhibition.active_licenses.saturating_sub(1);
    EXHIBITIONS.save(deps.storage, exhibition_id, &exhibition)?;
    
    Ok(Response::new()
        .add_attribute("method", "complete_license")
        .add_attribute("exhibition_id", exhibition_id.to_string()))
}

// ============================================================================
// QUERY MESSAGES
// ============================================================================

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum QueryMsg {
    GetExhibition { exhibition_id: u64 },
    GetArtistExhibitions { artist: String },
    GetCuratorExhibitions { curator: String },
    ListActiveExhibitions { start_after: Option<u64>, limit: Option<u32> },
    GetConfig {},
}

#[entry_point]
pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
    match msg {
        QueryMsg::GetExhibition { exhibition_id } => {
            to_json_binary(&EXHIBITIONS.load(deps.storage, exhibition_id)?)
        }
        QueryMsg::GetArtistExhibitions { artist } => {
            let addr = deps.api.addr_validate(&artist)?;
            let ids = ARTIST_EXHIBITIONS.may_load(deps.storage, &addr)?.unwrap_or_default();
            to_json_binary(&ids)
        }
        QueryMsg::GetCuratorExhibitions { curator } => {
            let addr = deps.api.addr_validate(&curator)?;
            let ids = CURATOR_EXHIBITIONS.may_load(deps.storage, &addr)?.unwrap_or_default();
            to_json_binary(&ids)
        }
        QueryMsg::ListActiveExhibitions { start_after, limit } => {
            let limit = limit.unwrap_or(30).min(100) as usize;
            let start = start_after.unwrap_or(0);
            
            let exhibitions: Vec<Exhibition> = EXHIBITIONS
                .range(deps.storage, None, None, cosmwasm_std::Order::Ascending)
                .filter(|r| {
                    if let Ok((id, exh)) = r {
                        *id > start && exh.status == ExhibitionStatus::Active
                    } else {
                        false
                    }
                })
                .take(limit)
                .filter_map(|r| r.ok().map(|(_, e)| e))
                .collect();
            
            to_json_binary(&exhibitions)
        }
        QueryMsg::GetConfig {} => to_json_binary(&CONFIG.load(deps.storage)?),
    }
}

// ============================================================================
// ERRORS
// ============================================================================

#[derive(Debug)]
pub enum ContractError {
    Std(StdError),
    Unauthorized {},
    ExhibitionNotFound {},
    InvalidStatus {},
}

impl From<StdError> for ContractError {
    fn from(err: StdError) -> Self {
        ContractError::Std(err)
    }
}

6.2 License Contract

The License contract manages time-bound exhibition licenses. Like exhibitions, licenses are non-transferable—bound permanently to the issuing host.

6.3 Payment Contract

The Payment contract handles automatic distribution of license fees according to the fixed allocation.

6.4 Governance Contract

The Governance contract implements multi-stakeholder voting with constitutional protections.

6.5 Provenance Contract

The Provenance contract maintains immutable circulation history for each exhibition.

Last updated