@openzeppelin/compact-contracts 0.1.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 +15 -16
- package/access/AccessControl.compact +3 -35
- package/access/Ownable.compact +47 -60
- package/access/ShieldedAccessControl.compact +43 -10
- package/access/ZOwnablePK.compact +45 -10
- 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 +12 -6
- package/security/Allowlist.compact +101 -0
- package/security/Blocklist.compact +99 -0
- package/security/Initializable.compact +18 -2
- package/security/Pausable.compact +2 -2
- package/token/FungibleToken.compact +79 -103
- package/token/MultiToken.compact +67 -93
- package/token/NativeShieldedToken.compact +285 -0
- package/token/NativeShieldedTokenCore.compact +343 -0
- package/token/NativeShieldedTokenFamily.compact +287 -0
- package/token/NonFungibleToken.compact +85 -110
- package/utils/Utils.compact +86 -2
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// OpenZeppelin Compact Contracts v0.3.0-alpha (multisig/ProposalManager.compact)
|
|
3
|
+
|
|
4
|
+
pragma language_version >= 0.23.0;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @module ProposalManager
|
|
8
|
+
* @description Token-agnostic proposal lifecycle management for multisig
|
|
9
|
+
* governance contracts.
|
|
10
|
+
*
|
|
11
|
+
* Supports shielded and unshielded proposals through a unified
|
|
12
|
+
* Recipient type with a RecipientKind tag. Typed helper circuits
|
|
13
|
+
* provide safe construction of recipients without exposing the
|
|
14
|
+
* internal Bytes<32> representation to consumers.
|
|
15
|
+
*/
|
|
16
|
+
module ProposalManager {
|
|
17
|
+
import CompactStandardLibrary;
|
|
18
|
+
|
|
19
|
+
// ─── Types ──────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
export enum ProposalStatus {
|
|
22
|
+
Inactive,
|
|
23
|
+
Active,
|
|
24
|
+
Executed,
|
|
25
|
+
Cancelled
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export enum RecipientKind {
|
|
29
|
+
ShieldedUser,
|
|
30
|
+
UnshieldedUser,
|
|
31
|
+
Contract
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export struct Recipient {
|
|
35
|
+
kind: RecipientKind,
|
|
36
|
+
address: Bytes<32>
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export struct Proposal {
|
|
40
|
+
to: Recipient,
|
|
41
|
+
color: Bytes<32>,
|
|
42
|
+
amount: Uint<128>,
|
|
43
|
+
status: ProposalStatus
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ─── State ──────────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
export ledger _nextProposalId: Counter;
|
|
49
|
+
export ledger _proposals: Map<Uint<64>, Proposal>;
|
|
50
|
+
|
|
51
|
+
// ─── Recipient Helpers ──────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @description Constructs a shielded user recipient.
|
|
55
|
+
*
|
|
56
|
+
* @param {ZswapCoinPublicKey} key - The shielded recipient's public key.
|
|
57
|
+
*
|
|
58
|
+
* @returns {Recipient} The typed recipient.
|
|
59
|
+
*/
|
|
60
|
+
export circuit shieldedUserRecipient(key: ZswapCoinPublicKey): Recipient {
|
|
61
|
+
return Recipient { kind: RecipientKind.ShieldedUser, address: key.bytes };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* @description Constructs an unshielded user recipient.
|
|
66
|
+
*
|
|
67
|
+
* @param {UserAddress} addr - The unshielded recipient's address.
|
|
68
|
+
*
|
|
69
|
+
* @returns {Recipient} The typed recipient.
|
|
70
|
+
*/
|
|
71
|
+
export circuit unshieldedUserRecipient(addr: UserAddress): Recipient {
|
|
72
|
+
return Recipient { kind: RecipientKind.UnshieldedUser, address: addr.bytes };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* @description Constructs a contract recipient.
|
|
77
|
+
*
|
|
78
|
+
* @param {ContractAddress} addr - The contract address.
|
|
79
|
+
*
|
|
80
|
+
* @returns {Recipient} The typed recipient.
|
|
81
|
+
*/
|
|
82
|
+
export circuit contractRecipient(addr: ContractAddress): Recipient {
|
|
83
|
+
return Recipient { kind: RecipientKind.Contract, address: addr.bytes };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* @description Converts a Recipient to a shielded send recipient.
|
|
88
|
+
* Handles both ShieldedUser and Contract kinds.
|
|
89
|
+
*
|
|
90
|
+
* Requirements:
|
|
91
|
+
*
|
|
92
|
+
* - Recipient kind must be ShieldedUser or Contract.
|
|
93
|
+
*
|
|
94
|
+
* @param {Recipient} r - The recipient.
|
|
95
|
+
*
|
|
96
|
+
* @returns {Either<ZswapCoinPublicKey, ContractAddress>} The shielded recipient.
|
|
97
|
+
*/
|
|
98
|
+
export circuit toShieldedRecipient(r: Recipient): Either<ZswapCoinPublicKey, ContractAddress> {
|
|
99
|
+
if (r.kind == RecipientKind.ShieldedUser) {
|
|
100
|
+
return left<ZswapCoinPublicKey, ContractAddress>(
|
|
101
|
+
ZswapCoinPublicKey { bytes: r.address }
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
assert(r.kind == RecipientKind.Contract, "ProposalManager: invalid shielded recipient");
|
|
105
|
+
return right<ZswapCoinPublicKey, ContractAddress>(
|
|
106
|
+
ContractAddress { bytes: r.address }
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* @description Converts a Recipient to an unshielded send recipient.
|
|
112
|
+
* Handles both UnshieldedUser and Contract kinds.
|
|
113
|
+
*
|
|
114
|
+
* Requirements:
|
|
115
|
+
*
|
|
116
|
+
* - Recipient kind must be UnshieldedUser or Contract.
|
|
117
|
+
*
|
|
118
|
+
* @param {Recipient} r - The recipient.
|
|
119
|
+
*
|
|
120
|
+
* @returns {Either<ContractAddress, UserAddress>} The unshielded recipient.
|
|
121
|
+
*/
|
|
122
|
+
export circuit toUnshieldedRecipient(r: Recipient): Either<ContractAddress, UserAddress> {
|
|
123
|
+
if (r.kind == RecipientKind.Contract) {
|
|
124
|
+
return left<ContractAddress, UserAddress>(
|
|
125
|
+
ContractAddress { bytes: r.address }
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
assert(r.kind == RecipientKind.UnshieldedUser, "ProposalManager: invalid unshielded recipient");
|
|
129
|
+
return right<ContractAddress, UserAddress>(
|
|
130
|
+
UserAddress { bytes: r.address }
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ─── Guards ─────────────────────────────────────────────────────
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* @description Asserts that a proposal exists.
|
|
138
|
+
*
|
|
139
|
+
* Requirements:
|
|
140
|
+
*
|
|
141
|
+
* - Proposal with `id` must have been created.
|
|
142
|
+
*
|
|
143
|
+
* @param {Uint<64>} id - The proposal ID.
|
|
144
|
+
*
|
|
145
|
+
* @returns {[]} Empty tuple.
|
|
146
|
+
*/
|
|
147
|
+
export circuit assertProposalExists(id: Uint<64>): [] {
|
|
148
|
+
assert(
|
|
149
|
+
_proposals.member(disclose(id)),
|
|
150
|
+
"ProposalManager: proposal not found"
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* @description Asserts that a proposal exists and is active.
|
|
156
|
+
*
|
|
157
|
+
* Requirements:
|
|
158
|
+
*
|
|
159
|
+
* - Proposal must exist.
|
|
160
|
+
* - Proposal status must be Active.
|
|
161
|
+
*
|
|
162
|
+
* @param {Uint<64>} id - The proposal ID.
|
|
163
|
+
*
|
|
164
|
+
* @returns {[]} Empty tuple.
|
|
165
|
+
*/
|
|
166
|
+
export circuit assertProposalActive(id: Uint<64>): [] {
|
|
167
|
+
assertProposalExists(id);
|
|
168
|
+
assert(
|
|
169
|
+
_proposals.lookup(disclose(id)).status == ProposalStatus.Active,
|
|
170
|
+
"ProposalManager: proposal not active"
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ─── Proposal Lifecycle ─────────────────────────────────────────
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* @description Creates a new proposal.
|
|
178
|
+
*
|
|
179
|
+
* @notice Access control is NOT enforced here.
|
|
180
|
+
* The consuming contract must gate this behind its own
|
|
181
|
+
* authorization policy.
|
|
182
|
+
*
|
|
183
|
+
* Requirements:
|
|
184
|
+
*
|
|
185
|
+
* - `amount` must be greater than 0.
|
|
186
|
+
*
|
|
187
|
+
* @param {Recipient} to - The recipient (constructed via helper circuits).
|
|
188
|
+
* @param {Bytes<32>} color - The token color.
|
|
189
|
+
* @param {Uint<128>} amount - The amount to transfer.
|
|
190
|
+
*
|
|
191
|
+
* @returns {Uint<64>} The new proposal ID.
|
|
192
|
+
*/
|
|
193
|
+
export circuit _createProposal(
|
|
194
|
+
to: Recipient,
|
|
195
|
+
color: Bytes<32>,
|
|
196
|
+
amount: Uint<128>
|
|
197
|
+
): Uint<64> {
|
|
198
|
+
assert(amount > 0, "ProposalManager: zero amount");
|
|
199
|
+
|
|
200
|
+
_nextProposalId.increment(1);
|
|
201
|
+
const id = _nextProposalId;
|
|
202
|
+
|
|
203
|
+
_proposals.insert(id, disclose(Proposal {
|
|
204
|
+
to: to,
|
|
205
|
+
color: color,
|
|
206
|
+
amount: amount,
|
|
207
|
+
status: ProposalStatus.Active
|
|
208
|
+
}));
|
|
209
|
+
|
|
210
|
+
return id;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* @description Cancels a proposal.
|
|
215
|
+
*
|
|
216
|
+
* @notice Access control is NOT enforced here.
|
|
217
|
+
* The consuming contract must gate this behind its own
|
|
218
|
+
* authorization policy.
|
|
219
|
+
*
|
|
220
|
+
* Requirements:
|
|
221
|
+
*
|
|
222
|
+
* - Proposal must be active.
|
|
223
|
+
*
|
|
224
|
+
* @param {Uint<64>} id - The proposal ID.
|
|
225
|
+
*
|
|
226
|
+
* @returns {[]} Empty tuple.
|
|
227
|
+
*/
|
|
228
|
+
export circuit _cancelProposal(id: Uint<64>): [] {
|
|
229
|
+
assertProposalActive(id);
|
|
230
|
+
|
|
231
|
+
const proposal = _proposals.lookup(disclose(id));
|
|
232
|
+
_proposals.insert(disclose(id), Proposal {
|
|
233
|
+
to: proposal.to,
|
|
234
|
+
color: proposal.color,
|
|
235
|
+
amount: proposal.amount,
|
|
236
|
+
status: ProposalStatus.Cancelled
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* @description Marks a proposal as executed.
|
|
242
|
+
*
|
|
243
|
+
* @notice Access control is NOT enforced here.
|
|
244
|
+
* The consuming contract must gate this behind its own
|
|
245
|
+
* authorization policy.
|
|
246
|
+
*
|
|
247
|
+
* Requirements:
|
|
248
|
+
*
|
|
249
|
+
* - Proposal must be active.
|
|
250
|
+
*
|
|
251
|
+
* @param {Uint<64>} id - The proposal ID.
|
|
252
|
+
*
|
|
253
|
+
* @returns {[]} Empty tuple.
|
|
254
|
+
*/
|
|
255
|
+
export circuit _markExecuted(id: Uint<64>): [] {
|
|
256
|
+
assertProposalActive(id);
|
|
257
|
+
|
|
258
|
+
const proposal = _proposals.lookup(disclose(id));
|
|
259
|
+
_proposals.insert(disclose(id), Proposal {
|
|
260
|
+
to: proposal.to,
|
|
261
|
+
color: proposal.color,
|
|
262
|
+
amount: proposal.amount,
|
|
263
|
+
status: ProposalStatus.Executed
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// ─── View ───────────────────────────────────────────────────────
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* @description Returns the full proposal data.
|
|
271
|
+
*
|
|
272
|
+
* @param {Uint<64>} id - The proposal ID.
|
|
273
|
+
*
|
|
274
|
+
* @returns {Proposal} The proposal.
|
|
275
|
+
*/
|
|
276
|
+
export circuit getProposal(id: Uint<64>): Proposal {
|
|
277
|
+
assertProposalExists(id);
|
|
278
|
+
return _proposals.lookup(disclose(id));
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* @description Returns the recipient of a proposal.
|
|
283
|
+
*
|
|
284
|
+
* @param {Uint<64>} id - The proposal ID.
|
|
285
|
+
*
|
|
286
|
+
* @returns {Recipient} The recipient.
|
|
287
|
+
*/
|
|
288
|
+
export circuit getProposalRecipient(id: Uint<64>): Recipient {
|
|
289
|
+
assertProposalExists(id);
|
|
290
|
+
return _proposals.lookup(disclose(id)).to;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* @description Returns the amount of a proposal.
|
|
295
|
+
*
|
|
296
|
+
* @param {Uint<64>} id - The proposal ID.
|
|
297
|
+
*
|
|
298
|
+
* @returns {Uint<128>} The amount.
|
|
299
|
+
*/
|
|
300
|
+
export circuit getProposalAmount(id: Uint<64>): Uint<128> {
|
|
301
|
+
assertProposalExists(id);
|
|
302
|
+
return _proposals.lookup(disclose(id)).amount;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* @description Returns the token color of a proposal.
|
|
307
|
+
*
|
|
308
|
+
* @param {Uint<64>} id - The proposal ID.
|
|
309
|
+
*
|
|
310
|
+
* @returns {Bytes<32>} The token color.
|
|
311
|
+
*/
|
|
312
|
+
export circuit getProposalColor(id: Uint<64>): Bytes<32> {
|
|
313
|
+
assertProposalExists(id);
|
|
314
|
+
return _proposals.lookup(disclose(id)).color;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* @description Returns the status of a proposal.
|
|
319
|
+
*
|
|
320
|
+
* @param {Uint<64>} id - The proposal ID.
|
|
321
|
+
*
|
|
322
|
+
* @returns {ProposalStatus} The proposal status.
|
|
323
|
+
*/
|
|
324
|
+
export circuit getProposalStatus(id: Uint<64>): ProposalStatus {
|
|
325
|
+
assertProposalExists(id);
|
|
326
|
+
return _proposals.lookup(disclose(id)).status;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// OpenZeppelin Compact Contracts v0.3.0-alpha (multisig/ShieldedTreasury.compact)
|
|
3
|
+
|
|
4
|
+
pragma language_version >= 0.23.0;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @module ShieldedTreasury
|
|
8
|
+
* @description Manages shielded (private) token deposits, accounting,
|
|
9
|
+
* and transfers for multisig governance contracts.
|
|
10
|
+
*
|
|
11
|
+
* Coins are stored on the contract ledger in a map keyed by token color,
|
|
12
|
+
* with one UTXO per color. Deposits are merged with existing coins of
|
|
13
|
+
* the same color via `mergeCoinImmediate`. This simplifies coin selection
|
|
14
|
+
* at spend time — the executor doesn't need to choose between multiple
|
|
15
|
+
* UTXOs of the same color.
|
|
16
|
+
*
|
|
17
|
+
* Cumulative received and sent totals are tracked per color for audit
|
|
18
|
+
* purposes. The canonical balance query is `getTokenBalance`, which
|
|
19
|
+
* reads the actual coin value from the UTXO map.
|
|
20
|
+
*
|
|
21
|
+
* Underscore-prefixed circuits (_deposit, _send) have no access control
|
|
22
|
+
* enforcement. The consuming contract must gate these behind its own
|
|
23
|
+
* authorization policy.
|
|
24
|
+
*/
|
|
25
|
+
module ShieldedTreasury {
|
|
26
|
+
import CompactStandardLibrary;
|
|
27
|
+
import { selfAsRecipient, UINT128_MAX } from "../utils/Utils" prefix Utils_;
|
|
28
|
+
|
|
29
|
+
// ─── State ──────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
export ledger _coins: Map<Bytes<32>, QualifiedShieldedCoinInfo>;
|
|
32
|
+
export ledger _received: Map<Bytes<32>, Uint<128>>;
|
|
33
|
+
export ledger _sent: Map<Bytes<32>, Uint<128>>;
|
|
34
|
+
|
|
35
|
+
// ─── Deposit ────────────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @description Receives a shielded coin into the treasury.
|
|
39
|
+
*
|
|
40
|
+
* The coin is first claimed at the protocol level via `receiveShielded`,
|
|
41
|
+
* which allocates the Merkle tree index required by `insertCoin`.
|
|
42
|
+
* The coin is then merged with any existing coin of the same color,
|
|
43
|
+
* or inserted as a new entry if no coin of that color exists.
|
|
44
|
+
*
|
|
45
|
+
* Zero-value deposits are permitted. While currently a no-op
|
|
46
|
+
* economically, they may serve as signaling mechanisms when events
|
|
47
|
+
* are supported, or as decoy transactions for privacy.
|
|
48
|
+
*
|
|
49
|
+
* @notice Access control is NOT enforced here.
|
|
50
|
+
* The consuming contract must gate this behind its own
|
|
51
|
+
* authorization policy.
|
|
52
|
+
*
|
|
53
|
+
* Requirements:
|
|
54
|
+
*
|
|
55
|
+
* - Deposit must not cause received total overflow.
|
|
56
|
+
*
|
|
57
|
+
* @param {ShieldedCoinInfo} coin - The incoming shielded coin descriptor.
|
|
58
|
+
*
|
|
59
|
+
* @returns {[]} Empty tuple.
|
|
60
|
+
*/
|
|
61
|
+
export circuit _deposit(coin: ShieldedCoinInfo): [] {
|
|
62
|
+
const currentReceived = getReceivedTotal(coin.color);
|
|
63
|
+
assert(currentReceived <= Utils_UINT128_MAX() - coin.value, "ShieldedTreasury: overflow");
|
|
64
|
+
|
|
65
|
+
receiveShielded(disclose(coin));
|
|
66
|
+
|
|
67
|
+
const coinColor = disclose(coin.color);
|
|
68
|
+
|
|
69
|
+
if (_coins.member(coinColor)) {
|
|
70
|
+
const merged = mergeCoinImmediate(_coins.lookup(coinColor), disclose(coin));
|
|
71
|
+
_coins.insertCoin(coinColor, merged, Utils_selfAsRecipient());
|
|
72
|
+
} else {
|
|
73
|
+
_coins.insertCoin(coinColor, disclose(coin), Utils_selfAsRecipient());
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
_received.insert(
|
|
77
|
+
coinColor,
|
|
78
|
+
disclose(currentReceived + coin.value as Uint<128>)
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ─── Send ───────────────────────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* @description Sends shielded tokens from the treasury.
|
|
86
|
+
*
|
|
87
|
+
* Looks up the stored coin by color, verifies sufficient value,
|
|
88
|
+
* and executes the shielded send. If the send produces change,
|
|
89
|
+
* it is sent back to the contract via `sendImmediateShielded`.
|
|
90
|
+
*
|
|
91
|
+
* @notice Access control is NOT enforced here.
|
|
92
|
+
* The consuming contract must gate this behind its own
|
|
93
|
+
* authorization policy.
|
|
94
|
+
*
|
|
95
|
+
* Requirements:
|
|
96
|
+
*
|
|
97
|
+
* - A coin of the given `color` must exist in the treasury.
|
|
98
|
+
* - The coin's value must be >= `amount`.
|
|
99
|
+
* - Send must not cause sent total overflow.
|
|
100
|
+
*
|
|
101
|
+
* @param {Either<ZswapCoinPublicKey, ContractAddress>} recipient - The recipient.
|
|
102
|
+
* @param {Bytes<32>} color - The token color.
|
|
103
|
+
* @param {Uint<128>} amount - The amount to send.
|
|
104
|
+
*
|
|
105
|
+
* @returns {ShieldedSendResult} The result containing sent coin and any change.
|
|
106
|
+
*/
|
|
107
|
+
export circuit _send(
|
|
108
|
+
recipient: Either<ZswapCoinPublicKey, ContractAddress>,
|
|
109
|
+
color: Bytes<32>,
|
|
110
|
+
amount: Uint<128>
|
|
111
|
+
): ShieldedSendResult {
|
|
112
|
+
assert(_coins.member(disclose(color)), "ShieldedTreasury: no balance");
|
|
113
|
+
|
|
114
|
+
const coin = _coins.lookup(disclose(color));
|
|
115
|
+
assert(coin.value >= amount, "ShieldedTreasury: coin value insufficient");
|
|
116
|
+
|
|
117
|
+
const currentSent = getSentTotal(color);
|
|
118
|
+
assert(currentSent <= Utils_UINT128_MAX() - amount, "ShieldedTreasury: overflow");
|
|
119
|
+
|
|
120
|
+
const result = sendShielded(coin, disclose(recipient), disclose(amount));
|
|
121
|
+
|
|
122
|
+
if (disclose(result.change.is_some)) {
|
|
123
|
+
const changeCoin = result.change.value;
|
|
124
|
+
sendImmediateShielded(
|
|
125
|
+
changeCoin,
|
|
126
|
+
Utils_selfAsRecipient(),
|
|
127
|
+
changeCoin.value
|
|
128
|
+
);
|
|
129
|
+
_coins.insertCoin(disclose(color), changeCoin, Utils_selfAsRecipient());
|
|
130
|
+
} else {
|
|
131
|
+
_coins.remove(disclose(color));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
_sent.insert(
|
|
135
|
+
disclose(color),
|
|
136
|
+
disclose(currentSent + amount as Uint<128>)
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
return result;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ─── View ───────────────────────────────────────────────────────
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* @description Returns the current token balance for a color.
|
|
146
|
+
* Reads the actual coin value from the UTXO map.
|
|
147
|
+
*
|
|
148
|
+
* @param {Bytes<32>} color - The token color.
|
|
149
|
+
*
|
|
150
|
+
* @returns {Uint<128>} The current balance.
|
|
151
|
+
*/
|
|
152
|
+
export circuit getTokenBalance(color: Bytes<32>): Uint<128> {
|
|
153
|
+
if (!_coins.member(disclose(color))) {
|
|
154
|
+
return 0;
|
|
155
|
+
}
|
|
156
|
+
return _coins.lookup(disclose(color)).value;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ─── Accounting ─────────────────────────────────────────────────
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* @description Returns the cumulative received total for a color.
|
|
163
|
+
*
|
|
164
|
+
* @param {Bytes<32>} color - The token color.
|
|
165
|
+
*
|
|
166
|
+
* @returns {Uint<128>} Total received.
|
|
167
|
+
*/
|
|
168
|
+
export circuit getReceivedTotal(color: Bytes<32>): Uint<128> {
|
|
169
|
+
if (!_received.member(disclose(color))) {
|
|
170
|
+
return 0;
|
|
171
|
+
}
|
|
172
|
+
return _received.lookup(disclose(color));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* @description Returns the cumulative sent total for a color.
|
|
177
|
+
*
|
|
178
|
+
* @param {Bytes<32>} color - The token color.
|
|
179
|
+
*
|
|
180
|
+
* @returns {Uint<128>} Total sent.
|
|
181
|
+
*/
|
|
182
|
+
export circuit getSentTotal(color: Bytes<32>): Uint<128> {
|
|
183
|
+
if (!_sent.member(disclose(color))) {
|
|
184
|
+
return 0;
|
|
185
|
+
}
|
|
186
|
+
return _sent.lookup(disclose(color));
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* @description Returns the difference between cumulative received
|
|
191
|
+
* and cumulative sent totals for a color. Should equal
|
|
192
|
+
* `getTokenBalance` if accounting is consistent.
|
|
193
|
+
*
|
|
194
|
+
* @param {Bytes<32>} color - The token color.
|
|
195
|
+
*
|
|
196
|
+
* @returns {Uint<128>} Received minus sent.
|
|
197
|
+
*/
|
|
198
|
+
export circuit getReceivedMinusSent(color: Bytes<32>): Uint<128> {
|
|
199
|
+
return getReceivedTotal(color) - getSentTotal(color) as Uint<128>;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// OpenZeppelin Compact Contracts v0.3.0-alpha (multisig/ShieldedTreasuryStateless.compact)
|
|
3
|
+
|
|
4
|
+
pragma language_version >= 0.23.0;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @module ShieldedTreasury
|
|
8
|
+
* @description Manages shielded (private) token deposits, accounting,
|
|
9
|
+
* and transfers for multisig governance contracts.
|
|
10
|
+
*
|
|
11
|
+
* Coins are stored on the contract ledger in a map keyed by token color,
|
|
12
|
+
* with one UTXO per color. Deposits are merged with existing coins of
|
|
13
|
+
* the same color via `mergeCoinImmediate`. This simplifies coin selection
|
|
14
|
+
* at spend time — the executor doesn't need to choose between multiple
|
|
15
|
+
* UTXOs of the same color.
|
|
16
|
+
*
|
|
17
|
+
* Cumulative received and sent totals are tracked per color for audit
|
|
18
|
+
* purposes. The canonical balance query is `getTokenBalance`, which
|
|
19
|
+
* reads the actual coin value from the UTXO map.
|
|
20
|
+
*/
|
|
21
|
+
module ShieldedTreasuryStateless {
|
|
22
|
+
import CompactStandardLibrary;
|
|
23
|
+
import { selfAsRecipient } from "../utils/Utils" prefix Utils_;
|
|
24
|
+
|
|
25
|
+
// ─── Deposit ────────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @description Receives a shielded coin into the treasury.
|
|
29
|
+
*/
|
|
30
|
+
export circuit _deposit(coin: ShieldedCoinInfo): [] {
|
|
31
|
+
receiveShielded(disclose(coin));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ─── Send ───────────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @description Sends shielded tokens using a caller-provided coin.
|
|
38
|
+
*
|
|
39
|
+
* The coin is supplied by the caller rather than looked up from
|
|
40
|
+
* on-chain state; the token color is derived from `coin.color`.
|
|
41
|
+
* Executes the shielded send and, if change is produced, returns it
|
|
42
|
+
* to the contract via `sendImmediateShielded`. No balance or
|
|
43
|
+
* received/sent accounting is tracked on the ledger.
|
|
44
|
+
*
|
|
45
|
+
* @notice Access control is NOT enforced here.
|
|
46
|
+
* The consuming contract must gate this behind its own
|
|
47
|
+
* authorization policy.
|
|
48
|
+
*
|
|
49
|
+
* Requirements:
|
|
50
|
+
*
|
|
51
|
+
* - `coin.value` must be >= `amount`.
|
|
52
|
+
*
|
|
53
|
+
* @param {QualifiedShieldedCoinInfo} coin - The coin to spend (provided by the caller).
|
|
54
|
+
* @param {Either<ZswapCoinPublicKey, ContractAddress>} recipient - The recipient.
|
|
55
|
+
* @param {Uint<128>} amount - The amount to send.
|
|
56
|
+
*
|
|
57
|
+
* @returns {ShieldedSendResult} The result containing sent coin and any change.
|
|
58
|
+
*/
|
|
59
|
+
export circuit _send(
|
|
60
|
+
coin: QualifiedShieldedCoinInfo,
|
|
61
|
+
recipient: Either<ZswapCoinPublicKey, ContractAddress>,
|
|
62
|
+
amount: Uint<128>
|
|
63
|
+
): ShieldedSendResult {
|
|
64
|
+
const result = sendShielded(disclose(coin), disclose(recipient), disclose(amount));
|
|
65
|
+
|
|
66
|
+
if (disclose(result.change.is_some)) {
|
|
67
|
+
sendImmediateShielded(
|
|
68
|
+
disclose(result.change.value),
|
|
69
|
+
Utils_selfAsRecipient(),
|
|
70
|
+
disclose(result.change.value.value)
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return result;
|
|
75
|
+
}
|
|
76
|
+
}
|