# PYUSDx Token & Yield

> The PYUSDx token: parameters, the earner manager, the per-account claimable yield model, transfers, mint/burn, and rate limiting.

## Token parameters

PYUSDx is an upgradeable, non-rebasing ERC-20 token. Its `name` and `symbol` are configurable and set at initialization. It uses **6 decimals** (PYUSD standard). The yield index uses a precision of `1e12` (`EXP_SCALED_ONE`), and fee and earner rates are expressed in basis points with a maximum of 10,000 bps (100%).

## Earner manager

The earner manager is a single address (not a role) that controls the yield system. It can:

- `setAccountInfo()`: enable, disable, or modify an account's earner configuration.
- `distributeReward()`: mint additional tokens to any account.

The earner manager also receives fees from yield claims. It is set at initialization and can be changed by the `DEFAULT_ADMIN_ROLE`.

## Yield model

PYUSDx implements **non-rebasing, per-account claimable yield** via continuous compounding. Unlike a rebasing token, an account's stored `balance` does not grow on its own. Yield accrues in the background and is only added to the balance when it is claimed (or otherwise materialized).

### How yield accrues

1. The earner manager calls `setAccountInfo(account, earnerRate, feeRate, claimRecipient)` to enable earning.
2. The account's `earningPrincipal` is derived from its current balance.
3. Each account maintains a **personal** `lastIndex` that compounds continuously:<span className="katex-display">
<span className="katex">
<span className="katex-mathml">
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block">
<semantics>
<mrow>
<mi>

c

</mi>

<mi>

u

</mi>

<mi>

r

</mi>

<mi>

r

</mi>

<mi>

e

</mi>

<mi>

n

</mi>

<mi>

t

</mi>

<mi>

I

</mi>

<mi>

n

</mi>

<mi>

d

</mi>

<mi>

e

</mi>

<mi>

x

</mi>

<mo>

=

</mo>

<mi>

l

</mi>

<mi>

a

</mi>

<mi>

s

</mi>

<mi>

t

</mi>

<mi>

I

</mi>

<mi>

n

</mi>

<mi>

d

</mi>

<mi>

e

</mi>

<mi>

x

</mi>

<mo>

×

</mo>

<mi>

exp

</mi>

<mo>

⁡

</mo>

<mrow>
<mo fence="true">

(

</mo>

<mfrac>
<mrow>
<mi>

e

</mi>

<mi>

a

</mi>

<mi>

r

</mi>

<mi>

n

</mi>

<mi>

e

</mi>

<mi>

r

</mi>

<mi>

R

</mi>

<mi>

a

</mi>

<mi>

t

</mi>

<mi>

e

</mi>

<mo>

×

</mo>

<mi>

e

</mi>

<mi>

l

</mi>

<mi>

a

</mi>

<mi>

p

</mi>

<mi>

s

</mi>

<mi>

e

</mi>

<mi>

d

</mi>

<mi>

T

</mi>

<mi>

i

</mi>

<mi>

m

</mi>

<mi>

e

</mi>
</mrow>

<mrow>
<mi>

S

</mi>

<mi>

E

</mi>

<mi>

C

</mi>

<mi>

O

</mi>

<mi>

N

</mi>

<mi>

D

</mi>

<mi>

S

</mi>

<mi mathvariant="normal">

_

</mi>

<mi>

P

</mi>

<mi>

E

</mi>

<mi>

R

</mi>

<mi mathvariant="normal">

_

</mi>

<mi>

Y

</mi>

<mi>

E

</mi>

<mi>

A

</mi>

<mi>

R

</mi>
</mrow>
</mfrac>

<mo fence="true">

)

</mo>
</mrow>
</mrow>

<annotation encoding="application/x-tex">

currentIndex = lastIndex \times \exp\left(\frac{earnerRate \times elapsedTime}{SECONDS\_PER\_YEAR}\right)

</annotation>
</semantics>
</math>
</span>

<span className="katex-html" ariaHidden="true">
<span className="base">
<span className="strut" style="height:0.6944em;">



</span>

<span className="mord,mathnormal">

c

</span>

<span className="mord,mathnormal">

u

</span>

<span className="mord,mathnormal" style="margin-right:0.0278em;">

r

</span>

<span className="mord,mathnormal" style="margin-right:0.0278em;">

r

</span>

<span className="mord,mathnormal">

e

</span>

<span className="mord,mathnormal">

n

</span>

<span className="mord,mathnormal">

t

</span>

<span className="mord,mathnormal" style="margin-right:0.0785em;">

I

</span>

<span className="mord,mathnormal">

n

</span>

<span className="mord,mathnormal">

d

</span>

<span className="mord,mathnormal">

e

</span>

<span className="mord,mathnormal">

x

</span>

<span className="mspace" style="margin-right:0.2778em;">



</span>

<span className="mrel">

=

</span>

<span className="mspace" style="margin-right:0.2778em;">



</span>
</span>

<span className="base">
<span className="strut" style="height:0.7778em;vertical-align:-0.0833em;">



</span>

<span className="mord,mathnormal" style="margin-right:0.0197em;">

l

</span>

<span className="mord,mathnormal">

a

</span>

<span className="mord,mathnormal">

s

</span>

<span className="mord,mathnormal">

t

</span>

<span className="mord,mathnormal" style="margin-right:0.0785em;">

I

</span>

<span className="mord,mathnormal">

n

</span>

<span className="mord,mathnormal">

d

</span>

<span className="mord,mathnormal">

e

</span>

<span className="mord,mathnormal">

x

</span>

<span className="mspace" style="margin-right:0.2222em;">



</span>

<span className="mbin">

×

</span>

<span className="mspace" style="margin-right:0.2222em;">



</span>
</span>

<span className="base">
<span className="strut" style="height:2.446em;vertical-align:-0.996em;">



</span>

<span className="mop">

exp

</span>

<span className="mspace" style="margin-right:0.1667em;">



</span>

<span className="minner">
<span className="mopen,delimcenter" style="top:0em;">
<span className="delimsizing,size3">

(

</span>
</span>

<span className="mord">
<span className="mopen,nulldelimiter">



</span>

<span className="mfrac">
<span className="vlist-t,vlist-t2">
<span className="vlist-r">
<span className="vlist" style="height:1.3714em;">
<span style="top:-2.314em;">
<span className="pstrut" style="height:3em;">



</span>

<span className="mord">
<span className="mord,mathnormal" style="margin-right:0.0576em;">

S

</span>

<span className="mord,mathnormal" style="margin-right:0.0576em;">

E

</span>

<span className="mord,mathnormal" style="margin-right:0.0715em;">

C

</span>

<span className="mord,mathnormal" style="margin-right:0.0278em;">

O

</span>

<span className="mord,mathnormal" style="margin-right:0.109em;">

N

</span>

<span className="mord,mathnormal" style="margin-right:0.0278em;">

D

</span>

<span className="mord,mathnormal" style="margin-right:0.0576em;">

S

</span>

<span className="mord" style="margin-right:0.0278em;">

_

</span>

<span className="mord,mathnormal" style="margin-right:0.1389em;">

P

</span>

<span className="mord,mathnormal" style="margin-right:0.0576em;">

E

</span>

<span className="mord,mathnormal" style="margin-right:0.0077em;">

R

</span>

<span className="mord" style="margin-right:0.0278em;">

_

</span>

<span className="mord,mathnormal" style="margin-right:0.2222em;">

Y

</span>

<span className="mord,mathnormal" style="margin-right:0.0576em;">

E

</span>

<span className="mord,mathnormal">

A

</span>

<span className="mord,mathnormal" style="margin-right:0.0077em;">

R

</span>
</span>
</span>

<span style="top:-3.23em;">
<span className="pstrut" style="height:3em;">



</span>

<span className="frac-line" style="border-bottom-width:0.04em;">



</span>
</span>

<span style="top:-3.677em;">
<span className="pstrut" style="height:3em;">



</span>

<span className="mord">
<span className="mord,mathnormal">

e

</span>

<span className="mord,mathnormal">

a

</span>

<span className="mord,mathnormal" style="margin-right:0.0278em;">

r

</span>

<span className="mord,mathnormal">

n

</span>

<span className="mord,mathnormal" style="margin-right:0.0278em;">

er

</span>

<span className="mord,mathnormal" style="margin-right:0.0077em;">

R

</span>

<span className="mord,mathnormal">

a

</span>

<span className="mord,mathnormal">

t

</span>

<span className="mord,mathnormal">

e

</span>

<span className="mspace" style="margin-right:0.2222em;">



</span>

<span className="mbin">

×

</span>

<span className="mspace" style="margin-right:0.2222em;">



</span>

<span className="mord,mathnormal">

e

</span>

<span className="mord,mathnormal" style="margin-right:0.0197em;">

l

</span>

<span className="mord,mathnormal">

a

</span>

<span className="mord,mathnormal">

p

</span>

<span className="mord,mathnormal">

se

</span>

<span className="mord,mathnormal">

d

</span>

<span className="mord,mathnormal" style="margin-right:0.1389em;">

T

</span>

<span className="mord,mathnormal">

im

</span>

<span className="mord,mathnormal">

e

</span>
</span>
</span>
</span>

<span className="vlist-s">

​

</span>
</span>

<span className="vlist-r">
<span className="vlist" style="height:0.996em;">
<span>



</span>
</span>
</span>
</span>
</span>

<span className="mclose,nulldelimiter">



</span>
</span>

<span className="mclose,delimcenter" style="top:0em;">
<span className="delimsizing,size3">

)

</span>
</span>
</span>
</span>
</span>
</span>
</span>

<br />

where `earnerRate` is the annual rate converted from basis points (via `convertFromBasisPoints`), `elapsedTime` is the seconds since the last update, and `SECONDS_PER_YEAR` is `31,536,000`. Dividing by `SECONDS_PER_YEAR` expresses the elapsed time as a fraction of a year.
4. The `earningPrincipal` stays fixed while the present value grows as the index grows:<span className="katex-display">
<span className="katex">
<span className="katex-mathml">
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block">
<semantics>
<mrow>
<mi>

p

</mi>

<mi>

r

</mi>

<mi>

e

</mi>

<mi>

s

</mi>

<mi>

e

</mi>

<mi>

n

</mi>

<mi>

t

</mi>

<mi>

V

</mi>

<mi>

a

</mi>

<mi>

l

</mi>

<mi>

u

</mi>

<mi>

e

</mi>

<mo>

=

</mo>

<mfrac>
<mrow>
<mi>

p

</mi>

<mi>

r

</mi>

<mi>

i

</mi>

<mi>

n

</mi>

<mi>

c

</mi>

<mi>

i

</mi>

<mi>

p

</mi>

<mi>

a

</mi>

<mi>

l

</mi>

<mo>

×

</mo>

<mi>

c

</mi>

<mi>

u

</mi>

<mi>

r

</mi>

<mi>

r

</mi>

<mi>

e

</mi>

<mi>

n

</mi>

<mi>

t

</mi>

<mi>

I

</mi>

<mi>

n

</mi>

<mi>

d

</mi>

<mi>

e

</mi>

<mi>

x

</mi>
</mrow>

<mrow>
<mi>

E

</mi>

<mi>

X

</mi>

<mi>

P

</mi>

<mi mathvariant="normal">

_

</mi>

<mi>

S

</mi>

<mi>

C

</mi>

<mi>

A

</mi>

<mi>

L

</mi>

<mi>

E

</mi>

<mi>

D

</mi>

<mi mathvariant="normal">

_

</mi>

<mi>

O

</mi>

<mi>

N

</mi>

<mi>

E

</mi>
</mrow>
</mfrac>
</mrow>

<annotation encoding="application/x-tex">

presentValue = \frac{principal \times currentIndex}{EXP\_SCALED\_ONE}

</annotation>
</semantics>
</math>
</span>

<span className="katex-html" ariaHidden="true">
<span className="base">
<span className="strut" style="height:0.8889em;vertical-align:-0.1944em;">



</span>

<span className="mord,mathnormal">

p

</span>

<span className="mord,mathnormal" style="margin-right:0.0278em;">

r

</span>

<span className="mord,mathnormal">

ese

</span>

<span className="mord,mathnormal">

n

</span>

<span className="mord,mathnormal">

t

</span>

<span className="mord,mathnormal" style="margin-right:0.2222em;">

V

</span>

<span className="mord,mathnormal">

a

</span>

<span className="mord,mathnormal" style="margin-right:0.0197em;">

l

</span>

<span className="mord,mathnormal">

u

</span>

<span className="mord,mathnormal">

e

</span>

<span className="mspace" style="margin-right:0.2778em;">



</span>

<span className="mrel">

=

</span>

<span className="mspace" style="margin-right:0.2778em;">



</span>
</span>

<span className="base">
<span className="strut" style="height:2.3674em;vertical-align:-0.996em;">



</span>

<span className="mord">
<span className="mopen,nulldelimiter">



</span>

<span className="mfrac">
<span className="vlist-t,vlist-t2">
<span className="vlist-r">
<span className="vlist" style="height:1.3714em;">
<span style="top:-2.314em;">
<span className="pstrut" style="height:3em;">



</span>

<span className="mord">
<span className="mord,mathnormal" style="margin-right:0.0576em;">

E

</span>

<span className="mord,mathnormal" style="margin-right:0.0785em;">

X

</span>

<span className="mord,mathnormal" style="margin-right:0.1389em;">

P

</span>

<span className="mord" style="margin-right:0.0278em;">

_

</span>

<span className="mord,mathnormal" style="margin-right:0.0576em;">

S

</span>

<span className="mord,mathnormal" style="margin-right:0.0715em;">

C

</span>

<span className="mord,mathnormal">

A

</span>

<span className="mord,mathnormal">

L

</span>

<span className="mord,mathnormal" style="margin-right:0.0576em;">

E

</span>

<span className="mord,mathnormal" style="margin-right:0.0278em;">

D

</span>

<span className="mord" style="margin-right:0.0278em;">

_

</span>

<span className="mord,mathnormal" style="margin-right:0.0278em;">

O

</span>

<span className="mord,mathnormal" style="margin-right:0.109em;">

N

</span>

<span className="mord,mathnormal" style="margin-right:0.0576em;">

E

</span>
</span>
</span>

<span style="top:-3.23em;">
<span className="pstrut" style="height:3em;">



</span>

<span className="frac-line" style="border-bottom-width:0.04em;">



</span>
</span>

<span style="top:-3.677em;">
<span className="pstrut" style="height:3em;">



</span>

<span className="mord">
<span className="mord,mathnormal">

p

</span>

<span className="mord,mathnormal" style="margin-right:0.0278em;">

r

</span>

<span className="mord,mathnormal">

in

</span>

<span className="mord,mathnormal">

c

</span>

<span className="mord,mathnormal">

i

</span>

<span className="mord,mathnormal">

p

</span>

<span className="mord,mathnormal">

a

</span>

<span className="mord,mathnormal" style="margin-right:0.0197em;">

l

</span>

<span className="mspace" style="margin-right:0.2222em;">



</span>

<span className="mbin">

×

</span>

<span className="mspace" style="margin-right:0.2222em;">



</span>

<span className="mord,mathnormal">

c

</span>

<span className="mord,mathnormal">

u

</span>

<span className="mord,mathnormal" style="margin-right:0.0278em;">

r

</span>

<span className="mord,mathnormal" style="margin-right:0.0278em;">

r

</span>

<span className="mord,mathnormal">

e

</span>

<span className="mord,mathnormal">

n

</span>

<span className="mord,mathnormal">

t

</span>

<span className="mord,mathnormal" style="margin-right:0.0785em;">

I

</span>

<span className="mord,mathnormal">

n

</span>

<span className="mord,mathnormal">

d

</span>

<span className="mord,mathnormal">

e

</span>

<span className="mord,mathnormal">

x

</span>
</span>
</span>
</span>

<span className="vlist-s">

​

</span>
</span>

<span className="vlist-r">
<span className="vlist" style="height:0.996em;">
<span>



</span>
</span>
</span>
</span>
</span>

<span className="mclose,nulldelimiter">



</span>
</span>
</span>
</span>
</span>
</span>
5. Yield is the difference between the compounded value and the stored balance: `presentValue - balance`.

Each account carries its own index, so earners accrue independently.

### Index math

- `ContinuousIndexingMath` (from m-extensions) provides the `exp()` approximation.
- `IndexingMath` handles principal ↔︎ present-amount conversions.
- `UIntMath.bound128()` caps index growth to `uint128.max`.
- Earner rates are in basis points (0–10,000), converted via `convertFromBasisPoints`.

### Claiming yield

Anyone can call `claimFor(account)`. It mints the account's accrued yield (`yieldWithFee = presentValue - balance`) as new tokens (increasing `totalSupply`), then splits it two ways:

- **Fee** (`yieldWithFee × feeRate / 10000`) → the earner manager.
- **Net yield** (`yieldNetOfFee = yieldWithFee - fee`) → the account's `claimRecipient`.

If the `claimRecipient` is the account itself, the yield simply stays in its balance, and no transfer occurs.

Special cases:

- `skipTransfer=true` (used by freeze and pause-time `setAccountInfo`): yield materializes to the earner's own balance; routing and fee transfers are skipped.
- Non-earners: `claimFor` is a no-op.

Whenever yield is materialized, `claimFor` refreshes the account's `lastIndex` and `lastUpdateTimestamp` to the current index on every path (including a zero-fee self-claim where no recipient transfer occurs), so subsequent yield always accrues from the moment of the claim.

### Pause/freeze-time yield behavior

Yield can also materialize via the `skipTransfer=true` path (no fee or claim-recipient routing) in two scenarios outside a normal `claimFor`:

- **While paused**: `claimFor()` reverts (it is `whenNotPaused`). `setAccountInfo()` remains callable as an emergency lever and materializes yield to the earner's own balance.
- **At freeze**: `_beforeFreeze()` uses `skipTransfer=true`, so yield materializes before freezing. Once materialized, the yield is part of the account's regular `balance` and can subsequently be seized by a [forced transfer](/protocol/pyusdx/compliance-and-security#forced-transfer).

### Overflow safety

- `_addEarningAmount` reverts when `earningPrincipal` would exceed `uint112`.
- `_getPrincipalAmountRoundedDown` and `_getPrincipalAmountRoundedUp` use `UIntMath.safe240()` for the principal cast; inputs above `type(uint240).max` revert with `InvalidUInt240` instead of truncating.

## Transfer mechanics

`_transfer(sender, recipient, amount)` behaves as follows:

1. Reverts if paused, or if the sender, recipient, or `msg.sender` is frozen.
2. If the sender is an earner: subtracts from both `balance` and `earningPrincipal` (principal rounded **up**).
3. If the sender is a non-earner: subtracts from `balance` only.
4. If the recipient is an earner: adds to both `balance` and `earningPrincipal` (principal rounded **down**).
5. If the recipient is a non-earner: adds to `balance` only.
6. Earners have their index snapshotted (`_updateIndexOf`) on every balance change.

<note title="Earner rounding">

Because incoming principal is rounded down and outgoing principal is rounded up, moving value through an earning balance is slightly lossy in **yield** terms, never in balance or backing. A vault-style integrator that mints shares against `balanceOf + accruedYieldOf` should call `claimFor(self)` first, since a small incoming transfer can leave `accruedYieldOf` decreased by up to one unit. The exact per-operation rounding matrices are on the [PYUSDx specification](/protocol/pyusdx-spec#rounding) page.

</note>

## Mint and burn

- **Mint** (`_mint`): only `ISSUER_ROLE`. Rate-limited. Adds to `balance` (and `earningPrincipal` if the account is an earner). Increases `totalSupply`.
- **Burn** (`burn`): only `ISSUER_ROLE`. The account must not be frozen. Subtracts from `balance` (and `earningPrincipal` if an earner). Decreases `totalSupply`. Burned tokens are sent to `address(0)`.
- **Distribute reward** (`distributeReward`): only the earner manager. Mints new tokens to an account (also rate-limited). Emits `RewardDistributed`.

<note title="Who ISSUER_ROLE burns from">

The two `ISSUER_ROLE` holders only ever burn their **own** tokens: `Portal` burns `address(this)`, and `IssuerGateway` burns the operator that called it (`burn(msg.sender, amount)`). No path lets an issuer burn a third party's balance. `burn` handles both account types: for a non-earner it reduces `balance` only, and for an earner it also reduces `earningPrincipal`, with the consumed principal rounded **up** (in the protocol's favor). As a result, any unclaimed yield at the moment of burn absorbs a sub-unit rounding loss in present value.

</note>

## Rate limiting

Mints and reward distributions are governed by a per-issuer token-bucket rate limit:

- `capacity`: the maximum burst size.
- `refillPerSecond`: the refill rate.

Rate-limit configuration is **mandatory**: `_enforceRateLimit` reverts with `RateLimitNotConfigured(issuer)` when called for an issuer that has never been configured, and `getRateLimitConfig` / `getRemainingAmount` return `(0, 0)` and `0` respectively for unconfigured issuers. Enabling a rate limit with `capacity == 0` reverts with `InvalidRateLimitConfig` to prevent a "configured but always reverts" bucket state. The limit is enforced on every `mint` and `distributeReward` call.

## Key invariants

1. `totalSupply` equals the sum of all account balances. Unclaimed yield is **not** included in `totalSupply`; yield enters supply only when it is materialized by `claimYield`, `setAccountInfo`, or `_beforeFreeze` (each mints `yieldWithFee` into the account's balance and increments `totalSupply` by the same amount).
2. An earner's `earningPrincipal` is always ≤ their `balance`.
3. An earner's `lastIndex` is monotonically increasing between the `_setAccountInfo` call that enables earning and the `_stopEarningFor` call that disables it. `_stopEarningFor` clears `lastIndex` to 0; a subsequent `_setAccountInfo` that re-enables earning re-initializes `lastIndex` to `EXP_SCALED_ONE` (`1e12`). `currentIndexOf` returns `EXP_SCALED_ONE` (`1e12`) for non-earners.
4. Freezing an account stops its earning and claims all pending yield first.
5. `totalSupply` can increase during pause only through `setAccountInfo` / `_beforeFreeze` yield materialization.
6. No regular transfers can occur while paused.

The exact rounding matrices behind these invariants are on the [PYUSDx specification](/protocol/pyusdx-spec#rounding) page.
