RADIX WikiRADIX Wiki

Introduction

In most smart contract platforms, access control is based on the caller's address – a pattern that leads to fragile permission systems and common exploits like reentrancy. Radix takes a fundamentally different approach: access is gated by badges, which are standard resources (fungible or non-fungible) that serve as unforgeable credentials. A caller is authorised not because of who they are but because of what they hold.

This pattern is central to Scrypto development and appears in virtually every non-trivial dApp on Radix.

How It Works

Access Rules

When a component is instantiated, its methods can be protected with access rules that specify which badge(s) must be present for a call to succeed. The Radix Engine checks these rules automatically before executing any method – there is no manual require(msg.sender == owner) logic. That is the whole of the check the Engine has historically made on a method call; a second one, on whether the caller may reach the receiver at all, is being added – see The check below the access rule.

enable_method_auth! {
    roles {
        admin => updatable_by: [];
        minter => updatable_by: [admin];
    },
    methods {
        mint_tokens => restrict_to: [minter, admin];
        update_price => restrict_to: [admin];
        buy => PUBLIC;
    }
}

Proofs and the Auth Zone

When a method requires a badge, the caller provides a Proof – a cryptographic attestation that a resource exists in the caller's possession without transferring it. Proofs can be placed on the Auth Zone (a transaction-scoped container) so that multiple method calls within the same transaction can share the same authorisation context.

# 

The check below the access rule

An access rule answers one question: does the caller hold the badge this method demands. It does not answer a second one – may the caller reach the object it is calling a method on at all. Until September 2026 the Radix Engine did not ask that question of every invocation, and a pull request opened while Mainnet was halted adds it.

radixdlt-scrypto #2093, opened on 2 September 2026 from a branch in Radix’s own repository, introduces a protocol update named Eagle Ray (logical_name: "eagle-ray"). Its entire content is one flash batch that advances the SystemBoot substate to SystemVersion::V5. V5 enables one behaviour, should_check_method_receiver_access: before an invocation runs, the kernel tests its visibility of the method’s receiver. A Direct method – the type used by recall and direct vault access – needs direct visibility; an ordinary Main or module method needs some visibility; functions and blueprint hooks are unaffected. Anything else fails with a new error, SystemError::InvalidInvokeAccess.

What makes this a badge story rather than a kernel footnote is the test coverage the same pull request adds to radix-engine-tests/tests/system/reference.rs. Passing a typed internal reference to somebody else’s vault into a blueprint function was already legal and still is – the pre-existing test_internal_typed_reference does exactly that and expects a successful commit, because it recalls a recallable resource under the owner’s signature. The new tests take the same reference and call ordinary vault methods on it through ScryptoVmV1Api::object_call, which demand no recaller badge and no signature:

  • take_via_normal_call and take_non_fungibles_via_normal_call – withdraw from a vault the caller does not own;
  • lock_fee_via_normal_call – pay the transaction fee out of somebody else’s XRD vault;
  • forge_proof_via_normal_call – mint a Proof from a vault the caller does not own;
  • forge_nft_proof_and_call_gated – the same, then LocalAuthZone::push the forged proof and call a component whose method runs Runtime::assert_access_rule(rule!(require(resource))).

Every one of them now fails with InvalidInvokeAccess. The last is the one that matters here: it satisfies a badge-gated access rule with a proof drawn from a badge held in another account. The rule itself never misbehaves – it is handed a genuine proof of a genuine resource and does what it is told. “Authorised by what you hold” depends on the engine below establishing that the vault a proof came from is one you may reach, and that is the check being added, not an access rule you write.

Nothing on this page changes as a result. Eagle Ray adds no Scrypto API: badges, proofs, enable_method_auth! and the auth zone are untouched, and a blueprint written against them needs no edit. What has changed since this section was first written is that it is now enforcing on Mainnet.

From pull request to enacted, in eleven days

An earlier reading of this page, at 19:00 UTC on 4 September 2026, found the pull request open with no reviews, develop unchanged at 858c70f1 of 27 March 2026, and babylon-node’s newest release still v1.3.0.5 of 1 June 2026 with no Eagle Ray branch – so no validator yet had a node version to signal readiness for. All four of those facts have since been overtaken:

  • #2093 merged into develop at 17:33:31 UTC on 7 September 2026, and Scrypto v1.4.0, named Eagle Ray, was published 98 seconds later. The tag carries mod eagle_ray in radix-engine/src/updates/mod.rs.
  • babylon-node v1.4.0.0 (Eagle Ray) shipped on 10 September 2026, after an RC1 on 8 September.
  • The fork enacted at an epoch boundary, not on a readiness signal. Mainnet resumed rounds at state version 557,840,628 at 11:35:28.960 UTC on 11 September under an upgrade moratorium – consensus running, no user transaction committed. The first user transaction is state version 557,840,694 at 11:39:25.129 UTC, in epoch 339,898: three minutes fifty-six seconds later, and one epoch on. The moratorium is legible in the ledger as the 66-state gap between the two.
  • docs.radixdlt.com/docs/eagle-ray still answers HTTP 404, re-checked 12 September 2026. So does /docs/scrypto-v1-4-0. The protocol update reached Mainnet before it reached the documentation.

The error, on Mainnet, against a real attempt

On 11 September a node runner tested the deployed fix in the open, publishing a Vault Drainer blueprint to Mainnet (announced in the Radix Developer Discussion group; the publishing transaction committed at state version 557,842,200, epoch 339,909, 12:35:05.915 UTC) and then submitting a draining transaction against it. The Gateway's verdict on that transaction is PermanentlyRejected, and it names the reason:

ErrorBeforeLoanAndDeferredCostsRepaid(SystemError(InvalidInvokeAccess))

That is the error described above, returned by Mainnet against a live attempt to call a vault method through a reference the caller was not entitled to reach. The check is not a proposal on this page any more; it is the thing standing between a typed internal reference and somebody else's vault. The chronology of its authorship remains on the record and its causation remains unstated: the branch’s earliest commit is dated 31 August 2026 at 22:41 UTC, 82 minutes after the last round Mainnet committed before the halt, and the receiver check itself 1 September at 00:12 UTC. See Radix Protocol Updates and Installing Scrypto, whose pins the release moved.

Moving Badge-Gated and Restricted Resources

Because authorisation on Radix depends on presenting a Proof, a resource whose withdrawal is gated by a badge cannot be moved with the Radix Wallet's built-in transfer screen – that flow builds a plain withdraw-and-deposit transaction manifest and never creates the proof the access rule demands. Moving it requires a manifest – typically supplied by a dApp – that creates the required proof first (the CREATE_PROOF_FROM_ACCOUNT_OF_AMOUNT pattern shown above) and then performs the withdrawal within the same transaction.

For dApp builders this means: if your users hold badge-gated assets, give them an in-app action that sends the wallet a correctly-authorised manifest, rather than expecting a manual wallet transfer to succeed. It is also why soulbound badges stay put – their Withdraw action is locked outright, so no proof can unlock it.

Wallet support for this may be coming. Two pull requests opened on 23 July 2026 by community developer genkipool would have the wallet attach the required badge itself: sargon #452 injects badge ResourceSpecifiers into the PerAssetTransfers manifest builder via create_proof_of_amount and create_proof_of_non_fungibles, and babylon-wallet-android #1446 builds the transfer feature on top. Both were still open as of 29 July 2026.

For the full picture of movement rules on a resource – including freeze, recall, and why a deposit rule cannot check what the recipient holds – see Permissioned and Regulated Assets.

Common Badge Patterns

Admin Badge

The most basic pattern: mint a single non-fungible badge at instantiation and return it to the deployer. Methods like withdraw_fees, update_config, or pause are gated behind this badge.

User Badge

The User Badge Pattern issues a non-fungible badge to each user when they register. The badge's non-fungible data stores user-specific state (balances, permissions, membership tier). Methods read the caller's badge data to personalise behaviour without maintaining a separate user registry.

Multi-Signature

Access rules support boolean logic: require_n_of(2, [badge_a, badge_b, badge_c]) creates a 2-of-3 multi-sig gate. This is useful for treasury management, protocol upgrades, or any high-stakes operation.

Soulbound Badges

By creating a non-transferable resource (restrict Deposit and Withdraw actions), a badge becomes soulbound to the original recipient's account. This is ideal for identity credentials, certificates, or membership tokens that should not change hands.

Next Steps

HydrateLast updated 2d agov3.0.016 revisionsVerified Sep 12, 2026