Use explicit container types with Scrypto! Leverage the Rust compiler's type checking to increase security and productivity when developing Radix blueprints.
A Scrypto (Rust) library for static types, featuring:
- A simple, usable, safe, and (by default) zero-cost API for compile-time static type checking of resources.
- Safe drop in replacements coexist with existing types. Gradually apply these only where you need them:
Bucket
-->BucketOf<MYTOKEN>
Vault
-->VaultOf<MYTOKEN>
ResourceAddress
-->ResourceOf<MYTOKEN>
Proof
-->ProofOf<MYTOKEN>
- Conveniently defined
XRD
Resource to use withVaultOf<XRD>
, and friends. - Simple macro to declare new resources:
declare_resource!(MYTOKEN)
- Optional feature
runtime_typechecks
for safety critical code, or use in testing.
It's also worth pointing out what scrypto_statictypes
is not:
- A silver bullet. This library can ensure that within a single component there are no resource mismatches, and it can do this all at compile time. But, There is no completely static way to ensure proper usage of resources received by a component. Any (incorrect) ABI may be used when calling a function or method on a component. Transactions are inherently dynamic. Runtime checks are still needed, and luckily are performed by the Radix Engine. However, this library can still help avoid implementation errors which would go undetected by the Radix Engine (ie. logic errors manipulating the wrong type of resource).
Add this to your Cargo.toml
:
[dependencies]
scrypto_statictypes = { git = "https://github.com/devmannic/scrypto_statictypes" }
Optionally, add this to your Cargo.toml
for extra checks at runtime:
[features]
default = ["scrypto_statictypes/runtime_typechecks"]
Setup static types in your code near the top:
use scrypto::prelude::*; // This doesn't replace Scrypto's builtin types. You will still need them sometimes.
use scrypto_statictypes::prelude::*; // opt-in to use scrypto_statictypes!
// Give a name to any resource you want to statically type check.
declare_resource!(MYTOKEN); // without a known address
// declare_resource!(XRD, RADIX_TOKEN); // or with a known address (but this line isn't needed, scrypto_statictypes already conveniently includes it)
Simple example for a blueprint which creates a token with a declared MYTOKEN
resource:
use scrypto::prelude::*; // This doesn't replace Scrypto's builtin types. You will still need them sometimes.
use scrypto_statictypes::prelude::*; // opt-in to use scrypto_statictypes
// the new way
declare_resource!(MYTOKEN); // declare a name for a resource that this blueprint creates, or when the `Address` is unknown or not needed.
blueprint! {
struct MyComponent {
// my_vault: Vault // the old way
my_vault: VaultOf<MYTOKEN> // the new way
}
pub fn new() -> ComponentAddress {
let my_bucket = ResourceBuilder::new_fungible() // notice we didn't have to explicitly write out the type ie. Bucket<MYTOKEN>
.metadata("name", "MyToken")
.metadata("symbol", "MYTOKEN")
.initial_supply(1000)
.into(); // the new way: .into() needed to convert Bucket -> Bucket<MYTOKEN>
Self {
// my_vault: Vault::with_bucket(my_bucket) // the old way
my_vault: VaultOf::with_bucket(my_bucket) // the new way: use VaultOf instead of Vault
}
.instantiate()
.globalize()
}
// or even fewer changes when only setting up a vault.
pub fn new_easier() -> ComponentAddress {
let my_bucket = ResourceBuilder::new_fungible() // this is a regular Bucket, but we're only using it to fill the vault
.metadata("name", "MyToken")
.metadata("symbol", "MYTOKEN")
.initial_supply(1000);
Self {
// my_vault: Vault::with_bucket(my_bucket) // the old way
my_vault: Vault::with_bucket(my_bucket).into() // the new way: .into() needed to convert Vault -> VaultOf<MYTOKEN>
}
.instantiate()
.globalize()
}
// old way (or for when the resource type really is allowed to be anything, just keep using Bucket)
pub fn receive_any_tokens(&mut self, bucket: Bucket) { /* ... */ }
// new way - and when optional feature `runtime_typechecks` is enabled, the resource is confirmed to match a "MYTOKEN" before the function body executes.
pub fn receive_my_tokens(&mut self, bucket: BucketOf<MYTOKEN>) { /* ... */ }
}
For any buckets where the asset is of a known type (most of them) replace:
Bucket
-> BucketOf<MYTOKEN>
Similarly with vaults, replace:
Vault
-> VaultOf<MYTOKEN>
This provides a way to explicitly name any resource. It can be used for anything that can go
in a Bucket
or Vault
. This includes badges and NFTs.
That's just the beginning.... You can also replace:
ResourceAddress
-> ResourceOf<MYTOKEN>
Proof
-> ProofOf<MYTOKEN>
And with runtime checks enabled, you get a more
convenient and safe API for doing "pass-by-intent" style Proof arguments. You can
use one or more ProofOf
arguments to limit access without manual checks against
some ResourceAddress
. Simply change the type from ResourceAddress
to ResourceOf<SOMETHING>
in the Component struct. Or if you already have a VaultOf<SOMETHING>
no extra ResourceOf
is needed.
To let the compiler help as much as possible the ProofOf
type will automatically
drop the reference to its bucket when it goes out of scope. This works correctly when returning ProofOf
s or calling other functions/methods. Never worry about explicitly calling some_proof.drop()
.
These new types are usable everywhere: replace function/method argument and return types, local variables, fields in structs (including the main component storage struct). Everything just works.
For converting existing blueprints just add typing gradually and follow the compiler warnings as a way to audit the code and reach a more secure implementation. At the "boundaries" where these static types need to be converted, simply use .into()
for type checked conversions into any of these new types, and .unwrap()
to convert back to the old, dynamic (standard Scrypto) types.
More details can be found in the API documentation including a more complex example.
Optionally, the following features can be enabled:
runtime_checks
enables checking for type mismatches at runtime when converting between builtin dynamic types (ie.Bucket
orVault
) and static types.- This may catch errors earlier than the builtin checks that happen on ie.
Bucket::put
and similar, and in some cases could detect bugs that would go undetected, such as callingBucket::burn
on the wrong bucket. - This relies on either an address provided when declaring the resource with
declare_resource!(NAME, ADDRESS)
or if no address is provided the implementation remembers the first address seen and checks all future accesses match. This works nicely when a component containsVaultOf<NAME>
because the Radix Engine will instantiate the component struct very early and decode aVid
into theVaultOf<NAME>
which correctly binds the address. - Errors caught are trapped to the Radix Engine runtime failing the transaction immediately in
exactly the same way as when using
Bucket
orVault
, even with the exact same error as with a "bad"Bucket::put
orVault::put
. RespectivelyErr(InvokeError(Trap(Trap { kind: Host(BucketError(MismatchingResourceManager)) })))
andErr(InvokeError(Trap(Trap { kind: Host(VaultError(AccountingError(MismatchingResourceManager))) })))
- This may catch errors earlier than the builtin checks that happen on ie.
See the directories in /examples for complete scrypto packages utilizing this functionality.
- /examples/mycomponent - Same example as in the API reference (master branch) so you can easily try and see the compiler errors
- /examples/badburn1 - Example blueprint which does NOT use
scrypto_statictypes
and has a logic error which leads to burning the bucket argument even if it was the wrong asset - /examples/fixburn1 - Direct modification of
BadBurn
to use static types everywhere, and enable runtime type checks. The test case shows the "bad burn" is caught and the tx fails. -- checkout just the diff of changes in /misc/bad2fixburn1.diff - /examples/manyrefs - Example using ProofOf a whole lot showing it's usefulness for nuanced authentication/verification
Scrypto Static Types is suitable for general usage, but not yet at 1.0. We maintain compatibility with Scrypto and pinned versions of the Rust compiler (see below).
Latest Scrypto Static Types versions for Scrypto versions are:
- Version 0.5.0 depending on Scrypto version 0.4.1
- Version 0.4.1 depending on Scrypto version 0.3.0
- Version 0.3.1 depending on Alexandria Scrypto version 0.2.0
- Version 0.1.1 depending on Pre-Alexandria Scrypto version 0.1.1
The intent is to:
- Keep the
main
branch in sync with theradixdlt-scrypto
main
branch - Keep the
develop
branch in sync with theradixdlt-scrypto
develop
branch- As a best effort, mostly to support keeping up with
main
as quickly as possible
- As a best effort, mostly to support keeping up with
A detailed changelog is available for releases
Scrypto Static Types has not yet reached 1.0 implying some breaking changes may arrive in the future (SemVer allows each 0.x.0 release to include breaking changes), but breaking changes are minimized and breaking releases are infrequent.
When specifying the dependency requirement using scrypto_statictypes = { git = "https://github.com/devmannic/scrypto_statictypes" }
the latest release will always be used, ignoring SemVer. When Scrypto is
published to crates.io, this library will also be published and it is advised to
update your Cargo.toml
then to scrypto_statictypes = "^0"
or later.
Some versions of Scrypto Static Types crates may be yanked ("unreleased") from crates.io. Where this occurs,
the crate's CHANGELOG should be updated with a rationale, and a search on the
issue tracker with the keyword yank
should uncover the motivation.
Since version 0.5, Scrypto Static Types requires Rustc version 1.60 (2021 edition) or greater.
Continuous Integration (CI) will always test the minimum supported Rustc version (the MSRV). The current policy is that this can be updated in any Scrypto Static Types release if required, but the change must be noted in the changelog.
I believe Radix will be a game-changing technology stack for Decentralized
Finance. Scrypto is already amazing and going to continue to evolve. I think
at this very early stage it does so many things right, however it's a missed
opportunity to treat all Bucket
s and Vault
s as dynamic types that could
hold anything, when in fact they are bound by their held ResourceAddress
upon creation.
There is also a lot of duplicate code to check Proof
s which are often security critical when they are used for authentication. These omissions can lead
to entire classes of logic errors which could be avoided. This means
lost productivity at best, and real vulnerabilities at worst. I didn't want
development practices to standardize leaving these gaps.
So, I took up the challenge to find a usable way to recreate strong static
types with no (or at least minimal) runtime cost. This meant not introducing a new API for dealing
with Vaults and Buckets. This is possible with minimal re-implementation which
is inlined and effectively disappears since they are just proxies around the
original types. The main changes are to enable type propagation in parameters
and return types, and then implementing Rust's Deref
and DerefMut
traits
gets us the rest of the way. It makes the usage seamless. And since
it's implemented with Rust's generics and PhantomData
there is no extra
storage of the type information.
Then, going a step further I added runtime tests of the underlying ResourceAddress
es.
This is behind a feature flag so is opt-in as it does add some extra
performance overhead. But, there are cases where it can absolutely detect a
logic error due to misuse of a Bucket or Vault, when the current Radix Engine
constraints would not stop the error. This is because the Radix Engine just doesn't have
the information about the developer's intent. By using
specific type annotations this intent can be captured and then appropriately tested
leading to failed transactions instead of vulnerabilities.
All of this is completely optional, and it can be used to gradually add types to programs where it is helpful.
My hope is that others find this valuable and make good use of it. I would actually love to see this merged upstream into Scrypto. But if that never happens at least we have what is hopefully a high quality library.
You can support the original author (devmannic
) with tips by sending XRD or other
tokens on the Radix protocol mainnet to:
rdx1qsppkruj82y8zsceswugc8hmm6m6x22vjgwq8tqj8jnt2vcjtmafw8geyjaj9
Scrypto Static Types is distributed under the terms of both the MIT license and the Apache License (Version 2.0).
See LICENSE-APACHE and LICENSE-MIT, and COPYRIGHT for details.