JOVANA
Explore Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

Royalties, soulbound tokens, and account abstraction (ERC-4337)

Three frontiers where token standards stop being about who owns what and start bending the rules: money that tries to follow art forever, ownership that refuses to be sold, and wallets that become programmable smart contracts.

Beyond "who owns it"

An illustrator mints a piece, sells it for 1 ETH, and writes "10% royalty" into the listing. A year later the same image resells for 80 ETH. Her cut of that windfall? Often zero. Not because anyone hacked her contract — because on most chains a royalty is a polite request, not a rule the protocol enforces.

The earlier guides in this rung taught you what a token is: ERC-20 balances, the unique tokenId of an ERC-721 NFT, the multi-token efficiency of ERC-1155, and where an NFT's image really lives. Those standards all answer one question — *who owns what, and how does it move?* This final guide is about three frontiers that bend that question: money that tries to follow a token forever (EIP-2981 royalties), ownership that refuses to move at all (soulbound tokens), and the wallet itself becoming a smart contract you can program (ERC-4337 account abstraction).

EIP-2981: royalties the chain can describe but not enforce

EIP-2981 is deliberately tiny. It adds exactly one read-only function, `royaltyInfo`, that takes a sale price and answers two things: who should be paid and how much. That's it. It moves no money and touches no balances — it is a label a marketplace can read.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import {IERC2981} from "@openzeppelin/contracts/interfaces/IERC2981.sol";

contract ArtNFT is ERC721, IERC2981 {
    address public royaltyReceiver;
    uint96  public royaltyBps;          // basis points: 500 = 5%

    constructor(address receiver, uint96 bps) ERC721("ArtNFT", "ART") {
        royaltyReceiver = receiver;
        royaltyBps      = bps;          // e.g. 500 for 5%
    }

    // EIP-2981: given a sale price, report who is owed how much.
    // NOTE: this only *reports*. It never receives or forwards funds.
    function royaltyInfo(uint256 /*tokenId*/, uint256 salePrice)
        external view returns (address receiver, uint256 royaltyAmount)
    {
        return (royaltyReceiver, (salePrice * royaltyBps) / 10_000);
    }

    // Tell the world (and marketplaces) we implement the standard.
    function supportsInterface(bytes4 id) public view override returns (bool) {
        return id == type(IERC2981).interfaceId || super.supportsInterface(id);
    }
}
A minimal EIP-2981 NFT. Royalties are quoted in basis points (10,000 = 100%), so 500 bps on a 10 ETH sale is (10 × 500) / 10,000 = 0.5 ETH owed to the creator.

Here is the catch that defines the topic: `royaltyInfo` is a view function. When a buyer transfers an NFT, nothing in the ERC-721 standard calls it. The marketplace contract has to choose to read the number, split the payment, and send the creator their share. If it doesn't, the transfer still succeeds and the creator gets nothing. The royalty is real on-chain data attached to a sale that may never happen the way the artist hoped.

This wasn't hypothetical. Through 2022–2023 a "royalty war" played out: trading venues like Blur competed by making creator fees optional, and effective royalty payments across many collections collapsed toward zero. OpenSea fought back with an Operator Filter Registry that blocked transfers to marketplaces which didn't honor royalties — then quietly deprecated it in August 2024 when the ecosystem rejected the heavy-handedness. Projects that truly want enforcement now reach for designs like Limit Break's ERC-721-C, which gates every transfer through an on-chain validator that can require royalties to be paid.

Soulbound tokens: ownership you can't sell

Now flip the problem. Royalties struggle because transfer is so free. What if transfer is exactly what you don't want? A university diploma, a "completed this course" badge, a proof-of-attendance, a credit history — these only mean something if they're stuck to you. A diploma you can buy from a stranger is worthless. That is the idea behind soulbound tokens (SBTs): non-transferable tokens bound to one account.

The term comes from Vitalik Buterin (a January 2022 blog post, borrowing the word from World of Warcraft), expanded in the May 2022 paper Decentralized Society: Finding Web3's Soul. Crucially, you don't need a brand-new standard to build one — you take an ordinary ERC-721 and remove the ability to transfer. In OpenZeppelin's v5 contracts, mint, transfer, and burn all funnel through a single internal hook, `_update`, so one override seals the token shut:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol";

/// A credential that can be MINTED to a wallet and BURNED by it,
/// but never TRANSFERRED to another wallet.
contract Credential is ERC721 {
    error TokenIsSoulbound();

    // EIP-5192: a minimal interface so wallets/marketplaces know it's locked.
    event Locked(uint256 tokenId);

    constructor() ERC721("Credential", "CRED") {}

    function locked(uint256) external pure returns (bool) {
        return true; // always locked
    }

    // In OZ v5, mint/transfer/burn all route through _update.
    //   from == 0  -> mint   (allowed)
    //   to   == 0  -> burn   (allowed: you may give it up)
    //   otherwise  -> transfer (blocked)
    function _update(address to, uint256 tokenId, address auth)
        internal override returns (address)
    {
        address from = _ownerOf(tokenId);
        if (from != address(0) && to != address(0)) revert TokenIsSoulbound();
        return super._update(to, tokenId, auth);
    }
}
A soulbound credential. EIP-5192 standardizes just locked()/Locked/Unlocked so tooling can detect non-transferability; we still allow mint (issue) and burn (the holder renounces it).

Real uses are already here: POAPs (proof you attended an event), on-chain credentials and KYC attestations, Gitcoin Passport-style reputation, and token-gated access where the key can't simply be sold to a scalper. SBTs also hint at something DeFi can't do today — undercollateralized lending, where a non-transferable credit reputation, not just locked capital, backs a loan.

Account abstraction: when the wallet becomes a smart contract

Every regular Ethereum wallet — the one behind your MetaMask seed phrase — is an externally owned account (EOA). It has exactly one hard-wired rule: a transaction is valid only if it carries one ECDSA signature from one private key. That single key is your whole identity. Lose it and the money is gone forever. There is no "forgot password," no spending limits, no "require two of my three keys," and you must always hold ETH yourself to pay gas. The signature rule is baked into the protocol; you cannot change it.

Account abstraction asks: what if the rule for "is this transaction valid?" were itself a piece of code you wrote? ERC-4337 delivers exactly that — and the clever part is it does so without changing Ethereum's consensus at all. Instead of touching the protocol, it builds a parallel transaction system in smart contracts on top of it. Your wallet becomes a smart account: a contract that decides for itself what a valid action looks like.

Four moving parts make this work:

  1. UserOperation — not a normal transaction but a pseudo-transaction object describing what your smart account wants to do. You sign it however your account chooses (one key, multisig, a passkey…) and drop it into a separate "alt mempool."
  2. Bundler — an actor (much like a block builder) that watches the alt mempool, collects many UserOperations, and wraps them into one ordinary on-chain transaction. The bundler fronts the gas and gets reimbursed.
  3. EntryPoint — a single, audited singleton contract every smart account trusts. It runs each UserOperation in two phases (verify, then execute) and handles paying the bundler back, so individual accounts don't have to.
  4. Paymaster — an optional contract that agrees to pay the gas for your operation: it can sponsor users entirely (gasless onboarding) or let them pay the fee in an ERC-20 like USDC instead of ETH.

Inside a UserOperation

A UserOperation looks like a transaction's ambitious cousin. It carries the usual gas and nonce fields, but two things stand out: an `initCode` that can deploy the account on first use (via CREATE2, so the address is known before the contract exists — "counterfactual" deployment), and a `signature` that the protocol never checks. Instead, your account's own code validates it.

// The pseudo-transaction a smart account signs (ERC-4337 v0.6).
struct UserOperation {
    address sender;               // the smart account itself
    uint256 nonce;                // replay protection (per-account)
    bytes   initCode;             // deploys the account on first use (CREATE2)
    bytes   callData;             // what the account should actually do
    uint256 callGasLimit;
    uint256 verificationGasLimit;
    uint256 preVerificationGas;
    uint256 maxFeePerGas;
    uint256 maxPriorityFeePerGas;
    bytes   paymasterAndData;     // optional sponsor (paymaster) + its data
    bytes   signature;            // checked by the ACCOUNT, not the protocol
}

// During the verification phase the EntryPoint calls this on your account.
// Return 0 to accept; this is where you put YOUR rules.
interface IAccount {
    function validateUserOp(
        UserOperation calldata userOp,
        bytes32 userOpHash,
        uint256 missingAccountFunds   // top-up the account owes the EntryPoint
    ) external returns (uint256 validationData);
}
ERC-4337 v0.6 shapes. Because validateUserOp is your code, "valid" can mean a passkey signature, 2-of-3 multisig, a daily spend limit, or a session key — anything you can express in Solidity.

The EntryPoint processes a bundle in two strict loops. First a verification loop: for each op it calls `validateUserOp`, which checks the signature and nonce under your custom rules and pays the EntryPoint a prefund for gas. Only if every op passes does the execution loop run each op's `callData`. Splitting verification from execution is what lets the bundler safely batch strangers' operations together: a bad signature is rejected for free in phase one, before any expensive work in phase two.

What account abstraction unlocks — and what it costs

Once "valid" is programmable, the long-standing wallet pain points become features you can ship:

  1. Gas sponsorship — a paymaster pays gas so a new user signs their first action with zero ETH, or pays the fee in USDC. Onboarding stops requiring a trip to an exchange just to buy gas money.
  2. Social recovery — define guardians (friends, devices, an institution) who can together rotate your signing key if you lose it. A social recovery wallet turns "lost seed phrase = lost funds forever" into something survivable.
  3. Batching & session keys — approve-then-swap in one click instead of two transactions, or grant a game a limited "session key" that can make small moves for an hour without re-signing each one.
  4. Custom signatures — verify a phone's passkey (secp256r1) instead of a seed phrase, so a wallet can feel like Face ID rather than 24 scary words.

None of this is free. A smart account costs more gas than an EOA (extra contract calls on every operation), the alt-mempool/bundler infrastructure is younger and more centralized than Ethereum's base layer, and a paymaster is a trusted party you rely on to keep sponsoring — and a target for griefing if it sponsors carelessly. And EOAs remain the overwhelming majority of wallets, so dApps must support both worlds for years.

Tying the frontier together

Step back and these three frontiers rhyme. Royalties want money to follow a freely-moving token, and discover that without enforcement a standard can only suggest. Soulbound tokens want a token to never move, and discover that fixing it to one address creates new problems of privacy and recovery. Account abstraction answers the recovery problem — and much more — by making the account itself programmable, so the wallet stops being a single fragile key and becomes a contract with rules.

That is also the through-line of this whole rung: a token standard is never just a data format — it's a tiny social contract about what wallets, marketplaces, and other contracts may assume about each other. From here the ladder turns to where these tokens live and trade at scale: the contracts you now know how to write are exactly what the security, DeFi, and scaling rungs will stress-test, attack, and optimize.