@openzeppelin/compact-contracts 0.2.0 → 0.3.0-alpha
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/access/AccessControl.compact +3 -35
- package/access/Ownable.compact +5 -51
- package/access/ShieldedAccessControl.compact +2 -2
- package/access/ZOwnablePK.compact +2 -2
- package/crypto/ElGamal.compact +473 -0
- package/multisig/ForwarderPrivate.compact +229 -0
- package/multisig/ForwarderShielded.compact +149 -0
- package/multisig/ForwarderUnshielded.compact +152 -0
- package/multisig/ProposalManager.compact +328 -0
- package/multisig/ShieldedTreasury.compact +201 -0
- package/multisig/ShieldedTreasuryStateless.compact +76 -0
- package/multisig/Signer.compact +312 -0
- package/multisig/SignerManager.compact +205 -0
- package/multisig/UnshieldedTreasury.compact +114 -0
- package/multisig/presets/ShieldedMultiSig.compact +236 -0
- package/multisig/presets/ShieldedMultiSigV2.compact +280 -0
- package/multisig/presets/ShieldedMultiSigV3.compact +368 -0
- package/multisig/presets/forwarder/ForwarderPrivate.compact +125 -0
- package/multisig/presets/forwarder/ForwarderShielded.compact +62 -0
- package/multisig/presets/forwarder/ForwarderUnshielded.compact +64 -0
- package/package.json +9 -5
- package/security/Allowlist.compact +101 -0
- package/security/Blocklist.compact +99 -0
- package/security/Initializable.compact +2 -2
- package/security/Pausable.compact +2 -2
- package/token/FungibleToken.compact +24 -82
- package/token/MultiToken.compact +21 -80
- package/token/NativeShieldedToken.compact +285 -0
- package/token/NativeShieldedTokenCore.compact +343 -0
- package/token/NativeShieldedTokenFamily.compact +287 -0
- package/token/NonFungibleToken.compact +25 -83
- package/utils/Utils.compact +86 -2
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// OpenZeppelin Compact Contracts v0.3.0-alpha (multisig/ForwarderPrivate.compact)
|
|
3
|
+
|
|
4
|
+
pragma language_version >= 0.23.0;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @module ForwarderPrivate
|
|
8
|
+
* @description Private-parent forwarder primitives. The parent is a coin
|
|
9
|
+
* public key, hidden behind a `persistentHash` commitment on the ledger.
|
|
10
|
+
* Deposits accumulate at the contract (no atomic forward); the operator
|
|
11
|
+
* drains coins by presenting the `(parent, opSecret)` preimage at drain
|
|
12
|
+
* time. The drained coin is sent as a shielded note to the parent key, so
|
|
13
|
+
* the parent stays hidden on-chain. The parent is deliberately a coin public
|
|
14
|
+
* key and not a contract address: a shielded send to a contract publishes
|
|
15
|
+
* that contract's address in cleartext, which would defeat the guarantee.
|
|
16
|
+
*
|
|
17
|
+
* Knowledge of the preimage is the sole authorization gate — there is
|
|
18
|
+
* no signer set and no nullifier scheme. The operational secret is
|
|
19
|
+
* held off-chain; the contract never sees it except during a drain.
|
|
20
|
+
* Two forwarders bound to the same parent with different operational
|
|
21
|
+
* secrets produce different commitments and are unlinkable on-chain.
|
|
22
|
+
*
|
|
23
|
+
* Underscore-prefixed circuits have no access control beyond the
|
|
24
|
+
* preimage check inside `_drain`. State access is gated by the
|
|
25
|
+
* initialization pattern: every state-touching circuit asserts the
|
|
26
|
+
* module has been initialized via `initialize`. The pure
|
|
27
|
+
* `_calculateParentCommitment` helper does not access state and is
|
|
28
|
+
* intentionally callable without initialization.
|
|
29
|
+
*/
|
|
30
|
+
module ForwarderPrivate {
|
|
31
|
+
import CompactStandardLibrary;
|
|
32
|
+
import "../utils/Utils" prefix Utils_;
|
|
33
|
+
|
|
34
|
+
// ─── State ──────────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
export ledger _parentCommitment: Bytes<32>;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @description Initialization flag, tracked per-module to avoid the compiler's
|
|
40
|
+
* shared transitive-dependency state bug (LFDT-Minokawa/compact#270). See the
|
|
41
|
+
* Initializable module for rationale.
|
|
42
|
+
*/
|
|
43
|
+
export ledger _isInitialized: Boolean;
|
|
44
|
+
|
|
45
|
+
// ─── Init ───────────────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @description Initializes the forwarder with a parent commitment.
|
|
49
|
+
* Called once from the preset constructor. The commitment is
|
|
50
|
+
* immutable after init.
|
|
51
|
+
*
|
|
52
|
+
* Requirements:
|
|
53
|
+
*
|
|
54
|
+
* - Contract must not be initialized.
|
|
55
|
+
* - `parentCommitment` must not be the zero bytes. A zero commitment
|
|
56
|
+
* is the sole drain gate and would leave every deposited coin
|
|
57
|
+
* permanently unrecoverable (no preimage exists for `default<Bytes<32>>`
|
|
58
|
+
* under the domain-tagged hash).
|
|
59
|
+
*
|
|
60
|
+
* @param {Bytes<32>} parentCommitment - Domain-tagged
|
|
61
|
+
* `persistentHash([pad(32, "ForwarderPrivate:commitment"), parentAddr, opSecret])`
|
|
62
|
+
* computed off-chain by the deployer (see `_calculateParentCommitment`).
|
|
63
|
+
*
|
|
64
|
+
* @returns {[]} Empty tuple.
|
|
65
|
+
*/
|
|
66
|
+
export circuit initialize(parentCommitment: Bytes<32>): [] {
|
|
67
|
+
assert(parentCommitment != default<Bytes<32>>, "ForwarderPrivate: zero commitment");
|
|
68
|
+
assertNotInitialized();
|
|
69
|
+
_isInitialized = true;
|
|
70
|
+
_parentCommitment = disclose(parentCommitment);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ─── Deposit ────────────────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* @description Receives a shielded coin into the forwarder's custody.
|
|
77
|
+
* No ledger write — coins dwell at the contract address until drained.
|
|
78
|
+
*
|
|
79
|
+
* @circuitInfo k=13, rows=6538
|
|
80
|
+
*
|
|
81
|
+
* Requirements:
|
|
82
|
+
*
|
|
83
|
+
* - Contract must be initialized.
|
|
84
|
+
*
|
|
85
|
+
* @param {ShieldedCoinInfo} coin - The incoming shielded coin.
|
|
86
|
+
*
|
|
87
|
+
* @returns {[]} Empty tuple.
|
|
88
|
+
*/
|
|
89
|
+
export circuit _deposit(coin: ShieldedCoinInfo): [] {
|
|
90
|
+
assertInitialized();
|
|
91
|
+
receiveShielded(disclose(coin));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ─── Drain ──────────────────────────────────────────────────────
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* @description Spends a previously-deposited shielded coin to the parent,
|
|
98
|
+
* a coin public key. The caller proves knowledge of `(parent, opSecret)`
|
|
99
|
+
* matching the stored commitment; the coin is sent as a plain shielded note,
|
|
100
|
+
* so the parent stays hidden on-chain. If `coin.value` exceeds `value`, the
|
|
101
|
+
* change is re-emitted back to the contract for future drains.
|
|
102
|
+
*
|
|
103
|
+
* The parent is a `ZswapCoinPublicKey`, never a `ContractAddress`: a shielded
|
|
104
|
+
* send to a contract publishes that contract's address in cleartext on the
|
|
105
|
+
* transaction (the protocol must route the coin to a named contract), which
|
|
106
|
+
* would defeat the private-parent guarantee. A coin public key is encrypted
|
|
107
|
+
* inside the Zswap output and never appears on the public transcript.
|
|
108
|
+
*
|
|
109
|
+
* @circuitInfo k=16, rows=41961
|
|
110
|
+
*
|
|
111
|
+
* Requirements:
|
|
112
|
+
*
|
|
113
|
+
* - Contract must be initialized.
|
|
114
|
+
* - `parent` must not be the zero key.
|
|
115
|
+
* - `_calculateParentCommitment(parent.bytes, opSecret) == _parentCommitment`.
|
|
116
|
+
* - `coin.value` must be >= `value` (enforced by `sendShielded`).
|
|
117
|
+
*
|
|
118
|
+
* @param {QualifiedShieldedCoinInfo} coin - The coin to spend.
|
|
119
|
+
* @param {ZswapCoinPublicKey} parent - The parent coin public key. Its 32
|
|
120
|
+
* bytes are the preimage to the stored commitment.
|
|
121
|
+
* @param {Bytes<32>} opSecret - The operational secret. Never appears
|
|
122
|
+
* on the public transcript.
|
|
123
|
+
*
|
|
124
|
+
* @warning **Losing the operational secret is permanent fund loss.** It
|
|
125
|
+
* is the sole drain authorization, with no rotation, revocation, or
|
|
126
|
+
* recovery path. If the operator misplaces it, every shielded coin
|
|
127
|
+
* accumulated at this contract is forever inaccessible, equivalent to
|
|
128
|
+
* losing a hot-wallet private key. Back it up offline before the first
|
|
129
|
+
* deposit and treat it with the same hygiene as a signing key.
|
|
130
|
+
*
|
|
131
|
+
* @param {Uint<128>} value - The amount to send.
|
|
132
|
+
*
|
|
133
|
+
* @returns {ShieldedSendResult} The result containing the sent coin
|
|
134
|
+
* and any change.
|
|
135
|
+
*/
|
|
136
|
+
export circuit _drain(
|
|
137
|
+
coin: QualifiedShieldedCoinInfo,
|
|
138
|
+
parent: ZswapCoinPublicKey,
|
|
139
|
+
opSecret: Bytes<32>,
|
|
140
|
+
value: Uint<128>
|
|
141
|
+
): ShieldedSendResult {
|
|
142
|
+
assertInitialized();
|
|
143
|
+
|
|
144
|
+
// Reject a zero parent before the commitment gate.
|
|
145
|
+
assert(!Utils_isKeyZero(parent), "ForwarderPrivate: zero parent");
|
|
146
|
+
|
|
147
|
+
// Commitment gate — the preimage is the parent key's 32 bytes.
|
|
148
|
+
assert(
|
|
149
|
+
_calculateParentCommitment(parent.bytes, opSecret) == _parentCommitment,
|
|
150
|
+
"ForwarderPrivate: invalid parent"
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
// Send to the parent coin public key (the `left` arm). `disclose` reveals
|
|
154
|
+
// nothing: a coin-public-key recipient is encrypted in the Zswap output, so
|
|
155
|
+
// the parent never appears on the public transcript. A contract recipient
|
|
156
|
+
// is intentionally unsupported — it would publish the address in cleartext.
|
|
157
|
+
const result = sendShielded(
|
|
158
|
+
disclose(coin),
|
|
159
|
+
left<ZswapCoinPublicKey, ContractAddress>(disclose(parent)),
|
|
160
|
+
disclose(value)
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
if (disclose(result.change.is_some)) {
|
|
164
|
+
sendImmediateShielded(
|
|
165
|
+
disclose(result.change.value),
|
|
166
|
+
Utils_selfAsRecipient(),
|
|
167
|
+
disclose(result.change.value.value)
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return result;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ─── Pure helpers ───────────────────────────────────────────────
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* @description Computes the parent commitment from `(parentAddr, opSecret)`.
|
|
178
|
+
* Pure circuit — used off-chain by the deployer to compute the
|
|
179
|
+
* constructor argument, and inside `_drain` for the preimage check.
|
|
180
|
+
* Callable without initialization.
|
|
181
|
+
*
|
|
182
|
+
* The first hash input is a fixed domain tag
|
|
183
|
+
* (`pad(32, "ForwarderPrivate:commitment")`). The tag prevents
|
|
184
|
+
* preimage collisions with other `persistentHash` users in the
|
|
185
|
+
* system that hash two `Bytes<32>` values — a colliding preimage
|
|
186
|
+
* crafted under a different domain cannot satisfy this commitment.
|
|
187
|
+
*
|
|
188
|
+
* @param {Bytes<32>} parentAddr - The parent address.
|
|
189
|
+
* @param {Bytes<32>} opSecret - The operational secret.
|
|
190
|
+
*
|
|
191
|
+
* @returns {Bytes<32>} `persistentHash([pad(32, "ForwarderPrivate:commitment"), parentAddr, opSecret])`.
|
|
192
|
+
*/
|
|
193
|
+
export pure circuit _calculateParentCommitment(
|
|
194
|
+
parentAddr: Bytes<32>,
|
|
195
|
+
opSecret: Bytes<32>
|
|
196
|
+
): Bytes<32> {
|
|
197
|
+
return persistentHash<Vector<3, Bytes<32>>>(
|
|
198
|
+
[pad(32, "ForwarderPrivate:commitment"), parentAddr, opSecret]
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ─── Init guards ────────────────────────────────────────────────
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* @description Asserts that the contract has been initialized, throwing an error if not.
|
|
206
|
+
*
|
|
207
|
+
* Requirements:
|
|
208
|
+
*
|
|
209
|
+
* - Contract must be initialized.
|
|
210
|
+
*
|
|
211
|
+
* @return {[]} - Empty tuple.
|
|
212
|
+
*/
|
|
213
|
+
circuit assertInitialized(): [] {
|
|
214
|
+
assert(_isInitialized, "ForwarderPrivate: contract not initialized");
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* @description Asserts that the contract has not been initialized, throwing an error if it has.
|
|
219
|
+
*
|
|
220
|
+
* Requirements:
|
|
221
|
+
*
|
|
222
|
+
* - Contract must not be initialized.
|
|
223
|
+
*
|
|
224
|
+
* @return {[]} - Empty tuple.
|
|
225
|
+
*/
|
|
226
|
+
circuit assertNotInitialized(): [] {
|
|
227
|
+
assert(!_isInitialized, "ForwarderPrivate: contract already initialized");
|
|
228
|
+
}
|
|
229
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// OpenZeppelin Compact Contracts v0.3.0-alpha (multisig/ForwarderShielded.compact)
|
|
3
|
+
|
|
4
|
+
pragma language_version >= 0.23.0;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @module ForwarderShielded
|
|
8
|
+
* @description Public-parent forwarder for shielded coins. Provides an
|
|
9
|
+
* atomic forward pattern: receive a shielded coin and immediately send
|
|
10
|
+
* it to the configured parent recipient.
|
|
11
|
+
*
|
|
12
|
+
* The parent is stored generically as
|
|
13
|
+
* `Either<ZswapCoinPublicKey, ContractAddress>` so the ledger layout is
|
|
14
|
+
* forward-compatible, but `initialize` accepts only a `ZswapCoinPublicKey`
|
|
15
|
+
* today and stores it in the `left` arm. A contract address is the
|
|
16
|
+
* unsupported arm: an atomic forward delivers the coin in the same
|
|
17
|
+
* transaction as the deposit, and a shielded send to a contract is only
|
|
18
|
+
* valid if that contract claims the output in the same transaction. The
|
|
19
|
+
* only contract executing in `_deposit` is this forwarder, so a
|
|
20
|
+
* third-party contract parent would never claim the output and the
|
|
21
|
+
* deposit would be rejected by the node. A coin public key needs no
|
|
22
|
+
* in-tx claim, so it is the only recipient an atomic forward can reach.
|
|
23
|
+
*
|
|
24
|
+
* Contract-address support is reserved for a future circuit upgrade (via
|
|
25
|
+
* the Contract Maintenance Authority) once cross-contract UTXO delivery
|
|
26
|
+
* exists. Because the ledger already stores the generic `Either`, that
|
|
27
|
+
* upgrade changes only the circuits (verifier keys), not the state
|
|
28
|
+
* layout — no state migration is required.
|
|
29
|
+
*
|
|
30
|
+
* Underscore-prefixed circuits have no access control. The forwarder is
|
|
31
|
+
* intentionally permissionless — the recipient is fixed at init, so any
|
|
32
|
+
* caller may deposit without compromising the parent. State access is
|
|
33
|
+
* gated by a per-module `_isInitialized` flag: every circuit asserts the
|
|
34
|
+
* contract has been initialized via `initialize`.
|
|
35
|
+
*
|
|
36
|
+
* The parent is set once at init and the module exposes no setter, so a
|
|
37
|
+
* preset that omits one keeps the parent fixed. A consuming contract
|
|
38
|
+
* that needs to change the parent may add its own setter circuit.
|
|
39
|
+
*
|
|
40
|
+
* @notice The deployer must set a parent that can spend the forwarded
|
|
41
|
+
* coins. Forwarding to a key no one controls strands every deposit
|
|
42
|
+
* permanently. The zero-key guard rejects only the all-zero key, not an
|
|
43
|
+
* otherwise-unspendable one.
|
|
44
|
+
*/
|
|
45
|
+
module ForwarderShielded {
|
|
46
|
+
import CompactStandardLibrary;
|
|
47
|
+
import "../utils/Utils" prefix Utils_;
|
|
48
|
+
|
|
49
|
+
// ─── State ──────────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* @description The parent recipient, stored generically as
|
|
53
|
+
* `Either<ZswapCoinPublicKey, ContractAddress>` for forward compatibility.
|
|
54
|
+
* `initialize` only ever writes the `left` (coin public key) arm today. See
|
|
55
|
+
* the module header.
|
|
56
|
+
*/
|
|
57
|
+
export ledger _parent: Either<ZswapCoinPublicKey, ContractAddress>;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @description Initialization flag, tracked per-module to avoid the compiler's
|
|
61
|
+
* shared transitive-dependency state bug (LFDT-Minokawa/compact#270). See the
|
|
62
|
+
* Initializable module for rationale.
|
|
63
|
+
*/
|
|
64
|
+
export ledger _isInitialized: Boolean;
|
|
65
|
+
|
|
66
|
+
// ─── Init ───────────────────────────────────────────────────────
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* @description Initializes the forwarder with a parent recipient.
|
|
70
|
+
* Called once from the preset constructor. The coin public key is
|
|
71
|
+
* stored in the `left` arm of the generic `_parent` ledger field.
|
|
72
|
+
*
|
|
73
|
+
* Requirements:
|
|
74
|
+
*
|
|
75
|
+
* - Contract must not be initialized.
|
|
76
|
+
* - `parent` must not be the zero key. A zero parent would forward
|
|
77
|
+
* every deposit to an unspendable key with no recovery path.
|
|
78
|
+
*
|
|
79
|
+
* @param {ZswapCoinPublicKey} parent - The coin public key that
|
|
80
|
+
* receives every forwarded coin. The parameter is deliberately narrower
|
|
81
|
+
* than the `_parent` ledger type: only a coin public key can be
|
|
82
|
+
* delivered to by an atomic forward today (see the module header).
|
|
83
|
+
*
|
|
84
|
+
* @notice Beyond the zero-key check, the caller must ensure `parent` can
|
|
85
|
+
* spend forwarded coins. An unspendable parent strands every deposit with
|
|
86
|
+
* no recovery path.
|
|
87
|
+
*
|
|
88
|
+
* @returns {[]} Empty tuple.
|
|
89
|
+
*/
|
|
90
|
+
export circuit initialize(parent: ZswapCoinPublicKey): [] {
|
|
91
|
+
assert(!Utils_isKeyZero(parent), "ForwarderShielded: zero parent");
|
|
92
|
+
assertNotInitialized();
|
|
93
|
+
_isInitialized = true;
|
|
94
|
+
_parent = left<ZswapCoinPublicKey, ContractAddress>(disclose(parent));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ─── Deposit ────────────────────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* @description Receives a shielded coin and atomically forwards it to
|
|
101
|
+
* `_parent`. The coin is claimed at the protocol level via
|
|
102
|
+
* `receiveShielded`, then immediately re-sent via
|
|
103
|
+
* `sendImmediateShielded` to the stored parent recipient (always the
|
|
104
|
+
* `left` / coin-public-key arm today).
|
|
105
|
+
*
|
|
106
|
+
* @circuitInfo k=15, rows=18573
|
|
107
|
+
*
|
|
108
|
+
* Requirements:
|
|
109
|
+
*
|
|
110
|
+
* - Contract must be initialized.
|
|
111
|
+
*
|
|
112
|
+
* @param {ShieldedCoinInfo} coin - The incoming shielded coin.
|
|
113
|
+
*
|
|
114
|
+
* @returns {[]} Empty tuple.
|
|
115
|
+
*/
|
|
116
|
+
export circuit _deposit(coin: ShieldedCoinInfo): [] {
|
|
117
|
+
assertInitialized();
|
|
118
|
+
receiveShielded(disclose(coin));
|
|
119
|
+
sendImmediateShielded(disclose(coin), _parent, disclose(coin.value));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ─── Init guards ────────────────────────────────────────────────
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* @description Asserts that the contract has been initialized, throwing an error if not.
|
|
126
|
+
*
|
|
127
|
+
* Requirements:
|
|
128
|
+
*
|
|
129
|
+
* - Contract must be initialized.
|
|
130
|
+
*
|
|
131
|
+
* @returns {[]} Empty tuple.
|
|
132
|
+
*/
|
|
133
|
+
circuit assertInitialized(): [] {
|
|
134
|
+
assert(_isInitialized, "ForwarderShielded: contract not initialized");
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* @description Asserts that the contract has not been initialized, throwing an error if it has.
|
|
139
|
+
*
|
|
140
|
+
* Requirements:
|
|
141
|
+
*
|
|
142
|
+
* - Contract must not be initialized.
|
|
143
|
+
*
|
|
144
|
+
* @returns {[]} Empty tuple.
|
|
145
|
+
*/
|
|
146
|
+
circuit assertNotInitialized(): [] {
|
|
147
|
+
assert(!_isInitialized, "ForwarderShielded: contract already initialized");
|
|
148
|
+
}
|
|
149
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// OpenZeppelin Compact Contracts v0.3.0-alpha (multisig/ForwarderUnshielded.compact)
|
|
3
|
+
|
|
4
|
+
pragma language_version >= 0.23.0;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @module ForwarderUnshielded
|
|
8
|
+
* @description Public-parent forwarder for unshielded coins. Provides an
|
|
9
|
+
* atomic forward pattern: receive an unshielded amount of a given color
|
|
10
|
+
* and immediately send it to the configured parent recipient.
|
|
11
|
+
*
|
|
12
|
+
* The parent is stored generically as
|
|
13
|
+
* `Either<ContractAddress, UserAddress>` so the ledger layout is
|
|
14
|
+
* forward-compatible, but `initialize` accepts only a `UserAddress`
|
|
15
|
+
* today and stores it in the `right` arm. A contract address is the
|
|
16
|
+
* unsupported arm: an atomic forward delivers the amount in the same
|
|
17
|
+
* transaction as the deposit, and an unshielded send to a contract is
|
|
18
|
+
* only valid if that contract claims the amount in the same transaction
|
|
19
|
+
* (the same `receiveUnshielded` step this forwarder runs to accept the
|
|
20
|
+
* deposit). The only contract executing in `_deposit` is this forwarder,
|
|
21
|
+
* so a third-party contract parent would never claim the output and the
|
|
22
|
+
* deposit would be rejected by the node. A user address needs no in-tx
|
|
23
|
+
* claim, so it is the only recipient an atomic forward can reach.
|
|
24
|
+
*
|
|
25
|
+
* Contract-address support is reserved for a future circuit upgrade (via
|
|
26
|
+
* the Contract Maintenance Authority) once cross-contract UTXO delivery
|
|
27
|
+
* exists. Because the ledger already stores the generic `Either`, that
|
|
28
|
+
* upgrade changes only the circuits (verifier keys), not the state
|
|
29
|
+
* layout — no state migration is required.
|
|
30
|
+
*
|
|
31
|
+
* Unshielded transfers are publicly visible on the chain: depositor,
|
|
32
|
+
* recipient, color, and amount all appear on the public transcript.
|
|
33
|
+
*
|
|
34
|
+
* Underscore-prefixed circuits have no access control. The forwarder is
|
|
35
|
+
* intentionally permissionless — the recipient is fixed at init, so any
|
|
36
|
+
* caller may deposit without compromising the parent. State access is
|
|
37
|
+
* gated by a per-module `_isInitialized` flag: every circuit asserts the
|
|
38
|
+
* contract has been initialized via `initialize`.
|
|
39
|
+
*
|
|
40
|
+
* The parent is set once at init and the module exposes no setter, so a
|
|
41
|
+
* preset that omits one keeps the parent fixed. A consuming contract
|
|
42
|
+
* that needs to change the parent may add its own setter circuit.
|
|
43
|
+
*
|
|
44
|
+
* @notice The deployer must set a parent that can spend the forwarded
|
|
45
|
+
* amounts. Forwarding to an address no one controls strands every deposit
|
|
46
|
+
* permanently. The zero-address guard rejects only the all-zero address,
|
|
47
|
+
* not an otherwise-unspendable one.
|
|
48
|
+
*/
|
|
49
|
+
module ForwarderUnshielded {
|
|
50
|
+
import CompactStandardLibrary;
|
|
51
|
+
|
|
52
|
+
// ─── State ──────────────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @description The parent recipient, stored generically as
|
|
56
|
+
* `Either<ContractAddress, UserAddress>` for forward compatibility.
|
|
57
|
+
* `initialize` only ever writes the `right` (user address) arm today. See
|
|
58
|
+
* the module header.
|
|
59
|
+
*/
|
|
60
|
+
export ledger _parent: Either<ContractAddress, UserAddress>;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* @description Initialization flag, tracked per-module to avoid the compiler's
|
|
64
|
+
* shared transitive-dependency state bug (LFDT-Minokawa/compact#270). See the
|
|
65
|
+
* Initializable module for rationale.
|
|
66
|
+
*/
|
|
67
|
+
export ledger _isInitialized: Boolean;
|
|
68
|
+
|
|
69
|
+
// ─── Init ───────────────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* @description Initializes the forwarder with a parent recipient.
|
|
73
|
+
* Called once from the preset constructor. The user address is stored
|
|
74
|
+
* in the `right` arm of the generic `_parent` ledger field.
|
|
75
|
+
*
|
|
76
|
+
* Requirements:
|
|
77
|
+
*
|
|
78
|
+
* - Contract must not be initialized.
|
|
79
|
+
* - `parent` must not be the zero address. A zero parent would forward
|
|
80
|
+
* every deposit to an unspendable address with no recovery path.
|
|
81
|
+
*
|
|
82
|
+
* @param {UserAddress} parent - The user address that receives every
|
|
83
|
+
* forwarded amount. The parameter is deliberately narrower than the
|
|
84
|
+
* `_parent` ledger type: only a user address can be delivered to by an
|
|
85
|
+
* atomic forward today (see the module header).
|
|
86
|
+
*
|
|
87
|
+
* @notice Beyond the zero-address check, the caller must ensure `parent`
|
|
88
|
+
* can spend forwarded amounts. An unspendable parent strands every
|
|
89
|
+
* deposit with no recovery path.
|
|
90
|
+
*
|
|
91
|
+
* @returns {[]} Empty tuple.
|
|
92
|
+
*/
|
|
93
|
+
export circuit initialize(parent: UserAddress): [] {
|
|
94
|
+
const isZero = default<UserAddress> == parent;
|
|
95
|
+
assert(!isZero, "ForwarderUnshielded: zero parent");
|
|
96
|
+
assertNotInitialized();
|
|
97
|
+
_isInitialized = true;
|
|
98
|
+
_parent = right<ContractAddress, UserAddress>(disclose(parent));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ─── Deposit ────────────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* @description Receives an unshielded amount of `color` and atomically
|
|
105
|
+
* forwards it to `_parent` via `sendUnshielded` (always the `right` /
|
|
106
|
+
* user-address arm today).
|
|
107
|
+
*
|
|
108
|
+
* @circuitInfo k=9, rows=436
|
|
109
|
+
*
|
|
110
|
+
* Requirements:
|
|
111
|
+
*
|
|
112
|
+
* - Contract must be initialized.
|
|
113
|
+
*
|
|
114
|
+
* @param {Bytes<32>} color - The token color.
|
|
115
|
+
* @param {Uint<128>} amount - The amount to deposit.
|
|
116
|
+
*
|
|
117
|
+
* @returns {[]} Empty tuple.
|
|
118
|
+
*/
|
|
119
|
+
export circuit _deposit(color: Bytes<32>, amount: Uint<128>): [] {
|
|
120
|
+
assertInitialized();
|
|
121
|
+
receiveUnshielded(disclose(color), disclose(amount));
|
|
122
|
+
sendUnshielded(disclose(color), disclose(amount), _parent);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ─── Init guards ────────────────────────────────────────────────
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* @description Asserts that the contract has been initialized, throwing an error if not.
|
|
129
|
+
*
|
|
130
|
+
* Requirements:
|
|
131
|
+
*
|
|
132
|
+
* - Contract must be initialized.
|
|
133
|
+
*
|
|
134
|
+
* @returns {[]} Empty tuple.
|
|
135
|
+
*/
|
|
136
|
+
circuit assertInitialized(): [] {
|
|
137
|
+
assert(_isInitialized, "ForwarderUnshielded: contract not initialized");
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* @description Asserts that the contract has not been initialized, throwing an error if it has.
|
|
142
|
+
*
|
|
143
|
+
* Requirements:
|
|
144
|
+
*
|
|
145
|
+
* - Contract must not be initialized.
|
|
146
|
+
*
|
|
147
|
+
* @returns {[]} Empty tuple.
|
|
148
|
+
*/
|
|
149
|
+
circuit assertNotInitialized(): [] {
|
|
150
|
+
assert(!_isInitialized, "ForwarderUnshielded: contract already initialized");
|
|
151
|
+
}
|
|
152
|
+
}
|