> 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/mint-and-redeem.md).

# Mint & Redeem

The Mint & Redeem contract (ThUSDMinter). EIP-712 signed orders, per-block caps, whitelist gate, and a hard-capped redemption fee.

|                         |                                                                                                                         |
| ----------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| Address                 | [`0x2D99aC801DC0edadD53f5688FeF2317932E8696e`](https://etherscan.io/address/0x2D99aC801DC0edadD53f5688FeF2317932E8696e) |
| Contract                | `ThUSDMinter` in `contracts/thusd/ThUSDMinter.sol`. Direct deployment, no proxy.                                        |
| Source                  | [Sourcify, exact match](https://repo.sourcify.dev/1/0x2D99aC801DC0edadD53f5688FeF2317932E8696e), solc 0.8.28            |
| Deployed                | Block 24,837,116                                                                                                        |
| `thusd` (immutable)     | `0xa3fE…85b3`                                                                                                           |
| `whitelist` (immutable) | TheoWhitelist, `0x14d3…f388`                                                                                            |
| `mintDestination`       | Cash Wallet, `0xEc41…3c2f`                                                                                              |
| `redeemDestination`     | Cash Wallet, `0xEc41…3c2f`                                                                                              |
| `maxMintPerBlock`       | `1000000000000` (1,000,000 thUSD)                                                                                       |
| `maxRedeemPerBlock`     | `200000000000` (200,000 thUSD)                                                                                          |
| `redeemFeeBps`          | `5` (0.05%). Hard cap `MAX_FEE_BPS` = `10`.                                                                             |
| `MINIMUM_MINT_AMOUNT`   | `1000000` (1.000000, in 6-decimal units)                                                                                |
| Supported assets        | USDC `0xA0b8…eB48`, USDT `0xdAC1…1ec7`                                                                                  |

## Inheritance

`AccessControl` → `ReentrancyGuard` → `Pausable` → `EIP712` → `IThUSDMinter`

OpenZeppelin Contracts v5, non-upgradeable variants. The contract is not a proxy and has no upgrade function. Its `thusd` and `whitelist` references are `immutable`, set in the constructor.

## Roles

| Role                 | Hash                                                                 | Holder                     | Can                                                                                                                |
| -------------------- | -------------------------------------------------------------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| `DEFAULT_ADMIN_ROLE` | `0x00…00`                                                            | Timelock (2-day delay)     | Add or remove supported assets, set destinations, set per-block caps, set the fee, unpause, grant and revoke roles |
| `MINTER_ROLE`        | `0x9f2df0fed2c77648de5860a4cc508cd0818c85b8b8a1ab4ceeef8d981c8956a6` | Minter EOA `0x09ec…53b6`   | Call `mint` and `redeem` with a depositor's signed order                                                           |
| `EMERGENCY_ROLE`     | `0xbf233dd2aafeb4d50879c4aa5c81e96d92f6e6945c906a58f9f2d1c1631b4b26` | Guardian EOA `0xF936…DD8F` | `pause`                                                                                                            |

Role administration follows OpenZeppelin `AccessControl`: `DEFAULT_ADMIN_ROLE` is the admin of every role, and `grantRole`, `revokeRole`, `renounceRole`, `hasRole`, and `getRoleAdmin` are exposed. Verification commands are on [Roles & access control](/security-and-transparency/roles-and-access-control.md).

## The order

Every mint and redeem is driven by a single struct, signed by the depositor under EIP-712.

```solidity
enum OrderType { MINT, REDEEM }   // 0, 1

struct Order {
    OrderType order_type;
    uint256   expiry;             // unix seconds; block.timestamp must be <= expiry
    uint256   nonce;              // any value that makes the order hash unique
    address   signer;             // who signs, who pays collateral (MINT) or thUSD (REDEEM)
    address   recipient;          // who receives thUSD (MINT) or collateral (REDEEM)
    address   collateral_asset;   // must be a supported asset
    uint256   collateral_amount;  // in the collateral's own decimals
    uint256   thusd_amount;       // in 6 decimals
}
```

**EIP-712 domain**

| Field               | Value                                        |
| ------------------- | -------------------------------------------- |
| `name`              | `ThUSDMinter`                                |
| `version`           | `1`                                          |
| `chainId`           | `1`                                          |
| `verifyingContract` | `0x2D99aC801DC0edadD53f5688FeF2317932E8696e` |

**Type string**

```
Order(uint8 order_type,uint256 expiry,uint256 nonce,address signer,address recipient,address collateral_asset,uint256 collateral_amount,uint256 thusd_amount)
```

Type hash: `0x2a75fcd2cf5bae81f57028932215d4ad255a37706b3cbd27eaeb2016381a72a3`

`hashOrder(Order)` returns the final digest (`_hashTypedDataV4` over the struct hash), so a client can confirm its own hashing against the contract before signing.

**Typed data for `eth_signTypedData_v4`**

```json
{
  "types": {
    "EIP712Domain": [
      { "name": "name", "type": "string" },
      { "name": "version", "type": "string" },
      { "name": "chainId", "type": "uint256" },
      { "name": "verifyingContract", "type": "address" }
    ],
    "Order": [
      { "name": "order_type", "type": "uint8" },
      { "name": "expiry", "type": "uint256" },
      { "name": "nonce", "type": "uint256" },
      { "name": "signer", "type": "address" },
      { "name": "recipient", "type": "address" },
      { "name": "collateral_asset", "type": "address" },
      { "name": "collateral_amount", "type": "uint256" },
      { "name": "thusd_amount", "type": "uint256" }
    ]
  },
  "primaryType": "Order",
  "domain": {
    "name": "ThUSDMinter",
    "version": "1",
    "chainId": 1,
    "verifyingContract": "0x2D99aC801DC0edadD53f5688FeF2317932E8696e"
  },
  "message": {
    "order_type": 0,
    "expiry": 1756800000,
    "nonce": 1,
    "signer": "0xYourWhitelistedWallet",
    "recipient": "0xYourWhitelistedWallet",
    "collateral_asset": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
    "collateral_amount": 1000000000,
    "thusd_amount": 1000000000
  }
}
```

Depositors sign; we submit. The signed order goes to our mint API together with any needed `permit` for the collateral. [Mint & redeem](/products/thusd/mint-and-redeem.md) covers onboarding.

## Validation

`mint` and `redeem` both run `_validateOrder` after their own checks. Every condition below must hold or the call reverts with the named error.

| Check                                                                                                             | Error                                                   |
| ----------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- |
| Caller holds `MINTER_ROLE`                                                                                        | `AccessControlUnauthorizedAccount`                      |
| Contract not paused                                                                                               | `EnforcedPause`                                         |
| `order_type` matches the function called                                                                          | `InvalidOrder`                                          |
| Block total plus this order ≤ `maxMintPerBlock` (or `maxRedeemPerBlock`)                                          | `MaxMintPerBlockExceeded` / `MaxRedeemPerBlockExceeded` |
| `collateral_amount ≥ 1000000` and `thusd_amount ≥ 1000000`                                                        | `InvalidAmount`                                         |
| `block.timestamp ≤ expiry`                                                                                        | `OrderExpired`                                          |
| `supportedAssets[collateral_asset]`                                                                               | `UnsupportedAsset`                                      |
| `signer` and `recipient` non-zero                                                                                 | `InvalidZeroAddress`                                    |
| `signer` is neither destination wallet                                                                            | `SignerIsDestination`                                   |
| MINT: `thusd_amount ≤ collateral_amount`, both scaled to 18 decimals. REDEEM: `collateral_amount ≤ thusd_amount`. | `InvalidAssetRatio`                                     |
| `hashOrder(order)` not previously used                                                                            | `OrderAlreadyUsed`                                      |
| `ECDSA.recover(hash, signature) == signer`                                                                        | `InvalidSignature`                                      |
| `signer` and `recipient` both whitelisted                                                                         | `TheoWhitelistNotWhitelisted(account)`                  |
| `signer` and `recipient` both not blacklisted                                                                     | `TheoWhitelistBlacklisted(account)`                     |

The scaling step reads `decimals()` from the collateral and from thUSD and multiplies each amount by `10^(18 − decimals)`. A collateral with more than 18 decimals would underflow and revert; none is supported today.

The order hash is marked used before the whitelist checks run. Because a failed check reverts the whole call, the mark is rolled back with it, and a rejected order can be resubmitted unchanged once the cause is fixed.

## Execution

**`mint(Order order, bytes signature)`**

1. Validate as above and add `thusd_amount` to `mintedPerBlock[block.number]`.
2. `collateral_asset.transferFrom(signer, mintDestination, collateral_amount)`. The signer must have approved this contract.
3. `thUSD.mint(recipient, thusd_amount)`.
4. Emit `Mint(signer, recipient, collateral_asset, collateral_amount, thusd_amount)`.

**`redeem(Order order, bytes signature)`**

1. Validate as above and add `thusd_amount` to `redeemedPerBlock[block.number]`.
2. `thUSD.burnFrom(signer, thusd_amount)`. The signer must have approved this contract for thUSD.
3. `fee = ceil(collateral_amount × redeemFeeBps / 10000)`. `collateral_asset.transferFrom(redeemDestination, recipient, collateral_amount − fee)`. The Cash Wallet must have approved this contract; the fee remains in the Cash Wallet.
4. Emit `Redeem(signer, recipient, collateral_asset, thusd_amount, collateral_amount − fee, fee)`.

Both functions are `nonReentrant`.

## Functions

### Order execution

| Function                                 | Access                    |
| ---------------------------------------- | ------------------------- |
| `mint(Order calldata, bytes calldata)`   | `MINTER_ROLE`, not paused |
| `redeem(Order calldata, bytes calldata)` | `MINTER_ROLE`, not paused |
| `hashOrder(Order calldata)` → `bytes32`  | view                      |

### Views

| Function                                                           | Returns                                    |
| ------------------------------------------------------------------ | ------------------------------------------ |
| `thusd()`, `whitelist()`                                           | Immutable wiring                           |
| `mintDestination()`, `redeemDestination()`                         | Current destination wallets                |
| `maxMintPerBlock()`, `maxRedeemPerBlock()`                         | Per-block caps in 6-decimal thUSD units    |
| `mintedPerBlock(uint256 block)`, `redeemedPerBlock(uint256 block)` | Running totals for a block                 |
| `redeemFeeBps()`, `MAX_FEE_BPS()`                                  | Fee and its constant cap                   |
| `MINIMUM_MINT_AMOUNT()`                                            | `1000000`                                  |
| `supportedAssets(address)`, `isSupportedAsset(address)`            | Whether an asset may be used as collateral |
| `usedOrderHashes(bytes32)`                                         | Whether an order digest has been executed  |
| `paused()`                                                         | Pause state                                |
| `eip712Domain()`                                                   | Domain fields                              |
| `MINTER_ROLE()`, `EMERGENCY_ROLE()`, `DEFAULT_ADMIN_ROLE()`        | Role identifiers                           |
| `hasRole`, `getRoleAdmin`, `supportsInterface`                     | Standard                                   |

### Admin

| Function                                  | Access                   | Notes                                                                         |
| ----------------------------------------- | ------------------------ | ----------------------------------------------------------------------------- |
| `addSupportedAsset(address)`              | `DEFAULT_ADMIN_ROLE`     | Rejects zero, thUSD itself, and already-supported assets. Emits `AssetAdded`. |
| `removeSupportedAsset(address)`           | `DEFAULT_ADMIN_ROLE`     | Emits `AssetRemoved`                                                          |
| `setMintDestination(address)`             | `DEFAULT_ADMIN_ROLE`     | Rejects zero, thUSD, and this contract. Emits `MintDestinationChanged`.       |
| `setRedeemDestination(address)`           | `DEFAULT_ADMIN_ROLE`     | Same checks. Emits `RedeemDestinationChanged`.                                |
| `setMaxMintPerBlock(uint256)`             | `DEFAULT_ADMIN_ROLE`     | Emits `MaxMintPerBlockChanged`                                                |
| `setMaxRedeemPerBlock(uint256)`           | `DEFAULT_ADMIN_ROLE`     | Emits `MaxRedeemPerBlockChanged`                                              |
| `setRedeemFeeBps(uint256)`                | `DEFAULT_ADMIN_ROLE`     | Must be ≤ 10. Emits `RedeemFeeChanged`.                                       |
| `pause()`                                 | `EMERGENCY_ROLE`         |                                                                               |
| `unpause()`                               | `DEFAULT_ADMIN_ROLE`     |                                                                               |
| `grantRole`, `revokeRole`, `renounceRole` | `AccessControl` defaults |                                                                               |

## Events

| Event                                                                                                                                                         | Emitted by        |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------- |
| `Mint(address indexed signer, address indexed recipient, address indexed collateralAsset, uint256 collateralAmount, uint256 thusdAmount)`                     | `mint`            |
| `Redeem(address indexed signer, address indexed recipient, address indexed collateralAsset, uint256 thusdAmount, uint256 collateralTransferred, uint256 fee)` | `redeem`          |
| `AssetAdded(address indexed asset)`, `AssetRemoved(address indexed asset)`                                                                                    | Asset admin       |
| `MintDestinationChanged(address indexed old, address indexed new)`, `RedeemDestinationChanged(address indexed old, address indexed new)`                      | Destination admin |
| `MaxMintPerBlockChanged(uint256 old, uint256 new)`, `MaxRedeemPerBlockChanged(uint256 old, uint256 new)`                                                      | Cap admin         |
| `RedeemFeeChanged(uint256 oldFeeBps, uint256 newFeeBps)`                                                                                                      | `setRedeemFeeBps` |
| `Paused(address)`, `Unpaused(address)`                                                                                                                        | Pause             |
| `RoleGranted`, `RoleRevoked`, `RoleAdminChanged`                                                                                                              | `AccessControl`   |
| `EIP712DomainChanged()`                                                                                                                                       | Inherited, unused |

## Errors

| Error                                                                                                | When                                                                  |
| ---------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- |
| `InvalidOrder()`                                                                                     | Order type does not match the function, or is neither MINT nor REDEEM |
| `InvalidSignature()`                                                                                 | Recovered signer differs from `order.signer`                          |
| `InvalidZeroAddress()`                                                                               | Zero signer or recipient; also constructor guards                     |
| `InvalidAmount()`                                                                                    | Either amount below `MINIMUM_MINT_AMOUNT`                             |
| `InvalidAssetRatio()`                                                                                | Ratio check failed for the order type                                 |
| `InvalidAssetAddress()`                                                                              | Asset admin called with zero, thUSD, or a duplicate                   |
| `InvalidBackingAddress()`                                                                            | Destination set to zero, thUSD, or this contract                      |
| `OrderAlreadyUsed()`                                                                                 | Digest already executed                                               |
| `OrderExpired()`                                                                                     | `block.timestamp > expiry`                                            |
| `UnsupportedAsset()`                                                                                 | Collateral not in `supportedAssets`                                   |
| `MaxMintPerBlockExceeded()`, `MaxRedeemPerBlockExceeded()`                                           | Per-block cap                                                         |
| `FeeTooHigh()`                                                                                       | `setRedeemFeeBps` above 10                                            |
| `SignerIsDestination()`                                                                              | Signer equals a destination wallet                                    |
| `TheoWhitelistNotWhitelisted(address)`, `TheoWhitelistBlacklisted(address)`                          | Raised by TheoWhitelist during validation                             |
| `AccessControlUnauthorizedAccount(address, bytes32)`, `AccessControlBadConfirmation()`               | Role guards                                                           |
| `EnforcedPause()`, `ExpectedPause()`                                                                 | Pausable                                                              |
| `ReentrancyGuardReentrantCall()`                                                                     | Reentrancy guard                                                      |
| `ECDSAInvalidSignature()`, `ECDSAInvalidSignatureLength(uint256)`, `ECDSAInvalidSignatureS(bytes32)` | Signature parsing                                                     |
| `SafeERC20FailedOperation(address)`                                                                  | Collateral transfer failed                                            |

## Integration notes

* **You do not call this contract.** `mint` and `redeem` are restricted to `MINTER_ROLE`. Whitelisted depositors sign orders and send them through our API.
* **Approve before you sign.** MINT needs a collateral allowance from `signer` to `0x2D99…696e`. REDEEM needs a thUSD allowance from `signer` to the same address. USDC supports `permit`; USDT does not, so USDT depositors approve with a transaction.
* **Amounts are in native decimals.** `collateral_amount` uses the collateral's decimals (6 for USDC and USDT). `thusd_amount` uses 6. The ratio check scales both to 18 internally.
* **Orders are single-use and time-bound.** Choose a fresh `nonce` per order and a short `expiry`.
* **Whitelist covers both ends.** `signer` and `recipient` must each be whitelisted and not blacklisted. Minting to an unwhitelisted recipient reverts.
* **Indexing supply changes.** `Mint` and `Redeem` on this contract, plus `Transfer` to or from `address(0)` on thUSD, give a complete picture of issuance. Redemption `fee` is reported per event and never leaves the Cash Wallet.
* **Nothing here upgrades.** A change to validation logic means a new contract and a timelocked `thUSD.setMinter` call. Watch `MinterSet` on thUSD.

## Verify

```bash
RPC=https://ethereum-rpc.publicnode.com
MINTER=0x2D99aC801DC0edadD53f5688FeF2317932E8696e
USDC=0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48
USDT=0xdAC17F958D2ee523a2206206994597C13D831ec7

cast call $MINTER 'thusd()(address)'              --rpc-url $RPC   # 0xa3fe5c7596024e6811e14f029937d5bd8ae485b3
cast call $MINTER 'whitelist()(address)'          --rpc-url $RPC   # 0x14d38a3ed85ebddb3e22ff022e38e645a311f388
cast call $MINTER 'mintDestination()(address)'    --rpc-url $RPC   # 0xec417ccb6dd26868cca993a92f37217b1d4b3c2f
cast call $MINTER 'redeemDestination()(address)'  --rpc-url $RPC   # 0xec417ccb6dd26868cca993a92f37217b1d4b3c2f
cast call $MINTER 'maxMintPerBlock()(uint256)'    --rpc-url $RPC   # 1000000000000
cast call $MINTER 'maxRedeemPerBlock()(uint256)'  --rpc-url $RPC   # 200000000000
cast call $MINTER 'redeemFeeBps()(uint256)'       --rpc-url $RPC   # 5
cast call $MINTER 'supportedAssets(address)(bool)' $USDC --rpc-url $RPC   # true
cast call $MINTER 'supportedAssets(address)(bool)' $USDT --rpc-url $RPC   # true
cast call $MINTER 'paused()(bool)'                --rpc-url $RPC   # false

# No proxy: the ERC-1967 implementation slot is empty
cast storage $MINTER 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc --rpc-url $RPC   # 0x0

# Complete asset history from the deploy block (needs an archive-capable RPC)
cast logs --address $MINTER --from-block 24837116 'AssetAdded(address)'   --rpc-url $RPC
cast logs --address $MINTER --from-block 24837116 'AssetRemoved(address)' --rpc-url $RPC
```
