> For the complete documentation index, see [llms.txt](https://docs.theo.xyz/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.theo.xyz/developers/contract-reference/thusd.md).

# thUSD

The thUSD token contract. ERC-20 with permit and burn, 6 decimals, one authorized minter.

|                          |                                                                                                                         |
| ------------------------ | ----------------------------------------------------------------------------------------------------------------------- |
| Proxy                    | [`0xa3fE5c7596024E6811E14F029937D5bd8Ae485b3`](https://etherscan.io/token/0xa3fE5c7596024E6811E14F029937D5bd8Ae485b3)   |
| Implementation           | [`0xCD04F57464d25F257089A286AAb47DF096d300Ad`](https://etherscan.io/address/0xCD04F57464d25F257089A286AAb47DF096d300Ad) |
| Contract                 | `ThUSD` in `contracts/thusd/ThUSD.sol`                                                                                  |
| Source                   | [Sourcify, exact match](https://repo.sourcify.dev/1/0xCD04F57464d25F257089A286AAb47DF096d300Ad), solc 0.8.28            |
| Name / symbol / decimals | `thUSD` / `thUSD` / `6`                                                                                                 |
| Owner                    | Timelock, `0x2bB4…CA02`, 2-day delay                                                                                    |
| Minter                   | Mint & Redeem, `0x2D99…696e`                                                                                            |

## Inheritance

`ERC20Upgradeable` → `ERC20BurnableUpgradeable` → `ERC20PermitUpgradeable` → `Ownable2StepUpgradeable` → `UUPSUpgradeable`

All from OpenZeppelin Contracts Upgradeable v5. The contract adds one state variable, `minter`, and three functions of its own.

## Behaviour

thUSD is a plain ERC-20. Transfers, approvals, and balances follow the standard with no hooks, no fees, no pause, and no transfer allowlist. The whitelist that gates minting and redemption does not apply to holding or moving thUSD.

Supply changes through two paths. `mint` is callable only by the single `minter` address, which the owner sets. `burn` and `burnFrom` are public, inherited from `ERC20Burnable`, so any holder can destroy their own thUSD and any approved spender can destroy thUSD they have an allowance for. The Mint & Redeem contract uses `burnFrom` during redemption.

`decimals()` is hard-coded to `6`, matching USDC and USDT. Amounts on this contract and on Mint & Redeem are in 6-decimal units; `1000000` is one thUSD.

## Functions

### Token

| Function                                                                                         | Access                | Notes                                                                                                        |
| ------------------------------------------------------------------------------------------------ | --------------------- | ------------------------------------------------------------------------------------------------------------ |
| `name()`, `symbol()`, `decimals()`                                                               | view                  | `thUSD`, `thUSD`, `6`                                                                                        |
| `totalSupply()`, `balanceOf(address)`, `allowance(address,address)`                              | view                  | Standard                                                                                                     |
| `transfer(address,uint256)`, `transferFrom(address,address,uint256)`, `approve(address,uint256)` | anyone                | Standard. Revert with ERC-6093 errors (`ERC20InsufficientBalance`, `ERC20InsufficientAllowance`, and so on). |
| `burn(uint256)`                                                                                  | anyone                | Burns the caller's tokens                                                                                    |
| `burnFrom(address,uint256)`                                                                      | anyone with allowance | Spends allowance, then burns                                                                                 |

### Permit (EIP-2612)

| Function                                           | Access | Notes                                                                  |
| -------------------------------------------------- | ------ | ---------------------------------------------------------------------- |
| `permit(owner, spender, value, deadline, v, r, s)` | anyone | Sets allowance from a signature                                        |
| `nonces(address)`                                  | view   | Current permit nonce                                                   |
| `DOMAIN_SEPARATOR()`                               | view   | EIP-712 domain separator                                               |
| `eip712Domain()`                                   | view   | Name `thUSD`, version `1`, chain `1`, verifying contract `0xa3fE…85b3` |

### Minting

| Function                           | Access        | Notes                                                                                                                          |
| ---------------------------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| `mint(address to, uint256 amount)` | `minter` only | Reverts `OnlyMinter()` for any other caller                                                                                    |
| `minter()`                         | view          | Current minter address                                                                                                         |
| `setMinter(address)`               | owner         | Replaces the minter. Emits `MinterSet`. No zero-address check, so the owner can disable minting by setting it to `address(0)`. |

### Ownership and upgrade

| Function                                         | Access        | Notes                                                                     |
| ------------------------------------------------ | ------------- | ------------------------------------------------------------------------- |
| `owner()`, `pendingOwner()`                      | view          | Two-step ownership                                                        |
| `transferOwnership(address)`                     | owner         | Nominates. The nominee must call `acceptOwnership()`.                     |
| `acceptOwnership()`                              | pending owner | Completes the transfer                                                    |
| `renounceOwnership()`                            | owner         | Sets owner to zero. Would leave the minter fixed and upgrades impossible. |
| `upgradeToAndCall(address, bytes)`               | owner         | UUPS upgrade. `_authorizeUpgrade` is `onlyOwner`.                         |
| `proxiableUUID()`, `UPGRADE_INTERFACE_VERSION()` | view          | ERC-1822 and OZ upgrade interface, `5.0.0`                                |
| `initialize(address owner)`                      | once          | Already called. Constructor disabled initializers on the implementation.  |

## Events

| Event                                                                               | Emitted by                                      |
| ----------------------------------------------------------------------------------- | ----------------------------------------------- |
| `Transfer(address indexed from, address indexed to, uint256 value)`                 | Transfers, mints (`from` = 0), burns (`to` = 0) |
| `Approval(address indexed owner, address indexed spender, uint256 value)`           | `approve`, `permit`, allowance spends           |
| `MinterSet(address indexed minter)`                                                 | `setMinter`                                     |
| `OwnershipTransferStarted(address indexed previousOwner, address indexed newOwner)` | `transferOwnership`                             |
| `OwnershipTransferred(address indexed previousOwner, address indexed newOwner)`     | `acceptOwnership`, `renounceOwnership`          |
| `Upgraded(address indexed implementation)`                                          | `upgradeToAndCall`                              |
| `Initialized(uint64 version)`                                                       | `initialize`                                    |
| `EIP712DomainChanged()`                                                             | Never in practice; inherited                    |

## Errors

| Error                                                                                                                                                                                 | When                                        |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------- |
| `OnlyMinter()`                                                                                                                                                                        | `mint` called by anyone other than `minter` |
| `ERC20InsufficientBalance`, `ERC20InsufficientAllowance`, `ERC20InvalidSender`, `ERC20InvalidReceiver`, `ERC20InvalidApprover`, `ERC20InvalidSpender`                                 | Standard ERC-6093                           |
| `ERC2612ExpiredSignature(uint256 deadline)`, `ERC2612InvalidSigner(address signer, address owner)`                                                                                    | `permit`                                    |
| `InvalidAccountNonce(address account, uint256 currentNonce)`                                                                                                                          | `permit` nonce mismatch                     |
| `OwnableUnauthorizedAccount(address)`, `OwnableInvalidOwner(address)`                                                                                                                 | Ownership guards                            |
| `UUPSUnauthorizedCallContext()`, `UUPSUnsupportedProxiableUUID(bytes32)`, `ERC1967InvalidImplementation(address)`, `ERC1967NonPayable()`, `FailedCall()`, `AddressEmptyCode(address)` | Upgrade machinery                           |
| `InvalidInitialization()`, `NotInitializing()`                                                                                                                                        | Initializer guards                          |
| `ECDSAInvalidSignature()`, `ECDSAInvalidSignatureLength(uint256)`, `ECDSAInvalidSignatureS(bytes32)`                                                                                  | Signature parsing in `permit`               |

## Integration notes

* **Decimals are 6.** Do not assume 18. `convertToAssets` on sthUSD and every amount on Mint & Redeem also use 6.
* **Permit works with USDC-style flows.** The domain name is `thUSD` and the version is `1`.
* **Burn is public.** A holder can reduce supply outside the redemption path. Indexers tracking supply should watch `Transfer` events to `address(0)` from any sender, not only from the minter.
* **`minter()` is the single mint authority.** If you monitor for supply integrity, watch `MinterSet` and alert on any change. A change requires a 2-day timelocked call from the owner.
* **The token itself can be upgraded** by the timelock. Watch `Upgraded` on the proxy.

## Verify

```bash
RPC=https://ethereum-rpc.publicnode.com
THUSD=0xa3fE5c7596024E6811E14F029937D5bd8Ae485b3

cast call $THUSD 'decimals()(uint8)'   --rpc-url $RPC   # 6
cast call $THUSD 'minter()(address)'   --rpc-url $RPC   # 0x2d99ac801dc0edadd53f5688fef2317932e8696e
cast call $THUSD 'owner()(address)'    --rpc-url $RPC   # 0x2bb4b7e6e83fa6b77d0143dad631843cb73dca02
cast call $THUSD 'pendingOwner()(address)' --rpc-url $RPC   # 0x0
cast storage $THUSD 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc --rpc-url $RPC
# ends in cd04f57464d25f257089a286aab47df096d300ad
```
