For now contract interfaces are defined by structures defining messages (queries/executes/migrations/instantiations) for contracts. On the one hand this seems simple, but writing contracts I found problems with this approach. First of all re usability and extend ability of contracts. If there is cw20-base contract defining some basic implementation of cw20 token, and then there is another contract which is based on it, but just extending it further, then the new contract is typically copying messages from base contract, adding on top of them, and in the implementation basically forward-calling implementation of cw20-base. I find this messy, and difficult to maintain - in case of update of cw20-base, all contracts basing on it should be probably updated, but in reality we technically cannot do that - because someone may just have another contract based on it, and he would not be notified anyhow about changes (unless he reads all changelogs).
My proposal is, to add possibility of more natural way of defining interfaces - as traits. For cw20 contract trait could look like:
#[cw_derive::interface]
trait Cw20Base {
#[exec]
fn transfer(&self, ctx: CtxMut, recipient: Addr, amount: u128) -> Result<Response, Error>;
#[exec]
fn burn(&self, ctx: CtxMut, amount: String) -> Result<Response, Error>;
// Another exec messages
// ...
#[query]
fn balance(&self, ctx: Ctx, address: Addr) -> Result<BalanceResponse, Error>;
#[query]
fn token_info(&self, ctx: Ctx) -> Result<TokenInfoResponse, Error>;
// Another queries
// ...
}
The whole idea is to provide interface attribute which generates structures for parsing from jsons, and generates dispatching function which takes parsed input and calls proper functions. Also this handles intermediate wrappers like Uint128 which as far as I understand are just for proper parsing, but using them in Rust code itself seems inconvenient. Also taking Addr in interfaces is proper here - dispatcher can know to convert it to validate it with Addr::validate(...). The additional Ctx type is helper providing access to things such as Deps/DepsMut, Env, MessageInfo.
The second part of this would be actually generating implementation structures, like:
#[cw_derive::contract(Cw20Base)]
struct Cw20BaseContract;
#[cw_derive::contract_impl]
impl Cw20BaseContract {
#[instantiate]
fn instantiate(&mut self, ctx: CtxMut, name: String, symbol: String, ...)
-> Result<Response, Error> { todo!() }
#[migrate]
fn migrate(&mut self, ctx: CtxMut) -> Result<Response, Error> { todo!() }
}
impl Cw20Base for Cw20BaseContract {
// ...
}
The contract macro takes as arguments all interfaces traits which would be implemented, so additional dispatch function is generated - basically creating intermediate serde deserializable untagged enum, and after deserialization dispatching to proper trait implementation. The downside of this is what happens in case of interface conflicts (same message possible for two interfaces) - but either it can be ignored, then just earlier interface in the list has precedence on handling message, or it is basically possible to detect such cases in generator itself to report an error. The last thing which the contract would take care of is generating entry points for the contracts, which would create Cw20Contract inplace, and call proper function on this.
Just to make whole thing more complete, here is how the contract extending Cw20Base could look like:
#[cw_derive::interface]
trait Cw20Bonding {
#[exec]
fn buy(&mut self, ctx: CtxMut) -> Result<Response, Error>;
#[query]
fn curve_info(&self, ctx: Ctx) -> Result<CurveInfoResponse, Error>;
}
#[cw_derive::contract(Cw20Base, Cw20Bonding)]
// This is generic so mock implementation can be injected. It could be simplified by `Box<dyn Cw20Base>`
// but it gains dynamic dispatch overhead, and even space overhead (in most cases ZST vs fat pointer)
struct Cw20BondingContract<Cw20Base> {
base: Cw20BaseContract,
}
// Instantiation and migration omitted, but should be similar to base contract
#[cw_derive::forward_impl(
base;
transfer, send, increase_allowance, decrease_allowance,
transfer_from, send_from
)]
impl<Base: Cw20Base> Cw20Base for Cw20BondingContract<Base> {
// Need to hand implement everything which is not forwarded
fn burn(&mut self, ctx: CtxMut, amount: u128) -> Result<Response, Error> { todo!() }
fn burn_from(&mut self, ctx: CtxMut, owner: Addr, amount: u128)
-> Result<Response, Error> { todo!() }
}
impl<Base: Cw20Base> Cw20Bonding for Cw20BondingContract<Base> {
// Implementation of `buy` and `curve_info`
}
The most upside of this approach is, that it does not impact interfacing with wasm at all - whole toolset can be even separated crate on top of wasmstd (possibly should be). And upsides of this approach are:
- Contracts are more behavior focused instead of messages focused. This strongly improves testability - on unit test level, the base contract can be substituted with mock implementation (possibly generated with mockall or similar tool). This way instead of testing all contract in the stack (which is good but on another level), only new implementation is tested assuming, as underlying one is correct
- Easier maintenance of extending contracts - when upgrading to new version of base contract, there would be missing functions implementations
- More similarity to solidity framework (maybe not so important, but at the end it makes dev transition easier)
- Possibility to extend it to generate additional helpers (some contracts actually contain helpers performing cross-contracts queries - it can be unified)
Downsides:
- Requires time to develop
- Probably problems I am not (yet) aware of
For now this is just brief idea, to get feedback what ppl think about such approach. If it would be positive, than I could process to implement some PoC (as separated crate - making it part of cosmwasm-std may be some step in future, but is actually not needed, however possibly nice for unification purposes).
Also this draft is basically long term direction how I see interfaces for contracts may look like, proof of concept would be developed in small chunks progressing to some final form. And obviously - there may be difficulties I cannot predict for now (any suggestions would be nice).
For now contract interfaces are defined by structures defining messages (queries/executes/migrations/instantiations) for contracts. On the one hand this seems simple, but writing contracts I found problems with this approach. First of all re usability and extend ability of contracts. If there is cw20-base contract defining some basic implementation of cw20 token, and then there is another contract which is based on it, but just extending it further, then the new contract is typically copying messages from base contract, adding on top of them, and in the implementation basically forward-calling implementation of cw20-base. I find this messy, and difficult to maintain - in case of update of cw20-base, all contracts basing on it should be probably updated, but in reality we technically cannot do that - because someone may just have another contract based on it, and he would not be notified anyhow about changes (unless he reads all changelogs).
My proposal is, to add possibility of more natural way of defining interfaces - as traits. For cw20 contract trait could look like:
The whole idea is to provide
interfaceattribute which generates structures for parsing from jsons, and generates dispatching function which takes parsed input and calls proper functions. Also this handles intermediate wrappers likeUint128which as far as I understand are just for proper parsing, but using them in Rust code itself seems inconvenient. Also takingAddrin interfaces is proper here - dispatcher can know to convert it to validate it withAddr::validate(...). The additionalCtxtype is helper providing access to things such asDeps/DepsMut,Env,MessageInfo.The second part of this would be actually generating implementation structures, like:
The
contractmacro takes as arguments all interfaces traits which would be implemented, so additional dispatch function is generated - basically creating intermediate serde deserializable untagged enum, and after deserialization dispatching to proper trait implementation. The downside of this is what happens in case of interface conflicts (same message possible for two interfaces) - but either it can be ignored, then just earlier interface in the list has precedence on handling message, or it is basically possible to detect such cases in generator itself to report an error. The last thing which thecontractwould take care of is generating entry points for the contracts, which would createCw20Contractinplace, and call proper function on this.Just to make whole thing more complete, here is how the contract extending
Cw20Basecould look like:The most upside of this approach is, that it does not impact interfacing with wasm at all - whole toolset can be even separated crate on top of wasmstd (possibly should be). And upsides of this approach are:
Downsides:
For now this is just brief idea, to get feedback what ppl think about such approach. If it would be positive, than I could process to implement some PoC (as separated crate - making it part of cosmwasm-std may be some step in future, but is actually not needed, however possibly nice for unification purposes).
Also this draft is basically long term direction how I see interfaces for contracts may look like, proof of concept would be developed in small chunks progressing to some final form. And obviously - there may be difficulties I cannot predict for now (any suggestions would be nice).