The User Badge Pattern

The problem that this pattern solves is the problem of how to tell that somebody is an admin in your system, or more generically, how to tell that a user has the authority to perform a certain action in the context of your system. If you come from the Ethereum world, your first intuition might be to use the caller’s address to perform authorization checks. If the caller’s address is in the list of whitelisted addresses then their call is valid, otherwise they’re not authorized to perform the call. The address-based approach, however, is far from being optimal as it introduces issues with composability and the smallest of mistakes in implementation could have the biggest of consequences, to the extent of privileged users losing access. The address-based approach to authorization has the following problems:

  • It assumes a one-to-one mapping of public keys to account addresses, as in, this approach assumes that one key-pair can only control one account. While this is true on Ethereum, it is not true on Radix where an account is a component and a single key-pair can have control over multiple accounts. Thus, authorizing one of my owned component accounts to perform an action while not authorizing the others feels weird and clunky.

  • It requires that additional methods are implemented on the smart contract to allow for additional authorized or whitelisted addresses to be added, removed, replaced, and so on. Failure to implement such functions correctly could mean that the contract "owner" is forever locked and may never be changed. This proves to be an issue when looking at the possibility of accounts being hacked, a dApp getting sold, or if the contract’s "owner" wishes to add other admins with them in a contract which used to be single-admin.

  • Unless the required functions are present on the contract, this approach mandates that the contract’s "owner" must never migrate to a new account—​not even in the case of a hack—​as their authority to perform privileged actions on a contract is directly linked to their account address.

  • Most implementations of the Ownable contract on Ethereum assumes that the contract creator is the contract’s owner. This would mean that in the case that the caller was using a proxy which they no longer have access to, then the proxy contract would be considered the "owner" of the contract.

  • In Ethereum, there may only be one contract caller at a time, as Ethereum has no native support for multi-signature transactions. This means that authorization logic which involves multiple-signatures is a lot harder to correctly implement.

With the above points in mind, using the caller’s address to perform authorization checks begins to seem less intuitive and moves from being a simple solution to implement, to a design decision which requires a lot of thought in terms of what the system’s current and future requirements.

Using Badges for Authorization

Radix solves the problems with address-based authorization techniques through the concept of badges. A badge is any normal resource—​meaning that it may be fungible or non-fungible—​which components use to perform authentication, and authorization checks. Components may be setup such that they require that some badge is present to allow certain method(s) to be called, then only when the caller presents a valid badge, through the auth zone or by passing it by intent, may they be authorized to call such methods.

Lets look at a Scrypto example of how badges may be used for the purpose of authorization. Say that we are developing some blueprint where there is a vault that only admins can withdraw funds from. To implement this pattern, we would make it so that when our component is instantiated, it creates an admin badge, sets up the access rules to limit withdrawals to only admins, then returns the admin badge back to the caller. This would look as follows in Scrypto:

struct DonationsBox {
    donations: Vault
}

impl DonationsBox {
    pub fn instantiate_donations_box() -> (ComponentAddress, Bucket) {
        // Create the admin badges
        let admin_badge: Bucket = ResourceBuilder::new_fungible() (1)
            .divisibility(DIVISIBILITY_NONE)
            .metadata("name", "Admin Badge")
            .initial_supply(1);

        // Define the access rules for this blueprint.
        let access_rules = AccessRules::new()
            .method("withdraw", rule!(require(admin_badge.resource_address()))) (2)
            .default(rule!(allow_all));

        // Return the component address and the admin_badge
        (
            Self {donations: Vault::new(RADIX_TOKEN)}
                .instantiate()
                .add_access_check(access_rules) (3)
                .globalize(),
            admin_badge
        )
    }

    pub fn withdraw(&mut self) {
        // This method can only be called if the caller presents an admin badge
        self.donations.take_all()
    }
}
1 Creating the admin badge which will be used for authorization checks throughout the component.
2 Specifying the access rules for the component. In here, we specify that a call to the withdraw method requires that an admin badge be present in the auth zone.
3 Wiring up the access rules to the component such that they now apply.

When comparing the badge-based approach of authorization to its address-based counterpart, we see that the badge-based approach triumphs as the clear winner of this comparison. The following are some of the advantages which the badge-based authorization approach brings:

  • Since badges are just normal resources, they may be moved around freely and without limitation. This off-loads a great deal of pressure from the developer of the smart contract as it means that the logic required for handing off authorization no longer needs to be implemented by them. Instead, it comes as a native feature of the framework. If badge transfers becomes a security risk, the badge can be made to have restricted_withdraw and restricted_deposit behavior such that it can not be moved around between components.

  • The badge can be made mintable (although it is not in the example provided above) which means that more of the badge can be created. This is useful when the system being developed becomes bigger and bigger and begins to necessitate that more admins are added.

  • Authorization through badges is extremely well integrated with Scrypto and the Radix Engine. This allows for the modeling of complex authorization problems to be effortlessly implemented in Scrypto. To better understand this point, consider an organization which runs on Radix and has the organizational structure shown below. The rules which govern this organization dictate that before the organization takes an action, either a supervisor, admin, and superadmin must all agree, or, the founder and at least 8 shareholders must agree.

    complex authorization

    Such an organizational structure would be very difficult to model on Ethereum, even more so considering that Ethereum has no native support for multi-signature transactions. However, with the powerful and complex authorization system in Scrypto and the Radix Engine, such a board structure could be boiled down to the following Scrypto AccessRule:

    let access_rule: AccessRule = rule!(
        (require(supervisor_bade) && require(admin_badge) && require(superadmin_badge))
        || (require(founder_badge) && require_amount(8, shareholder_badge))
    );

    This AccessRule may then be wired up to different components which we wish to control access for such that only when the conditions described by the rule is satisfied, the method call is allowed.

Using Badges for Authentication

So far, we have discussed the usage of badges in the context of authorization, as in, we were previously interested in knowing if the caller is an admin or not, however, we are now interested in knowing which admin it is. In the examples described above, the badges created were fungible. However, in the case of using badges for authentication, non-fungible badges make more sense as they would be able to hold unique data for each one of the badges.

Say that we are developing a simple blueprint for an art-related NFT project where we would like that each holder of our tokens is entitled to 10 XRD which they may claim from our component. A holder may only claim their 10 XRD once and nothing more than that. If a holder does not wish to claim their 10 XRD then it remains locked up in the component until they choose to claim it. In this case, we can’t treat everybody in the system as equal since some holders of our NFT have claimed their 10 XRD while some have not. This means that a non-fungible badge (which would be the NFT itself) is the way to go here!

use scrypto::prelude::*;

#[derive(NonFungibleData)]
struct MyNFTData { (1)
    // -- Snip --
    xrd_claimed: bool
    // -- Snip --
}

blueprint! {
    struct SimpleNFTProject {
        nft_resource: ResourceAddress,
        vault: Vault,
        admin_badge: Vault,
    }

    impl SimpleNFTProject {
        pub fn new(funds: Bucket) -> ComponentAddress {
            let admin_badge: Bucket = ResourceBuilder::new_fungible()
                .initial_supply(1);

            let nft_resource: ResourceAddress = ResourceBuilder::new_non_fungible()
                .updateable_non_fungible_data(
                    rule!(require(admin_badge.resource_address())), LOCKED
                )
                .no_initial_supply();

            Self{
                nft_resource,
                admin_badge: Vault::with_bucket(admin_badge),
                vault: Vault::with_bucket(funds)
            }
            .instantiate()
            .globalize()
        }

        pub fn claim_xrd(&mut self, auth: Proof) -> Bucket {
            assert_eq!(auth.resource_address(), self.nft_resource, "Invalid NFT passed"); (2)

            let mut nft_data: MyNFTData = auth.non_fungible().data();
            assert!(!nft_data.xrd_claimed, "You have already claimed your 10 XRD"); (2)

            self.admin_badge.authorize(|| {
                auth.non_fungible().update_data({
                    nft_data.xrd_claimed = true;
                    nft_data
                });
            });

            return self.vault.take(10);
        }
    }
}
1 This is the data which the NFT that we are creating will hold. In most blockchains, this is typically refereed to as the NFT’s "metadata". In addition to the other relevant data, this non-fungible token also defines a boolean field which stores whether this NFT has had its associated 10 XRD claimed or not.
2 When the caller tries to claim the 10 XRD, we check that they’ve supplied us with the NFT that we’re expecting and that they have not already claimed their 10 XRD.

There are a few important bits in the above example. First, the non-fungible token which includes the metadata to the art is the same non-fungible token being used as a badge. It is important to note that this is possible to do as the idea of badges exist as a high-level concept and not at the Scrypto-level.