// Token Vault Contract
// This file is part of midnight-ledger.
// Copyright (C) 2025 Midnight Foundation
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
// You may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Token Vault Contract
//!
//! **REFERENCE IMPLEMENTATION ONLY**
//! This code is provided for educational and testing purposes to demonstrate
//! Midnight ledger features. DO NOT use this code as-is in production without
//! proper security review, auditing, and hardening.
//!
//! A contract that demonstrates both shielded and unshielded token operations.
//! - Shielded tokens: Privacy-preserving using ZK proofs
//! - Unshielded tokens: Public UTXO-based transfers
export {
depositShielded,
withdrawShielded,
depositUnshielded,
withdrawUnshielded,
getShieldedBalance,
getUnshieldedBalance
}
import CompactStandardLibrary;
// ============================================================================
// Ledger State
// ============================================================================
// Shielded token storage (private tokens)
ledger shieldedVault: QualifiedShieldedCoinInfo;
ledger hasShieldedTokens: Boolean;
// Access control
ledger owner: Bytes<32>;
ledger authorized: Set<Bytes<32>>;
// Statistics
ledger totalShieldedDeposits: Counter;
ledger totalShieldedWithdrawals: Counter;
ledger totalUnshieldedDeposits: Counter;
ledger totalUnshieldedWithdrawals: Counter;
// ============================================================================
// Witness Functions
// ============================================================================
witness localSecretKey(): Bytes<32>;
// ============================================================================
// Helper Circuits
// ============================================================================
circuit publicKey(sk: Bytes<32>): Bytes<32> {
return persistentCommit<Bytes<14>>("token:vault:pk", disclose(sk));
}
circuit isOwner(): Boolean {
const sk = localSecretKey();
const pk = publicKey(sk);
return pk == owner;
}
circuit isAuthorized(): Boolean {
const sk = localSecretKey();
const pk = publicKey(sk);
return authorized.member(pk) || pk == owner;
}
// ============================================================================
// Shielded Token Operations (Private Tokens)
// ============================================================================
/// Deposit shielded tokens into the vault
circuit depositShielded(coin: ShieldedCoinInfo): [] {
assert(coin.value > 0, "Deposit amount must be positive");
receiveShielded(disclose(coin));
if (!hasShieldedTokens) {
shieldedVault.writeCoin(
disclose(coin),
right<ZswapCoinPublicKey, ContractAddress>(kernel.self())
);
hasShieldedTokens = true;
} else {
shieldedVault.writeCoin(
mergeCoinImmediate(shieldedVault, disclose(coin)),
right<ZswapCoinPublicKey, ContractAddress>(kernel.self())
);
}
totalShieldedDeposits.increment(1);
}
/// Withdraw shielded tokens from the vault
circuit withdrawShielded(amount: Uint<128>): ShieldedCoinInfo {
assert(isAuthorized(), "Not authorized to withdraw");
assert(hasShieldedTokens, "No shielded tokens in vault");
assert(shieldedVault.value >= amount, "Insufficient shielded balance");
const recipient = ownPublicKey();
const result = sendShielded(
shieldedVault,
left<ZswapCoinPublicKey, ContractAddress>(recipient),
disclose(amount)
);
if (result.change.is_some) {
shieldedVault.writeCoin(
result.change.value,
right<ZswapCoinPublicKey, ContractAddress>(kernel.self())
);
} else {
hasShieldedTokens = false;
shieldedVault = QualifiedShieldedCoinInfo {
nonce: pad(32, ""),
color: pad(32, ""),
value: 0,
mt_index: 0
};
}
totalShieldedWithdrawals.increment(1);
return result.sent;
}
circuit getShieldedBalance(): Uint<128> {
if (hasShieldedTokens) {
return shieldedVault.value;
}
return 0;
}
// ============================================================================
// Unshielded Token Operations (Public Tokens)
// ============================================================================
/// Deposit unshielded tokens into the vault
/// The caller must include the unshielded tokens as inputs to the transaction
/// This circuit declares the contract will receive the tokens
circuit depositUnshielded(color: Bytes<32>, amount: Uint<128>): [] {
assert(amount > 0, "Deposit amount must be positive");
// Declare that this contract is receiving unshielded tokens
// The actual tokens come from the transaction inputs
receiveUnshielded(disclose(color), disclose(amount));
totalUnshieldedDeposits.increment(1);
}
/// Withdraw unshielded tokens from the vault
circuit withdrawUnshielded(color: Bytes<32>, amount: Uint<128>, recipient: Either<ContractAddress, UserAddress>): [] {
assert(isAuthorized(), "Not authorized to withdraw");
assert(unshieldedBalanceGte(disclose(color), disclose(amount)), "Insufficient unshielded balance");
sendUnshielded(disclose(color), disclose(amount), disclose(recipient));
totalUnshieldedWithdrawals.increment(1);
}
/// Get unshielded balance for a token type
/// Note: This returns the balance at the START of execution,
/// not reflecting any sends/receives in the current transaction
circuit getUnshieldedBalance(color: Bytes<32>): Uint<128> {
return unshieldedBalance(disclose(color));
}
/// Check if balance is greater than a threshold
circuit hasUnshieldedBalanceAbove(color: Bytes<32>, threshold: Uint<128>): Boolean {
return unshieldedBalanceGt(disclose(color), disclose(threshold));
}