Use explicit container types with Scrypto! Leverage the Rust compiler's type checking to increase security and productivity when developing Radix blueprints.

Overview

Scrypto Static Types

Test Status Crate API Minimum rustc version License

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 for Bucket (use: BucketOf<MYTOKEN>) and Vault (use: VaultOf<MYTOKEN>). Perfectly coexists with existing builtin types so you can gradually apply these only where you need them.
  • Conveniently defined BucketOf<XRD> and VaultOf<XRD>
  • 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 with arguments referencing ResourceDef's which are simply wrappers around an Address. 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. burning the wrong type of resource).

Usage

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() -> Component {
        let my_bucket = ResourceBuilder::new()  // notice we didn't have to explicitly write out the type ie. Bucket<MYTOKEN>
            .metadata("name", "MyToken")
            .metadata("symbol", "MYTOKEN")
            .new_token_fixed(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
        }
    }
    // or even fewer changes when only setting up a vault.
    pub fn new_easier() -> Component {
        let my_bucket = ResourceBuilder::new()  // this is a regular Bucket, but we're only using it to fill the vault
            .metadata("name", "MyToken")
            .metadata("symbol", "MYTOKEN")
            .new_token_fixed(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>
        }
    }

    // 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 (once they land in main) NFTs.

You can replace function/method argument and return types, local variables, fields in structs (including the main component storage struct) etc,

It's also on the TODO list to add ABI support so that import!(...) can have these same explicit types listed so the generated stub functions are created with these new types too.

You can add typing gradually to your blueprint, or start at the beginning. Simply use some_bucket.into() or some_vault.into() at any boundaries between known/expected static types and unknown dynamic types.

Documentation:

More details can be found in the API documentation including a more complex example.

Crate Features

Optionally, the following features can be enabled:

  • runtime_checks enables checking for type mismatches at runtime when converting between builtin dynamic types (ie. Bucket or Vault) 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 calling Bucket::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 contains VaultOf<NAME> because the Radix Engine will instantiate the component struct very early and decode a Vid into the VaultOf<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 or Vault, even with the exact same error as with a "bad" Bucket::put or Vault::put. Respectively Err(InvokeError(Trap(Trap { kind: Host(BucketError(MismatchingResourceDef)) }))) and Err(InvokeError(Trap(Trap { kind: Host(VaultError(AccountingError(MismatchingResourceDef))) })))

Examples

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

Versions

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).

Current Scrypto Static Types versions are:

  • Version 0.1 was the initial release on December 1st 2021, when Scrypto was still "pre-0.1"

The intent is to:

  • Keep the main branch in sync with the radixdlt-scrypto main branch
  • Keep the develop branch in sync with the radixdlt-scrypto develop branch
    • As a best effort, mostly to support keeping up with main as quickly as possible

A detailed changelog is available for releases (or at least major changes, until we have 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.

Yanked versions

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.

Rust version requirements

Since version 0.1, Scrypto Static Types requires Rustc version 1.56 (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.

Why and How?

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 Buckets and Vaults as dynamic types that could hold anything, when in fact they are bound by their ResourceDef upon creation (for Buckets) and the moment something is deposited (for Vaults). This can lead to entire classes of logic errors which are otherwise not possible. 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 runtime cost. This meant not introducing a new API for dealing with Vaults and Buckets. This is possible with minimal reimplementation 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 Addresses. 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 what is effectly more 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 upstreamed completely into Scrypto. But if that never happens at least we have what is hopefully a high quality library.

Tips

You can support the original author (devmannic) with tips by sending XRD or other tokens on the Radix protocol mainnet to:

rdx1qsppkruj82y8zsceswugc8hmm6m6x22vjgwq8tqj8jnt2vcjtmafw8geyjaj9

License

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.

You might also like...
Annoyed that Rust has many string types? Well it doesn't have to

generic-str The one true string type in Rust! This project intends to be a proof-of-concept for an idea I had a few months back. There is lots of unsa

Rust types for the OASIS Common Alerting Protocol (CAP)

Rust types for the OASIS Common Alerting Protocol (CAP)

A Rust framework for building context-sensitive type conversion.

Xylem is a stateful type conversion framework for Rust.

The never type (the true one!) in stable Rust.

::never-say-never The ! type. In stable Rust. Since 1.14.0. Better than an enum Never {} definition would be, since an instance of type ! automagicall

A list of known SS58 account types as an enum.

A list of known SS58 account types as an enum.

🪣 Types for a `Vec`'s raw parts

raw-parts A wrapper around the decomposed parts of a VecT. This struct contains the Vec's internal pointer, length, and allocated capacity. RawParts

An unsafe botched job that doesn't rely on types being 'static lifetime.

An unsafe botched job that doesn't rely on types being 'static lifetime. Will panic if provided a 0 field struct. I will fix this when I figure out how.

A repository full of manually generated hand curated JSON files, which contain the API Types that the Discord API returns.

Discord API Types A repository full of manually generated hand curated JSON files, which contain the API Types that the Discord API returns. Also did

Application that simulates a large grid of Pokémon types fighting each other.
Application that simulates a large grid of Pokémon types fighting each other.

poke-fighting-rust Rust project that simulates a grid of Pokémon fighting with each other. Each Pokémon type is a pixel on the grid and is represented

Comments
  • Use specific version of Scrypto

    Use specific version of Scrypto

    Projects using scrypto_statictypes are failing because it's no long compatible with the latest Scrypto. This PR is a quick fix to use a tagged version of Scrypto as dependency temporarily.

    opened by iamyulong 3
  • Feature/scrypto v0.4.1

    Feature/scrypto v0.4.1

    Update for compatibility with scrypto v0.4.x (currently v0.4.1)

    Mostly done. Still TODO:

    • [x] review Account traits
    • [x] check for new/missing APIs for all wrapped types
    • [x] bring back some extra tests in the examples
    opened by devmannic 0
  • Add ResourceOf and BucketRefOf

    Add ResourceOf and BucketRefOf

    • Adds these new types and fully integrates them (with a bunch of refactoring)
    • runtime checks now ensure resource to address mapping is 1:1.
    • Includes version bump to 0.3.0, expecting to release after merge
    opened by devmannic 0
  • runtime check ideas for alternate lifetime of the resource binding

    runtime check ideas for alternate lifetime of the resource binding

    Currently the resources are bound in a 'static HashMap. This works fine for simple use cases and in many settings as it matches the lifetime of a transaction.

    But, for a component that could have methods called on multiple instances in the same transaction, a single mapping for the entire transaction may not be accurate. Instead there needs to be a mapping per component. We don't want to store this mapping on-ledger in generally, though maybe we could (optionally).

    • [ ] Need to see if the Radix Engine spins up a separate process per function/method call or not. If separate, everything is fine with the current implementation. If not, we need options:
    1. One option is a map of maps, the first level indexed on the component address, and the second level matching what currently exists. However, getting the current component without it being passed directly could be tricky.
      • Might require patching the dispatcher to store the current component or reference to the current map.
      • Or maybe Decode on the component could be overridden
      • or hooked via a custom type stored in the component. This would take up some space (unless we could do some matching with PhantomData) but could be minimal. -- this method is closest to just storing the entire mapping on-ledger so maybe this provides a consistent interface with levels of complexity/difficulty that can be adjusted later
    opened by devmannic 0
Releases(v0.5.0)
  • v0.5.0(Jul 3, 2022)

  • v0.4.1(Feb 20, 2022)

  • v0.4.0(Feb 20, 2022)

  • v0.3.1(Feb 20, 2022)

    Continues to support Scrypto v0.2.0

    Added

    • Common impl for From<$w> for $t to automatically unwrap making API usage cleaner
    • Account::deposit_of::<RESOURCE> and Account::withdraw_of::<RESOURCE>
    • Comparison between ResourceDef and ResourceOf with == and !=

    Fixed

    • warnings on resource names (any case is now allowed)
    • missing trait in prelude hiding BucketRefOf::unchecked_into

    See the CHANGELOG.md

    Full Changelog: https://github.com/devmannic/scrypto_statictypes/compare/v0.3.0...v0.3.1

    Source code(tar.gz)
    Source code(zip)
  • v0.3.0(Dec 25, 2021)

    • Added ResourceOf and BucketRefOf completing support for all Scrypto container types!
    • Still compatible with Scrypto v0.2.0 (and latest main branch revision at time of release).

    See the CHANGELOG.md

    Full Changelog: https://github.com/devmannic/scrypto_statictypes/compare/v0.2.0...v0.3.0

    Source code(tar.gz)
    Source code(zip)
  • v0.2.0(Dec 24, 2021)

Owner
null
An upper-level course for CS majors on formal languages theory and compilers.

CS4100 Introduction to Formal Languages and Compilers Spring 2022 An upper-level course for CS majors on formal languages theory and compilers. Topics

null 2 May 28, 2022
Milho (corn in portuguese) is a toy dialect of Lisp written as a way to learn more about compilers

Milho (corn in portuguese) is a toy dialect of Lisp written as a way to learn more about compilers. There are implementations in rust and go

Celso Bonutti 27 May 4, 2022
A collection of compilers based around compiling a high level language to a Brainfuck dialect.

tf A collection of compilers based around compiling a high level language to a Brainfuck dialect. Built at, and for, the VolHacks V hackathon during O

adam mcdaniel 6 Nov 25, 2021
A "cursed" container with opaque keys, usable as a static variable

cursedcontainer Please, for the love of all things good in the world, do not use this crate unless you've read the code thoroughly and understand the

the iris system 4 Mar 17, 2022
Applied offensive security with Rust

Black Hat Rust - Early Access Deep dive into offensive security with the Rust programming language Buy the book now! Summary Whether in movies or main

Sylvain Kerkour 2.2k Jan 2, 2023
An implementation of Messaging Layer Security (RFC 9420)

mls-rs   An implementation of the IETF Messaging Layer Security end-to-end encryption (E2EE) protocol. What is MLS? MLS is a new IETF end-to-end encry

Amazon Web Services - Labs 3 Nov 9, 2023
A rust program to try and detect some types of Hardware Keyloggers.

Hardware Keylogger Detection Warning: Certain Types of Hardware keyloggers can not be detected by this program, Passive Hardware Keyloggers are imposs

null 4 Dec 5, 2022
The most primitive and the fastest implementation of a fixed-size last-in-first-out stack on stack in Rust, for Copy-implementing types

This is the simplest and the fastest (faster than Vec!) implementation of a last-in-first-out stack data structure, on stack, when stack elements are

Yegor Bugayenko 10 Jun 18, 2023
Option and Either types with variants known at compile time.

Const Either Some types to allow deciding at compile time if an option contains a value or which variant from the either type is active. This might be

null 1 May 5, 2022
Traits for inspecting memory usage of Rust types

memuse This crate contains traits for measuring the dynamic memory usage of Rust types. About Memory-tracking is a common activity in large applicatio

null 13 Dec 23, 2022