SDK Background

What Is The Abstract Platform?

The Abstract platform provides a combination of CosmWasm-oriented products.

Our products are designed to be composable, allowing developers to re-use the components they need to build their applications. While Abstract aims to simplify the development experience, it functions as a powerful tool, enabling you to innovate with less effort.

Info

In this page you are introduced to the Abstract platform and its components. You can skip the introduction and go straight to the Getting Started guide if you’re already familiar with the platform.

The Abstract SDK

The Abstract SDK is a modular smart-contract framework designed to enhance the development of decentralized applications. The SDK is built on top of CosmWasm: a battle-tested WASM-based smart-contract framework, written in Rust. The Abstract SDK is comprised of two main components:

Abstract Accounts

An Abstract Account is a smart-contract wallet developed by Abstract. That means that the smart-contract is capable of holding tokens and interacting with other smart-contracts. The contract’s ownership structure is customizable to fit the needs of users or applications. It is designed to be highly programmable, allowing developers to build and distribute complex applications on top of it.

Abstract Apps

Abstract Apps are smart-contracts that add functionality to an Abstract Account. Here’s a small snippet of code to give you an idea of how an App is created with the Abstract SDK:

#![allow(unused)]
fn main() {
pub const COUNTER_APP: CounterApp = CounterApp::new(COUNTER_ID, APP_VERSION, None)
    .with_instantiate(handlers::instantiate)
    .with_execute(handlers::execute)
    .with_query(handlers::query)
    .with_sudo(handlers::sudo)
    .with_receive(handlers::receive)
    .with_replies(&[(1u64, handlers::reply)])
    .with_migrate(handlers::migrate);
}

The code above defines an Abstract App. This app can be installed on any Abstract Account through the Abstract App store, allowing developers to monetize their code.

The customizable handlers that are used in the builder are functions similar to the native CosmWasm entry-point functions. They expose an additional App object which, via the abstract-sdk, empowers you to execute intricate multi-contract transactions with minimum code. Importantly, this simplification does not limit the contract’s programmability. Instead, it provides a balance of efficient coding and comprehensive control over inter-contract interactions.

Info

Already familiar with cw-orchestrator? Skip to the SDK’s introduction page.

Cw-Orchestrator

cw-orchestrator is a smart-contract scripting library that simplifies smart-contract interactions. It allows you to re-use code between testing and deployments and acts as our primary tool in making Abstract’s infrastructure highly available.

Here’s a snippet that sets up the complete Abstract SDK framework on a cw-multi-test environment, and deploys the previously shown App contract to the framework.

#![allow(unused)]
fn main() {
// Create a sender and instantiate the mock environment
let sender = Addr::unchecked("sender");
let mock = Mock::new(&sender);

// Construct the counter interface (a wrapper around the contract's entry points)
let contract = CounterApp::new(COUNTER_ID, mock.clone());

// Deploy Abstract to the mock
let abstr_deployment = Abstract::deploy_on(mock, Empty{})?;

// Create a new account to install the app onto
let account =
    abstr_deployment
        .account_factory
        .create_default_account(GovernanceDetails::Monarchy {
            monarch: sender.to_string(),
        })?;

// Claim the namespace so app can be deployed
abstr_deployment
    .version_control
    .claim_namespace(1, "my-namespace".to_string())?;

// Deploy the app!
contract.deploy(APP_VERSION.parse()?)?;
}

Using cw-orchestrator for your smart-contract interactions reduces your testing/deployment overhead and improves both the code’s readability and maintainability.

Abstract.js

Abstract.js is the Javascript package for interacting with the on-chain Abstract framework. More documentation will be added soon.

Abstract SDK - Overview

The Abstract SDK is at the core of the Abstract development platform. It’s a modular smart-contract framework designed to simplify and accelerate the development of CosmWasm dApps. It does this by prioritizing re-usability and composability through an account-abstraction oriented architecture.

From a high-level perspective, smart-contracts built with abstract can use on-chain dependencies (other smart-contracts) to isolate specific functionalities. In this way, a smart-contract built with the Abstract SDK can explicitly define its dependencies and use them to perform complex multi-contract interactions with very minimal code. This, in turn, allows you to focus on the novel functionality of your application without inheriting the complexity of the underlying infrastructure.

Visual Example

Visually this can be represented as an application, for example an Autocompounder, that has a dependency on one or multiple other smart-contracts. In this case a Dex and Staking contract.

flowchart LR
    subgraph Autocompounder Application
        direction BT
        Autocompounder-.->Dex
        Autocompounder-.->Staking
        Staking-->Account
        Autocompounder-->Account
        Dex-->Account
    end

    User[fa:fa-users Users]==>Autocompounder

Each solid-line arrow signifies execution permissions of the contract on the account. These permissions allow the contract to move funds, interact with other contracts through the account, and perform other actions. It does this by sending messages to the account, which then executes them on behalf of the contract. This is the basic idea behind account abstraction and is further elaborated in on the account abstraction page.

Each dotted arrow indicates a dependency between the contracts. These dependencies are explicitly defined in the contract and are asserted at contract instantiation. In this example the autocompounder contract is able to access specific functionality (like swapping or staking assets) from its dependencies (the dex and staking contract). Through this mechanism, a major reduction in the amount of code and its complexity is achieved, as otherwise every dex or yield provider would have to be integrated with the autocompounder contract itself.

From a developer ecosystem standpoint this encourages collaboration and cross-team code re-use, a practice that has been proven to accelerate development and increase developers’ productivity.
As the saying goes, a rising tide lifts all boats.

The Actor Model

Info

Already a CosmWasm expert? Jump to the next page to learn about account abstraction!

The actor model is a computational model used in the design and implementation of the CosmWasm framework. It provides a deterministic execution environment by employing message-based communication between individual actors. In this model, each actor is an autonomous entity capable of managing its internal state, creating other actors (other smart-contracts), and send and receive structured messages.

The actor model plays a crucial role in designing the architecture of a smart-contract application within the CosmWasm framework.

Benefits of the Actor Model

The application of the actor model in the CosmWasm framework provides the following benefits:

1. Encapsulation and Isolation

Each smart contract operates as an isolated actor, processing messages and state independently. This isolation prevents interference and unintended side effects between contracts, enhancing security and reducing the risk of vulnerabilities like re-entrancy attacks.

2. Sequential Message Processing

Contracts handle only one message at a time, ensuring deterministic execution and eliminating the need for call stacks. This approach prevents complex control flow issues and helps maintain the predictability of the contract’s state. It also simplifies the mental model required to understand and reason about execution order in multi-contract interactions.

3. Controlled Interactions

When a contract wants to invoke another contract, it sends a message containing the necessary information. This controlled interaction allows for well-defined communication patterns, promoting modularity, and ensuring that contracts can safely collaborate without exposing their internal states.

4. Error Handling and Atomic Transactions

A transaction in CosmWasm can represent a state-transition that involves multiple sub-messages and spans multiple actors. If any sub-message on any actor encounters an error, the entire transaction is rolled back, reverting all state changes made within that transaction. This ensures atomicity and prevents inconsistent states in the contract

Account Abstraction

Account abstraction is a new concept that is making headlines on blockchain and smart-contract platforms. It’s a popular subject because it is designed to streamline how users interact with decentralized applications (dApps). The fundamental idea is to abstract away the complexities of blockchain interactions and provide a user-friendly, secure interface for using and managing applications.

In traditional blockchain interactions, a transaction is typically initiated by a users directly signing some data with their private key and transmitting that to the blockchain for validation. Account abstraction simplifies this process by making the transaction initiation and validation programmable. Essentially, it allows the transaction logic to be customized within a smart-contract, vastly extending the scope of UX possibilities.

Info

See EIP-4337 to read about account abstraction in the Ethereum ecosystem.

This concept of account abstraction, when implemented correctly, can provide numerous benefits:

  1. Improved User Experience: Users can interact with smart contracts more seamlessly, without worrying about the underlying blockchain complexities. The verification model can be tailored to feel like familiar web2 experiences.
  2. Enhanced Security: By shifting validation logic to smart contracts, a variety of security checks can be implemented to guard against unauthorized transactions. This could include multi-factor authentication, whitelisting, and more.
  3. Reliable Fee Payment: Account abstraction can enable smart contracts to pay for gas, thereby relieving end-users from managing volatile gas prices or even paying for gas at all.

In the following sections, we’ll discuss how Abstract utilizes the concept of account abstraction, ensuring modularity, security, and scalability in applications built using the Abstract SDK.

Account Abstraction on Abstract

Within Abstract, account abstraction manifests itself in the Abstract Accounts or smart contract wallets, which are designed to offer an easy-to-use and secure interaction model for users. You can read more about their architecture in the next section.

Abstract Account Architecture

Introduction

Abstract’s account architecture is skillfully designed, merging modularity, scalability, and security. This architectural design is anchored by the ideas of account abstraction. For a detailed exploration of account abstraction, read the preceding chapter. In the upcoming sections, we will delve deeper into the architecture of Abstract Accounts, providing insights into its design principles and components.

Abstract Account

The Abstract SDK provides users with a sovereign smart-contract wallet. We call this smart-contract wallet an Abstract Account. The account’s architecture has two primary components (smart-contracts): the Manager contract and the Proxy contract.

flowchart LR
    subgraph Abstr[Abstract Account]
        direction TB
        Manager-->Proxy
    end

    Owner -.-> Manager

The owner of the account can configure the account by sending messages to the manager contract. We don’t make any assumptions about the nature of this owner, it can be a wallet, multi-sig or any other ownership structure, allowing you to customize your account’s ownership to fit your needs.

Info

You can read up on the different ownership structures in our Ownership section.

The account’s architecture centers around configurable programmability. I.e. how can one configure the account (install applications, set permissions, etc.) to enable users and developers to easily configure/program it to do what they want? Let’s see how the account architecture achieves this.

Manager Contract

The Manager serves as the orchestrator of the Abstract Account. It is responsible for various important operations, including:

  • Owner Authentication: Authenticating privileged calls and ensuring only approved entities can interact with the account.
  • Application Management: Managing and storing information about the applications installed on the account, their inter-dependencies, permissions and configurations.
  • Account Details: Storing the account’s details, such as its name, description, and other relevant information.

The Manager is responsible for the account’s configuration and security.

Proxy Contract

The Proxy serves as the asset vault of the Account, taking care of:

  • Asset Management & Pricing: Holding the account’s assets, including tokens, NFTs, and other fungible and non-fungible assets as well as allows for pricing assets based on decentralized exchange or oracle prices.
  • Transaction Forwarding (Proxying): Routing approved transactions from the Manager or other connected smart-contracts to other actors.

The Proxy is responsible for the account’s programmability and assets management.

Account Interactions

The diagram below depicts a User interacting with their account through the Manager, and proxying a call to an external contract through the Proxy.

sequenceDiagram
    actor User
    participant Manager
    participant Proxy
    participant External Contract


    User->>Manager: Account Action
    Manager->>Proxy: Forward to Proxy
    Proxy->>External Contract: Execute
  

Info

Not interested in account ownership? Skip to our section on Modularity in Abstract.

Account Ownership

Governance structures are a wildly under-developed field in the realm of smart contract technology. Abstract allows for any custom governance type to be used with its chain-agnostic framework. While most users appreciate an easy-to-use interface to control their dApps, Abstract opts to provide two fully integrated governance choices (token-based and DaoDao integration coming soon) that ensure a seamless user experience.

When setting up governance for your dApp, you will be prompted to choose between supported governance types:

Monarchy

In a monarchy, a single wallet has full control over the dApp. If you’re connected with a wallet, your address will be automatically inserted as the root user. Visit our documentation to learn more about dApp ownership and sovereignty.

Multi-signature

Multi-signature (“multisig”) governance is a governance structure that requires a subset of its members to approve an action before it can be executed. Though multiple implementations exist, Abstract uses a cw-3 multisig contract with the goal of providing the most flexible solution to users.

Knowledge about a few terms is required to configure your multisig:

Voter weight: The weight that the voter has when voting on a proposal.

Threshold: The minimal % of the total weight that needs to vote YES on a proposal for it to pass.

Example: Suppose you are building a DeFi platform using Abstract and want to implement multisig governance. You have five stakeholders, and you want at least 60% of the total voting weight to approve a proposal for it to pass.

  1. Set up the multisig module in your dApp.

  2. Assign voter weights to each of the five stakeholders. For instance, A: 30%, B: 20%, C: 20%, D: 15%, and E: 15%.

  3. Configure the multisig module with a 60% threshold.

With this configuration, any proposal will require approval from stakeholders with a combined voting weight of at least 60% to be executed. This ensures a more democratic decision-making process and reduces the risk of a single stakeholder making unilateral decisions.

Modularity in Abstract

Modularity is a fundamental design principle of the Abstract platform, contributing significantly to its flexibility and extensibility. A module in Abstract is a self-contained unit of functionality that can be added to an Abstract Account to extend its capabilities. This modular approach promotes the construction of bespoke applications, enhances the scalability of the system, and improves overall maintainability.

For application developers, modules simplify the development process by offering pre-built functionalities. Instead of building every aspect from scratch, developers can leverage modules either from Abstract’s extensive library or those crafted by other developers available in the module marketplace. Additionally, developers have the freedom to create custom modules using the Abstract SDK, tailoring them to specific application needs and even contributing back to the community.

Info

Skip to Modules to learn what the kinds of modules are and why they’re relevant to your Account

How Modules Work

In Abstract, the process of integrating these modules is managed through the Manager contract within an Abstract Account. The Manager keeps track of all installed modules, managing their permissions and interactions. This system enables the customization of each Account, as modules can be installed or uninstalled as per the user’s requirements, thereby adjusting the Account’s functionality.

From the perspective of a developer, the Abstract framework sets conventions and standards that allow leveraging existing modules during the development of new ones. This layer of abstraction saves considerable time and effort while promoting consistent design across different modules.

Security

Security is a priority at Abstract, especially when it comes to the modules that extend the capabilities of an Abstract Account. Every module listed on the mainnet marketplaces must undergo a thorough auditing process before it’s made available to users. This process scrutinizes the module’s code, checking for potential vulnerabilities, and ensuring that it adheres to best security practices.

While no system can guarantee absolute security, this rigorous vetting process, coupled with the inherent security measures in Abstract and CosmWasm’s architecture, mitigates potential risks to a considerable extent.

The Abstract platform also maintains a Version Control (TODO link) for all the modules, allowing users and developers to track changes, understand the evolution of a module, and choose versions that have passed security audits.

Module Types

Within Abstract, a module is a contract that adds functionality to your Account. You can explore all the available modules on the modules tab of your Account through the web-app.

Info

In the previous sections we referred to these modules as “applications”. We did this to simplify the mental framework of the Abstract SDK. However, the term “application” is not entirely accurate, instead we should refer to them as “modules”. These modules come in different types, each with their own purpose. The most common of which is the “App” module, or “Abstract App”.

Modules are classified in the following categories:

  • App: modules that add an additional functionality, exposing new entry-points for you or your users
  • Adapter: modules that act as a standard interface between your Account and external services
  • Standalone: modules not built within Abstract, but registered to your account so that the manager can execute commands on them

Adapters

Adapters serve as standard interfaces that facilitate communication between your Abstract Account and various external services. They act like bridges, enabling your account to interact with different smart-contract and blockchain services, thereby enhancing the interoperability of your applications.

The key function of an Adapter is to generalize functionality. Regardless of the underlying blockchain or smart contract protocol, the Adapter provides a standard interface that maintains consistency and simplifies the interaction process. As such, Adapters significantly simplify the developer experience and reduce the time required to integrate with various external systems.

Unlike other modules specific to each Abstract Account, Adapters are “global” in nature. This means that they are shared between multiple accounts. Due to this, Adapter modules are not migratable. This design choice is aimed at preventing supply-chain attacks that could potentially compromise the security of the Abstract ecosystem.

While individual Abstract Account owners can decide which Adapters and versions they wish to utilize, the overall control and maintenance of Adapters are handled at a platform level. This approach ensures that Adapters remain reliable, secure, and consistent across all Accounts.

The abstract:dex module allows Accounts to access standard functions on dexes with the same interface, regardless of whether they’re local to the chain or across IBC.

Apps

An App module adds additional functionality to your Abstract Account, exposing new entry-points for you or your users. This could range from adding advanced financial logic, data management features, or complex computation capabilities, depending on your application’s needs.

Each App module is exclusive to a single Abstract Account, meaning the instance is owned by the Account owner, ensuring the owner has full control over the module’s functionality and lifecycle. This level of control extends to the management of upgrades, maintenance, and any customization that might be required for the specific use case of the application.

Because each Account has its own instance, App modules can be tightly integrated with the Account’s existing infrastructure. This includes the ability to interact directly with other modules within the same account, enabling powerful synergies and cross-module functionality.

The abstract:etf module is an app that exposes entry-points allowing external users to buy and sell “shares” in your Account, representing a portion of the Accounts’ value.

flowchart LR
	subgraph Accounts[" "]
		direction BT
		subgraph Acc1["Account 1"]
		    App1["abstract:etf"]
		end
		subgraph Acc2["Account 2"]
			App2["abstract:etf"]
		end
  end


  subgraph Adapters
	  Acc1-->Adapter1{{"abstract:dex"}}
	  Acc2-->Adapter1
  end
  
  Adapter1-->dex1([Osmosis])
  Adapter1-->dex2([Wyndex])
  Adapter1-->dex3([Astroport])

Two Accounts with the abstract:etf and abstract:dex modules installed
------

Module Upgradability

Smart-contract migrations are a highly-debated feature in smart-contract development. Nonetheless Abstract believes it to be a powerful feature that allows for fast product iteration. In the spirit of crypto we’ve designed a system that allows for permissionless software upgrades while maintaining trustlessness.

Module version storage

Permissionless software upgradeability is provided by a module version storage in the version control contract. The mapping allows your Account to:

  • Instantiate a module of the latest versions.
  • Upgrade a module to a new version as soon as it’s available.
  • Provide custom modules to other users.
  • Do all this without any third-party permissions.

There are two types of possible migration paths, although they appear the same to a user.

Migration Update

Most module updates will perform a contract migration. The migration can be evoked by the root user and is executed by the manager contract. You can learn more about contract migrations in the CosmWasm documentation.

Move Updates

In we outlined the reasoning behind our module classification system. More specifically we outlined why the API modules should not be migratable because it would remove the trustlessness of the system.

Therefore, if we still want to allow for upgradeable API’s we need instantiate each API version on a different address. When a user decides to upgrade an API module, the abstract infrastructure moves his API configuration to the new addresses and removes the permissions of the old API contract.

Crucially, all other modules that depend on this API don’t have to change any stored addresses as module address resolution is performed through the manager contract, similar to how DNS works!

Platform Details

Abstract Name Service

The Abstract Name Service (or ANS in short) is an on-chain store of the most important address space related data of the blockchain it is deployed on. In Abstract it allows for chain-agnostic action execution and dynamic address resolution. These features allow both users and developers to interact with the blockchain in a more user-friendly way.

ANS Architecture

The ANS is a smart contract that stores the following data:

  • Assets: The most relevant assets on the local blockchain.
  • Contracts: Contracts related to certain protocols or applications that could be dynamically resolved. This could be used to store the address for an asset-pair for a dex. I.e. “osmosis/juno,osmo” could be resolved to the address of the osmosis pool that allows you to swap osmo for juno.
  • Channels: IBC channel data to map a protocol + destination chain to a channel id. This allows for dynamic IBC transfers without having to know the channel id beforehand.

The ANS contract state layout is defined here. It consists of key-value mappings for the different entries.

#![allow(unused)]
fn main() {
    /// Stores name and address of tokens and pairs
    /// LP token pairs are stored alphabetically
    pub const ASSET_ADDRESSES: Map<&AssetEntry, AssetInfo> = Map::new("assets");
    pub const REV_ASSET_ADDRESSES: Map<&AssetInfo, AssetEntry> = Map::new("rev_assets");

    /// Stores contract addresses
    pub const CONTRACT_ADDRESSES: Map<&ContractEntry, Addr> = Map::new("contracts");

    /// stores channel-ids
    pub const CHANNELS: Map<&ChannelEntry, String> = Map::new("channels");

    /// Stores the registered dex names
    pub const REGISTERED_DEXES: Item<Vec<DexName>> = Item::new("registered_dexes");

    /// Stores the asset pairing entries to their pool ids
    /// (asset1, asset2, dex_name) -> {id: uniqueId, pool_id: poolId}
    pub const ASSET_PAIRINGS: Map<&DexAssetPairing, Vec<PoolReference>> = Map::new("pool_ids");

    /// Stores the metadata for the pools using the unique pool id as the key
    pub const POOL_METADATA: Map<UniquePoolId, PoolMetadata> = Map::new("pools");
}

Resolving Entries

This data is a nice-to-have but calling CosmWasm smart queries on the memory contract would invariably clutter your code and substantially increase gas-consumption. Therefore we provide three ways to easily and reliably perform low-gas queries on the memory contract.

Resolving your asset/contract name to its matching value is much like resolving a domain name like abstract.money to its IP address (172.67.163.181).

There are two ways to resolve your entry into its matching value.

Both App and Adapter objects implement the AbstractNameService trait which allows you to resolve entries.

let juno_name = AssetEntry::new("juno");
let juno_asset_info = module.name_service(deps).query(&juno_name)?;

Resolve Trait

Entries that are resolvable by the Abstract Name Service implement the Resolve trait which gives them the ability to be resolved by ANS explicitly.

let ans_host = module.ans_host(deps)?;
let juno_name = AssetEntry::new("juno");
let juno_asset_info = juno_name.resolve(&deps.querier, &ans_host)?;

AnsHost Object

You can also load or create an AnsHost struct. This struct is a simple wrapper around an Addr and implements methods that perform raw queries on the wrapped address.

let ans_host = AnsHost {address: "juno1...."};
let juno_name = AssetEntry::new("juno");
let juno_asset_info = ans_host.query_asset(deps, &juno_name)?;

Version Control

The Version Control contract acts as the registry for all modules and accounts within the Abstract platform. Each module can be queried by its namespace, name, and version, returning its reference which may be a code id or address.

Account Factory

Create Account

Below you can find the internal flow when the user requests that an account be created.

sequenceDiagram
  autonumber
  actor U as User
  participant F as Account Factory
  participant VC as Version Control
  participant M as New Manager
  participant P as New Proxy

  U->>F: CreateAccount
  F-->>+VC: Query for Manager reference
  VC-->>-F: Manager code_id
  

  F-x+M: Instantiate Manager
  Note over VC: Reply
  M-->>-F: Manager Address
  F->F: Store manager address


  F-->>+VC: Query for Proxy reference
  VC-->>-F: Proxy code_id
  F-x+P: Instantiate Proxy
    Note over VC: Reply
  P-->>-F: Proxy Address
  

  F->>VC: Register Account
  F->>M: Register proxy address
  F->>P: Allowlist Manager address
  F->>P: Set owner to Manager
  F->>M: Set migrate admin to Self


Install Module

sequenceDiagram
  autonumber
  actor U as User
  participant M as Manager
  participant MF as Module Factory
  participant VC as Version Control
  participant P as Proxy

  U->>M: InstallModule
  M->>MF: InstallModule
  MF-->>+VC: Query reference
  alt adapter
    VC-->>+MF: Return address
  else app / standalone
    VC-->>-MF: Return code_id
    MF->MF: Instantiate module
  end
  MF->>M: Register module address
  
  M->>P: Update module allowlist

Execute On Module

Non-dependent Execution

sequenceDiagram
  autonumber
  actor U as User
  participant M as Manager
  participant Md as Module

  U->>M: ExecOnModule
  Note right of U: ModuleMsg
  
  M-->>M: Load module address
  M->>Md: Execute
  Note right of M: ModuleMsg

Adapter Execution

In the following example, the abstract:dex module is installed on an Account, and the user requests a swap on a dex.

sequenceDiagram
  autonumber
  actor U as User
  participant M as Manager
  participant D as abstract:dex
  participant VC as Version Control
  participant A as ANS
  participant P as Proxy
  participant T as Dex Pool

  U->>M: ExecOnModule
  Note right of U: Dex::Swap
  M-->M: Load module address
  M->>D: Call module
  Note right of M: Adapter Msg
  D-->+VC: Load proxy address for Account
  VC-->-D: Address

  D-->>+A: Resolve asset names
  A-->>D: Asset infos
  D-->A: Resolve dex pool
  A-->>-D: Pool metadata
  D-->D: Build swap msg for target dex
  
  D->>P: Forward execution
  Note over VC,A: DexMsg
  P->>T: Execute
  Note right of P: DexMsg

Module-dependent Execution

In this example, we use Equilibrium’s Rebalance function as an example. Modules with dependencies (equilibrium:balancer is dependent on abstract:etf and abstract:dex) have their addresses dynamically resolved when called.

sequenceDiagram
  autonumber
  actor U as User
  participant B as equilibrium:balancer
  participant P as Proxy
  participant M as Manager
  participant D as abstract:dex
  participant T as Target Dex

  U->>B: Rebalance
  B-->>+P: Query Allocations
  P-->>-B: Allocations
  B-->B: Calculate rebalancing requirements

  B-->>+M: Query abstract:dex address
  M-->>-B: Address

  B->>D: Call SwapRouter on dex
  D-->D: Build swap msg for target dex
  D-->D: Load proxy address
  
  D->>P: Forward execution
  Note over M: DexMsg
  P->>T: Execute
  Note over D,M: DexMsg

Update Settings

Enable IBC

Enabling the IBC functionality of your account will register the IBC client to your account, enabling your modules to execute cross-chain commands.

sequenceDiagram
  autonumber
  actor U as User
  participant M as Manager
  participant VC as Version Control
  participant P as Proxy

  U->>M: UpdateSettings
  Note right of U: ibc_enabled
    M-->>+VC: Query IBC Client reference
    VC-->>-M: Return IBC Client address
    M->>M: Register IBC Client
    M->>P: Add IBC client to allowlist

For disabling IBC, see Uninstall Module

Upgrade Modules

One of the key strengths of Abstract is that it is designed to minimize your maintenance workload while maximizing the control you have over your infrastructure.

Abstract manages module upgrades for you, ensuring your infrastructure remains intact and your workflows continue to function smoothly through every upgrade. This process is carried out in a manner that consistently maintains the integrity and security of your system.

sequenceDiagram
  autonumber
  actor U as User
  participant M as Manager
  participant VC as Version Control
  participant P as Proxy

  U->>M: Upgrade
  loop for each module
    M-->>VC: Query reference
    alt adapter
        VC-->>M: Return address
        M->>M: Update module address
        M->>+P: Remove old adapter from allowlist
        M->>P: Add new adapter to allowlist
        deactivate P
    else app / standalone
        VC-->>M: Return code_id
        M->>M: Migrate module to new code_id
    end
  end

  alt
    M->M: Migrate self
   end
  M->M: Update dependencies
  M-->M: Check dependencies  

Expanding The Smart-Contract Design Space

If you are well-versed with smart-contracts, you will find that the Abstract SDK is a tool that gives you more leverage. Enabling you to get things done faster and better. It does this by giving you an extensive design space that transcends traditional smart-contract capabilities, introducing superior code reusability and an unmatched software distribution process.

Designing with Abstract

The Abstract SDK broadens your design space beyond traditional smart-contract application architectures. While applications built with stand-alone smart contracts can also be crafted using the SDK, the Abstract SDK promotes a level of code reusability incomparable to stand-alone smart-contract development. It is through this code reusability that novel applications can be constructed with little effort.

Design Spaces Explained

Traditional: Hosted Applications

Traditionally applications have been created by composing “stand-alone” smart contracts. With each smart-contract designed to fulfill a different role in the application’s logic. We call these applications hosted applications since they’re deployed and controlled by the code maintainers, and to use them, users transfer funds to the application’s smart contract. Dexes, lending markets, yield aggregators are all examples of hosted applications.

flowchart LR
    subgraph Developer Team [fas:fa-users-cog Developer Team]
        %% subgraph Application
            direction BT
            A[Application]
        %% end
    end

    User[fa:fa-users Users]==>A

Building a Hosted Auto-Compounder

Hosted applications can be built more efficiently with the Abstract SDK because of it’s modular design. As an example, let’s consider an auto-compounder application. The auto-compounder provides liquidity to DEX trading pairs and re-invests the received rewards into the pairs. The application’s logic can be split into three modules:

  • DEX Adapter: Provides an interface to perform DEX operations. (e.g., swap tokens, provide liquidity, etc.)
  • Staking Adapter: Provides an interface to perform staking operations. (e.g., claim rewards, stake, unstake, etc.)
  • Auto-Compounder: Orchestrates the DEX and staking adapters to perform the auto-compounding logic.

If we visualize this application, we can see that the DEX and staking adapters are reusable components that can be used in other applications. The auto-compounder, in this approach, is a unique application that can be installed on an account and used by users to deposit into and withdraw from the auto-compounder application. The account essentially acts as a vault that holds all the users’ funds.

flowchart LR
    subgraph Autocompounder Application
        direction BT
        Autocompounder-.->Dex
        Autocompounder-.->Staking
        Staking-->Account
        Autocompounder-->Account
        Dex-->Account
    end
    
    User[fa:fa-users Users]==>Autocompounder

This approach offers two significant benefits:

  • Code Reusability: Developers can reuse the DEX and staking adapters in other applications. Furthermore Abstract already provides a library of adapters for the most popular protocols. This saves you both time and money as you don’t need to write the integrations yourself.
  • Security: The auto-compounder application’s logic is reduced to it’s bare minimum, making it easier to audit and maintain. Furthermore, the DEX and staking adapters are battle-tested smart-contracts, which further reduces the attack surface.

Innovative: Self-Hosted Applications

Self-hosted applications, on the other hand, represent a novel concept only achievable with the Abstract SDK. Here, users own their applications and don’t need to transfer funds to the application’s smart contract. Instead, they deploy the smart contract to their account, which grants the application rights to access those funds. Each application is a new instantiation of a smart-contract that is owned and configurable by the user. The user can thus update the application’s code, parameters, and permissions at any time, without relying on the application’s maintainers.

flowchart LR
    subgraph Developers [fas:fa-users-cog Developers]
            direction RL
            A[App]
    end

    subgraph Acc1 [fas:fa-user User's Account]
        direction TB
        Ap1[App]-->A1[Account]
    end

    subgraph Acc2 [fas:fa-user User's Account]
        direction TB
        Ap2[App]-->A2[Account]
    end

    subgraph Acc3 [fas:fa-user User's Account]
        direction TB
        Ap3[App]-->A3[Account]
    end

        Store-.->Ap1
        Store-.->Ap2
        Store-.->Ap3

    A==>Store[fa:fa-store App Store]

This approach offers two significant benefits:

  • Sovereignty: Users have more control over their funds as they don’t need to trust application maintainers.
  • Customizability: Users can tailor their application, leading to novel customization options unavailable with hosted applications.

Let’s see how this applies to the auto-compounder application from before:

Building a Self-Hosted Auto-Compounder

The auto-compounder application can easily be converted into a self-hosted application. Again, by self-hosted we mean that instead of users moving their funds to an externally owned account, they deploy the auto-compounder application to their own account. The auto-compounder application is now owned by the user and can be configured to their liking.

flowchart BT
    subgraph Alex[Alex's Account]
        direction TB
        A1[Autocompounder]-.->D1[Dex]
        A1[Autocompounder]-.->S1[Staking]
        S1[Staking]-->C1[Account]
        A1[Autocompounder]-->C1[Account]
        D1[Dex]-->C1[Account]
    end

    subgraph Sarah[Sarah's Account]
        direction TB
        A2[Autocompounder]-.->D2[Dex]
        A2[Autocompounder]-.->S2[Staking]
        S2[Staking]-->C2[Account]
        A2[Autocompounder]-->C2[Account]
        D2[Dex]-->C2[Account]
    end

    AppStore[fa:fa-store App Store]==>A1
    AppStore[fa:fa-store App Store]==>A2

With this setup Alex and Sarah can both use the auto-compounder application, but they can configure it to their liking. For example, Alex can configure the auto-compounder to compound his rewards every 24 hours, while Sarah can configure the auto-compounder to compound her rewards every 12 hours. This approach allows for a very customizable and personalized experience.

Abstract SDK - How to get started

This tutorial will walk you through the process of setting up your development environment, creating an app module, and deploying your first app to our on-chain store with cw-orchestator.

If you want to learn more about Abstract before writing code you can check out the About Abstract section of our documentation.

Prerequisites

  1. A minimal understanding of Rust is expected. If you are new to Rust, you can find a great introduction to the language in the The Rust Book.

  2. The Abstract SDK is built using the CosmWasm smart-contract framework. If you are new to CosmWasm, you can find a great introduction to the framework in the CosmWasm Book.

  3. Abstract also makes extensive use of cw-orchestrator, our CosmWasm scripting library. You can read its documentation here.

Setting up the environment

Before you get started with the Abstract SDK, you will need to set up your development environment. This guide will walk you through the process of setting up your environment and creating your first Abstract app module.

Rust

To work with the SDK you will need the Rust programming language installed on your machine. If you don’t have it installed, you can find installation instructions on the Rust website.

WASM

Additionally, you will need the Wasm compiler installed to build WASM binaries. To install it, run:

$ rustup target add wasm32-unknown-unknown
> installing wasm32-unknown-unknown

Git

You will also need git installed to clone our template repository. You can find instructions how to do so here.

  • Rust Analyzer: Rust Analyzer is a language server that provides IDE support for Rust. If you use VS-Code it’s highly recommended.
  • Docker: Our testing infrastructure uses Docker to run the tests. If you want to run the tests locally, you will need to install Docker.
  • Just: Just is a command runner that we use to improve the development flow. You can install it by following the instructions.

Using the Template

Time to start building! We’ll get you set up with the Abstract App template which contains:

  • A scaffold app module with:
    • A basic contract
    • cw-orchestrator interface and deployment script
    • Integration tests
  • A scaffold front-end with:
    • A basic UI
    • A basic API client
  • A set of just commands that will boost your productivity.

Go to our App Template on Github and click on the “Use this template” button to create a new repository based on the template. You can name the repository whatever you want, but we recommend using the name of your module.

Go ahead and read through the readme of the template repository. It contains instructions on how to set up your development environment and how the template is structured.

Contract layout

The template contains a scaffold contract that you can use as a starting point for your own contract. The contract is located in the src directory and is structured as follows:

  • contract.rs: This file is the top-level file for your module. It contains the type definition of you module and the const builder that constructs your contract. It also contains a macro that exports your contract’s entry points. You can also specify the contract’s dependencies here.
  • error.rs: This file contains the error types that your contract can return.
  • msg.rs: This file contains the custom message types that your contract can receive. These messages also have cw-orchestrator macros attached to them which comes in useful when you are writing your integration tests.
  • state.rs: This file contains the state types that your contract will use to store state to the blockchain.
  • interface.rs: This file contains the interface that your contract will use to interact with the cw-orchestrator library.
  • replies/: This directory contains the reply handlers that your contract will use to handle replies.
  • handlers/: This directory contains the message handlers that your contract will use to handle the different messages it can receive.

Front-end layout

TODO: explain the front-end layout.

Tools used in the template

The following Rust tools are used extensively in our template to improve your productivity.

  • Taplo: The CI shipped with the template will perform formatting checks. To ensure you pass the checks, you can install Taplo and use the just format command to format your code and toml files.
  • Nextest: A better cargo test runner.
  • Cargo Limit: Prioritizes errors over warnings in compile output as well as some other small improvements.
  • Cargo Watch: Allows you to automatically re-run compilation when files change. This is useful when you are working on the contracts and want to fix compiler errors one by one.

You can install them by running just install-tools. All of the tools are built from source by Cargo.

Module Builder

Abstract provides multiple module bases, as detailed in our section on modules. These base implementation provide you with the minimal state and configuration required to start building your modular application. After setting up your module base from our template you’ll probably want to customize it. Our module builder pattern allows you to do just that. It also gives you a great overview on all the entry points to you module, and those that others have built.

Overview

The builder pattern employed in building an Abstract module is a slight variation of the actual design pattern. Instead, the module builder lets you set custom entry point handlers at compile time, meaning you end up with a const value that is heavily optimized by the compiler. This ensures that the overhead of using Abstract has a negatable effect on both the code’s runtime and WASM binary size.

Info

The code-snippets in this example can be found in the counter app example.

App Type

To compose your module, first alias the app type by inserting your custom messages. These messages are inserted in the top-level entry point message types which we will cover later on. Here’s an example:

pub type CounterApp = AppContract<
    CounterError,
    CounterInitMsg,
    CounterExecMsg,
    CounterQueryMsg,
    CounterMigrateMsg,
    CounterReceiveMsg,
    CounterSudoMsg,
>;

All of these messages can be customized and will be used to type-check the rest if your implementation.

Build The App

Now that you have your type defined you can start using the builder. To do this, first construct the base version of the app:

pub const COUNTER_APP: CounterApp = CounterApp::new(COUNTER_ID, APP_VERSION, None)

The constructor takes three variables:

  1. contract_id: The contract ID is a string that will be saved to a cw2 storage item. This ensures that the contract can not be migrated to a different contract with a different function and also acts as an informational tag for off-chain processes.
  2. contract_version: The contract version should be the version of the crate, it is also stored in the cw2 item and is checked when performing migrations and on-chain dependency resolution.
  3. metadata: An optional URL that can be used to retrieve data off-chain. Can be used with the Abstract Metadata Standard to automatically generate interactive front-end components for the module.

All these fields are used in a custom ModuleData store as well, along with the module’s dependencies, which we will come back to later. Here’s the definition of the ModuleData field:

pub const MODULE: Item<ModuleData> = Item::new("module_data");

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ModuleData {
    /// The name of the module, which should be composed of
    /// the publisher's namespace and module id. eg. `cw-plus:cw20-base`
    pub module: String,
    /// Semantic version of the module's crate on release.
    /// Is used for migration assertions
    pub version: String,
    /// List of modules that this module depends on
    /// along with its version requirements.
    pub dependencies: Vec<Dependency>,
    /// URL to data that follows the Abstract metadata standard for
    /// resolving off-chain module information.
    pub metadata: Option<String>,
}

Handlers

The app can then be customized by adding whatever handler functions you need. These functions are executed whenever a specific endpoint is called on the module. A special feature about the functions is that we insert the instance of your module into the function’s attributes. This enables you to access the module struct in your code. You will learn why this is such a powerful feature in our section on the Abstract SDK.

Here’s an example of a module with some handlers set:

pub const COUNTER_APP: CounterApp = CounterApp::new(COUNTER_ID, APP_VERSION, None)
    .with_instantiate(handlers::instantiate)
    .with_execute(handlers::execute)
    .with_query(handlers::query)
    .with_sudo(handlers::sudo)
    .with_receive(handlers::receive)
    .with_replies(&[(1u64, handlers::reply)])
    .with_migrate(handlers::migrate);

These handlers are functions that allow you to customize the smart-contract’s behavior. For example, here’s a custom execute handler that updates the contract’s config state.

#![allow(unused)]
fn main() {
    pub fn execute(
        deps: DepsMut,
        _env: Env,
        info: MessageInfo,
        app: CounterApp, // <-- Notice how the `CounterApp` is available here
        msg: CounterExecMsg,
    ) -> CounterResult {
        match msg {
            CounterExecMsg::UpdateConfig {} => update_config(deps, info, app),
        }
    }

    /// Update the configuration of the app
    fn update_config(deps: DepsMut, msg_info: MessageInfo, app: CounterApp) -> CounterResult {
        // Only the admin should be able to call this
        app.admin.assert_admin(deps.as_ref(), &msg_info.sender)?;

        Ok(app
            .tag_response(Response::default(), "update_config")
            .set_data("counter_exec".as_bytes()))
    }
}

Info

You can find more application code to read in our 💥 Awesome Abstract repository 💥.

The available handlers are:

  • with_execute: Called when the App’s ExecuteMsg is called on the instantiate entry point.
  • with_instantiate: Called when the App’s InstantiateMsg is called on the instantiate entry point.
  • with_query: Called when the App’s QueryMsg::Module is called on the query entry point.
  • with_migrate: Called when the App’s MigrateMsg is called on the migrate entry point.
  • with_replies: Called when the App’s reply entry point is called. Matches the function’s associated reply-id.
  • with_sudo: Called when the App’s SudoMsg is called on the sudo entry point.
  • with_receive: Called when the App’s ExecuteMsg::Receive variant is called on the execute entry point.
  • with_ibc_callbacks: Called when the App’s ExecuteMsg::IbcCallback is called on the execute entry point. Matches the callback’s callback ID to its associated function.

Below we detail each one more closely. The base fields and variants mentioned in the messages below are defined by the base module type that you chose to use. In this page we’re working with an App.

Instantiate

The instantiate entry point is a mutable entry point of the contract that can only be called on contract instantiation. Instantiation of a contract is essentially the association of a public address to a contract’s state.

Function Signature

Expected function signature for the custom instantiate handler:

/// Function signature for an instantiate handler.
pub type InstantiateHandlerFn<Module, CustomInitMsg, Error> =
    fn(DepsMut, Env, MessageInfo, Module, CustomInitMsg) -> Result<Response, Error>;

Message

In order to instantiate an Abstract Module, you need to provide an InstantiateMsg with the following structure:

#[cosmwasm_schema::cw_serde]
pub struct InstantiateMsg<BaseMsg, CustomInitMsg = Empty> {
    /// base instantiate information
    pub base: BaseMsg,
    /// custom instantiate msg
    pub module: CustomInitMsg,
}

When the module’s instantiate function is called the struct’s module field is passed to your custom instantiation handler for you to perform any custom logic.

Execute

The execute entry point is a mutable entry point of the contract. Logic in this function can update the contract’s state and trigger state changes in other contracts by calling them. It is where the majority of your contract’s logic will reside.

Function Signature

Expected function signature for the custom execute handler:

/// Function signature for an execute handler.
pub type ExecuteHandlerFn<Module, CustomExecMsg, Error> =
    fn(DepsMut, Env, MessageInfo, Module, CustomExecMsg) -> Result<Response, Error>;

Message

Called when the App’s ExecuteMsg::Module variant is called on the execute entry point.

/// Wrapper around all possible messages that can be sent to the module.
#[cosmwasm_schema::cw_serde]
pub enum ExecuteMsg<BaseMsg, CustomExecMsg, ReceiveMsg = Empty> {
    /// A configuration message, defined by the base.
    Base(BaseMsg),
    /// An app request defined by a base consumer.
    Module(CustomExecMsg),
    /// IbcReceive to process IBC callbacks
    IbcCallback(IbcResponseMsg),
    /// Receive endpoint for CW20 / external service integrations
    Receive(ReceiveMsg),
    #[cfg(feature = "nois")]
    /// Nois receive endpoint to process nois callbacks
    NoisReceive { callback: nois::NoisCallback }
}

The content of the Module variant is passed to your custom execute handler.

Query

The query entry point is the non-mutable entry point of the contract. Like its name implies it it used to retrieve data from the contract’s state. This state retrieval can have a computation component but it can not alter the contract’s or any other state.

Function Signature

Expected function signature for the custom query handler:

/// Function signature for a query handler.
pub type QueryHandlerFn<Module, CustomQueryMsg, Error> =
    fn(Deps, Env, &Module, CustomQueryMsg) -> Result<Binary, Error>;

Message

Called when the App’s QueryMsg::Module variant is called on the query entry point.

#[cosmwasm_schema::cw_serde]
#[derive(QueryResponses)]
#[query_responses(nested)]
pub enum QueryMsg<BaseMsg, CustomQueryMsg = Empty> {
    /// A query to the base.
    Base(BaseMsg),
    /// Custom query
    Module(CustomQueryMsg),
}

The content of the Module variant is passed to your custom query handler.

Migrate

The migrate entry point is a mutable entry point that is called after a code_id change is applied to the contract. A migration in CosmWasm essentially swaps out the code that’s executed at the contract’s address while keeping the state as-is. The implementation of this function is often used to change the format of the contract’s state by loading the data as the original format and overwriting it with a new format. All adapter base implementations already perform version assertions that make it impossible to migrate to a contract with a different ID or with a version that is lesser or equal to the old version.

Function Signature

Expected function signature for the custom migrate handler:

/// Function signature for a migrate handler.
pub type MigrateHandlerFn<Module, CustomMigrateMsg, Error> =
    fn(DepsMut, Env, Module, CustomMigrateMsg) -> Result<Response, Error>;

Message

Called when the App’s migrate entry point is called. Uses the struct’s module field to customize the migration. Only this field is passed to the handler function.

#[cosmwasm_schema::cw_serde]
pub struct MigrateMsg<BaseMsg = Empty, CustomMigrateMsg = Empty> {
    /// base migrate information
    pub base: BaseMsg,
    /// custom migrate msg
    pub module: CustomMigrateMsg,
}

Reply

The reply entry point is a mutable entry point that is optionally called after a previous mutable action. It is often used by factory contracts to retrieve the contract of a newly instantiated contract. It essentially provides the ability perform callbacks on actions. A reply can be requested using CosmWasm’s SubMsg type and requires a unique ReplyId which is a u64. The customizable handler takes an array of (ReplyId, ReplyFn) tuples and matches any incoming reply on the correct ReplyId for you.

Function Signature

Expected function signature for the custom reply handler:

/// Function signature for a reply handler.
pub type ReplyHandlerFn<Module, Error> = fn(DepsMut, Env, Module, Reply) -> Result<Response, Error>;

Message

There is no customizable message associated with this entry point.

Sudo

The sudo entry point is a mutable entry point that can only be called by the chain’s governance module. I.e. any calls made to this contract should have been required to have gone through the chain’s governance process. This can vary from chain to chain.

Function Signature

Expected function signature for the custom sudo handler:

/// Function signature for a sudo handler.
pub type SudoHandlerFn<Module, CustomSudoMsg, Error> =
    fn(DepsMut, Env, Module, CustomSudoMsg) -> Result<Response, Error>;

Message

There is no base message for this entry point. Your message will be the message that the endpoint accepts.

Receive

The receive handler is a mutable entry point of the contract. It is similar to the execute handler but is specifically geared towards handling messages that expect a Receive variant in the ExecuteMsg. Examples of this include but are not limited to:

  • Cw20 send messages
  • Nois Network random number feed

Function Signature

Expected function signature for the custom receive handler:

/// Function signature for a receive handler.
pub type ReceiveHandlerFn<Module, ReceiveMsg, Error> =
    fn(DepsMut, Env, MessageInfo, Module, ReceiveMsg) -> Result<Response, Error>;

Message

Called when the App’s ExecuteMsg::Receive variant is called on the execute entry point.

/// Wrapper around all possible messages that can be sent to the module.
#[cosmwasm_schema::cw_serde]
pub enum ExecuteMsg<BaseMsg, CustomExecMsg, ReceiveMsg = Empty> {
    /// A configuration message, defined by the base.
    Base(BaseMsg),
    /// An app request defined by a base consumer.
    Module(CustomExecMsg),
    /// IbcReceive to process IBC callbacks
    IbcCallback(IbcResponseMsg),
    /// Receive endpoint for CW20 / external service integrations
    Receive(ReceiveMsg),
    #[cfg(feature = "nois")]
    /// Nois receive endpoint to process nois callbacks
    NoisReceive { callback: nois::NoisCallback }
}

Ibc Callback

The ibc callback handler is a mutable entry point of the contract. It is similar to the execute handler but is specifically geared towards handling callbacks from IBC actions. Since interacting with IBC is an asynchronous process we aim to provide you with the means to easily work with IBC. Our SDK helps you send IBC messages while this handler helps you execute logic whenever the IBC action succeeds or fails. Our framework does this by optionally allowing you to add callback information to any IBC action. A callback requires a unique CallbackId which is a String. The callback handler takes an array of (CallbackId, IbcCallbackFn) tuples and matches any incoming callback on the correct CallbackId for you. Every call to this handler is verified by asserting that the caller is the framework’s IBC-Client contract.

Function Signature

/// Function signature for an IBC callback handler.
pub type IbcCallbackHandlerFn<Module, Error> =
    fn(DepsMut, Env, MessageInfo, Module, CallbackId, StdAck) -> Result<Response, Error>;

Message

Called when the App’s ExecuteMsg::IbcCallback variant is called on the execute entry point. The receiving type is not customizable but contains the IBC action acknowledgment.

/// Wrapper around all possible messages that can be sent to the module.
#[cosmwasm_schema::cw_serde]
pub enum ExecuteMsg<BaseMsg, CustomExecMsg, ReceiveMsg = Empty> {
    /// A configuration message, defined by the base.
    Base(BaseMsg),
    /// An app request defined by a base consumer.
    Module(CustomExecMsg),
    /// IbcReceive to process IBC callbacks
    IbcCallback(IbcResponseMsg),
    /// Receive endpoint for CW20 / external service integrations
    Receive(ReceiveMsg),
    #[cfg(feature = "nois")]
    /// Nois receive endpoint to process nois callbacks
    NoisReceive { callback: nois::NoisCallback }
}

Dependencies

Theres is one additional contractor method available on the module builder and that’s the with_dependencies function. As it states it allows you to specify any smart-contract dependencies that your application might require. This is a key requirement for building truly composable and secure applications. We’ll cover dependencies further in our section on them

Summary

The Abstract SDK allows you to easily construct modules by using our low-overhead smart-contract builder. By employing this pattern you re-use the base contract’s code, allowing you to focus on the ideas that make your product unique.

In the next section we’ll cover how you can use the module object that we make available in the function handlers to write highly functional smart-contract code.

Ever wanted to swap on any cosmos DEX with only one line of code? Look no further!

SDK

Now that you’ve got your module set up you’re ready for our hot sauce. While you can create any regular smart-contract in a module, it’s where our software shines. Instead we’ve created an account abstraction programming toolbox that allows you to easily control an Abstract Account’s interactions, as well as create your own APIs that can be used by other developers to interact with your unique application. Composability galore!

How it works

The abstract-sdk crate is a toolbox for developers to create composable smart-contract APIs. It allows you to use composed functionality with a few keystrokes through it’s combination of supertraits and blanket implementations. Supertraits are Rust traits that have one or multiple trait bounds while a blanket implementation is a Rust implementation that is automatically implemented for every object that meets the trait bounds. The Abstract SDK uses both to achieve its modular design.

APIs

Abstract API objects are Rust structs that expose some smart-contract functionality. Such an API can only be retrieved if a contract (or feature-object) implements the required features/api traits. Access to an API is automatically provided if the trait constraints for the API are met by the contract.

Most of the APIs either return a CosmosMsg or an AccountAction. The CosmosMsg is a message that should be added as-is to the Response to perform some action.

CosmosMsg Example

This example sends coins from the local contract (module) to the account that the application is installed on which does not require the account itself to execute the action.

            // Get bank API struct from the app
            let bank: Bank<'_, MockModule> = app.bank(deps.as_ref());
            // Create coins to deposit
            let coins: Vec<Coin> = coins(100u128, "asset");
            // Construct messages for deposit (transfer from this contract to the account)
            let deposit_msgs: Vec<CosmosMsg> = bank.deposit(coins.clone()).unwrap();
            // Add to response
            let response: Response = Response::new().add_messages(deposit_msgs);

Alternatively AccountAction structs can also be returned by an API. An AccountAction is supposed to be forwarded to the Abstract Account to let the account perform action. AccountActions can be executed with the Executor API. The returned CosmosMsg should be added to the action’s Response.

AccountAction Example

This example sends coins from the account to another address which requires the account itself to execute the action.

            let recipient: Addr = Addr::unchecked("recipient");
            let bank: Bank<'_, MockModule> = app.bank(deps.as_ref());
            let coins: Vec<Coin> = coins(100u128, "asset");
            let bank_transfer: AccountAction = bank.transfer(coins.clone(), &recipient).unwrap();

            let executor: Executor<'_, MockModule> = app.executor(deps.as_ref());
            let account_message: CosmosMsg = executor.execute(vec![bank_transfer]).unwrap();
            let response: Response = Response::new().add_message(account_message);

Creating your own API

The Bank API allows developers to transfer assets from and to the Account. We now want to use this API to create a Splitter API that splits the transfer of some amount of funds between a set of receivers.

The code behind this example is available here.

// Trait to retrieve the Splitter object
// Depends on the ability to transfer funds
pub trait SplitterInterface: TransferInterface {
    fn splitter<'a>(&'a self, deps: Deps<'a>) -> Splitter<Self> {
        Splitter { base: self, deps }
    }
}

// Implement for every object that can transfer funds
impl<T> SplitterInterface for T where T: TransferInterface {}

#[derive(Clone)]
pub struct Splitter<'a, T: SplitterInterface> {
    base: &'a T,
    deps: Deps<'a>,
}

impl<'a, T: SplitterInterface> Splitter<'a, T> {
    /// Split an asset to multiple users
    pub fn split(&self, asset: AnsAsset, receivers: &[Addr]) -> AbstractSdkResult<AccountAction> {
        // split the asset between all receivers
        let receives_each = AnsAsset {
            amount: asset
                .amount
                .multiply_ratio(Uint128::one(), Uint128::from(receivers.len() as u128)),
            ..asset
        };

        // Retrieve the bank API
        let bank = self.base.bank(self.deps);
        receivers
            .iter()
            .map(|receiver| {
                // Construct the transfer message
                bank.transfer(vec![&receives_each], receiver)
            })
            .try_fold(AccountAction::new(), |mut acc, v| match v {
                Ok(action) => {
                    // Merge two AccountAction objects
                    acc.merge(action);
                    Ok(acc)
                }
                Err(e) => Err(e),
            })
    }
}

These APIs can then be used by any contract that implements its required traits, in this case the TransferInterface.

        let asset = AnsAsset {
            amount: Uint128::from(100u128),
            name: "usd".into(),
        };

        let receivers = vec![
            Addr::unchecked("receiver1"),
            Addr::unchecked("receiver2"),
            Addr::unchecked("receiver3"),
        ];

        let split_funds = module.splitter(deps.as_ref()).split(asset, &receivers)?;
        assert_eq!(split_funds.messages().len(), 3);

        let msg: CosmosMsg = module.executor(deps.as_ref()).execute(vec![split_funds])?;

        Ok(Response::new().add_message(msg))

Available API Objects

The following API objects are available in the Abstract SDK:

Other projects have also started building APIs. Here are some examples:

Features

Features are the lowest-level traits that are contained within the SDK and they don’t have any trait bounds. They generally act as data accessor traits. I.e. if a struct implements a feature it means that it has some way to get the information required by that feature.

Here’s an example of such a feature:

#![allow(unused)]
fn main() {
use crate::{ans_resolve::Resolve, cw_helpers::wasm_smart_query, AbstractSdkResult};
use abstract_core::{
    ans_host::{AssetPairingFilter, AssetPairingMapEntry, PoolAddressListResponse, QueryMsg},
    objects::{ans_host::AnsHost, DexAssetPairing},
};
use cosmwasm_std::Deps;

/// Accessor to the Abstract Name Service.
pub trait AbstractNameService: Sized {
    /// Get the ANS host address.
    fn ans_host(&self, deps: Deps) -> AbstractSdkResult<AnsHost>;

    /// Construct the name service client.
    fn name_service<'a>(&'a self, deps: Deps<'a>) -> AbstractNameServiceClient<Self> {
        AbstractNameServiceClient {
            _base: self,
            deps,
            host: self.ans_host(deps).unwrap(),
        }
    }
}
}

Any structure that implements this traits has access to the Abstract Name Service, and thus has a way to resolve ANS entries. By composing these features it is possible to write advanced APIs that are automatically implemented on objects that support its required features.

Now instead of letting you implement these traits yourself, we’ve already gone ahead and implemented them for the App and Adapter structs. Here’s the implementation for the App:

#![allow(unused)]
fn main() {
impl<
        Error: ContractError,
        CustomInitMsg,
        CustomExecMsg,
        CustomQueryMsg,
        CustomMigrateMsg,
        ReceiveMsg,
        SudoMsg,
    > AbstractNameService
    for AppContract<
        Error,
        CustomInitMsg,
        CustomExecMsg,
        CustomQueryMsg,
        CustomMigrateMsg,
        ReceiveMsg,
        SudoMsg,
    >
{
    fn ans_host(&self, deps: Deps) -> AbstractSdkResult<AnsHost> {
        // Retrieve the ANS host address from the base state.
        Ok(self.base_state.load(deps.storage)?.ans_host)
    }
}
}

So when you’re building your application the module struct already has the features and data required to do the basic abstract operations. With this we can start creating more advanced functionality.

Other structs that implement a feature without being module bases are called Feature Objects.

Usage

Add abstract-sdk to your Cargo.toml by running:

cargo add abstract-sdk

Then import the prelude in your contract. This will ensure that you have access to all the traits which should help your IDE with auto-completion.

use abstract_sdk::prelude::*;

Account Creation

We know you’re really excited to start playing with your module but there’s one more thing you need to do first. Abstract cares about your efforts and we believe you should have the ability to monetize your modules in any way that you see fit. To help us provide that feature we created the concept of module namespaces. A module namespace is your (or your team’s) publishing domain for Abstract modules. Through this design you can monetize your product through a namespace or per-modules basis as explained in more detail in the monetization section.

Each namespace must be associated with an Abstract account, hence you will need to create one.

Creating an Account

Creating an account is straight forward process. Go to the Abstract Account Portal and click “Create Account”. Come back here once you have your account set up.

Are you having trouble creating an account? Please contact us on Discord and we’ll help you out.

Claim a Namespace

Now that you have your account you can proceed to claim your namespace. The namespace will be exclusively linked to your Abstract Account and will prefix your module names to form a unique module identifier.

For example, if your namespace is myapp and your module name is mymodule then your module identifier will be myapp:mymodule.

You can easily claim your namespace by going to your Account on our website and click the “Claim Namespace” button on the account page. You will be asked to pay a small fee to claim your namespace. This fee is used to prevent namespace squatting and to help us maintain the Abstract ecosystem.

With the setup out of the way let’s get to the fun part: building your module!

Testing Your Module

Testing your smart-contracts is a crucial step in its development. Without proper testing you risk compromising the accounts of your users and with it the funds that they hold. For that reason we expect modules to be thoroughly tested before they are allowed on our platform.

This section of the documentation outlines the different testing methods. Each method is accompanied with an Abstract helper. These helpers assist you in setting up your testing environment.

Unit-testing

The lowest level of testing is unit testing. Unit-tests allow you to easily test complex, self-contained logic. Because unit-tests should be self-contained, any queries made to other contracts need to be mocked. These mocks acts as “query catchers”, allowing you to specify a response for a specific query.

Sadly constructing these mock queries is time consuming and involves a lot of boiler-plate. Additionally there are queries that your module should always support as they are part of its base implementation. For those reasons we created an abstract-testing package.

The abstract-testing provides you with some small abstractions that allow you to mock Smart and Raw queries.

Info

What’s the difference between a Smart and a Raw query?

  • Smart Queries: A smart-query is a query that contains a message in its request. It commonly involves computation on the queried contract. After this optional computation and state-loading the contract responds with a ResponseMsg. Mocking this type of query involves matching the serialized query request message (Binary) to a specific message type and returning a serialized response. Any expected computation needs to be mocked as well.

  • Raw Queries: A raw-query is a simple database key-value lookup. To mock this type of query you need to provide a mapping of the raw key to a raw value. The returned value then needs to be interpreted correctly according to the store’s type definitions.

Mock Querier

The abstract-testing package contains a MockQuerierBuilder. It uses the common builder pattern to allow for efficient mock construction. Let’s see how!

Mocking Smart Queries

Mocking a smart-query with the MockQuerierBuilder is easy! You do it by calling the with_smart_handler function.

Example

#![allow(unused)]
fn main() {
    /// let querier = MockQuerierBuilder::default().with_smart_handler("contract_address", |msg| {
    ///    // handle the message
    ///     let res = match from_binary::<MockModuleQueryMsg>(msg).unwrap() {
    ///         // handle the message
    ///         MockModuleQueryMsg =>
    ///                         return to_binary(&MockModuleQueryResponse {}).map_err(|e| e.to_string())
    ///    };
    /// }).build();
}

Mocking Raw Queries

Instead of manually mapping the key-value relation and it’s types, we can use the available contract storage types. Using the storage types ensures that the mock and its data operations are the same as in the actual implementation. It also saves us a lot of work related to key serialization.

This approach allow you to easily map Item and Map datastores.

Warning

Multi-index maps are currently not supported. PRs on this issue are welcome! 🤗

Example

TODO

Abstract Querier

The easiest and best way to start using the querier is to use the AbstractMockQuerierBuilder::mocked_account_querier_builder() method. This method sets up a mock querier with an initial abstract account.

Integration Testing

Integration testing your contract involves deploying your contract and any of its dependencies to a mock environment. Abstract uses cw-orchestrator’s Mock struct that is backed by a cw-multi-test::App which you might be familiar with.

Info

cw-orchestrator is a CosmWasm scripting tool that we developed to improve the speed at which developers can test and deploy their applications. We recommend reading the cw-orchestrator documentation if you are not yet familiar with it.

TODO: talk about the mock and show a test-setup with it.

Linked Testing

@Kayanski

Local Daemon Testing

Once you have confirmed that your module works as expected you can spin up a local node and deploy Abstract + your app onto the chain. You can do this by running the test-local example, which uses a locally running juno daemon to deploy to. At this point you can also test your front-end with the contracts.

Note

Locally testing your Abstract deployment is difficult if it depends on other protocols, and those protocols don’t make use of cw-orchestrator.

Testing

You can test the module using the different provided methods.

  1. Integration testing:
  2. Local Daemon:

Module Deployment

Deploying your module is an easy 3-step process: Module Uploading, Registration and Schema Linking. Let’s go over each step in detail.

This doc assumes you’re using the module app template, if you’re not we recommend looking at the relevant files in the template to set up your own deployment process.

Module Uploading

Uploading your module involves first compiling your module as a WASM binary and then uploading it to the network(s) you want your module to be available on. This will yield you a code_id that is a unique identifier for your module on the network.

Compiling your module

You can compile your module by running the following command:

$ just wasm
> Compiling to WASM...

The WASM optimizer uses a docker container to compile your module. If you don’t have docker installed you can install it from here.

This should result in an artifacts directory being created in your project root. Inside you will find a my_module.wasm file that is your module’s binary.

Now you can go ahead and deploy the module to the network(s) you want to make it available on. You can do this by running the following command:

$ just deploy uni-1
> Deploying module...

This will use the module’s examples/deploy.rs script to deploy the module to the uni-1 network. The resulting code-id of your contract should now be in the state.json file created for you. The script will also attempt to register the module on the Abstract Version Control, hence the mnemonic used in the script should be the same as the one you used to create the account and register the namespace.

JSON Schema Linking

To improve the user-experience for developers using your module we recommend linking your module’s JSON schema to the Abstract Version Control. This will allow developers (and you) to use the Abstract web app to interact with your module.

To link your module’s schema you can run the following command:

$ just publish-schemas <namespace> <name> <version>
> Publishing schemas...

Where you fill the <namespace>, <name> and <version> with the same values you used to register your module on the Abstract Version Control.

Module Installation

To install your module, go to the Abstract Account Dashboard, go to your (or a new one) Account and click on the Modules tab. Here you will find a list of all the modules you have registered on the Abstract Version Control. Click on the Install button next to your module and select the network you want to install it on. This will open a modal with the following fields:

Use Cases

Welcome to the Use Cases section of the Abstract documentation. This part of our guide is designed to show you the breadth and depth of possibilities that Abstract unlocks in the world of blockchain development.

As you navigate through this section, you will discover a variety of applications where Abstract’s unique approach to modular development and perpetual rewarding system can truly shine. We will explore real-life scenarios across different domains, such as Decentralized Application Development, Open Source Contribution, Decentralized Finance, and Educational Use. For each application, we’ll present concrete examples to illustrate how Abstract’s principles and technology have been used to drive value and innovation.

The journey through these use cases will provide you with a deeper understanding of Abstract’s potential and how its distinctive approach can revolutionize the way you develop on the blockchain. By the end of this section, we hope you’ll be inspired to consider new ways in which you could leverage Abstract in your own projects.

Decentralized Application Development

Abstract’s modular design allows developers to leverage pre-built functionalities, minimizing redundant work and accelerating the creation process. With Abstract, developers simply choose the modules they need—user authentication, data storage, payment processing—and integrate them effortlessly. Meanwhile, the platform’s blockchain nature enhances security, providing users with a safer, transparent experience. Plus, Abstract’s usage-based rewards mean that every use of a module generates income for its creator, promoting a cycle of continuous improvement and fair compensation.

Case Study

Open Source Contribution

Open source contribution is no longer a thankless job with Abstract. The platform has revolutionized the way open source developers are compensated, ensuring they are rewarded every time their code is used. Abstract’s unique model, powered by blockchain and tokenomics, ensures perpetual rewards based on the usage of their work.

Rather than the traditional one-off donations or sponsorships, Abstract brings a sustainable, fair, and motivating environment. The more your module is used, the more you earn. This directly ties your effort with your reward and incentivizes the production of quality work. It’s an open-source world where every contribution counts, and every use of your module is a vote of confidence and a token of appreciation.

Case Study

Decentralized Finance

Abstract simplifies the creation of DeFi applications, empowering developers to extend financial services to anyone with internet access. With its modular architecture, Abstract allows developers to create, share, and reuse DeFi modules, reducing development time, effort, and cost.

In the Abstract ecosystem, you can seamlessly integrate pre-existing DeFi modules into your applications, streamlining the process and boosting your development speed. Need a lending protocol or an AMM (Automated Market Maker) feature? Simply find a module, plug it into your application, and let Abstract do the heavy lifting.

Moreover, as every module is openly available on the platform, developers across the globe are continuously contributing to and refining the DeFi tools at your disposal. It’s never been easier to take part in the DeFi revolution and bring financial services to the unbanked and underbanked, all thanks to Abstract.

Case Study

Educational Use

As an open, modular blockchain platform, Abstract is not just a tool for development but also a fantastic learning resource for budding developers keen to delve into the world of blockchain, modular architecture, and decentralized governance.

For blockchain enthusiasts, Abstract offers a real-world application of blockchain technology. By interacting with Abstract’s tokenomics, developers can understand how blockchain can be used to create secure, transparent, and decentralized systems, from DeFi applications to governance protocols.

The modular architecture of Abstract allows developers to explore how complex applications can be built from reusable, interchangeable modules. By experimenting with the platform’s modules, users can understand how to design, implement, and integrate modules into larger systems effectively.

Moreover, Abstract’s decentralized governance model offers invaluable insights into how decentralized systems can be managed and maintained. Through participating in governance with the ACT token, developers can learn about consensus mechanisms, voting systems, and the challenges and solutions involved in decentralized decision-making.

In essence, Abstract provides an all-in-one educational platform for any developer seeking to deepen their understanding of these critical areas in today’s tech landscape.

Case Study

There’s no better way to grasp the power and potential of Abstract than by diving in and exploring it firsthand. Whether you’re a seasoned developer, an open-source contributor, a DeFi enthusiast, or a curious learner, Abstract opens up a world of possibilities for you to discover. Start creating modules, contributing to the open-source community, building DeFi applications, or simply learning about the fascinating domains of blockchain, modular architecture, and decentralized governance. The journey with Abstract is certain to enrich your development skills, broaden your understanding, and potentially pave the way for you to create lasting value in the tech world.

Equilibrium

About Abstract

Abstract is the smart-contract platform that empowers developers to create secure and powerful financial applications with ease. With Abstract, you can unleash your creativity and build innovative solutions without much of the complexities typically associated with blockchain development.

Continuous Funding

Developers on the Abstract platform can earn ongoing revenue for their contributions by leveraging the platform’s tokenomics and community-driven incentives. They design and offer smart contracts on the Module Marketplace and receive a portion of Abstract tokens from each sale. By participating in or forming Developer DAOs, developers gain access to funding for building smart contracts and dApps, sharing profits among members. Furthermore, they can create smart contracts based on user bounties, earning tokens for their efforts. This collaborative environment encourages the creation of top-quality smart contracts, offering developers a continuous income stream and promoting value for all parties involved.

Simplified Development

Abstract simplifies the development process by providing a modular architecture and a comprehensive set of tools. Developers can leverage pre-built functionalities and smart contract templates to accelerate application development. The modular approach allows you to focus on the specific features and logic of your application, reducing development time and effort.

Secure and Reliable Infrastructure

Abstract offers a secure and reliable infrastructure, built on robust blockchain technology. You can trust that your applications and transactions are protected by advanced cryptographic protocols and decentralized consensus mechanisms. The platform’s smart contract framework ensures the integrity and immutability of your financial applications, providing a solid foundation for trust and transparency.

Seamless Integration with Financial Services

With Abstract, you can seamlessly integrate your applications with a wide range of financial services. Whether you’re building decentralized lending platforms, automated market makers, or yield farming protocols, Abstract provides the necessary tools and interoperability to connect with existing financial systems and protocols. This allows you to tap into the vast ecosystem of decentralized finance and provide users with a seamless and comprehensive financial experience.

Continuous Innovation and Collaboration

Abstract fosters a vibrant developer community where collaboration and innovation thrive. Engage with like-minded developers, share knowledge, and contribute to the growth of the ecosystem. Together, we can push the boundaries of what’s possible in the world of decentralized finance and drive the industry forward.

Developer-Centric Approach

At Abstract, we are developer-centric in our approach. We understand the challenges and complexities that developers face in the blockchain space, and we’re committed to providing the necessary tools, resources, and support to make your journey smooth and rewarding. Whether you’re an experienced blockchain developer or new to the space, Abstract is designed to empower you and unlock your full potential.

By focusing on the technical capabilities, simplified development process, secure infrastructure, seamless integration with financial services, and the developer-centric approach, Abstract enables developers to unleash their creativity, build innovative financial applications, and contribute to the growth of the ecosystem. We are here to support you on your journey and help you create the next generation of decentralized finance solutions.

Please note that this is a high-level overview of Abstract’s value proposition, and further details can be found in the accompanying documentation and resources tailored specifically for developers.

IBC

Interchain Abstract Accounts

Interchain Abstract Accounts is Abstract’s solution to chain-agnostic accounts. It allows users to create an account on one chain and use it on any other chain that supports Abstract. This is achieved by using a combination of the Inter-Blockchain Communication (IBC) protocol and the Abstract Accounts.

Overview

IAA allow users to interact with any smart-contract on any chain using their local account. This mechanism is powered by a set of Abstract smart-contracts that will dispatch messages that users send locally to a distant chain.

Account creation

The first step of using Interchain Abstract Account is creating a remote account.

Abstract IBC

Synopsis

This standard document specifies packet data structure, state machine handling logic, and encoding details for the transfer of messages and creation of Abstract accounts over an IBC channel between a client and a host on separate chains. The state machine logic presented allows for safe multi-chain account creation and execution.

Motivation

Users of a set of chains connected over the IBC protocol might wish to interact with smart-contracts and dapps present on another chain than their origin, while not having to onboard the distant chain, create a new wallet or transfer the necessary funds to this other chain. This application-layer standard describes a protocol for interacting with a distant chain and creating abstract account on chains connected with IBC which preserves asset ownership, limits the impact of Byzantine faults, and requires no additional permissioning.

Definitions

The Abstract IBC Account interface is described in the following guide and the specifications are roughly presented here

Desired Properties

  • Preservation of account and funds ownership
  • All interactions that can be done by a local account should be possible for a distant account as well.

Technical Specification

Data Structures

Only one packet data type is added in this spec to be able to interact across IBC chains

#![allow(unused)]
fn main() {
pub struct PacketMsg {
    /// Chain of the client
    pub client_chain: String,
    /// Amount of retries to attempt if packet returns with StdAck::Error
    pub retries: u8,
    pub account_id: AccountId,
    /// Callback performed after receiving an StdAck::Result
    pub callback_info: Option<CallbackInfo>,
    /// execute the custom host function
    pub action: HostAction,
}
}

Execution

  • client_chain specifies the chain from which the message originates. Once a channel is created between client and host, this channel will always be checked to match the registered configuration

  • account_id specifies the account that is calling the action on the local chain.

  • action specifies what the remote chain should execute upon receiving this packet

Acknowledgement

When the action is executed on the remote chain, it can either be successful or yield an error.

  • retries specifies the number of attemps left to submit the packet. In case an error is yielded by the remote chain, the original packet will be sent back to the original chain and retried as long as retries > 0. Because IBC actions are asynchronous, some packets may need to wait other packet to go through before they can be executed. This parameter allows the packet action to fail multiple times before it’s indeed sent across a channel

  • call_back_info is an optional object that specifies any action that needs to be executed after the packet has been sucessfully executed and a positive (StdAck::Result) acknowledgement has been transfered back.

Cross chain trace

Because accounts created across chains using the IAA protocol are controlled by an account located on a remote chain, the account_id parameter should specify which chain is calling an action. In order to follow which chains a message is called from, the IBC Abstract module leverages the AccountId::trace field. An account is wether AccountTrace::Local or AccountTrace::Remote. When a PacketMsg is sent across an IBC channel, the account id is transformed in the following manner :

  • If it was AccountTrace::Local before transfer, it turns into an AccountTrace::Remote account with one chain in the associated vector being the chain calling the PacketMsg (PacketMsg::client_chain)
  • If it was AccountTrace::Remote before transfer, it stays remote and the client_chain field is added to the associated vector.

This allows full traceability of the account creations and calls.

We don’t need to enforce the same logic as with token transfer (channel + port), because we don’t need fungibility here. Only the chains on which the accounts exist is important

The acknowledgement data type describes whether the transfer succeeded or failed, and the reason for failure (if any).

type FungibleTokenPacketAcknowledgement = FungibleTokenPacketSuccess | FungibleTokenPacketError;

interface FungibleTokenPacketSuccess {
  // This is binary 0x01 base64 encoded
  result: "AQ=="
}

interface FungibleTokenPacketError {
  error: string
}

Note that both the FungibleTokenPacketData as well as FungibleTokenPacketAcknowledgement must be JSON-encoded (not Protobuf encoded) when they serialized into packet data. Also note that uint256 is string encoded when converted to JSON, but must be a valid decimal number of the form [0-9]+.

The fungible token transfer bridge module tracks escrow addresses associated with particular channels in state. Fields of the ModuleState are assumed to be in scope.

interface ModuleState {
  channelEscrowAddresses: Map<Identifier, string>
}

Sub-protocols

The sub-protocols described herein should be implemented in a “fungible token transfer bridge” module with access to a bank module and to the IBC routing module.

Port & channel setup

The setup function must be called exactly once when the module is created (perhaps when the blockchain itself is initialised) to bind to the appropriate port and create an escrow address (owned by the module).

function setup() {
  capability = routingModule.bindPort("bank", ModuleCallbacks{
    onChanOpenInit,
    onChanOpenTry,
    onChanOpenAck,
    onChanOpenConfirm,
    onChanCloseInit,
    onChanCloseConfirm,
    onRecvPacket,
    onTimeoutPacket,
    onAcknowledgePacket,
    onTimeoutPacketClose
  })
  claimCapability("port", capability)
}

Once the setup function has been called, channels can be created through the IBC routing module between instances of the fungible token transfer module on separate chains.

An administrator (with the permissions to create connections & channels on the host state machine) is responsible for setting up connections to other state machines & creating channels to other instances of this module (or another module supporting this interface) on other chains. This specification defines packet handling semantics only, and defines them in such a fashion that the module itself doesn’t need to worry about what connections or channels might or might not exist at any point in time.

Routing module callbacks

Channel lifecycle management

Both machines A and B accept new channels from any module on another machine, if and only if:

  • The channel being created is unordered.
  • The version string is ics20-1.
function onChanOpenInit(
  order: ChannelOrder,
  connectionHops: [Identifier],
  portIdentifier: Identifier,
  channelIdentifier: Identifier,
  counterpartyPortIdentifier: Identifier,
  counterpartyChannelIdentifier: Identifier,
  version: string) => (version: string, err: Error) {
  // only unordered channels allowed
  abortTransactionUnless(order === UNORDERED)
  // assert that version is "ics20-1" or empty
  // if empty, we return the default transfer version to core IBC
  // as the version for this channel
  abortTransactionUnless(version === "ics20-1" || version === "")
  // allocate an escrow address
  channelEscrowAddresses[channelIdentifier] = newAddress()
  return "ics20-1", nil
}
function onChanOpenTry(
  order: ChannelOrder,
  connectionHops: [Identifier],
  portIdentifier: Identifier,
  channelIdentifier: Identifier,
  counterpartyPortIdentifier: Identifier,
  counterpartyChannelIdentifier: Identifier,
  counterpartyVersion: string) => (version: string, err: Error) {
  // only unordered channels allowed
  abortTransactionUnless(order === UNORDERED)
  // assert that version is "ics20-1"
  abortTransactionUnless(counterpartyVersion === "ics20-1")
  // allocate an escrow address
  channelEscrowAddresses[channelIdentifier] = newAddress()
  // return version that this chain will use given the
  // counterparty version
  return "ics20-1", nil
}
function onChanOpenAck(
  portIdentifier: Identifier,
  channelIdentifier: Identifier,
  counterpartyChannelIdentifier: Identifier,
  counterpartyVersion: string) {
  // port has already been validated
  // assert that counterparty selected version is "ics20-1"
  abortTransactionUnless(counterpartyVersion === "ics20-1")
}
function onChanOpenConfirm(
  portIdentifier: Identifier,
  channelIdentifier: Identifier) {
  // accept channel confirmations, port has already been validated, version has already been validated
}
function onChanCloseInit(
  portIdentifier: Identifier,
  channelIdentifier: Identifier) {
    // always abort transaction
    abortTransactionUnless(FALSE)
}
function onChanCloseConfirm(
  portIdentifier: Identifier,
  channelIdentifier: Identifier) {
  // no action necessary
}
Packet relay

In plain English, between chains A and B:

  • When acting as the source zone, the bridge module escrows an existing local asset denomination on the sending chain and mints vouchers on the receiving chain.
  • When acting as the sink zone, the bridge module burns local vouchers on the sending chains and unescrows the local asset denomination on the receiving chain.
  • When a packet times-out, local assets are unescrowed back to the sender or vouchers minted back to the sender appropriately.
  • Acknowledgement data is used to handle failures, such as invalid denominations or invalid destination accounts. Returning an acknowledgement of failure is preferable to aborting the transaction since it more easily enables the sending chain to take appropriate action based on the nature of the failure.

sendFungibleTokens must be called by a transaction handler in the module which performs appropriate signature checks, specific to the account owner on the host state machine.

function sendFungibleTokens(
  denomination: string,
  amount: uint256,
  sender: string,
  receiver: string,
  sourcePort: string,
  sourceChannel: string,
  timeoutHeight: Height,
  timeoutTimestamp: uint64): uint64 {
    prefix = "{sourcePort}/{sourceChannel}/"
    // we are the source if the denomination is not prefixed
    source = denomination.slice(0, len(prefix)) !== prefix
    if source {
      // determine escrow account
      escrowAccount = channelEscrowAddresses[sourceChannel]
      // escrow source tokens (assumed to fail if balance insufficient)
      bank.TransferCoins(sender, escrowAccount, denomination, amount)
    } else {
      // receiver is source chain, burn vouchers
      bank.BurnCoins(sender, denomination, amount)
    }

    // create FungibleTokenPacket data
    data = FungibleTokenPacketData{denomination, amount, sender, receiver}

    // send packet using the interface defined in ICS4
    sequence = handler.sendPacket(
      getCapability("port"),
      sourcePort,
      sourceChannel,
      timeoutHeight,
      timeoutTimestamp,
      data
    )

    return sequence
}

onRecvPacket is called by the routing module when a packet addressed to this module has been received.

function onRecvPacket(packet: Packet) {
  FungibleTokenPacketData data = packet.data
  // construct default acknowledgement of success
  FungibleTokenPacketAcknowledgement ack = FungibleTokenPacketAcknowledgement{true, null}
  prefix = "{packet.sourcePort}/{packet.sourceChannel}/"
  // we are the source if the packets were prefixed by the sending chain
  source = data.denom.slice(0, len(prefix)) === prefix
  if source {
    // receiver is source chain: unescrow tokens
    // determine escrow account
    escrowAccount = channelEscrowAddresses[packet.destChannel]
    // unescrow tokens to receiver (assumed to fail if balance insufficient)
    err = bank.TransferCoins(escrowAccount, data.receiver, data.denom.slice(len(prefix)), data.amount)
    if (err !== nil)
      ack = FungibleTokenPacketAcknowledgement{false, "transfer coins failed"}
  } else {
    prefix = "{packet.destPort}/{packet.destChannel}/"
    prefixedDenomination = prefix + data.denom
    // sender was source, mint vouchers to receiver (assumed to fail if balance insufficient)
    err = bank.MintCoins(data.receiver, prefixedDenomination, data.amount)
    if (err !== nil)
      ack = FungibleTokenPacketAcknowledgement{false, "mint coins failed"}
  }
  return ack
}

onAcknowledgePacket is called by the routing module when a packet sent by this module has been acknowledged.

function onAcknowledgePacket(
  packet: Packet,
  acknowledgement: bytes) {
  // if the transfer failed, refund the tokens
  if (!ack.success)
    refundTokens(packet)
}

onTimeoutPacket is called by the routing module when a packet sent by this module has timed-out (such that it will not be received on the destination chain).

function onTimeoutPacket(packet: Packet) {
  // the packet timed-out, so refund the tokens
  refundTokens(packet)
}

refundTokens is called by both onAcknowledgePacket, on failure, and onTimeoutPacket, to refund escrowed tokens to the original sender.

function refundTokens(packet: Packet) {
  FungibleTokenPacketData data = packet.data
  prefix = "{packet.sourcePort}/{packet.sourceChannel}/"
  // we are the source if the denomination is not prefixed
  source = data.denom.slice(0, len(prefix)) !== prefix
  if source {
    // sender was source chain, unescrow tokens back to sender
    escrowAccount = channelEscrowAddresses[packet.srcChannel]
    bank.TransferCoins(escrowAccount, data.sender, data.denom, data.amount)
  } else {
    // receiver was source chain, mint vouchers back to sender
    bank.MintCoins(data.sender, data.denom, data.amount)
  }
}
function onTimeoutPacketClose(packet: Packet) {
  // can't happen, only unordered channels allowed
}

Using the Memo Field

Note: Since earlier versions of this specification did not include a memo field, implementations must ensure that the new packet data is still compatible with chains that expect the old packet data. A legacy implementation MUST be able to unmarshal a new packet data with an empty string memo into the legacy FungibleTokenPacketData struct. Similarly, an implementation supporting memo must be able to unmarshal a legacy packet data into the current struct with the memo field set to the empty string.

The memo field is not used within transfer, however it may be used either for external off-chain users (i.e. exchanges) or for middleware wrapping transfer that can parse and execute custom logic on the basis of the passed in memo. If the memo is intended to be parsed and interpreted by higher-level middleware, then these middleware are advised to namespace their additions to the memo string so that they do not overwrite each other. Chains should ensure that there is some length limit on the entire packet data to ensure that the packet does not become a DOS vector. However, these do not need to be protocol-defined limits. If the receiver cannot accept a packet because of length limitations, this will lead to a timeout on the sender side.

Memos that are intended to be read by higher level middleware for custom execution must be structured so that different middleware can read relevant data in the memo intended for them without interfering with data intended for other middlewares.

Thus, for any memo that is meant to be interpreted by the state machine; it is recommended that the memo is a JSON object with each middleware reserving a key that it can read into and retrieve relevant data. This way the memo can be constructed to pass in information such that multiple middleware can read the memo without interference from each other.

Example:

{
  "wasm": {
    "address": "contractAddress",
    "arguments": "marshalledArguments",
  },
  "callback": "contractAddress",
  "router": "routerArgs",
}

Here, the “wasm”, “callback”, and “router” fields are all intended for separate middlewares that will exclusively read those fields respectively in order to execute their logic. This allows multiple modules to read from the memo. Middleware should take care to reserve a unique key so that they do not accidentally read data intended for a different module. This issue can be avoided by some off-chain registry of keys already in-use in the JSON object.

Reasoning

Correctness

This implementation preserves both fungibility & supply.

Fungibility: If tokens have been sent to the counterparty chain, they can be redeemed back in the same denomination & amount on the source chain.

Supply: Redefine supply as unlocked tokens. All send-recv pairs sum to net zero. Source chain can change supply.

Multi-chain notes

This specification does not directly handle the “diamond problem”, where a user sends a token originating on chain A to chain B, then to chain D, and wants to return it through D -> C -> A — since the supply is tracked as owned by chain B (and the denomination will be “{portOnD}/{channelOnD}/{portOnB}/{channelOnB}/denom”), chain C cannot serve as the intermediary. It is not yet clear whether that case should be dealt with in-protocol or not — it may be fine to just require the original path of redemption (and if there is frequent liquidity and some surplus on both paths the diamond path will work most of the time). Complexities arising from long redemption paths may lead to the emergence of central chains in the network topology.

In order to track all of the denominations moving around the network of chains in various paths, it may be helpful for a particular chain to implement a registry which will track the “global” source chain for each denomination. End-user service providers (such as wallet authors) may want to integrate such a registry or keep their own mapping of canonical source chains and human-readable names in order to improve UX.

Optional addenda

  • Each chain, locally, could elect to keep a lookup table to use short, user-friendly local denominations in state which are translated to and from the longer denominations when sending and receiving packets.
  • Additional restrictions may be imposed on which other machines may be connected to & which channels may be established.

Backwards Compatibility

Not applicable.

Forwards Compatibility

This initial standard uses version “ics20-1” in the channel handshake.

A future version of this standard could use a different version in the channel handshake, and safely alter the packet data format & packet handler semantics.

Example Implementations

History

Jul 15, 2019 - Draft written

Jul 29, 2019 - Major revisions; cleanup

Aug 25, 2019 - Major revisions, more cleanup

Feb 3, 2020 - Revisions to handle acknowledgements of success & failure

Feb 24, 2020 - Revisions to infer source field, inclusion of version string

July 27, 2020 - Re-addition of source field

Nov 11, 2022 - Addition of a memo field

All content herein is licensed under Apache 2.0.

Frequently Asked Questions (FAQ)

  1. What is Abstract?

Abstract is a CosmWasm development platform that enables developers to create secure and powerful applications with ease. It provides a modular architecture and comprehensive tools to simplify the development process and enable the creation of innovative solutions in the blockchain space.

  1. Who can use Abstract?

There are two faces to Abstract:

  • For developers who want to build any CosmWasm-based application quickly and securely, leveraging the security and modularity of the Abstract framework.
  • For project ideators who want to setup their applications quickly and get their MVP out the door ASAP. Create an Abstract Account and install modules available in the marketplace to expose application functionality.
  1. How can I get started with Abstract?

To get started with Abstract, check out getting started! You will find comprehensive guides, tutorials, and resources to help you understand the platform’s features and functionality. Additionally, you can join our developer community on Discord to connect with like-minded developers and seek assistance if needed.

  1. Can I contribute to the Abstract ecosystem?

Absolutely! Abstract values community contributions and welcomes developers to contribute to the growth of CosmWasm. The best ways for you to contribute are by creating modules (see the getting started docs), sharing your insights and knowledge, participating in discussions, and collaborating on some of our open-source projects. Check out the Contributing page to learn more about how you can get involved.

  1. How does Abstract ensure the security of financial applications?

The marketplace on which all modules are registered and installed requires that each module be audited and conform to our security standards. Additionally, the modular architecture allows developers to leverage pre-built functionalities and best practices, reducing the risk of vulnerabilities. We are partnering with Oak Security (https://www.oaksecurity.io/) to ensure every module is up to spec.

  1. How can I stay updated with Abstract’s latest developments?

Follow us on Twitter @AbstractSDK!

  1. What about cw-orchestrator?

cw-orchestrator is a CosmWasm scripting, testing, and deployment library. Check it out!

Contributing to Abstract SDK

Thank you for considering to contribute to the Abstract SDK project! We appreciate your support and welcome contributions to help improve this multi-environment CosmWasm smart-contract scripting library. This document provides guidelines and instructions on how to contribute to the project effectively.

Table of Contents

Getting Started

To get started with contributing to the Abstract SDK project, you should first familiarize yourself with the repository structure and the codebase. Please read the project’s README to understand the purpose, features, and usage of the Abstract SDK library as well as its documentation.

How to Contribute

There are multiple ways to contribute to the Abstract SDK project, including reporting bugs, suggesting enhancements, and submitting code contributions.

Reporting Bugs

If you encounter any bugs or issues while using the Abstract SDK library, please report them by creating a new issue in the issue tracker. When reporting a bug, please provide the following information:

  • A clear and descriptive title
  • A detailed description of the issue, including steps to reproduce it
  • Any relevant logs, error messages, or screenshots
  • Information about your environment, such as the OS, software versions, and hardware specifications

Suggesting Enhancements

We welcome suggestions for new features or improvements to the existing functionality of the Abstract SDK library. To suggest an enhancement, create a new issue in the issue tracker with the following information:

  • A clear and descriptive title
  • A detailed explanation of the proposed enhancement, including its benefits and potential use cases
  • If applicable, any examples or mockups of the proposed feature

Code Contributions

To contribute code to the Abstract SDK project, please follow these steps:

  1. Fork the repository to your own GitHub account.
  2. Clone your fork to your local machine.
  3. Create a new branch for your changes using the git checkout -b feature/your-feature-name command.
  4. Make your changes and commit them with a clear and concise commit message.
  5. Push your branch to your fork on GitHub.
  6. Create a new pull request against the main branch of the Abstract SDK repository.

Pull Requests

When submitting a pull request, please make sure that your code follows the Style Guide and that all tests pass. Please provide a detailed description of your changes, including the motivation for the changes and any potential impact on the project. This will help maintainers review your pull request more effectively.

Style Guide

The Abstract SDK project follows the Rust coding style and conventions. Please ensure that your code adheres to these guidelines to maintain consistency and readability throughout the codebase.

  • Use proper indentation (4 spaces) and consistent formatting (cargo fmt).
  • Write descriptive variable and function names.
  • Use comments to explain complex or non-obvious code.
  • Follow the Rust API Guidelines for API design.
  • Add documentation for public functions, types, and modules.
  • Write doc tests for public functions and methods.

Community

To join the Abstract SDK community, please join the Abstract Discord server and the #Abstract SDK channel. You can also follow the project on Twitter and GitHub.