Skip to main content
Full working code: The complete source code for this example is available in example-contracts/proxy/
This example demonstrates proxying (a common pattern used in contract upgrades), utilizing the fallback hook and introducing the foreign::call built-in and aliased imports.

WIT Interface

The proxy contract exports functions for delegation and configuration:
package root:component;

world root {
  include kontor:built-in/built-in;
  use kontor:built-in/context.{fall-context, proc-context, view-context, signer};
  use kontor:built-in/foreign.{contract-address};

  export fallback: func(ctx: borrow<fall-context>, expr: string) -> string;

  export init: func(ctx: borrow<proc-context>);

  export get-contract-address: func(ctx: borrow<view-context>) -> option<contract-address>;

  export set-contract-address: func(ctx: borrow<proc-context>, contract-address: contract-address);
}

Rust Implementation

The proxy contract stores a target contract address and forwards all calls to it using the fallback hook:
use stdlib::*;

contract!(name = "proxy");

#[derive(Clone, StorageRoot, Default)]
struct ProxyStorage {
    contract_address: Option<ContractAddress>,
}

impl Guest for Proxy {
    fn fallback(ctx: &FallContext, expr: String) -> String {
        let ctx_ = &ctx.view_context();
        if let Some(contract_address) = ctx_.model().contract_address() {
            foreign::call(ctx.signer(), &contract_address, &expr)
        } else {
            "".to_string()
        }
    }

    fn init(ctx: &ProcContext) {
        ProxyStorage::default().init(ctx)
    }

    fn get_contract_address(ctx: &ViewContext) -> Option<ContractAddress> {
        ctx.model().contract_address()
    }

    fn set_contract_address(ctx: &ProcContext, contract_address: ContractAddress) {
        ctx.model().set_contract_address(Some(contract_address));
    }
}
Key points:
  • Storage accessed via ctx.model()
  • fallback handles missing function calls by forwarding to the target contract
  • Uses if let Some(...) to safely handle the Option<ContractAddress>
  • FallContext can be converted to ViewContext for read-only access
  • foreign::call is the low-level API used by interface! and import! macros

Testing

The test demonstrates configuring the proxy and forwarding calls to a shared-account contract:
#[cfg(test)]
mod tests {
    use testlib::*;

    interface!(name = "proxy");
    interface!(name = "token", path = "../token/contract/wit");
    interface!(name = "shared-account", path = "../shared-account/contract/wit");

    #[testlib::test]
    async fn test_shared_account_contract() -> Result<()> {
        let alice = runtime.identity().await?;
        let bob = runtime.identity().await?;
        let claire = runtime.identity().await?;
        let dara = runtime.identity().await?;

        let proxy = runtime.publish(&alice, "proxy").await?;
        let token = runtime.publish(&alice, "token").await?;
        let shared_account = runtime.publish(&alice, "shared-account").await?;

        // Configure proxy to forward to shared-account
        proxy::set_contract_address(runtime, &proxy, &alice, shared_account.clone()).await?;
        
        let result = proxy::get_contract_address(runtime, &proxy).await?;
        assert_eq!(result, Some(shared_account.clone()));

        // Use token contract
        token::mint(runtime, &token, &alice, 100.into()).await??;

        // Call shared-account THROUGH the proxy using the fallback function
        let account_id = shared_account::open(
            runtime,
            &proxy,
            &alice,
            token.clone(),
            50.into(),
            vec![&bob, &dara],
        )
        .await??;

        let result = shared_account::balance(runtime, &proxy, &account_id).await?;
        assert_eq!(result, Some(50.into()));

        // Further deposits and withdrawals through proxy
        shared_account::deposit(runtime, &proxy, &alice, token.clone(), &account_id, 25.into()).await??;
        
        let result = shared_account::balance(runtime, &proxy, &account_id).await?;
        assert_eq!(result, Some(75.into()));

        // Test withdrawals
        shared_account::withdraw(runtime, &proxy, &bob, token.clone(), &account_id, 25.into()).await??;
        
        let result = shared_account::balance(runtime, &proxy, &account_id).await?;
        assert_eq!(result, Some(50.into()));

        // Test error handling
        let result = shared_account::withdraw(runtime, &proxy, &claire, token.clone(), &account_id, 1.into()).await?;
        assert_eq!(result, Err(Error::Message("unauthorized".to_string())));

        Ok(())
    }
}
Key points:
  • Uses #[testlib::test] with auto-injected runtime
  • Publishes all three contracts (proxy, token, shared-account)
  • Configures proxy to point to shared-account
  • Calls shared-account functions through the proxy address
  • The proxy’s fallback function automatically forwards calls