Skip to content

Commit

Permalink
Merge pull request #2 from devmannic/feature/resourceof-bucketrefof
Browse files Browse the repository at this point in the history
Add ResourceOf and BucketRefOf
  • Loading branch information
devmannic authored Dec 25, 2021
2 parents 23f85e4 + 7b34487 commit 5a074ec
Show file tree
Hide file tree
Showing 24 changed files with 1,458 additions and 274 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,4 +66,4 @@ jobs:
- name: Test examples
run: |
cargo install --git https://github.com/radixdlt/radixdlt-scrypto --tag v0.1.1 simulator
for example in examples/*; do (cd $example && scrypto test); done
./utils/test_examples.sh
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,21 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.3.0] - 2021-12-24
### Added
- Implemented ResourceOf and BucketRefOf
- Added more tests
### Changed
- Bucket and Vault container types - have methods which require ResourceOf and BucketRefOf
- Refactored with macros for better code reuse while retaining good error messages
- Runtime checks ensure resource name to address mapping is 1:1 to catch certain errors

## [0.2.0] - 2021-12-16
### Changed
- Compatibility updates for Alexandria Scrypto v0.2.0

## [0.1.1] - 2021-12-16
### Changed
- Pin versions to pre-Alexandria Scrypto v0.1.1

## [0.1.0] - 2021-12-01
Expand Down
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "scrypto_statictypes"
version = "0.2.0"
version = "0.3.0"
rust-version = "1.56"
authors = ["devmannic <[email protected]>"]
license = "MIT OR Apache-2.0"
Expand Down
57 changes: 35 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,12 @@ 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>`
- Safe drop in replacements coexist with existing types. Gradually apply these only where you need them:
- `Bucket` --> `BucketOf<MYTOKEN>`
- `Vault` --> `VaultOf<MYTOKEN>`
- `ResourceDef` --> `ResourceOf<MYTOKEN>`
- `BucketRef` --> `BucketRefOf<MYTOKEN>`
- Conveniently defined `XRD` Resource to use with `VaultOf<XRD>`, and friends.
- Simple macro to declare new resources: `declare_resource!(MYTOKEN)`
- Optional feature `runtime_typechecks` for safety critical code, or use in
testing.
Expand Down Expand Up @@ -115,17 +117,25 @@ 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.
in a `Bucket` or `Vault`. This includes badges and NFTs.

You can replace function/method argument and return types, local variables,
fields in structs (including the main component storage struct) etc,
That's just the beginning.... You can also replace:

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.
`ResourceDef` -> `ResourceOf<MYTOKEN>`

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.
`BucketRef` -> `BucketRefOf<MYTOKEN>`

And with runtime checks enabled, you get a more
convenient and safe API for checks that used to be done with `#[auth(some_resource_def)]`. You can
use one or more `BucketRefOf` arguments to limit access without manual checks against
`some_resource_def`. Simply change the type of `some_resource_def` to `ResourceOf<SOMETHING>` in the Component struct. Or if you already have a `VaultOf<SOMETHING>` no new `ResourceOf` is needed.

To let the compiler help as much as possible the `BucketRefOf` type will automatically
drop the reference to its bucket when it goes out of scope. This works correctly when returning `BucketRefOf`s or calling other functions/methods. Never worry about explicitly calling `bucketref.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.

## Documentation:

Expand Down Expand Up @@ -159,15 +169,16 @@ See the directories in [/examples](/examples) for complete scrypto packages util
* [/examples/mycomponent](/examples/mycomponent) - Same example as in the [API reference (master branch)](https://devmannic.github.io/scrypto_statictypes) so you can easily try and see the compiler errors
* [/examples/badburn1](/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](/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](/misc/bad2fixburn1.diff)

* [/examples/manyrefs](/examples/manyrefs) - Example using BucketRefOf a whole lot showing it's usefulness for nuanced authentication/verification

## 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:
Latest Scrypto Static Types versions for Scrypto versions are:

- Version 0.1 was the initial release on December 1st 2021, when Scrypto was still "pre-0.1"
- Version 0.3.0 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:

Expand Down Expand Up @@ -206,15 +217,17 @@ 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 `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
hold anything, when in fact they are bound by their `ResourceDef` upon creation.
There is also a lot of duplicate code checking `BucketRef`s as the main form of
authentication when it could be done declaratively in more places than just the existing
auth macros. 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 runtime cost. This meant not introducing a new API for dealing
with Vaults and Buckets. This is possible with minimal reimplementation which
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
Expand All @@ -235,15 +248,15 @@ 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
love to see this merged upstream 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
`rdx1qsppkruj82y8zsceswugc8hmm6m6x22vjgwq8tqj8jnt2vcjtmafw8geyjaj9`


# License
Expand Down
1 change: 0 additions & 1 deletion SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ exceptions for theoretical issues without a known exploit:

| Crate | Versions | Exceptions |
| ----- | -------- | ---------- |
| `scrypto_statictypes` | 0.1 | |


## Known issues
Expand Down
4 changes: 3 additions & 1 deletion examples/fixburn1/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@ panic = 'abort' # Abort on panic.
[lib]
crate-type = ["cdylib", "lib"]
name = "out"
path = "src/choose.rs"

[features]
default = ["scrypto_statictypes/runtime_typechecks"]
default = ["runtime_typechecks"]
runtime_typechecks = ["scrypto_statictypes/runtime_typechecks"]

[workspace]
5 changes: 5 additions & 0 deletions examples/fixburn1/src/choose.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#[cfg(feature = "runtime_typechecks")]
mod lib;

#[cfg(not(feature = "runtime_typechecks"))]
mod staticlib;
32 changes: 25 additions & 7 deletions examples/fixburn1/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,24 @@ use scrypto_statictypes::prelude::*;

declare_resource!(FLAM);
declare_resource!(INFLAM);
declare_resource!(AUTH);
declare_resource!(MINTER);

blueprint! {
struct FixBurn {
// Define what resources and data will be managed by Hello components
flam_vault: VaultOf<FLAM>,
inflam_vault: VaultOf<INFLAM>,
auth_def: ResourceDef,
minter: Vault,
auth_def: ResourceOf<AUTH>,
minter: VaultOf<MINTER>,
}

impl FixBurn {
pub fn new() -> (Component, Bucket, BucketOf<FLAM>) {
// create one owner badge for auth for the burn_it functino
let minter = ResourceBuilder::new_fungible(DIVISIBILITY_NONE).initial_supply_fungible(1);
// create 1 minter badge for 2 resources, FLAM and INFLAM
let owner = ResourceBuilder::new_fungible(DIVISIBILITY_NONE).initial_supply_fungible(1);
// create 1 minter badge type for 2 resources, FLAM and INFLAM, but quanity 2 to test the take_all_inflam method
let owner = ResourceBuilder::new_fungible(DIVISIBILITY_NONE).initial_supply_fungible(2);
// create FLAM and mint 1000
let flammable_bucket: BucketOf<FLAM> = ResourceBuilder::new_fungible(DIVISIBILITY_MAXIMUM)
.metadata("name", "BurnMe")
Expand All @@ -42,8 +44,8 @@ blueprint! {
let c = Self {
flam_vault: flam_vault,
inflam_vault: VaultOf::with_bucket(inflammable_bucket), // all 1000 INFLAM stay here
auth_def: owner.resource_def(), // save this so we can use #[auth(auth_def)]
minter: Vault::with_bucket(minter), // keep this so we can burn)
auth_def: owner.resource_def().into(), // save this so we can use #[auth(auth_def)] or BucketRefOf<AUTH> as an argument
minter: Vault::with_bucket(minter).into(), // keep this so we can burn)
}
.instantiate();
(c, owner, flammable_bucket)
Expand All @@ -59,5 +61,21 @@ blueprint! {
self.minter.authorize(|auth| incoming.burn_with_auth(auth));
result
}

// auth works the same way here, as long as the runtime type checks feature is enabled. auth will drop without needing the macro
pub fn take_all_inflam(&mut self, auth: BucketRefOf<AUTH>) -> BucketOf<INFLAM> {
// let auth: BucketRefOf<AUTH> = auth.into(); // alternately, do this while using the auth macro with keep_auth and remove the auth parameter
assert_eq!(auth.amount(), 2.into()); // need 2 badges to take everything
self.inflam_vault.take_all()
}

// alternately, if runtime checks are not enabled, it will fail to compile BucketRefOf<AUTH> as an argument since it purposefully does not allow Decode
// and if used with .into() (as shown below) it will always panic since it cannot be be checked
#[auth(auth_def, keep_auth)]
pub fn take_all_inflam_static(&mut self, /*auth: BucketRefOf<AUTH>*/) -> BucketOf<INFLAM> {
let auth: BucketRefOf<AUTH> = auth.into(); // compiles, but panics
assert_eq!(auth.amount(), 2.into()); // need 2 badges to take everything
self.inflam_vault.take_all()
}
}
}
}
96 changes: 96 additions & 0 deletions examples/fixburn1/src/staticlib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
//! This is the same as lib.rs except it explicitly does not include BucketRefOf in a Decode location so it can compile without runtime typechecks
//!
//! But first, here's a short doctest showing how BucketRefOf wont compile when used as an argument
//! ```compile_fail
//! # #[macro_use] extern crate scrypto_statictypes;
//! # fn main() {}
//! use scrypto::prelude::*;
//! use scrypto_statictypes::prelude::*;
//!
//! declare_resource!(AUTH);
//!
//! blueprint! {
//!
//! struct StaticComponent {
//! auth_def: ResourceOf<AUTH>,
//! }
//!
//! impl StaticComponent {
//! pub fn do_with_auth(&mut self, _auth: BucketRefOf<AUTH>) { /* ... */ }
//! }
//! }
//! ```
use scrypto::prelude::*;
use scrypto_statictypes::prelude::*;

declare_resource!(FLAM);
declare_resource!(INFLAM);
declare_resource!(AUTH);
declare_resource!(MINTER);

blueprint! {
struct FixBurn {
// Define what resources and data will be managed by Hello components
flam_vault: VaultOf<FLAM>,
inflam_vault: VaultOf<INFLAM>,
auth_def: ResourceOf<AUTH>,
minter: VaultOf<MINTER>,
}

impl FixBurn {
pub fn new() -> (Component, Bucket, BucketOf<FLAM>) {
// create one owner badge for auth for the burn_it functino
let minter = ResourceBuilder::new_fungible(DIVISIBILITY_NONE).initial_supply_fungible(1);
// create 1 minter badge type for 2 resources, FLAM and INFLAM, but quanity 2 to test the take_all_inflam method
let owner = ResourceBuilder::new_fungible(DIVISIBILITY_NONE).initial_supply_fungible(2);
// create FLAM and mint 1000
let flammable_bucket: BucketOf<FLAM> = ResourceBuilder::new_fungible(DIVISIBILITY_MAXIMUM)
.metadata("name", "BurnMe")
.metadata("symbol", "FLAM")
.flags(MINTABLE | BURNABLE)
.badge(minter.resource_address(), MAY_MINT | MAY_BURN)
.initial_supply_fungible(1000)
.into();

// create INFLAM and mint 1000
let inflammable_bucket = ResourceBuilder::new_fungible(DIVISIBILITY_MAXIMUM)
.metadata("name", "KeepMe")
.metadata("symbol", "INFLAM")
.flags(MINTABLE | BURNABLE) // this specific accidental burn bug could ALSO be fixed my removing the BURNABLE flag, but that doesn't fix this entire class of bug
.badge(minter.resource_address(), MAY_MINT | MAY_BURN)
.initial_supply_fungible(1000)
.into();

// setup component storage
let flam_vault = VaultOf::with_bucket(flammable_bucket.take(800)); // FLAM: 800 stay here, 200 are returned
let c = Self {
flam_vault: flam_vault,
inflam_vault: VaultOf::with_bucket(inflammable_bucket), // all 1000 INFLAM stay here
auth_def: owner.resource_def().into(), // save this so we can use #[auth(auth_def)] or BucketRefOf<AUTH> as an argument
minter: Vault::with_bucket(minter).into(), // keep this so we can burn)
}
.instantiate();
(c, owner, flammable_bucket)
}

#[auth(auth_def)]
pub fn burn_it(&mut self, incoming: BucketOf<FLAM>) -> BucketOf<INFLAM> {
// burn all but 5, give back same amount of inflam
if incoming.amount() > 5.into() {
self.flam_vault.put(incoming.take(5));
}
let result = self.inflam_vault.take(incoming.amount());
self.minter.authorize(|auth| incoming.burn_with_auth(auth));
result
}

// alternately, if runtime checks are not enabled, it will fail to compile BucketRefOf<AUTH> as an argument since it purposefully does not allow Decode
// and if used with .into() (as shown below) it will always panic since it cannot be be checked
#[auth(auth_def, keep_auth)]
pub fn take_all_inflam(&mut self, /*auth: BucketRefOf<AUTH>*/) -> BucketOf<INFLAM> {
let auth: BucketRefOf<AUTH> = auth.into(); // compiles, but panics
assert_eq!(auth.amount(), 2.into()); // need 2 badges to take everything
self.inflam_vault.take_all()
}
}
}
10 changes: 10 additions & 0 deletions examples/fixburn1/test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#!/usr/bin/env sh
set -e

# test iwth compile time only features
cargo build --target wasm32-unknown-unknown --release --no-default-features
cargo test --release --no-default-features

# test with default (runtime typechecks) features
cargo build --target wasm32-unknown-unknown --release
cargo test --release
Loading

0 comments on commit 5a074ec

Please sign in to comment.