Introduction
A badge is an ordinary resource that a component checks instead of checking who is calling. The Radix docs describe three ways of using one, for three different callers: a user badge proves a person's authority, an actor virtual badge proves a component's authority, and a transient badge proves that a caller has not yet finished what it started (User Badge Pattern, Actor Virtual Badge Pattern, Transient Badge Pattern).
Every example here was compiled against Scrypto 1.4.0 and run in the ledger simulator. Two changes from the docs' versions were needed. The docs store a new resource in a field typed ResourceManager; in 1.4.0 create_with_no_initial_supply returns a typed FungibleResourceManager or NonFungibleResourceManager, and the untyped field is a type error (E0308). The docs' flash loan also calls mint_uuid_non_fungible, which does not exist; the method is mint_ruid_non_fungible.
Why use a badge instead of the caller's address?
On Ethereum a contract usually checks msg.sender against a stored owner or whitelist. The docs list why that fits Radix badly: one key can control many Radix accounts, so authorising one account and not its siblings is arbitrary; the contract needs extra methods to add, remove or replace addresses, and a mistake in them can lock the owner out for good; and the owner can never move to a new account, even after a hack (User Badge Pattern).
A badge moves authority from the address to an asset. Whoever can produce a proof of the badge holds the authority, and the badge can be moved, held by several accounts, required in multiples, or held in an access controller with recovery rules. Authorization and Access Rules covers the admin and user badge in full. The rest of this guide covers the two patterns for callers that are not people.
How does a component prove its own authority?
A component often needs a privileged action of its own, such as minting a user badge each time someone signs up. It does not need a badge in a vault for this. Every global component has an actor virtual badge: a proof the engine adds automatically when that component makes a call. The global_caller(address) rule requires that proof, so a resource can name one component as its only minter (Actor Virtual Badge Pattern):
use scrypto::prelude::*;
#[blueprint]
mod rad_social {
struct RadSocial {
user_badge_manager: FungibleResourceManager,
}
impl RadSocial {
pub fn instantiate_rad_social() -> Global<RadSocial> {
// Reserve the component's address before the component exists.
let (address_reservation, component_address) =
Runtime::allocate_component_address(RadSocial::blueprint_id());
let user_badge = ResourceBuilder::new_fungible(OwnerRole::None)
.metadata(metadata!(init { "name" => "User Badge", locked; }))
.mint_roles(mint_roles! {
minter => rule!(require(global_caller(component_address)));
minter_updater => rule!(deny_all);
})
.create_with_no_initial_supply();
Self { user_badge_manager: user_badge }
.instantiate()
.prepare_to_globalize(OwnerRole::None)
.with_address(address_reservation) // use the reserved address
.globalize()
}
pub fn create_user(&mut self) -> FungibleBucket {
self.user_badge_manager.mint(1)
}
}
}The rule needs the component's address before the component exists. Runtime::allocate_component_address reserves an address and returns it with a reservation, and .with_address(reservation) globalizes the component at that address.
In the ledger simulator, create_user minted a badge into the caller's account. A manifest that called MINT_FUNGIBLE on the same resource directly failed with AuthError(Unauthorized): the minter role requires a non-fungible from the engine's global-caller badge resource, whose ID is derived from the component's address, and no manifest can present it.
A second virtual badge works at package level. package_of_direct_caller(package_address) is satisfied by any blueprint in that package when it makes the call directly, which suits a resource that several blueprints in one package should be able to mint (proof_rule.rs).
How do I make sure a caller calls a second method before the transaction ends?
Give them a transient badge: a resource no vault can accept. The first method mints it and returns it with whatever it hands out; the second method takes it back and burns it. Because nothing can hold it, the only way for the transaction to commit is to pass it to the second method (Transient Badge Pattern).
The docs' example is a flash loan: a loan taken and repaid within one transaction, with no collateral, which works only if repayment is guaranteed. The badge is a non-fungible whose data records the amount due:
#[derive(NonFungibleData, ScryptoSbor)]
pub struct LoanDue {
pub amount_due: Decimal,
}
#[blueprint]
mod basic_flash_loan {
struct BasicFlashLoan {
loan_vault: Vault,
loan_terms: NonFungibleResourceManager,
}
impl BasicFlashLoan {
pub fn instantiate(liquidity: Bucket) -> Global<BasicFlashLoan> {
let (address_reservation, component_address) =
Runtime::allocate_component_address(BasicFlashLoan::blueprint_id());
let loan_terms = ResourceBuilder::new_ruid_non_fungible::<LoanDue>(OwnerRole::None)
.metadata(metadata!(init { "name" => "Flash loan terms", locked; }))
.mint_roles(mint_roles! {
minter => rule!(require(global_caller(component_address)));
minter_updater => rule!(deny_all);
})
.burn_roles(burn_roles! {
burner => rule!(require(global_caller(component_address)));
burner_updater => rule!(deny_all);
})
// Transient: no vault anywhere can accept it.
.deposit_roles(deposit_roles! {
depositor => rule!(deny_all);
depositor_updater => rule!(deny_all);
})
.create_with_no_initial_supply();
Self { loan_vault: Vault::with_bucket(liquidity), loan_terms }
.instantiate()
.prepare_to_globalize(OwnerRole::None)
.with_address(address_reservation)
.globalize()
}
pub fn take_loan(&mut self, amount: Decimal) -> (Bucket, NonFungibleBucket) {
assert!(amount <= self.loan_vault.amount(), "Not enough liquidity");
let amount_due = amount.checked_mul(dec!("1.001")).unwrap();
let terms = self.loan_terms.mint_ruid_non_fungible(LoanDue { amount_due });
(self.loan_vault.take(amount), terms)
}
pub fn repay_loan(&mut self, repayment: Bucket, terms: NonFungibleBucket) {
assert_eq!(terms.resource_address(), self.loan_terms.address(), "Wrong terms");
assert_eq!(repayment.resource_address(), self.loan_vault.resource_address(), "Wrong token");
let due: LoanDue = terms.non_fungible().data();
assert!(repayment.amount() >= due.amount_due, "Insufficient repayment");
self.loan_vault.put(repayment);
terms.burn();
}
}
}The deposit_roles rule deny_all, with its updater also deny_all, is what makes the resource transient. The minter and burner roles name the component's own actor virtual badge, so only the lender can create or destroy the terms. repay_loan checks the resource of both buckets: the docs' version checks the terms but not the repayment, and relies on Vault::put rejecting a bucket of the wrong resource. Checking explicitly makes the failure state its reason.
A borrower's manifest takes the loan, uses it, and repays it:
CALL_METHOD Address("<lender>") "take_loan" Decimal("100");
# ...use the borrowed tokens on the worktop here: a swap, a liquidation...
CALL_METHOD Address("<account>") "withdraw" Address("<xrd>") Decimal("0.1");
TAKE_ALL_FROM_WORKTOP Address("<xrd>") Bucket("repayment");
TAKE_ALL_FROM_WORKTOP Address("<loan_terms>") Bucket("terms");
CALL_METHOD Address("<lender>") "repay_loan" Bucket("repayment") Bucket("terms");All four ways of running it were tested. Borrowing 100 $XRD and repaying 100.1 committed. Depositing the loan and the terms into the borrower's account failed with Unauthorized on the depositor role: the account owner's signature lets a deposit past the account's own rules, but not past the resource's. Depositing only the $XRD and leaving the terms on the worktop failed with DropNonEmptyBucket when the transaction ended. Repaying 100 failed with the panic Insufficient repayment. In each failure the whole transaction rolled back, the loan included.
The pattern generalises to any sequence: each method burns the badge the previous method minted and mints one only the next method can burn, and the transaction commits only if it reaches the last one.
Next Steps
- Deposit and Withdraw Patterns – how a component pays people who are not in the transaction
- Testing Scrypto Blueprints – the ledger simulator used for every result on this page
External Links
- User Badge Pattern – Radix documentation
- Actor Virtual Badge Pattern – Radix documentation
- Transient Badge Pattern – Radix documentation
- Basic Flash Loan – scrypto-examples repository
