ERC-8056: Scaled UI Amount Extension for ERC-20 Tokens

For the full ERC, see Add ERC: Scaled UI Amount Extension for ERC-20 Tokens by cridmann · Pull Request #1283 · ethereum/ERCs · GitHub

ABSTRACT


This EIP proposes a standard extension to ERC-20 tokens that enables issuers to apply an updatable multiplier to the UI (user interface) amount of tokens. This allows for efficient representation of stock splits, without requiring actual token minting or transfers. The extension provides a cosmetic layer that modifies how token balances are displayed to users while maintaining the underlying token economics.

MOTIVATION


Current ERC-20 implementations lack an efficient mechanism to handle real-world asset scenarios such as stock splits: When a company performs a 2-for-1 stock split, all shareholders should see their holdings double. Currently, this requires minting new tokens to all holders, which is gas-intensive and operationally complex. Moreover, the internal accounting in DeFi protocols would break from such a split.

The inability to efficiently handle this scenario limits the adoption of tokenized real-world assets (RWAs) on Ethereum. This EIP addresses these limitations by introducing a multiplier mechanism that adjusts the displayed balance without altering the actual token supply.

SPECIFICATION


The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119.

Interface:


interface IScaledUIAmount {
    // Emitted when the UI multiplier is updated
    event UIMultiplierUpdated(uint256 oldMultiplier, uint256 newMultiplier, uint256 setAtTimestamp, uint256 effectiveAtTimestamp);
    
    // Returns the current UI multiplier
    // Multiplier is represented with 18 decimals (1e18 = 1.0)
    function uiMultiplier() external view returns (uint256);
    
    // Converts a raw token amount to UI amount
    function toUIAmount(uint256 rawAmount) external view returns (uint256);
    
    // Converts a UI amount to raw token amount
    function fromUIAmount(uint256 uiAmount) external view returns (uint256);
    
    // Returns the UI-adjusted balance of an account
    function balanceOfUI(address account) external view returns (uint256);
    
    // Updates the UI multiplier (only callable by authorized role)
    function setUIMultiplier(uint256 newMultiplier, uint256 effectiveAtTimestamp) external;
}

Implementation Requirements:

  1. Multiplier Precision: The UI multiplier MUST use 18 decimal places for precision (1e18 represents a multiplier of 1.0).

  2. Backwards Compatibility: The standard ERC-20 functions (balanceOf, transfer, transferFrom, etc.) MUST continue to work with raw amounts.

  3. Event Emission: The UIMultiplierUpdated event MUST be emitted whenever the multiplier is changed.

REFERENCE IMPLEMENTATION


contract ScaledUIToken is ERC20, IScaledUIAmount, Ownable {
    uint256 private constant MULTIPLIER_DECIMALS = 1e18;
    uint256 private _uiMultiplier = MULTIPLIER_DECIMALS; // Initially 1.0
    uint256 public _nextUiMultiplier = MULTIPLIER_DECIMALS;   
    uint256 public _nextUiMultiplierEffectiveAt = 0; 

    constructor(string memory name, string memory symbol) ERC20(name, symbol) {}
    
    function uiMultiplier() public view override returns (uint256) {
        uint256 currentTime = block.timestamp;
	   if (currentTime >= _nextUiMultiplierEffectiveAt) {
		return _nextUiMultiplier;
   } else {
      return _uiMultiplier;
   }
    }	
    
    function toUIAmount(uint256 rawAmount) public view override returns (uint256) {
	   uint256 currentTime = block.timestamp;
        if (currentTime >= _nextUiMultiplierEffectiveAt) {
        	return (rawAmount * _nextUiMultiplier) / MULTIPLIER_DECIMALS;
        } else {
        	return (rawAmount * _uiMultiplier) / MULTIPLIER_DECIMALS;
   }
    }
    
    function fromUIAmount(uint256 uiAmount) public view override returns (uint256) {
	   if (currentTime >= _nextUiMultiplierEffectiveAt) {
return (uiAmount * MULTIPLIER_DECIMALS) / _nextUiMultiplier;
   } else {
	return (uiAmount * MULTIPLIER_DECIMALS) / _uiMultiplier;
    }
        
    }
    
    function balanceOfUI(address account) public view override returns (uint256) {
        return toUIAmount(balanceOf(account));
    }
    
    function setUIMultiplier(uint256 newMultiplier, uint256 effectiveAtTimestamp) external override onlyOwner {
        require(newMultiplier > 0, "Multiplier must be positive");
		
	   uint256 currentTime = block.timestamp;
        require(effectiveAtTimestamp > currentTime, "Effective At must be in the future");

	   if (currentTime > _nextUiMultiplierEffectiveAt) {
	uint256 oldMultiplier = _nextUiMultiplier;
	_uiMultiplier = oldMultiplier;
	_nextUiMultiplier = newMultiplier;
     _nextUiMultiplierEffectiveAt = effectiveAtTimestamp;
     emit UIMultiplierUpdated(oldMultiplier, newMultiplier,    block.timestamp, effectiveAtTimestamp);
   } else {
	uint256 oldMultiplier = _uiMultiplier;
     _nextUiMultiplier = newMultiplier;
     _nextUiMultiplierEffectiveAt = effectiveAtTimestamp;
     emit UIMultiplierUpdated(oldMultiplier, newMultiplier,    block.timestamp, effectiveAtTimestamp);
   }	
    }
}

RATIONALE


Design Decisions:

  1. Separate UI Functions: Rather than modifying the core ERC-20 functions, we provide separate UI-specific functions. This ensures backward compatibility and allows integrators to opt-in to the UI scaling feature.

  2. 18 Decimal Precision: Using 18 decimals for the multiplier provides sufficient precision for most use cases while aligning with Ethereum’s standard decimal representation.

  3. No Automatic Updates: The multiplier must be explicitly set by authorized parties, giving issuers full control over when and how adjustments are made.

  4. Raw Amount Preservation: All actual token operations continue to use raw amounts, ensuring that the multiplier is purely a display feature and doesn’t affect the underlying token economics.

Alternative Approaches Considered:

  1. Rebasing Tokens: While rebasing tokens adjust supply automatically, they create complexity for integrators and can break composability with DeFi protocols.

  2. Wrapper Tokens: Creating wrapper tokens for each adjustment event adds unnecessary complexity and gas costs.

  3. Index/Exchange Rate Tokens confer similar advantages to the proposed Scaled UI approach, but is ultimately less intuitive and requires more calculations on the UI layers.

  4. Off-chain Solutions: Purely off-chain solutions lack standardization and require trust in centralized providers.

BACKWARDS COMPATIBILITY


This EIP is fully backwards compatible with ERC-20. Existing ERC-20 functions continue to work as expected, and the UI scaling features are opt-in through additional functions.

TEST CASES


Example test scenarios:

  1. Initial Multiplier Test:
  • Verify that initial multiplier is 1.0 (1e18)

  • Confirm balanceOf equals balanceOfUI initially

  1. Stock Split Test:
  • Set multiplier to 2.0 (2e18) for 2-for-1 split

  • Verify UI balance is double the raw balance

  • Confirm conversion functions work correctly

SECURITY CONSIDERATIONS


  1. Multiplier Manipulation
  • Unauthorized changes to the UI multiplier could mislead users about their holdings

  • Implementations MUST use robust access control mechanisms

  • The setUIMultiplier function MUST be restricted to authorized addresses (e.g., contract owner or a designated role).

  1. Integer Overflow
  • Risk of overflow when applying the multiplier

  • Use SafeMath or Solidity 0.8.0+ automatic overflow protection

  1. User Confusion
  • Clear communication is essential when UI amounts differ from raw amounts

  • Integrators MUST clearly indicate when displaying UI-adjusted balances

  1. Oracle Dependency
  • For automated multiplier updates, the system may depend on oracles

  • Oracle failures or manipulations could affect displayed balances

  1. Overflow Protection: Implementations MUST handle potential overflow when applying the multiplier.

IMPLEMENTATION GUIDE FOR INTEGRATORS


WALLET INTEGRATION

Wallets supporting this standard should:

  1. Check if a token implements IScaledUIAmount interface

  2. Display both raw and UI amounts, clearly labeled

  3. Use balanceOfUI() for primary balance display

  4. Handle transfers using raw amounts (standard ERC-20 functions)

Example JavaScript integration:

async function displayBalance(tokenAddress, userAddress) {
    const token = new ethers.Contract(tokenAddress, ScaledUIAmountABI, provider);
    
    // Check if scaled UI is supported
    const supportsScaledUI = await supportsInterface(tokenAddress, SCALED_UI_INTERFACE_ID);
    
    if (supportsScaledUI) {
        const uiBalance = await token.balanceOfUI(userAddress);
        const rawBalance = await token.balanceOf(userAddress);
        const multiplier = await token.uiMultiplier();
        
        return {
            display: formatUnits(uiBalance, decimals),
            raw: formatUnits(rawBalance, decimals),
            multiplier: formatUnits(multiplier, 18)
        };
    } else {
        // Fall back to standard ERC-20
        const balance = await token.balanceOf(userAddress);
        return {
            display: formatUnits(balance, decimals),
            raw: formatUnits(balance, decimals),
            multiplier: "1.0"
        };
    }
}

EXCHANGE INTEGRATION

Exchanges should:

  1. Store and track the multiplier for each supported token

  2. Display UI amounts in user interfaces

  3. Use raw amounts for all internal accounting

  4. Provide clear documentation about the scaling mechanism

Example implementation:

class ScaledTokenHandler {
    async processDeposit(tokenAddress, amount, isUIAmount) {
        const token = new ethers.Contract(tokenAddress, ScaledUIAmountABI, provider);
        
        let rawAmount;
        if (isUIAmount && await this.supportsScaledUI(tokenAddress)) {
            rawAmount = await token.fromUIAmount(amount);
        } else {
            rawAmount = amount;
        }
        
        // Process deposit with raw amount
        return this.recordDeposit(tokenAddress, rawAmount);
    }
    
    async getDisplayBalance(tokenAddress, userAddress) {
        const token = new ethers.Contract(tokenAddress, ScaledUIAmountABI, provider);
        const rawBalance = await this.getInternalBalance(userAddress, tokenAddress);
        
        if (await this.supportsScaledUI(tokenAddress)) {
            return await token.toUIAmount(rawBalance);
        }
        return rawBalance;
    }
}

DEFI PROTOCOL INTEGRATION

DeFi protocols should:

  1. Continue using raw amounts for all protocol operations

  2. Provide UI helpers for displaying adjusted amounts

  3. Emit events with both raw and UI amounts where relevant

  4. Document clearly which amounts are used in calculations

Copyright

Copyright and related rights waived via CC0.

5 Likes

Love the work put into standardizing the UI and making a DeFi-compatible alternative for rebase tokens! I’d like to add a few suggestions to keep the interface minimal to accommodate a broader range of use cases.

  • totalSupplyUI(): If we are having balanceOfUI, I think this function might be good to add to the standard as well.

  • toUIAmount and fromUIAmount: I feel these conversion functions should likely be excluded from the standard or make them optional. This is due to:

    • Vulnerability to TOCTOU (Time-of-Check-to-Time-of-Use): A change in the UI multiplier between the frontend reading balanceOfUI and calling fromUIAmount to calculate the raw amount used in the transaction could lead to unexpected results.
    • Context-Dependent Rounding: Different use cases may require different rounding methods for converting between raw and UI amounts. For example, for depositing tokens into a protocol, the formula amount_deposit = amount_deposit_ui * balance / balance_ui may be preferred to avoid leaving “dust” in the user’s wallet when depositing the maximum amount.
  • UIMultiplierUpdated: Standardizing this event may not be necessary, as some applications may have their UI multipliers update over time or not update at a preset moment. I got a chance to chat with some API providers, and they don’t think the event is required to support UI balance to offer a similar experience to the Solana tokens that use Scaled UI amount extension or the interesting bearing extension.

  • It may be useful (but probably optional) to emit an event during token transfer for the UI transferred amount, like TransferWithUIAmount(from,to,amount,uiAmount)

  • setUIMultiplier: This is an administrative function. I feel it’s better to not be included in the standard.

1 Like

@gilbertS - thanks for the thoughtful reply.

We support all of your requested changes. We feel the TOCTOU can be mitigated through other ways, but agree that making it optional makes sense.

I’ll go ahead and makes these changes to the proposed RFC in github.

Hi @cridmann, congrats on the ERC proposal, much needed for tokenized stocks!


UIMultiplierUpdated Event

event UIMultiplierUpdated(uint256 oldMultiplier, uint256 newMultiplier, uint256 setAtTimestamp, uint256 effectiveAtTimestamp);

I would consider removing the setAtTimestamp parameter from the event, since it only conveys information for when action was triggered (block.timestamp), which can also be checked through the transaction receipt.

oldMultiplier could also be removed (available in previously emitted event), but it can be handy to have both at the same time. No strong opinion here.


setUIMultiplier Function

function setUIMultiplier(uint256 newMultiplier, uint256 effectiveAtTimestamp) external

The proposed signature seems fine. A bit opinionated to force the timestamp parameter, but it can be solved by removing the “timestamp in the future” requirement.

I agree with @gilbertS that there’s an argument for leaving the administrative function (and possibly the event) out of the standard. If we can envision a single setter covering all administrative use-cases, I don’t see a problem leaving it in though.


toUIAmount and fromUIAmount Helpers

Given that both toUIAmount and fromUIAmount always perform the same operations, and they should never vary from implementations, I’d argue these are not necessary within the standard and should be calculated through libraries:

  • Querying uiMultiplier and computing the scaled result off-chain.
  • Querying uiMultiplier and using a library to compute the scaled result on-chain.

Both methods are more efficient than their helper counterparts.

I’d consider removing them.


balanceOfUI Function

If we’re having balanceOfUI into the standard, I’m aligned with @gilbertS and I think we should also have totalSupplyUI.

Nonetheless, I lean towards making the standard leaner and removing them altogether, as I cannot think of a case to use different rounding. You’d always want to round down.


Summarizing, the following interface seems completely fine for the standard and I believe achieves the same client-facing functionality (scaling amounts).

interface IScaledUIAmount {
    // Returns the current UI multiplier
    // Multiplier is represented with 18 decimals (1e18 = 1.0)
    function uiMultiplier() external view returns (uint256);
}
1 Like

I also echo @gilbertS and believe the administrative function should be left out of the standard. I believe it should be enough to require that the relevant UIMultiplierUpdated is always emitted when the multiplier is changed.

I also agree with @tinom9 that the balanceOfUI could be removed, or considered optional.

Overall, I would agree with @tinom9 ‘s proposed interface, but in addition include the UIMultiplierUpdated event for consumption off-chain.

Thanks for your thoughts @tinom9 and @pakim249CAL .

I support removing setAtTimestamp from the UIMultiplierUpdatedEvent for the reasons cited - good call out. I would endorse leaving the event in the spec itself though, as serves an important function for off-chain indexers.

I’m fine with removing the toUIAmount and fromUIAmount helpers.

I would endorse with leaving balanceOfUI and totalSupplyUI in the spec, but am OK with it being optional.

Thanks for the update @cridmann!

UIMultiplierUpdated event in the spec seems reasonable for indexers.

balanceOfUI and totalSupplyUI can be helpful in contracts that are not constrained by bytecode size. Therefore, having them as optional extensions to the base spec seems like a good idea, similarly to how ERC721 and ERC1155 handle optional interfaces.

On a separate topic, I believe the standard would be much more powerful if it didn’t require the scaled amounts to be only used in a UI.

Unfortunately, a lot of tokens currently implement rebasing or other esoteric mechanics to achieve stock splits, management fees, or updated exchange rates, and I tend to think that part of the reason is not having sufficiently standardized and simple methods that achieve the same behaviour: a share amount (the underlying raw amounts), and an asset amount (the scaled amounts), that UIs know how to represent, and that can be used as a source of truth for any business logic from the asset issuer (on-ramping, off-ramping, oracle price consumption, redemptions).

I would consider dropping the UI-specific requirement, renaming UI amounts to scaled amounts, and defining that implementers can use the multiplier as a non-purely cosmetic value. Amount representation in wallets and ERC20 operations will still have the same implications as they currently have.

I would also consider explicitly implementing the ERC165 interface, since the provided JavaScript examples seem to use it.

Hi @tinom9,

Thank you for your valuable input! I also think it makes sense to exclude setAtTimestamp from the event, as this information can usually be obtained from the block timestamp when indexing the event.

Regarding the UI naming discussion, I suggest keeping the current naming. Business logic can differ from token to token and sometimes cannot be captured by a single multiplier. I think it would be best to keep it as a minimal standard, yet one that is complete enough for API providers, Oracle providers, and wallet developers to support.

For token-dependent logic, I think one can compose it with existing or future standards—for example, ERC-4626—when the underlying asset is also another ERC-20 token. Additionally, I think the UI naming echoes Solana’s “UI amount” concept used in the Scaled UI Amount and Interest-Bearing extensions.

Hey @gilbertS, thank you for your response!

I see the point in sticking to the UI-related naming, rather than an agnostic multiplier, due to the bias (or consistency) towards the existing Solana implementation.

Nonetheless, I believe we’re still very early in RWAs on-chain, and we should try to maximize usage, while properly solving the underlying problems we have.

  • UI displays are equally solvable by either UI-specific and non-specific namings.
  • Other use-cases may not be solvable by only UI-specific naming.

ERC4626 solves the ERC20 over ERC20 issue with a complete and potentially too heavy interface.

A leaner exchange rate logic can be achieved by a simple multiplier, especially when the underlying assets have real world counterparts that require on and off ramping, such as stocks or bonds.

From an indexer POV, 8056 feels pretty awkward.

  • It reuses the normal Transfer event, so you can’t tell from the log itself whether this is an 8056-adjusted token transfer. You first need reliable token-type detection / ERC165 detection (the rpc roundtrip can fail, which needs separate handling, etc), and if that ends up wrong the downstream transfer indexed + display lands wrong too.

  • The Transfer event still only gives the raw amount. The adjusted UI amount has to be reconstructed later from historical multiplier state, so correctness depends on the indexer getting that historical lookup exactly right.

  • TransferWithUIAmount being optional makes it more of a hint than something indexers can depend on.

If TransferWithUIAmount is optional, indexers can’t really depend on it. Ideally it’s mandatory, or another mandatory event which includes both raw and UI amounts for indexing consumption.

We have added support for 8056 on both BSCscan and Mantlescan (testing data on Mantle Sepolia at the moment). :slight_smile:

Hi all — jumpbox here; we build open-source tooling on Robinhood Chain and read uiMultiplier() on the live RHC stock-tokens, so we did a close pass on the spec alongside the deployed RHC Stock implementation (0xb35490d6f9163DE4F80d88dc75c3516eb64C5aE2). A few findings we think are worth folding in, plus an offer of a compilable reference implementation + conformance suite for assets/erc-8056/. All of this is meant to help the spec land cleanly — happy to do the PR work once you agree on direction.

1. The reference implementation has a boundary defect that your own production already fixes

uiMultiplier() resolves the effective value with >=, but setUIMultiplier() decides whether to commit a matured pending multiplier with >. They disagree at exactly block.timestamp == _effectiveAt: at that instant uiMultiplier() already reports _newUIMultiplier as live, but setUIMultiplier() takes the “still pending” branch, never commits the now-effective value into _uiMultiplier, and emits a stale oldMultiplier.

Concretely — schedule a 2:1 split effective at t=100; at t=100 the split is live; the issuer schedules the next update in that same block:

[reference impl, `>`]   uiMultiplier() after the second setUIMultiplier: 1.0   (the live 2× split silently reverts to 1× for [100,200))
                        UIMultiplierUpdated old = 1e18  (should be 2e18)
[fix]                   uiMultiplier(): 2.0            (preserved)

(Runnable PoC + a full 12-test conformance suite — clone and forge test to watch the split revert: https://github.com/jumpboxtech/erc-8056-conformance)

The clean fix is the one the deployed RHC Stock contract already uses — crystallize the currently-effective multiplier on every update rather than branching:

uint256 oldMultiplier = uiMultiplier();   // resolves through the same >= boundary
$._multiplier = oldMultiplier;            // commit it, unconditionally
$._newMultiplier = newMultiplier;
$._effectiveAt = effectiveAt_;
emit UIMultiplierUpdated(oldMultiplier, newMultiplier, effectiveAt_);

Aligning the spec’s reference implementation with this production approach removes the boundary case entirely.

2. The “pending” getters report an already-effective multiplier as pending

newUIMultiplier() / effectiveAt() are unconditional, so once effectiveAt passes, newUIMultiplier() equals the live uiMultiplier() and effectiveAt() is a past timestamp — yet the extension’s whole purpose is to signal a future, not-yet-applied change (e.g. an integrator rendering “upcoming split: X on date Y”). There’s no way to read “nothing pending.” (This is true of the deployed contract too.) Suggest gating the getters on block.timestamp < effectiveAt and specifying the return when nothing is scheduled.

3. Indexer reliability (picking up @Enigmatic331’s still-open point) — with a concrete production example

@Enigmatic331 raised that you can’t tell from a Transfer log whether it’s 8056-adjusted, and that the optional TransferWithUIAmount event can’t be depended on. Live evidence for exactly this: the deployed RHC contract emits the transfer-with-UI event under a different name and signatureTransferWithScaledUI(from, to, value, uiValue) rather than the spec’s TransferWithUIAmount(...). An indexer coded to the spec’s event misses every RHC transfer, and one reading only raw Transfer + balanceOf silently undercounts total-return balances. RWA.xyz measured this exact class of issue on Robinhood’s tokenized stocks: across 21 mismatched tokens, reported supply was overstated by ~64,000 tokens — a 56% discrepancy — because “third-parties relying on standard ERC-20 calls have no way to detect when a multiplier changes or query its current value.”

Suggestion: make a raw+scaled transfer event mandatory with a fixed signature (so indexers have one reliable key), or define a dedicated ERC-165 interface ID whose presence signals the event is emitted. Either resolves the “can’t detect / can’t depend on it” problem without forcing a specific business meaning.

4. A few smaller normative gaps

  • Multiplier MUST NOT be 0. fromUIAmount/balanceOfUI divide by uiMultiplier(); a zero multiplier is a division-by-zero DoS. The reference guards it in the setter, but there’s no normative MUST.

  • Rounding is unspecified. Integer division truncates; fromUIAmount∘toUIAmount loses dust; reverse splits round small balances toward zero; Σ balanceOfUI ≠ totalSupplyUI. Prior art all pin this — Aave rayMul rounds half-up at 1e27, ERC-4626 fixes direction per function, Solana documents trunc-toward-zero and “not guaranteed to round-trip.” Worth a required rounding direction + an explicit round-trip caveat.

  • Overflow. The reference multiplies with raw * and wires toUIAmount into _update, so a large multiplier can revert transfers / brick balanceOfUI. The deployed contract uses Math.mulDiv; the reference could too.

  • “Required Extension” via ERC-165 is slightly self-contradictory — if IScaledUIAmountNewUIMultiplier is mandatory, either fold newUIMultiplier()/effectiveAt() into the core interface, or make detection meaningful; and add explicit text that compliant contracts MUST return true for its ID.

Offer

We’ve written a compilable reference implementation + a 12-test Foundry conformance suite (ERC-165 IDs, initial/2:1/reverse split, raw-transfer backwards-compat, pending semantics, zero-multiplier revert, round-trip, and the boundary regression), suitable for assets/erc-8056/ — there’s no runnable test suite there today: https://github.com/jumpboxtech/erc-8056-conformance. Glad to open that as a PR, and to draft the spec text for any of the above once you signal which direction you’d like.

— jumpbox · jumpboxtech (JumpboxTech) · GitHub

Following up from an external tg thread with an optional extension to the proposal**: an optional UIMultiplierUpdateCancelled event**

Over at Base, we’ve completed a full EIP-8056 integration to Base’s enshrined B20 asset standard, slated to go live at the next hardfork.

In so doing, we hit one observability gap: when an issuer schedules a UI-multiplier update via the pending-multiplier extension, there’s no signal if that pending update is later cancelled or superseded before it takes effect.

If a more urgent in-kind dividend or stock split takes precedence before the original pending update kicks in, operators must take steps to overwrite the original update with the more urgent one and then reschedule the original, later update.

Using only the current UIMultiplierUpdated event during this shakeup leads to a confusing event trail, liable to be misunderstood by offchain consumers. An indexer that sees the flurry of UIMultiplierUpdatedevents has no concrete avenue to reconstruct the timeline of multiplier updates offchain, conceivably leading the multiplier timeline to silently diverge from the actual occurrences or the operator’s intents.

We’d suggest a single optional event:

// OPTIONAL: emitted when a scheduled UI-multiplier update is cancelled or
//           superseded before its effectiveAt.

event UIMultiplierUpdateCancelled(uint256 cancelledMultiplier, uint256 cancelledEffectiveAt);

// Suggested placement: optional event on IScaledUIAmount, alongside TransferWithUIAmount. 

Deliberately just an event, no new function, to stay consistent with the spec on two fronts: the cancel mechanism stays implementation-defined (a la setUIMultiplier), and it mirrors the existing optional TransferWithUIAmount event. This suggestion is tailored to fit the spec’s existing shape & design decisions.

A separate point in favor of this suggestion:
BNB’s BEP-677 independently added scheduled-update + overwrite-audit events, and Solana’s original ScaledUiAmount has the same overwrite-a-pending footgun (with a documented two-transaction workaround) and no cancellation signal. The gap (and developers’ choices to fill it) has shown up in multiple implementations of this proposal.

Thanks again and let me know what you think !

Hi all,

I believe the core mechanism in EIP-8056 for a uiMultiplier is a valuable primitive for improving the user experience of tokens with dynamic underlying values.

However, I propose that all references to “Stock Splits” be removed from the EIP’s title and descriptive text.

My reasoning is that this EIP functions as a display-layer tool and does not address the fundamental ledger mechanics required to properly execute a stock split or reverse stock split.

Specifically:

  1. No Fractional Handling: Real-world stock splits often create fractional entitlements (e.g., a 3-for-2 split on 101 shares). A proper implementation must handle these fractions, typically by paying cash-in-lieu (CIL). This EIP has no mechanism for this.

  2. No Balance Preservation: As the discussion has noted, Σ balanceOfUI ≠ totalSupplyUI due to unspecified rounding. This reconciliation break is unacceptable for regulated securities, where value cannot be created or destroyed by rounding dust.

  3. Reverse Split Failure: In a reverse split, the integer math could round a user’s balance to zero at the UI level, but this EIP provides no mechanism to extinguish the now-valueless on-chain balance and compensate the user, which is a critical part of the process.

By including “stock split” in the EIP’s framing, we risk misleading developers into believing this is a sufficient solution for tokenized equity, when it is not. Reframing it purely as a “Scaled UI Amount Extension” would be more accurate and prevent future implementation errors.

Happy to discuss further.

this is super useful, even outside of tokenized asset use case.

i wonder what people think about having a “scaled ui name/symbol”? this could avoid confusion for frontends that have not yet migrated, and could allow ui to also show the scaled and non scaled amount for a token under two different canonical names?

this feels rather useful from a ux standpoint.

Hi all — I built an optional extension layer for 8056 which I believe to be crucial to allow maximum DeFi friendliness for CA-governed Stock tokens such as Robinhood ones and B20.

I’d like to put it up for the spec discussion.

Repo: SolidityDrone/erc-8056-scaling-classes — Foundry test suite and docs.

Full the motivation and techincal reference is at: Docs

Notice: this is reference work for the discussion — not audited production code and should be consumed just as reference repository.

What it does

In one sentence: it decomposes the 8056 multiplier into named scaling classes — Supply (splits), Yield (dividends), Other — each with its own on-chain history.

This allows for more flexible and DeFi friendly standard.

Shipped as a retrocompatible extension set that wouldn’t harm protocols already integrating current Vanilla ERC-8056

The insight

8056’s uiMultiplier tells you how much, never why.

When it moves 1.0 → 2.0, two opposite events look identical on-chain: a 2-for-1 stock split, or a dividend reinvestment.

The split changes the denomination — nobody’s principal grew.

The dividend grows the backing pool pro-rata — every holder earned something new.

That missing “why” is the whole ballgame.

It’s the difference between a token you can only display differently and a token you can reason about financially.

What it unlocks

1. Principal + yield split — Pendle PT/YT style, but native.

Once multiplier changes carry a class and a history, the token splits into two ERC-20s: a Capital leg (principal) and a Yield leg (distributions over a window).

Lending lends the principal and sells the yield. Options write the coupon as a deterministic payoff. Auctions sell future distributions.

Everything a PT/YT system does — settled against the token’s own multiplier history.

2. Derivative and digital twin, from one oracle read.

A vanilla stock token is stuck as a total-return instrument: its price drifts from the underlying share as dividends compound.

With the Yield class separable, the same Chainlink feed read gives both: the derivative price as-is, and a split-adjusted digital-twin price with the yield elided.

A lending market can collateralize the asset. A settlement layer can reconcile against the real-world ledger.

This is deployed reality: Chainlink’s equity feeds already read uiMultiplier() from Robinhood stock tokens.

Base ships 8056 in its B20 asset standard.

The design

Two optional, ERC-165-discoverable interfaces. Vanilla IDs preserved byte-for-byte.

A vanilla token answers composite = true, wrapper = false.

Interface ID Adds
IERC8056Composite 0xf9712df3 MultiplierClass {Supply, Yield, Other} — class decomposition, history, pending state, issuer tooling
IERC8056PairWrapper 0x8a8c95d4 the Capital/Yield split, implemented by the token itself

IERC8056Composite0xf9712df3 (class decomposition)

Group Functions
Class factors uiScalingFactor(class) · uiScalingFactorAt(class, ts) · uiScalingFactorAtNonce(class, nonce)
Composed reads uiMultiplierAt(ts) · uiMultiplierAtNonce(nonce)
History & nonces getClassNonce(class) · classEventAtNonce(class, nonce) · scalingHistoryLength(class) · scalingCheckpointAt(class, index)
Pending state newUIMultiplier(class) · effectiveAt(class) · hasPendingUIMultiplier(class)
Issuer writes setUIMultiplier(class, newMultiplier, effectiveAt, id, description, uri) · cancelPendingUIMultiplier(class)
Guardrails minNoticePeriod() · setMinNoticePeriod(seconds)

IERC8056PairWrapper0x8a8c95d4 (Capital/Yield split, implemented by the token itself)

Group Functions
Windows & actions wrap(raw, lockNonces) → (start, target) · unwrap / unwrapYield / unwrapCapital → rawOut · isMatured(start, target)
Legs pairs(start, target) · capitalToken(start, target) · yieldToken(start, target)
Pricing couponOf(start, target) · capitalShareOf(start, target) · previewUnwrap · previewUnwrapYield · previewUnwrapCapital
Solvency & state rawLocked() · windowBackingOf(start, target) · currentNonce()
Identity underlying() · scaledUnderlying() · assetName() · assetSymbol()

Naming: uiScalingFactor* reads one class, uiMultiplier* reads the composed product — derived at read time, never stored. Vanilla readers see no difference.

The whole wrap system in one example

The issuer pays two dividends. Each Yield event ticks a nonce and raises the cumulative multiplier:

Yield nonce Y_n Event
0 1.00 genesis
1 1.50 dividend #1
2 2.00 dividend #2

Alice commits to the next two distributions — counted in events, not dates:

solidity

(uint s, uint t) = token.wrap(100 ether, 2);   // window (0,2): 100 raw escrowed, legs 1:1

// dividends land → Y: 1.00 → 1.50 → 2.00 → nonce = 2 → window matures
// coupon froze at the endpoints: 1 − Y_0/Y_2 = 1 − 1.00/2.00 = 0.5

token.previewUnwrapYield(100 ether, s, t);     // 50 raw — the yield side's cut
token.previewUnwrapCapital(100 ether, s, t);   // 50 raw — Alice's untouched principal

The Yield side owns half the escrow because the multiplier doubled.

Dividends accrued +1.00x on her principal — 1 − 1.00/2.00 says half the pool is attributable to that gain.

Legs are transferable. Alice sells her Yield legs to Bob on an AMM; each side then redeems its own claim:

Step Action Raw out Escrow after
0 Alice wraps 100 raw 100
1 Bob unwraps 100 Yield-0-2 50 — the dividends’ accrued value 50
2 Alice unwraps 100 Capital-0-2 50 — her principal, exactly what’s left 0

Bob never touched the stock; the escrow paid him the yield the dividends generated on it.

Total claims always equal the escrow — invariant-tested, every combined unwrap pays exactly par.

A fifth, seventh, tenth later dividend never reprices this closed window — the coupon froze at the endpoint nonces.

A latecomer who wrapped at nonce 1 into (1,2) gets coupon 0.25 — he only lived through dividend #2.

Same target, different start, different claim.

How it works under the hood

Every class keeps an append-only checkpoint log: {effectiveAt, cumulativeMultiplier, multiplierRatio} per event.

Historical reads are binary searches via OpenZeppelin’s Arrays — O(log n), gas independent of history length.

Effective entries are immutable; only still-pending future entries can be popped on reschedule or cancel — nonces stay stable indices forever.

ERC8056CompositePairWrapper implements both interfaces in one contract — the stock token is the factory.

wrap() self-escrows (no approval transaction). unwrap* returns the exact raw released. isMatured() is a first-class view.

Full consumer guide with a longer walkthrough: 3-INTEGRATION.md.

Upgrading a live vanilla proxy is handled explicitly: inherited vanilla slots serve the pre-upgrade denomination until the first schedule.

Docs & closing

Reading order: 1-MOTIVATION2-TECHNICAL3-INTEGRATION4-CHAINLINK-VALUATION.

Published as-is — a reference, not a competitor to the spec.

If the direction is useful for an optional 8056 extension, the repo is a starting point.

Questions and objections welcome