|
| 1 | +// SPDX-License-Identifier: MIT |
| 2 | +// OpenZeppelin Compact Contracts v0.2.0 (token/NativeShieldedToken.compact) |
| 3 | + |
| 4 | +pragma language_version >= 0.23.0; |
| 5 | + |
| 6 | +/** |
| 7 | + * @module NativeShieldedToken |
| 8 | + * @description A module for issuing a SINGLE native shielded (Zswap) token — |
| 9 | + * the ERC-20-shaped flavor of the native shielded token standard. The token's |
| 10 | + * domain separator is fixed at construction; no circuit takes a domain |
| 11 | + * parameter. To issue multiple token types from one contract, use |
| 12 | + * `NativeShieldedTokenFamily` instead. |
| 13 | + * |
| 14 | + * This module is a thin wrapper over `NativeShieldedTokenCore`: it stores the |
| 15 | + * one `sealed _domain` and forwards it to every core op, so the mint/burn logic |
| 16 | + * and metadata live in the shared core, not here. |
| 17 | + * |
| 18 | + * @notice A contract MUST NOT import both `NativeShieldedToken` and |
| 19 | + * `NativeShieldedTokenFamily`. Both wrap `NativeShieldedTokenCore`, so they |
| 20 | + * share one set of core state (a single init flag and one name/symbol/decimals). |
| 21 | + * Pick one flavor per contract. |
| 22 | + * |
| 23 | + * @notice This module exposes composable building blocks for minting and |
| 24 | + * burning native shielded coins. It does NOT include access control: consuming |
| 25 | + * contracts compose that via the module/contract pattern (Ownable, |
| 26 | + * AccessControl, etc.) and SHOULD gate the mint and burn circuits, which are |
| 27 | + * unrestricted at the module level. |
| 28 | + * |
| 29 | + * @dev The domain separator is stored as `sealed ledger _domain` at |
| 30 | + * construction and never exposed as a circuit parameter, eliminating |
| 31 | + * caller-supplied domain misuse. The coin's `color` is |
| 32 | + * `tokenType(_domain, kernel.self())`, computed at call time; only this |
| 33 | + * contract can mint coins of its color. Metadata follows the FungibleToken |
| 34 | + * (ERC-20) convention (contract-wide `name`, `symbol`, `decimals`, fixed at |
| 35 | + * construction). Mint amounts are `Uint<64>`, burn amounts `Uint<128>` — an |
| 36 | + * asymmetry imposed by the protocol primitives, not a module choice. The |
| 37 | + * mint/burn mechanics and the `_burn` vs `_burnFromSelf` spend paths live in |
| 38 | + * `NativeShieldedTokenCore`. |
| 39 | + * |
| 40 | + * @notice With a secret, cryptographically random `_mint` nonce the mint is |
| 41 | + * recipient-private; the caller is responsible for nonce uniqueness (a reused |
| 42 | + * nonce produces a duplicate commitment, which the ledger rejects). For mints |
| 43 | + * that need no caller-managed nonce, compose `NativeShieldedTokenDerivedNonce` |
| 44 | + * and pass its `_deriveNonce()` output, at the cost of making those mints |
| 45 | + * recipient-public. |
| 46 | + * |
| 47 | + * @notice Supply accounting is opt-in: this module tracks NO supply totals. |
| 48 | + * Compose the optional `NativeShieldedTokenSupply` extension only if you need an |
| 49 | + * on-chain `totalSupply()`; see it for the privacy trade-off it introduces (it |
| 50 | + * makes contract-mediated burn amounts public). |
| 51 | + * |
| 52 | + * @dev Composition |
| 53 | + * Dual-representation tokens (shielded + unshielded with conversion) MUST build |
| 54 | + * on the token-family modules and the `NativeTokenConverter` extension, not on |
| 55 | + * this module: it stores a single load-bearing sealed `_domain` (one token |
| 56 | + * type), while the converter composes both bases' Family profiles. |
| 57 | + * (Initialization is tracked per-module inline, so the former |
| 58 | + * shared-`Initializable`-flag "one `initialize` per contract" limit no longer |
| 59 | + * applies.) |
| 60 | + * |
| 61 | + * @notice Out of scope (phase two, pending contract-to-contract support): |
| 62 | + * `balanceOf`, `allowance`, and transfer mediation are not representable for |
| 63 | + * native shielded UTXOs — once a user holds a coin, the contract cannot observe |
| 64 | + * or restrict its movement. |
| 65 | + */ |
| 66 | +module NativeShieldedToken { |
| 67 | + import CompactStandardLibrary; |
| 68 | + import "./NativeShieldedTokenCore" prefix Core_; |
| 69 | + |
| 70 | + // Circuits use the `Core_` prefix above. The named import/re-export below is |
| 71 | + // only for the core state variables, so the implementing contract's ledger |
| 72 | + // keys read as `_name` etc. rather than `NativeShieldedTokenCore__name`. The |
| 73 | + // circuit names are intentionally NOT imported unprefixed, to avoid potential clashes. |
| 74 | + import { _name, _symbol, _decimals, _isInitialized } from "./NativeShieldedTokenCore"; |
| 75 | + export { _name, _symbol, _decimals, _isInitialized }; |
| 76 | + |
| 77 | + /** |
| 78 | + * @description Domain separator fixed at construction; with the contract |
| 79 | + * address it determines this token's color. |
| 80 | + */ |
| 81 | + export sealed ledger _domain: Bytes<32>; |
| 82 | + |
| 83 | + /** |
| 84 | + * @description Initializes the module's domain and metadata. |
| 85 | + * @dev This MUST be called in the implementing contract's constructor, and it |
| 86 | + * MUST be the only token operation performed there. Failure to call it can |
| 87 | + * lead to an irreparable contract; perform mint, burn, and `tokenColor` only |
| 88 | + * in post-deployment circuits, never in the constructor. |
| 89 | + * |
| 90 | + * @param {Bytes<32>} domainSep - Domain separator for this token's color. |
| 91 | + * @param {Opaque<"string">} name_ - The name of the token. |
| 92 | + * @param {Opaque<"string">} symbol_ - The symbol of the token. |
| 93 | + * @param {Uint<8>} decimals_ - The number of decimals used to get the user representation. |
| 94 | + * @return {[]} - Empty tuple. |
| 95 | + */ |
| 96 | + export circuit initialize( |
| 97 | + domainSep: Bytes<32>, |
| 98 | + name_: Opaque<"string">, |
| 99 | + symbol_: Opaque<"string">, |
| 100 | + decimals_: Uint<8> |
| 101 | + ): [] { |
| 102 | + Core_initialize(name_, symbol_, decimals_); |
| 103 | + _domain = disclose(domainSep); |
| 104 | + } |
| 105 | + |
| 106 | + /** |
| 107 | + * @description Returns whether the contract has been initialized. |
| 108 | + * |
| 109 | + * @circuitInfo k=6, rows=26 |
| 110 | + * |
| 111 | + * @return {Boolean} - True once `initialize` has run. |
| 112 | + */ |
| 113 | + export circuit isInitialized(): Boolean { |
| 114 | + return Core_isInitialized(); |
| 115 | + } |
| 116 | + |
| 117 | + /** |
| 118 | + * @description Returns the token name. |
| 119 | + * |
| 120 | + * @circuitInfo k=6, rows=28 |
| 121 | + * |
| 122 | + * Requirements: |
| 123 | + * |
| 124 | + * - Contract is initialized. |
| 125 | + * |
| 126 | + * @return {Opaque<"string">} - The token name. |
| 127 | + */ |
| 128 | + export circuit name(): Opaque<"string"> { |
| 129 | + return Core_name(); |
| 130 | + } |
| 131 | + |
| 132 | + /** |
| 133 | + * @description Returns the symbol of the token. |
| 134 | + * |
| 135 | + * @circuitInfo k=6, rows=28 |
| 136 | + * |
| 137 | + * Requirements: |
| 138 | + * |
| 139 | + * - Contract is initialized. |
| 140 | + * |
| 141 | + * @return {Opaque<"string">} - The token symbol. |
| 142 | + */ |
| 143 | + export circuit symbol(): Opaque<"string"> { |
| 144 | + return Core_symbol(); |
| 145 | + } |
| 146 | + |
| 147 | + /** |
| 148 | + * @description Returns the number of decimals used to get its user representation. |
| 149 | + * |
| 150 | + * @circuitInfo k=6, rows=28 |
| 151 | + * |
| 152 | + * Requirements: |
| 153 | + * |
| 154 | + * - Contract is initialized. |
| 155 | + * |
| 156 | + * @return {Uint<8>} - The decimals value. |
| 157 | + */ |
| 158 | + export circuit decimals(): Uint<8> { |
| 159 | + return Core_decimals(); |
| 160 | + } |
| 161 | + |
| 162 | + /** |
| 163 | + * @description Returns this token's coin color: |
| 164 | + * `tokenType(_domain, kernel.self())`, computed at call time. |
| 165 | + * |
| 166 | + * @circuitInfo k=13, rows=3971 |
| 167 | + * |
| 168 | + * Requirements: |
| 169 | + * |
| 170 | + * - Contract is initialized. |
| 171 | + * |
| 172 | + * @return {Bytes<32>} - The coin color. |
| 173 | + */ |
| 174 | + export circuit tokenColor(): Bytes<32> { |
| 175 | + return Core_tokenColor(_domain); |
| 176 | + } |
| 177 | + |
| 178 | + /** |
| 179 | + * @description Mints `amount` of the token to `recipient`, using a |
| 180 | + * caller-supplied nonce. |
| 181 | + * |
| 182 | + * @dev The caller is fully responsible for nonce uniqueness. Reusing a |
| 183 | + * nonce for the same (value, recipient) produces a duplicate commitment, |
| 184 | + * which the protocol rejects. With a secret random nonce this is the |
| 185 | + * recipient-private mint. For mints that require no caller-managed nonce, |
| 186 | + * compose the `NativeShieldedTokenDerivedNonce` extension and pass its |
| 187 | + * `_deriveNonce()` output. |
| 188 | + * |
| 189 | + * @notice The returned coin info is the only copy available to the |
| 190 | + * recipient; callers SHOULD deliver it out of band. Wallets cannot detect |
| 191 | + * contract-minted coins by scanning the chain. |
| 192 | + * |
| 193 | + * @circuitInfo k=14, rows=10843 |
| 194 | + * |
| 195 | + * Requirements: |
| 196 | + * |
| 197 | + * - Contract is initialized. |
| 198 | + * - `recipient` is not zero. |
| 199 | + * |
| 200 | + * @param {Either<ZswapCoinPublicKey, ContractAddress>} recipient - The coin recipient. |
| 201 | + * @param {Uint<64>} amount - Quantity to mint. Capped at `Uint<64>` by the protocol. |
| 202 | + * @param {Bytes<32>} nonce - Caller-supplied nonce. Must be unique for this |
| 203 | + * contract's color. |
| 204 | + * @return {ShieldedCoinInfo} - The newly created coin's info (nonce, color, value). |
| 205 | + */ |
| 206 | + export circuit _mint( |
| 207 | + recipient: Either<ZswapCoinPublicKey, ContractAddress>, |
| 208 | + amount: Uint<64>, |
| 209 | + nonce: Bytes<32> |
| 210 | + ): ShieldedCoinInfo { |
| 211 | + return Core__mint(_domain, recipient, amount, nonce); |
| 212 | + } |
| 213 | + |
| 214 | + /** |
| 215 | + * @description Burns `amount` from `coin`, a coin provided within the |
| 216 | + * current transaction (e.g. paid in by the caller's wallet), and routes the |
| 217 | + * remaining change to `refundTo`. |
| 218 | + * |
| 219 | + * To destroy `coin` in full, pass `coin.value` as `amount`; the change |
| 220 | + * branch will not fire and `none` is returned. The `refundTo` value is |
| 221 | + * inert in that case but must still be supplied and non-zero. |
| 222 | + * |
| 223 | + * @dev The coin is received by the contract and spent in the same |
| 224 | + * transaction, so the spend goes through `sendImmediateShielded` (the |
| 225 | + * transient path). For coins the contract already holds, use |
| 226 | + * `_burnFromSelf` instead. |
| 227 | + * |
| 228 | + * @notice The returned refund coin info is the only copy available to |
| 229 | + * `refundTo`; callers SHOULD deliver it out of band. Wallets cannot detect |
| 230 | + * contract-sent coins by scanning the chain. |
| 231 | + * |
| 232 | + * @circuitInfo k=16, rows=47656 |
| 233 | + * |
| 234 | + * Requirements: |
| 235 | + * |
| 236 | + * - Contract is initialized. |
| 237 | + * - `coin.color` is this contract's token color. |
| 238 | + * - `amount` is less than or equal to `coin.value`. |
| 239 | + * - `refundTo` is not zero (the zero key is the burn address; a zero |
| 240 | + * `refundTo` would silently burn the change as well). |
| 241 | + * |
| 242 | + * @param {ShieldedCoinInfo} coin - The coin to burn from. |
| 243 | + * @param {Uint<128>} amount - Value to destroy. Must be <= `coin.value`. |
| 244 | + * @param {Either<ZswapCoinPublicKey, ContractAddress>} refundTo - Where to |
| 245 | + * route the unspent portion (`coin.value - amount`). |
| 246 | + * @return {Maybe<ShieldedCoinInfo>} - The refund coin created for `refundTo`, |
| 247 | + * or `none` if the coin was burned in full. |
| 248 | + */ |
| 249 | + export circuit _burn( |
| 250 | + coin: ShieldedCoinInfo, |
| 251 | + amount: Uint<128>, |
| 252 | + refundTo: Either<ZswapCoinPublicKey, ContractAddress> |
| 253 | + ): Maybe<ShieldedCoinInfo> { |
| 254 | + return Core__burn(_domain, coin, amount, refundTo); |
| 255 | + } |
| 256 | + |
| 257 | + /** |
| 258 | + * @description Burns `amount` from `coin`, a coin this contract already |
| 259 | + * holds (a Merkle-tree entry with a valid `mt_index`). Any change is |
| 260 | + * auto-received by the contract and returned. |
| 261 | + * |
| 262 | + * @dev The consumer SHOULD persist the returned change coin info in its own |
| 263 | + * ledger state: the change replaces `coin` as the contract's holding, and |
| 264 | + * its info is not otherwise recoverable. |
| 265 | + * |
| 266 | + * @circuitInfo k=15, rows=23307 |
| 267 | + * |
| 268 | + * Requirements: |
| 269 | + * |
| 270 | + * - Contract is initialized. |
| 271 | + * - `coin.color` is this contract's token color. |
| 272 | + * - `amount` is less than or equal to `coin.value`. |
| 273 | + * |
| 274 | + * @param {QualifiedShieldedCoinInfo} coin - The contract-held coin to burn from. |
| 275 | + * @param {Uint<128>} amount - Value to destroy. Must be <= `coin.value`. |
| 276 | + * @return {Maybe<ShieldedCoinInfo>} - The change coin retained by the |
| 277 | + * contract, or `none` if the coin was burned in full. |
| 278 | + */ |
| 279 | + export circuit _burnFromSelf( |
| 280 | + coin: QualifiedShieldedCoinInfo, |
| 281 | + amount: Uint<128> |
| 282 | + ): Maybe<ShieldedCoinInfo> { |
| 283 | + return Core__burnFromSelf(_domain, coin, amount); |
| 284 | + } |
| 285 | +} |
0 commit comments