clonePhoto by Thomas Tastet on Unsplash

The Sylvia Framework Release

Bartłomiej Kuras
CosmWasm
Published in
7 min readDec 14, 2022

--

Today we released Sylvia version 0.2.2 — the first Release we consider production ready. Let me show you what we prepare for the next level of contract building.

What is Sylvia?

Sylvia is a framework for building CosmWasm Smart Contract. It is built on top of primitives delivered by the cosmwasm-std crate but with additional tooling around CosmWasm in mind: cw-storage-plus, cw-multi-test, and the cosmwasm-schema.

The idea of Sylvia is to deliver a more abstract way of Smart Contract development and switch from “messages” to “behavior” reasoning. For example, we do not define executemessage type using the Sylvia framework. Instead, we define some functions on the Smart Contract type as our contract capabilities. Then the framework is responsible for generating all the boilerplate — contract messages types, dispatching them to proper functions, and also, in the future — generating utilities to work with the contract.

Sylvia also focuses on abstracting the contract in a way, so it is possible to compose and reuse them. Let’s think about two examples to explain this.

Contracts encapsulation

The first example is contract encapsulation for extending its capabilities. Let’s consider the standard cw4-group contract from the cw-plus repository. This contract is a utility defining some group of addresses. Now we may be tempted to create another contract with the exact implementation as cw4-groupbut adding some more functionality to it — let’s say storing some additional optional data for group members. Traditionally we would copy the whole implementation from the other contract or use it as a dependency and use its global entry functions — but it doesn’t reflect the encapsulation structure and is a bit difficult to maintain and follow. Figuring out the state spread about multiple crates as global constants might be troubling.

Sylvia tackles this issue by moving the state accessors from global constants to the contract type. That makes sense, as those states are the contract data, and it is nice to express this relation in code. Then to create our new contract, we can make the whole contract type the member of our new contract. The downside of this, for now, is that we still need to redefine all the functions defined in the original contract to call its underlying implementation, like:

struct ExtendedGroup<'a> {
cw4group: Cw4Group<'a>,
metadata: Map<'a, &'a Addr, Metadata>,
}

#[contract]
impl ExtendedGroup {
#[msg(exec)]
fn update_admin(
&self,
ctx: (DepsMut, Env, MessageInfo),
admin: Option<String>
) -> Result<Response, ContractError> {
self.cw4group.update_admin(ctx, admin)
}

// Forward rest of functions and implement additional APIs
}

We are working on making it easier to forward generated implementations of particular functions and achieve simpler API.

Interfaces abstraction

The other example is the primary reason for Sylvia to exist. It is Interface isolation. The best contract to consider is the cw20 contract from the cw-plusrepository.

There is a sendmessage looking like this:

{
"send": {
"contract": "recipient addr",
"amount": "125",
"binary": "base64 encoded execution message",
}
}

Its idea is that it transfers some amount of tokens it manages (cw20 is a custom token contract), sending the given message (encoded in the binaryfield) to the particular recipient (contract field). What is very important is that the message is sent as part of the execution transaction, so the receiver can fail the message execution, in which case tokens will not be transferred. This makes this message very different from sending tokens with a transfermessage and then performing some execution on the other contract.

However, the binarymessage is not sent to the contract as it is. Instead, it is wrapped with additional data, and what the contractreceives is:

{
"receive": {
"sender": "original tokens sender addr",
"amount": "125",
"msg": "base64 encoded execution message",
}
}

So to be able to receive cw20tokens with the transfer message, the contract has to understand the receivemessage in addition to its other execution variants. Unfortunately, CosmWasm doesn’t provide a tool to abstract this message in any way. The best we can do is to provide the message “body” (and we do that with Cw20ReceiveMsgtype). Still, if, for example, the message discriminant changes from receive to cw20_receive in some future cw20 versions, nothing will alert implementors of this functionality.

Here the power of abandoning message-based API for behavior-based API shines. In Sylvia-based implementation of cw20 interfaces, we can define the receive behavior as a trait:

#[interface]
trait Cw20Receive {
#[msg(exec)]
fn receive(
&self,
ctx: (DepsMut, Env, MessageInfo),
sender: String,
amount: Uint128,
msg: Binary
) -> Result<Response, ContractError>;
}

Now to be able to receive the receive message from the cw20 contract, we need to implement the trait on our contract type.

How does it work?

Sylvia framework is a code generator using procedural macros. It provides two main macros: #[interface] for defining traits to be implemented on other contracts and the #[contract] macro for defining the main implementation of the contract functions. Usages of them were shown before, but let’s bring some additional examples:

#[interface]
trait Cw1 {
Error: From<StdError>;

#[msg(exec)]
fn execute(
&self,
ctx: (DepsMut, Env, MessageInfo),
msgs: Vec<CosmosMsg>
) -> Result<Response, Self::Error>;

#[msg(query)]
fn can_execute(
&self,
ctx: (Deps, Env),
sender: String,
msg: CosmosMsg
) -> Result<CanExecuteResp, ContractError>;
}

struct Cw1WhitelistContract<'a> {
admins: Map<'a, &'a Addr, Empty>,
mutable: Item<'a, bool>,
}

#[contract]
#[messages(cw1 as Cw1)]
#[messages(whitelist as Whitelist)]
impl Cw1WhitelistContract<'_> {
pub const fn new() -> Self {
Self {
admins: Map::new("admins"),
mutable: Item::new("mutable"),
}
}

#[msg(instantiate)]
pub fn instantiate(
&self,
ctx: (DepsMut, Env, MessageInfo),
admins: Vec<String>,
mutable: bool
) -> Result<Response, ContractError> {
todo!()
}
}

impl Cw1 for Cw1WhitelistContract<'_> {
// implementing the trait
}

You can find here the definition of a trait common for all contracts implementing cw1 API and the structure keeping together the whole state of a particular contract. Every interface and contract is built from functions annotated with #[msg] attributes — those would be translated to the contract messages. The interface can contain exec messages and query messages while contracts accept additional instantiate and migrate — both once per contract. You may notice a lack of reply and IBC-related messages. For now, Sylvia does not provide support for them, and they have to be handled traditionally. Hopefully, it is not a problem — typically, you define functions on contract types to handle them and manually dispatch them in entry points.

This code would generate tons of boilerplate. Contract messages would be generated from interfaces and contracts so that function names would become enum variants, and their arguments would be fields on those variants. The code above generates messages equivalent to:

enum ExecuteMsg {
Execute {
msgs: Vec<CosmosMsg>
},
// Other variants derived from the `Whitelist` trait
}

enum QueryMsg {
CanExecute {
msgs: Vec<CosmosMsg>
},
// Other variants derived from the `Whitelist` trait
}

struct InstantiateMsg {
admins: Vec<String>,
mutable: bool,
}

In fact, the thing is a bit more complicated — Sylvia generates separated message types for every interface trait and the messages defined on the contract, and then there are additional wrapping messages gluing them together for the contract, but it is an implementation detail.

Additionally, every message type gets a nice `dispatch` function on it with a signature as follows:

fn dispatch(&self, contract: &Contract, ctx: CtxType);

This function takes a final contract type together with the context type depending on what kind of message this is: context type for executing messages is a (DepsMut, Env, MessageInfo) tuple, while for query message, is it a (Deps, Env) tuple, and so on. Those functions are expected to be called in our WASM entry points:

const CONTRACT: Cw1Whitelist = Cw1Whitelist::new();

#[entry_point]
fn instantiate(
deps: DepsMut,
env: env,
info: MessageInfo,
msg: InstantiateMsg
) -> Result<Response, ContractError> {
msg.dispatch(&CONTRACT, (deps, env, info)
}


#[entry_point]
fn execute(
deps: DepsMut,
env: env,
info: MessageInfo,
msg: ContractExecMsg
) -> Result<Response, ContractError> {
msg.dispatch(&CONTRACT, (deps, env, info)
}

#[entry_point]
fn query(
deps: Deps,
env: env,
msg: ContractQueryMsg
) -> Result<Response, ContractError> {
msg.dispatch(&CONTRACT, (deps, env)
}

This way, we keep full compatibility with standard CosmWasm API, which makes it easy to take advantage of all the Sylvia goodies while still using some CosmWasm features Sylvia still lacks a solution for.

Where to start?

The best way to start using the Sylvia crate is by checking out our Sylvia book. It should give you a solid introduction to how to write Smart Contract using the framework. You can also look at our example contracts, which we keep in the Sylvia repository. We slowly add their standard contracts from cw-plus repository so that you can compare those implementations with the originals.

What’s next?

Sylvia’s framework is in the rapid development stage. At the moment of writing this text, I see 20 open issues, 9 of them being labeled as an idea, which means there is something we want to add, but we didn’t yet figure out what it should look like exactly — we are considering options. Right now, we are focusing on generating some helpers for multitests, so instead of calling:

app.execute_contract(
Addr::unchecked("owner"),
contract.clone(),
&ExecMsg::Execute { msgs },
&[]
);

we want you to be able to use a generated contract helper as it would be your contract type:

contract.execute(msgs).exec("owner");

The idea is simple — you pass your message fields as function arguments in the order you defined it on the contract. Then you call exec function to perform the execution (or queryto query the contract), providing the message sender. But what if you want to send some funds with the message? We follow the builder pattern for any additional but not necessary fields:

contract.execute(msgs)
.with_funds(&coins(10, ATOM))
.exec("owner");

And where did we lose the app? It is still there — just hidden in the contract object on its creation, so we don’t need to pass it every time. However, this feature is still in development, and its final shape of it might be slightly different.

Another feature we are working on is generating utilities for constructing messages in the external contract, similar to the multitest utilities. We are also working on dispatching replies from contracts called with sub-messages semantically — we don’t want you to keep track of ids and expected data format. Instead, we want to give you a way to define multiple reply type messages on contracts and give you a nice API to say that a particular handler should handle this sub-message. And there are much more features planned and considered — star us on GitHub and be up to date!

--

--

Bartłomiej Kuras
CosmWasm

Developer and trainer at Confio, CosmWasm maintainer. Rust evangelist, enthusiast of sharing his experience in Software Development.