RADIX WikiRADIX Wiki

Introduction

Radix's asset-oriented engine rules out several classes of smart-contract bug, but not the ones in a blueprint's own logic. The Radix docs collect the mistakes that recur in Scrypto code under "Before You Release": two pages, one on security and one on preparing a package for production (Code Hardening, Productionize Your Code). This guide condenses both into the checks to run before publishing to mainnet, with the arithmetic cases tested in the ledger simulator.

Check the resource of every proof

A method that takes a Proof as an argument must check which resource the proof is of before reading anything from it. Otherwise an attacker creates their own resource, mints a non-fungible with the local ID they want to impersonate, and passes a proof of that. Scrypto makes the check hard to skip: a Proof reveals nothing until it is converted with check, check_with_message or skip_checking, so the unsafe version has skip_checking written in it (Code Hardening).

pub fn claim(&mut self, proof: Proof) -> Bucket {
    let id = proof
        // Fails the call unless the proof is of the user badge resource.
        .check_with_message(self.user_badge.address(), "Not a user badge")
        .as_non_fungible()
        .non_fungible_local_id();
    self.claims.get_mut(&id).unwrap().take_all()
}

In the claim blueprint of Deposit and Withdraw Patterns, a proof of another resource failed with the check's message. The same applies to buckets passed in: check resource_address() before trusting an amount.

Can a proof be counted twice?

Yes. Proofs can be cloned, and the holder can create a new proof from the same vault as often as they like, so a vote weighted by a proof's amount can be cast any number of times. The docs' fix borrows from liquid stake units: take the tokens themselves in a bucket, hold them for the vote's duration, and return a claim token the voter redeems afterwards (Code Hardening):

pub fn vote(&mut self, votes: Bucket, proposal_id: u64, vote: Vote) -> Bucket {
    assert_eq!(votes.resource_address(), self.voting_resource.address());
    let weight = votes.amount();
    // ...record the vote...
    self.locked_votes.put(votes);
    // Return a claim the voter exchanges for their tokens after the vote closes.
    self.voting_claim_resource.mint(weight).into()
}

Keep state from growing without bound

A Vec, HashMap, BTreeMap, IndexMap or set in a component's state is loaded whole on every call, and reading state costs fees. A collection that grows with use makes every call dearer, and past the transaction's cost limit makes the component uncallable, with any vaults inside it locked for good. The docs' example is a DEX that keeps its pairs in an IndexMap: enough pairs and nobody can swap or withdraw liquidity (Code Hardening).

The fix is a KeyValueStore, which loads one entry at a time; a set is KeyValueStore<K, ()>. A KeyValueStore cannot be iterated on-ledger. Where iteration is needed, the docs point to community structures such as Ociswap's AVL tree, and warn that one transaction can still read and write only a limited number of substates. A fixed-size array, or a collection the code guarantees never grows, is safe in ordinary state.

Decimal arithmetic

The Decimal operators panic on overflow, which fails the transaction. A panic is safe, but for an input the developer did not foresee it can stop a component working, and in a pool it can trap deposits. The docs ask for checked_add, checked_mul and the like, which return None and force a decision about what an overflow means, and for ordering operations so they cannot overflow: units / total_supply * reserves cannot overflow where units * reserves / total_supply can (Code Hardening).

Dividing first loses precision, so the docs recommend computing in PreciseDecimal, with 36 decimal places, and converting to Decimal at the end (Data Types and the Runtime API).

The result then has to fit the resource. Taking an amount with more decimal places than the resource's divisibility allows fails the transaction; take_advanced rounds instead:

// A resource with divisibility 2, and an amount with 17 decimal places.
let amount = dec!(1) / dec!(3) * dec!(10); // 3.33333333333333333
 
supply.take(amount);
// fails: BucketError(InvalidAmount(3.33333333333333333))
 
supply.take_advanced(amount, WithdrawStrategy::Rounded(RoundingMode::ToZero));
// takes 3.33

Both lines ran in the simulator against a resource of divisibility 2, with the results shown. A DEX that skips this rounding fails some swaps and not others, depending on the amounts.

Make sure protected methods are protected

Without enable_method_auth!, every pub method of a blueprint can be called by anyone. With it, a method marked PUBLIC by mistake, or a role given rule!(allow_all) at instantiation, has the same effect. The macros enforce that every method is mapped to a role and every role to a rule, as a compile error, but not that the mapping is right. Read through both before release (Code Hardening):

enable_method_auth! {
    roles {
        manager => updatable_by: [];
    },
    methods {
        mint_admin_badge => restrict_to: [manager];
    }
}
// ...and at instantiation, a real rule for the role, not allow_all:
.roles(roles! { manager => manager_rule; })

Functions need the same review: enable_function_auth! restricts who can call a blueprint's functions.

Plan for bugs found after release

A published package's code cannot be changed: the native package blueprint offers publishing and royalty claims and no upgrade (package invocations). The docs ask every non-trivial application to answer, before release, how it will move assets and state to fixed code, how users will find the new version, and whether it can pause while a fix is prepared. One architecture they describe keeps state and vaults in one component and logic in another that reads and writes it with a badge, so retiring old logic means moving the badge, with a proxy component whose address never changes in front (Code Hardening).

Production settings

  • Give everything an owner. Every package, component and resource should have an owner role, because the docs expect future engine features, such as upgrades, to be controlled by it. An instantiation function should take an OwnerRole and pass it to everything it creates, and the owner badge must sit somewhere that can produce proofs, such as an account or an access controller. OwnerRole::Updatable lets the rule itself change later; OwnerRole::Fixed does not (Productionize Your Code).
  • Follow the metadata standards, and leave metadata updatable. The wallet and dashboard read names, icons and the two-way links to the dApp definition from metadata, and the standards change; locked metadata cannot follow them.
  • Check how it looks in the wallet. A manifest that matches one of the wallet's conforming types is shown as deposits and withdrawals; any other is shown as raw manifest text. A deposit of a known amount should use TAKE_FROM_WORKTOP with that amount rather than TAKE_ALL_FROM_WORKTOP or ENTIRE_WORKTOP, or the wallet asks the user to set a guarantee.
  • Register non-fungible and key-value store types. Listing them in #[types(...)] above the blueprint module moves their schemas into the package definition at compile time, which the docs say makes the WASM smaller and every call cheaper.
  • Build deterministically if users should be able to verify the code. scrypto build output differs between machines. The docs' answer is the radixdlt/scrypto-builder Docker image at the same version as the package's Scrypto dependency; on 14 November 2026 Docker Hub's newest tag was v1.3.1, with none for 1.4.0.

Next Steps

HydrateLast updated 7h agov1.0.01 revision
CiteRADIX Wiki. "13. Before You Release: Hardening a Blueprint." November 14, 2026. https://radix.wiki/developers/scrypto/13-before-you-release