SkillAgentSearch skills...

Sylvia

CosmWasm smart contract framework

Install / Use

/learn @hashedone/Sylvia
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

Sylvia Framework

Sylvia is the old name meaning Spirit of The Wood.

Sylvia is the Roman goddess of the forest.

Sylvia is also a framework created to give you the abstraction-focused and scalable solution for building your CosmWasm Smart Contracts. Find your way into the forest of Cosmos ecosystem. We provide you with the toolset, so instead of focusing on the raw structure of your contract, you can create it in proper and idiomatic Rust and then just let cargo make sure that they are sound.

Learn more about sylvia in the book

Sylvia contract template

The Sylvia template streamlines the development of CosmWasm smart contracts by providing a project scaffold that adheres to best practices and leverages the Sylvia framework's powerful features. It's designed to help developers focus more on their contract's business logic rather than boilerplate code.

Learn more here: Sylvia Template on GitHub

The approach

CosmWasm ecosystem core provides the base building blocks for smart contracts - the cosmwasm-std for basic CW bindings, the cw-storage-plus for easier state management, and the cw-multi-test for testing them. Sylvia framework is built on top of them, so for creating contracts, you don't have to think about message structure, how their API is (de)serialized, or how to handle message dispatching. Instead, the API of your contract is a set of traits you implement on your contract type. The framework generates things like entry point structures, functions dispatching the messages, or even helpers for multitest. It allows for better control of interfaces, including validating their completeness in compile time.

Code generation

Sylvia macros generate code in the sv module. This means that every contract and interface macro call must be made in a separate module to avoid collisions between the generated modules.

Contract type

In Sylvia, we define our contracts as structures:

use cw_storage_plus::Item;
use cosmwasm_schema::cw_serde;
use sylvia::ctx::QueryCtx;
use sylvia::cw_std::ensure;


/// Our new contract type.
///
struct MyContract<'a> {
    pub counter: Item<'a, u64>,
}


/// Response type returned by the
/// query method.
///
#[cw_serde]
pub struct CounterResp {
    pub counter: u64,
}

#[entry_points]
#[contract]
#[sv::error(ContractError)]
impl MyContract<'_> {
    pub fn new() -> Self {
        Self {
            counter: Item::new("counter")
        }
    }

    #[sv::msg(instantiate)]
    pub fn instantiate(&self, ctx: InstantiateCtx, counter: u64) -> StdResult<Response> {
        self.counter.save(ctx.deps.storage, &counter)?;
        Ok(Response::new())
    }

    #[sv::msg(exec)]
    pub fn increment(&self, ctx: ExecCtx) -> Result<Response, ContractError> {
        let counter = self.counter.load(ctx.deps.storage)?;
        ensure!(counter < 10, ContractError::LimitReached);
        self.counter.save(ctx.deps.storage, &(counter + 1))?;
        Ok(Response::new())
    }

    #[sv::msg(query)]
    pub fn counter(&self, ctx: QueryCtx) -> StdResult<CounterResp> {
        self
            .counter
            .load(ctx.deps.storage)
            .map(|counter| CounterResp { counter })
    }
}

Sylvia will generate the following new structures:

pub mod sv {
    use super::*;

    struct InstantiateMsg {
        counter: u64,
    }

    enum ExecMsg {
        Increment {}
    }

    enum ContractExecMsg {
        MyContract(ExecMsg)
    }

    enum QueryMsg {
        Counter {}
    }

    enum ContractQueryMsg {
        MyContract(QueryMsg)
    }

    // [...]
}

pub mod entry_points {
    use super::*;

    #[sylvia::cw_std::entry_point]
    pub fn instantiate(
        deps: sylvia::cw_std::DepsMut,
        env: sylvia::cw_std::Env,
        info: sylvia::cw_std::MessageInfo,
        msg: InstantiateMsg,
    ) -> Result<sylvia::cw_std::Response, StdError> {
        msg.dispatch(&MyContract::new(), (deps, env, info))
            .map_err(Into::into)
    }

    // [...]
}

entry_points macro generates instantiate, execute, query and sudo entry points. All those methods call dispatch on the msg received and run proper logic defined for the sent variant of the message.

What is essential - the field in the InstantiateMsg (and other messages) gets the same name as the function argument.

The ExecMsg is the primary one you may use to send messages to the contract. The ContractExecMsg is only an additional abstraction layer that would matter later when we define traits for our contract. Thanks to the entry_point macro it is already being used in the generated entry point and we don't have to do it manually.

What you might notice - we can still use StdResult (so StdError) if we don't need ContractError in a particular function. What is important is that the returned result type has to implement Into<ContractError>, where ContractError is a contract error type - it will all be commonized in the generated dispatching function (so entry points have to return ContractError as its error variant).

Interfaces

One of the fundamental ideas of the Sylvia framework is the interface, allowing the grouping of messages into their semantical groups. Let's define a Sylvia interface:

pub mod group {
    use super::*;
    use sylvia::interface;
    use sylvia::ctx::{ExecCtx, QueryCtx};
    use sylvia::cw_std::StdError;

    #[cw_serde]
    pub struct IsMemberResp {
        pub is_member: bool,
    }

    #[interface]
    pub trait Group {
        type Error: From<StdError>;

        #[sv::msg(exec)]
        fn add_member(&self, ctx: ExecCtx, member: String) -> Result<Response, Self::Error>;

        #[sv::msg(query)]
        fn is_member(&self, ctx: QueryCtx, member: String) -> Result<IsMemberResp, Self::Error>;
    }
}

Then we need to implement the trait on the contract type:

use sylvia::cw_std::{Empty, Addr};
use cw_storage_plus::{Map, Item};

pub struct MyContract<'a> {
    counter: Item<'a, u64>,
    // New field added - remember to initialize it in `new`
    members: Map<'a, &'a Addr, Empty>,
}

impl group::Group for MyContract<'_> {
    type Error = ContractError;

    fn add_member(&self, ctx: ExecCtx, member: String) -> Result<Response, ContractError> {
        let member = ctx.deps.api.addr_validate(&member)?;
        self.members.save(ctx.deps.storage, &member, &Empty {})?;
        Ok(Response::new())
    }

    fn is_member(&self, ctx: QueryCtx, member: String) -> Result<group::IsMemberResp, ContractError> {
        let is_member = self.members.has(ctx.deps.storage, &Addr::unchecked(&member));
        let resp = group::IsMemberResp {
            is_member,
        };

        Ok(resp)
    }
}

#[contract]
#[sv::messages(group as Group)]
impl MyContract<'_> {
    // Nothing changed here
}

First, note that I defined the interface trait in its separate module with a name matching the trait name, but written "snake_case" instead of CamelCase. Here I have the group module for the Group trait, but the CrossStaking trait should be placed in its own cross_staking module (note the underscore). This is a requirement right now - Sylvia generates all the messages and boilerplate in this module and will try to access them through this module. If the interface's name is a camel-case version of the last module path's segment, the as InterfaceName can be omitted. F.e. #[sv::messages(cw1 as Cw1)] can be reduced to #[sv::messages(cw1)]

Then there is the Error type embedded in the trait - it is also needed there, and the trait bound here has to be at least From<StdError>, as Sylvia might generate code returning the StdError in deserialization/dispatching implementation. The trait can be more strict - this is the minimum.

Finally, the implementation block has an additional #[sv::messages(module as Identifier)] attribute. Sylvia needs it to generate the dispatching properly - there is the limitation that every macro has access only to its local scope. In particular - we cannot see all traits implemented by a type and their implementation from the #[contract] crate.

To solve this issue, we put this #[sv::messages(...)] attribute pointing to Sylvia what is the module name where the interface is defined, and giving a unique name for this interface (it would be used in generated code to provide proper enum variant).

Macro attributes

struct MyMsg;
impl CustomMsg for MyMsg {}

struct MyQuery;
impl CustomQuery for MyMsg {}

#[entry_point]
#[contract]
#[sv::error(ContractError)]
#[sv::messages(interface as Interface)]
#[sv::messages(interface as InterfaceWithCustomType: custom(msg, query))]
#[sv::custom(msg=MyMsg, query=MyQuery)]
#[sv::msg_attr(exec, PartialOrd)]
#[sv::override_entry_point(sudo=crate::entry_points::sudo(crate::SudoMsg))]
impl MyContract {
    // ...
    #[sv::msg(query)]
    #[sv::attr(serde(rename(serialize = "CustomQueryMsg")))]
    fn query_msg(&self, _ctx: QueryCtx) -> StdResult<Response> {
        // ...
    }
}
  • sv::error is used by both contract and entry_point macros. It is necessary in case a custom error is being used by your contract. If omitted generated code will use StdError.

  • sv::messages is the attribute for the contract macro. Its purpose is to inform Sylvia about interfaces implemented for the contract. If the implemented interface does not use a default Empty message response for query and/or exec then the : custom(query), : custom(msg) or : custom(msg, query) should be indicated.

  • sv::override_entry_point - refer to the Overriding entry points section.

  • sv::custom allows to define CustomMsg and CustomQuery for the contract. By default generated code will ret

View on GitHub
GitHub Stars102
CategoryDevelopment
Updated4mo ago
Forks21

Languages

Rust

Security Score

92/100

Audited on Nov 16, 2025

No findings