Просмотр исходного кода

contract/money: Spread out lib.rs into more files.

parazyd 3 лет назад
Родитель
Сommit
27d277f014

+ 3 - 11
src/contract/money/proof/token_mint_v1.zk

@@ -1,3 +1,4 @@
+# Circuit used to mint arbitrary coins given a mint authority secret.
 constant "TokenMint_V1" {
 	EcFixedPointShort VALUE_COMMIT_VALUE,
 	EcFixedPoint VALUE_COMMIT_RANDOM,
@@ -9,15 +10,11 @@ contract "TokenMint_V1" {
 	Base mint_authority,
 	# Token supply
 	Base supply,
-	# Fixed supply
-	Base fixed_supply,
 	# Recipient's public key x coordinate
 	Base rcpt_x,
 	# Recipient's public key y coordinate
 	Base rcpt_y,
-	# Unique serial number corresponding to this coin
-	Base serial,
-	# Random blinding factor for coin
+	# Random blinding factor for the minted coin
 	Base coin_blind,
 	# Allows composing this ZK proof to invoke other contracts
 	Base spend_hook,
@@ -41,12 +38,7 @@ circuit "TokenMint_V1" {
 	token_id = poseidon_hash(mint_x, mint_y);
 	constrain_instance(token_id);
 
-	# Constrain whether this token has a fixed supply or not.
-	# In case it is, subsequent mints will not be allowed.
-	bool_check(fixed_supply);
-	constrain_instance(fixed_supply);
-
-	# Poseidon hash of the coin
+	# Poseidon hash of the minted coin
 	C = poseidon_hash(
 		rcpt_x,
 		rcpt_y,

+ 8 - 0
src/contract/money/src/client.rs

@@ -50,6 +50,9 @@ use crate::model::{
     StakedInput, StakedOutput,
 };
 
+/// Client API for token minting and freezing
+pub mod token_mint;
+
 // Wallet SQL table constant names. These have to represent the SQL schema.
 // TODO: They should also ideally be prefixed with the contract ID to avoid
 //       collisions.
@@ -81,6 +84,11 @@ pub const MONEY_COINS_COL_NULLIFIER: &str = "nullifier";
 pub const MONEY_COINS_COL_LEAF_POSITION: &str = "leaf_position";
 pub const MONEY_COINS_COL_MEMO: &str = "memo";
 
+pub const MONEY_TOKENS_TABLE: &str = "money_tokens";
+pub const MONEY_TOKENS_COL_SECRET: &str = "secret";
+pub const MONEY_TOKENS_COL_TOKEN_ID: &str = "token_id";
+pub const MONEY_TOKENS_COL_FROZEN: &str = "frozen";
+
 pub const MONEY_ALIASES_TABLE: &str = "money_aliases";
 pub const MONEY_ALIASES_COL_ALIAS: &str = "alias";
 pub const MONEY_ALIASES_COL_TOKEN_ID: &str = "token_id";

+ 97 - 0
src/contract/money/src/client/token_mint.rs

@@ -0,0 +1,97 @@
+/* This file is part of DarkFi (https://dark.fi)
+ *
+ * Copyright (C) 2020-2023 Dyne.org foundation
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+//! This API is experimental. Have to evaluate the best approach still.
+
+pub struct MoneyMintClient {
+    pub mint_authority: SecretKey,
+    pub amount: u64,
+    pub recipient: PublicKey,
+    pub spend_hook: pallas::Base,
+    pub user_data: pallas::Base,
+}
+
+impl MoneyMintClient {
+    pub fn build(&self) -> Result<MoneyMintParams> {
+        debug!(target: "money::client::token_mint", "Building params");
+        assert!(self.amount != 0);
+
+        let token_id = TokenId::derive(self.mint_authority);
+        let value_blind = pallas::Scalar::random(&mut OsRng);
+        let token_blind = pallas::Scalar::random(&mut OsRng);
+
+        let serial = pallas::Base::random(&mut OsRng);
+        let coin_blind = pallas::Base::random(&mut OsRng);
+
+        let (pub_x, pub_y) = self.recipient.xy();
+
+        // Create the clear input
+        let input = ClearInput {
+            value: self.amount,
+            token_id,
+            value_blind,
+            token_blind,
+            signature_public: PublicKey::from_secret(mint_authority),
+        };
+
+        // Create the anonymous output
+        let coin = poseidon_hash([
+            pub_x,
+            pub_y,
+            pallas::Base::from(self.amount),
+            token_id.inner(),
+            serial,
+            self.spend_hook,
+            self.user_data,
+            coin_blind,
+        ]);
+
+        let note = MoneyNote {
+            serial,
+            value: self.amount,
+            token_id,
+            spend_hook,
+            user_data,
+            coin_blind,
+            value_blind,
+            token_blind,
+            memo: vec![],
+        };
+
+        let encrypted_note = note.encrypt(&self.recipient)?;
+
+        let output = Output {
+            value_commit: pedersen_commitment_u64(self.amount, value_blind),
+            token_commit: pedersen_commitment_base(token_id, token_blind),
+            coin,
+            ciphertext: encrypted_note.ciphertext,
+            ephem_public: encrypted_note.ephem_public,
+        };
+
+        Ok(MoneyMintParams { input, output })
+    }
+
+    pub fn prove(&self, params: &MoneyMintParams, zkbin: &ZkBinary, proving_key: &ProvingKey) -> Result<Proof> {
+        let prover_witnesses = vec![];
+
+        let circuit = ZkCircuit::new(prover_witnesses, zkbin.clone());
+        let proof = Proof::create(proving_key, &[circuit], &public_inputs, &mut OsRng)?;
+
+        Ok(proof)
+    }
+}

+ 58 - 296
src/contract/money/src/lib.rs

@@ -19,12 +19,11 @@
 #[cfg(not(feature = "no-entrypoint"))]
 use darkfi_sdk::{
     crypto::{
-        pallas, pasta_prelude::*, pedersen_commitment_base, pedersen_commitment_u64, Coin,
-        ContractId, MerkleNode, MerkleTree, PublicKey, DARK_TOKEN_ID,
+        pallas, pasta_prelude::*, pedersen_commitment_base, Coin, ContractId, MerkleNode,
+        MerkleTree, PublicKey, DARK_TOKEN_ID,
     },
     db::{
-        db_contains_key, db_get, db_init, db_lookup, db_set, set_return_data,
-        SMART_CONTRACT_ZKAS_DB_NAME,
+        db_contains_key, db_init, db_lookup, db_set, set_return_data, SMART_CONTRACT_ZKAS_DB_NAME,
     },
     error::ContractResult,
     merkle::merkle_add,
@@ -66,10 +65,22 @@ impl TryFrom<u8> for MoneyFunction {
 /// Structures and object definitions
 pub mod model;
 
+// Contract functionalities
+mod mint;
+use mint::{money_mint_get_metadata, money_mint_process_instruction, money_mint_process_update};
+mod swap;
+use swap::{
+    money_otcswap_get_metadata, money_otcswap_process_instruction, money_otcswap_process_update,
+};
+mod transfer;
+use transfer::{
+    money_transfer_get_metadata, money_transfer_process_instruction, money_transfer_process_update,
+};
+
 #[cfg(not(feature = "no-entrypoint"))]
 use model::{
-    MoneyStakeParams, MoneyStakeUpdate, MoneyTransferParams, MoneyTransferUpdate,
-    MoneyUnstakeParams,
+    MoneyMintParams, MoneyMintUpdate, MoneyStakeParams, MoneyStakeUpdate, MoneyTransferParams,
+    MoneyTransferUpdate, MoneyUnstakeParams,
 };
 
 #[cfg(feature = "client")]
@@ -87,7 +98,6 @@ darkfi_sdk::define_contract!(
 // These are the different sled trees that will be created
 pub const MONEY_CONTRACT_COIN_ROOTS_TREE: &str = "coin_roots";
 pub const MONEY_CONTRACT_NULLIFIERS_TREE: &str = "nullifiers";
-pub const MONEY_CONTRACT_TOKEN_ROOTS_TREE: &str = "token_roots";
 pub const MONEY_CONTRACT_TOKEN_FREEZE_TREE: &str = "token_freezes";
 pub const MONEY_CONTRACT_INFO_TREE: &str = "info";
 // lead coin, nullifier sled trees.
@@ -137,8 +147,8 @@ fn init_contract(cid: ContractId, ix: &[u8]) -> ContractResult {
     let lead_mint_v1_bincode = include_bytes!("../proof/lead_mint_v1.zk.bin");
     let lead_burn_v1_bincode = include_bytes!("../proof/lead_burn_v1.zk.bin");
 
-    /* TODO: Do I really want to make zkas a dependency? Yeah, in the future.
-       For now we take anything.
+    /* For now we take anything, but the zkas db needs protection against
+       arbitrary data.
     let zkbin = ZkBinary::decode(mint_bincode)?;
     let mint_namespace = zkbin.namespace.clone();
     assert_eq!(&mint_namespace, ZKAS_MINT_NS);
@@ -166,11 +176,6 @@ fn init_contract(cid: ContractId, ix: &[u8]) -> ContractResult {
         db_init(cid, MONEY_CONTRACT_NULLIFIERS_TREE)?;
     }
 
-    // Set up a database tree to hold Merkle roots of all tokens
-    if db_lookup(cid, MONEY_CONTRACT_TOKEN_ROOTS_TREE).is_err() {
-        db_init(cid, MONEY_CONTRACT_TOKEN_ROOTS_TREE)?;
-    }
-
     // Set up a database tree to hold a set of frozen token mints
     if db_lookup(cid, MONEY_CONTRACT_TOKEN_FREEZE_TREE).is_err() {
         db_init(cid, MONEY_CONTRACT_TOKEN_FREEZE_TREE)?;
@@ -214,71 +219,26 @@ fn init_contract(cid: ContractId, ix: &[u8]) -> ContractResult {
 /// This function is used by the VM's host to fetch the necessary metadata for
 /// verifying signatures and zk proofs.
 #[cfg(not(feature = "no-entrypoint"))]
-fn get_metadata(_cid: ContractId, ix: &[u8]) -> ContractResult {
-    let (call_idx, call): (u32, Vec<ContractCall>) = deserialize(ix)?;
-    assert!(call_idx < call.len() as u32);
+fn get_metadata(cid: ContractId, ix: &[u8]) -> ContractResult {
+    let (call_idx, calls): (u32, Vec<ContractCall>) = deserialize(ix)?;
+    assert!(call_idx < calls.len() as u32);
 
-    let self_ = &call[call_idx as usize];
+    let self_ = &calls[call_idx as usize];
 
     match MoneyFunction::try_from(self_.data[0])? {
-        MoneyFunction::Transfer | MoneyFunction::OtcSwap => {
-            let params: MoneyTransferParams = deserialize(&self_.data[1..])?;
-
-            let mut zk_public_values: Vec<(String, Vec<pallas::Base>)> = vec![];
-            let mut signature_pubkeys: Vec<PublicKey> = vec![];
-
-            for input in &params.clear_inputs {
-                signature_pubkeys.push(input.signature_public);
-            }
-
-            for input in &params.inputs {
-                let value_coords = input.value_commit.to_affine().coordinates().unwrap();
-                let token_coords = input.token_commit.to_affine().coordinates().unwrap();
-                let (sig_x, sig_y) = input.signature_public.xy();
-
-                zk_public_values.push((
-                    MONEY_CONTRACT_ZKAS_BURN_NS_V1.to_string(),
-                    vec![
-                        input.nullifier.inner(),
-                        *value_coords.x(),
-                        *value_coords.y(),
-                        *token_coords.x(),
-                        *token_coords.y(),
-                        input.merkle_root.inner(),
-                        input.user_data_enc,
-                        sig_x,
-                        sig_y,
-                    ],
-                ));
-
-                signature_pubkeys.push(input.signature_public);
-            }
-
-            for output in &params.outputs {
-                let value_coords = output.value_commit.to_affine().coordinates().unwrap();
-                let token_coords = output.token_commit.to_affine().coordinates().unwrap();
-
-                zk_public_values.push((
-                    MONEY_CONTRACT_ZKAS_MINT_NS_V1.to_string(),
-                    vec![
-                        output.coin,
-                        *value_coords.x(),
-                        *value_coords.y(),
-                        *token_coords.x(),
-                        *token_coords.y(),
-                    ],
-                ));
-            }
-
-            let mut metadata = vec![];
-            zk_public_values.encode(&mut metadata)?;
-            signature_pubkeys.encode(&mut metadata)?;
-
+        MoneyFunction::Transfer => {
+            let metadata = money_transfer_get_metadata(cid, call_idx, calls)?;
             // Using this, we pass the above data to the host.
             set_return_data(&metadata)?;
             Ok(())
         }
 
+        MoneyFunction::OtcSwap => {
+            let metadata = money_otcswap_get_metadata(cid, call_idx, calls)?;
+            set_return_data(&metadata)?;
+            Ok(())
+        }
+
         MoneyFunction::Stake => {
             let params: MoneyStakeParams = deserialize(&self_.data[1..])?;
 
@@ -376,8 +336,10 @@ fn get_metadata(_cid: ContractId, ix: &[u8]) -> ContractResult {
         }
 
         MoneyFunction::Mint => {
-            msg!("[Mint] Entered match arm");
-            unimplemented!();
+            let metadata = money_mint_get_metadata(cid, call_idx, calls)?;
+            // Using this, we pass the above data to the host.
+            set_return_data(&metadata)?;
+            Ok(())
         }
 
         MoneyFunction::Freeze => {
@@ -391,222 +353,29 @@ fn get_metadata(_cid: ContractId, ix: &[u8]) -> ContractResult {
 /// update if everything is successful.
 #[cfg(not(feature = "no-entrypoint"))]
 fn process_instruction(cid: ContractId, ix: &[u8]) -> ContractResult {
-    let (call_idx, call): (u32, Vec<ContractCall>) = deserialize(ix)?;
-    assert!(call_idx < call.len() as u32);
+    let (call_idx, calls): (u32, Vec<ContractCall>) = deserialize(ix)?;
+
+    if call_idx >= calls.len() as u32 {
+        msg!("Error: call_idx >= calls.len()");
+        return Err(ContractError::Internal)
+    }
 
-    let self_ = &call[call_idx as usize];
+    let self_ = &calls[call_idx as usize];
 
     match MoneyFunction::try_from(self_.data[0])? {
         MoneyFunction::Transfer => {
             msg!("[Transfer] Entered match arm");
-            let params: MoneyTransferParams = deserialize(&self_.data[1..])?;
-
-            assert!(params.clear_inputs.len() + params.inputs.len() > 0);
-            assert!(!params.outputs.is_empty());
-
-            let info_db = db_lookup(cid, MONEY_CONTRACT_INFO_TREE)?;
-            let nullifiers_db = db_lookup(cid, MONEY_CONTRACT_NULLIFIERS_TREE)?;
-            let coin_roots_db = db_lookup(cid, MONEY_CONTRACT_COIN_ROOTS_TREE)?;
-
-            let Some(faucet_pubkeys) = db_get(info_db, &serialize(&MONEY_CONTRACT_FAUCET_PUBKEYS))? else {
-                msg!("[Transfer] Error: Missing faucet pubkeys from info db");
-                return Err(ContractError::Internal);
-            };
-            let faucet_pubkeys: Vec<PublicKey> = deserialize(&faucet_pubkeys)?;
-
-            // Accumulator for the value commitments
-            let mut valcom_total = pallas::Point::identity();
-
-            // State transition for payments
-            msg!("[Transfer] Iterating over clear inputs");
-            for (i, input) in params.clear_inputs.iter().enumerate() {
-                let pk = input.signature_public;
-
-                if !faucet_pubkeys.contains(&pk) {
-                    msg!("[Transfer] Error: Clear input {} has invalid faucet pubkey", i);
-                    return Err(ContractError::Custom(20))
-                }
-
-                valcom_total += pedersen_commitment_u64(input.value, input.value_blind);
-            }
-
-            let mut new_nullifiers = Vec::with_capacity(params.inputs.len());
-
-            msg!("[Transfer] Iterating over anonymous inputs");
-            for (i, input) in params.inputs.iter().enumerate() {
-                // The Merkle root is used to know whether this is a coin that existed
-                // in a previous state.
-                if !db_contains_key(coin_roots_db, &serialize(&input.merkle_root))? {
-                    msg!("[Transfer] Error: Merkle root not found in previous state (input {})", i);
-                    return Err(ContractError::Custom(21))
-                }
-
-                // The nullifiers should not already exist. It is the double-spend protection.
-                if new_nullifiers.contains(&input.nullifier) ||
-                    db_contains_key(nullifiers_db, &serialize(&input.nullifier))?
-                {
-                    msg!("[Transfer] Error: Duplicate nullifier found in input {}", i);
-                    return Err(ContractError::Custom(22))
-                }
-
-                // Check the invoked contract if spend hook is set
-                if !bool::from(input.spend_hook.is_zero()) {
-                    let next_call_idx = call_idx + 1;
-                    if next_call_idx >= call.len() as u32 {
-                        msg!(
-                            "[Transfer] Error: next_call_idx = {} but len(calls) = {} in input {}",
-                            next_call_idx,
-                            call.len(),
-                            i
-                        );
-                        return Err(ContractError::Custom(23))
-                    }
-
-                    let next = &call[next_call_idx as usize];
-                    if next.contract_id.inner() != input.spend_hook {
-                        msg!(
-                            "[Transfer] Error: invoking contract call does not match spend hook\
-                             in input {}",
-                            i
-                        );
-                        return Err(ContractError::Custom(24))
-                    }
-                }
-
-                new_nullifiers.push(input.nullifier);
-                valcom_total += input.value_commit;
-            }
-
-            // Newly created coins for this transaction are in the outputs.
-            let mut new_coins = Vec::with_capacity(params.outputs.len());
-            for (i, output) in params.outputs.iter().enumerate() {
-                // TODO: Should we have coins in a sled tree too to check dupes?
-                if new_coins.contains(&Coin::from(output.coin)) {
-                    msg!("[Transfer] Error: Duplicate coin found in output {}", i);
-                    return Err(ContractError::Custom(25))
-                }
-
-                // FIXME: Needs some work on types and their place within all these libraries
-                new_coins.push(Coin::from(output.coin));
-                valcom_total -= output.value_commit;
-            }
-
-            // If the accumulator is not back in its initial state, there's a value mismatch.
-            if valcom_total != pallas::Point::identity() {
-                msg!("[Transfer] Error: Value commitments do not result in identity");
-                return Err(ContractError::Custom(26))
-            }
-
-            // Verify that the token commitments are all for the same token
-            let tokcom = params.outputs[0].token_commit;
-            let mut failed_tokcom = params.inputs.iter().any(|input| input.token_commit != tokcom);
-
-            failed_tokcom =
-                failed_tokcom || params.outputs.iter().any(|output| output.token_commit != tokcom);
-
-            failed_tokcom = failed_tokcom ||
-                params.clear_inputs.iter().any(|input| {
-                    pedersen_commitment_base(input.token_id.inner(), input.token_blind) != tokcom
-                });
-
-            if failed_tokcom {
-                msg!("[Transfer] Error: Token commitments do not match");
-                return Err(ContractError::Custom(25))
-            }
-
-            // Create a state update
-            let update = MoneyTransferUpdate { nullifiers: new_nullifiers, coins: new_coins };
-            let mut update_data = vec![];
-            update_data.write_u8(MoneyFunction::Transfer as u8)?;
-            update.encode(&mut update_data)?;
+            let update_data = money_transfer_process_instruction(cid, call_idx, calls)?;
             set_return_data(&update_data)?;
             msg!("[Transfer] State update set!");
-
             Ok(())
         }
 
         MoneyFunction::OtcSwap => {
             msg!("[OtcSwap] Entered match arm");
-            let params: MoneyTransferParams = deserialize(&self_.data[1..])?;
-
-            let nullifiers_db = db_lookup(cid, MONEY_CONTRACT_NULLIFIERS_TREE)?;
-            let coin_roots_db = db_lookup(cid, MONEY_CONTRACT_COIN_ROOTS_TREE)?;
-
-            // State transition for OTC swaps
-            // For now we enforce 2 inputs and 2 outputs, which means the coins
-            // must be available beforehand. We might want to change this and
-            // allow transactions including leftover change.
-            assert!(params.clear_inputs.is_empty());
-            assert!(params.inputs.len() == 2);
-            assert!(params.outputs.len() == 2);
-
-            let mut new_nullifiers = Vec::with_capacity(params.inputs.len());
-
-            // inputs[0] is being swapped to outputs[1]
-            // inputs[1] is being swapped to outputs[0]
-            // So that's how we check the value and token commitments
-            if params.inputs[0].value_commit != params.outputs[1].value_commit {
-                msg!("[OtcSwap] Error: Value commitments for input 0 and output 1 do not match");
-                return Err(ContractError::Custom(24))
-            }
-
-            if params.inputs[1].value_commit != params.outputs[0].value_commit {
-                msg!("[OtcSwap] Error: Value commitments for input 1 and output 0 do not match");
-                return Err(ContractError::Custom(24))
-            }
-
-            if params.inputs[0].token_commit != params.outputs[1].token_commit {
-                msg!("[OtcSwap] Error: Token commitments for input 0 and output 1 do not match");
-                return Err(ContractError::Custom(25))
-            }
-
-            if params.inputs[1].token_commit != params.outputs[0].token_commit {
-                msg!("[OtcSwap] Error: Token commitments for input 1 and output 0 do not match");
-                return Err(ContractError::Custom(25))
-            }
-
-            msg!("[OtcSwap] Iterating over anonymous inputs");
-            for (i, input) in params.inputs.iter().enumerate() {
-                // The Merkle root is used to know whether this is a coin that
-                // existed in a previous state.
-                if !db_contains_key(coin_roots_db, &serialize(&input.merkle_root))? {
-                    msg!("[OtcSwap] Error: Merkle root not found in previous state (input {})", i);
-                    return Err(ContractError::Custom(21))
-                }
-
-                // The nullifiers should not already exist. It is the double-spend protection.
-                if new_nullifiers.contains(&input.nullifier) ||
-                    db_contains_key(nullifiers_db, &serialize(&input.nullifier))?
-                {
-                    msg!("[OtcSwap] Error: Duplicate nullifier found in input {}", i);
-                    return Err(ContractError::Custom(22))
-                }
-
-                new_nullifiers.push(input.nullifier);
-            }
-
-            // Newly created coins for this transaction are in the outputs.
-            let mut new_coins = Vec::with_capacity(params.outputs.len());
-            for (i, output) in params.outputs.iter().enumerate() {
-                // TODO: Should we have coins in a sled tree too to check dupes?
-                if new_coins.contains(&Coin::from(output.coin)) {
-                    msg!("[OtcSwap] Error: Duplicate coin found in output {}", i);
-                    return Err(ContractError::Custom(23))
-                }
-
-                // FIXME: Needs some work on types and their place within all these libraries
-                new_coins.push(Coin::from(output.coin));
-            }
-
-            // Create a state update. We also use the MoneyTransferUpdate because they're
-            // essentially the same thing just with a different transition ruleset.
-            let update = MoneyTransferUpdate { nullifiers: new_nullifiers, coins: new_coins };
-            let mut update_data = vec![];
-            update_data.write_u8(MoneyFunction::OtcSwap as u8)?;
-            update.encode(&mut update_data)?;
+            let update_data = money_otcswap_process_instruction(cid, call_idx, calls)?;
             set_return_data(&update_data)?;
             msg!("[OtcSwap] State update set!");
-
             Ok(())
         }
 
@@ -766,7 +535,10 @@ fn process_instruction(cid: ContractId, ix: &[u8]) -> ContractResult {
 
         MoneyFunction::Mint => {
             msg!("[Mint] Entered match arm");
-            unimplemented!();
+            let update_data = money_mint_process_instruction(cid, call_idx, calls)?;
+            set_return_data(&update_data)?;
+            msg!("[Mint] State update set!");
+            Ok(())
         }
 
         MoneyFunction::Freeze => {
@@ -779,26 +551,15 @@ fn process_instruction(cid: ContractId, ix: &[u8]) -> ContractResult {
 #[cfg(not(feature = "no-entrypoint"))]
 fn process_update(cid: ContractId, update_data: &[u8]) -> ContractResult {
     match MoneyFunction::try_from(update_data[0])? {
-        MoneyFunction::Transfer | MoneyFunction::OtcSwap => {
+        MoneyFunction::Transfer => {
             let update: MoneyTransferUpdate = deserialize(&update_data[1..])?;
+            money_transfer_process_update(cid, update)?;
+            Ok(())
+        }
 
-            let info_db = db_lookup(cid, MONEY_CONTRACT_INFO_TREE)?;
-            let nullifiers_db = db_lookup(cid, MONEY_CONTRACT_NULLIFIERS_TREE)?;
-            let coin_roots_db = db_lookup(cid, MONEY_CONTRACT_COIN_ROOTS_TREE)?;
-
-            for nullifier in update.nullifiers {
-                db_set(nullifiers_db, &serialize(&nullifier), &[])?;
-            }
-
-            msg!("Adding coins {:?} to Merkle tree", update.coins);
-            let coins: Vec<_> = update.coins.iter().map(|x| MerkleNode::from(x.inner())).collect();
-            merkle_add(
-                info_db,
-                coin_roots_db,
-                &serialize(&MONEY_CONTRACT_COIN_MERKLE_TREE),
-                &coins,
-            )?;
-
+        MoneyFunction::OtcSwap => {
+            let update: MoneyTransferUpdate = deserialize(&update_data[1..])?;
+            money_otcswap_process_update(cid, update)?;
             Ok(())
         }
 
@@ -826,8 +587,9 @@ fn process_update(cid: ContractId, update_data: &[u8]) -> ContractResult {
         }
 
         MoneyFunction::Mint => {
-            msg!("[Mint] Entered match arm");
-            unimplemented!();
+            let update: MoneyMintUpdate = deserialize(&update_data[1..])?;
+            money_mint_process_update(cid, update)?;
+            Ok(())
         }
 
         MoneyFunction::Freeze => {

+ 145 - 0
src/contract/money/src/mint.rs

@@ -0,0 +1,145 @@
+/* This file is part of DarkFi (https://dark.fi)
+ *
+ * Copyright (C) 2020-2023 Dyne.org foundation
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use darkfi_sdk::{
+    crypto::{
+        pasta_prelude::*, pedersen_commitment_base, pedersen_commitment_u64, poseidon_hash, Coin,
+        ContractId, MerkleNode, PublicKey, TokenId,
+    },
+    db::{db_contains_key, db_lookup},
+    error::ContractError,
+    merkle_add, msg,
+    pasta::pallas,
+    ContractCall,
+};
+use darkfi_serial::{deserialize, serialize, Encodable, WriteExt};
+
+use crate::{
+    MoneyFunction, MoneyMintParams, MoneyMintUpdate, MONEY_CONTRACT_COIN_MERKLE_TREE,
+    MONEY_CONTRACT_COIN_ROOTS_TREE, MONEY_CONTRACT_INFO_TREE, MONEY_CONTRACT_TOKEN_FREEZE_TREE,
+    MONEY_CONTRACT_ZKAS_TOKEN_MINT_NS_V1,
+};
+
+pub fn money_mint_get_metadata(
+    _cid: ContractId,
+    call_idx: u32,
+    calls: Vec<ContractCall>,
+) -> Result<Vec<u8>, ContractError> {
+    let self_ = &calls[call_idx as usize];
+    let params: MoneyMintParams = deserialize(&self_.data[1..])?;
+
+    let mut zk_public_values: Vec<(String, Vec<pallas::Base>)> = vec![];
+    let mut signature_pubkeys: Vec<PublicKey> = vec![];
+
+    signature_pubkeys.push(params.input.signature_public);
+
+    let value_coords = params.output.value_commit.to_affine().coordinates().unwrap();
+    let token_coords = params.output.token_commit.to_affine().coordinates().unwrap();
+
+    let (sig_x, sig_y) = params.input.signature_public.xy();
+    let token_id = poseidon_hash([sig_x, sig_y]);
+
+    zk_public_values.push((
+        MONEY_CONTRACT_ZKAS_TOKEN_MINT_NS_V1.to_string(),
+        vec![
+            sig_x,
+            sig_y,
+            token_id,
+            params.output.coin,
+            *value_coords.x(),
+            *value_coords.y(),
+            *token_coords.x(),
+            *token_coords.y(),
+        ],
+    ));
+
+    let mut metadata = vec![];
+    zk_public_values.encode(&mut metadata)?;
+    signature_pubkeys.encode(&mut metadata)?;
+
+    Ok(metadata)
+}
+
+pub fn money_mint_process_instruction(
+    cid: ContractId,
+    call_idx: u32,
+    calls: Vec<ContractCall>,
+) -> Result<Vec<u8>, ContractError> {
+    let self_ = &calls[call_idx as usize];
+    let params: MoneyMintParams = deserialize(&self_.data[1..])?;
+
+    //let info_db = db_lookup(cid, MONEY_CONTRACT_INFO_TREE)?;
+    //let coin_roots_db = db_lookup(cid, MONEY_CONTRACT_COIN_ROOTS_TREE)?;
+    let token_freeze_db = db_lookup(cid, MONEY_CONTRACT_TOKEN_FREEZE_TREE)?;
+
+    // Check that the signature public key is actually the token ID
+    let (mint_x, mint_y) = params.input.signature_public.xy();
+    let token_id = TokenId::from(poseidon_hash([mint_x, mint_y]));
+    if token_id != params.input.token_id {
+        msg!("[Mint] Token ID does not derive from mint authority");
+        return Err(ContractError::Custom(18))
+    }
+
+    // Check that the mint is not frozen
+    if db_contains_key(token_freeze_db, &serialize(&token_id))? {
+        msg!("[Mint] Error: The mint for token {} is frozen", token_id);
+        return Err(ContractError::Custom(19))
+    }
+
+    // TODO: Check that the new coin did not exist before. We should
+    //       probably have a sled tree of all coins ever in order to
+    //       assert against duplicates.
+
+    // Verify that the value and token commitments match
+    if pedersen_commitment_u64(params.input.value, params.input.value_blind) !=
+        params.output.value_commit
+    {
+        msg!("[Mint] Error: Value commitments do not match");
+        return Err(ContractError::Custom(10))
+    }
+
+    if pedersen_commitment_base(params.input.token_id.inner(), params.input.token_blind) !=
+        params.output.token_commit
+    {
+        msg!("[Mint] Error: Token commitments do not match");
+        return Err(ContractError::Custom(11))
+    }
+
+    // Create a state update. We only need the new coin.
+    let update = MoneyMintUpdate { coin: Coin::from(params.output.coin) };
+    let mut update_data = vec![];
+    update_data.write_u8(MoneyFunction::Mint as u8)?;
+    update.encode(&mut update_data)?;
+
+    Ok(update_data)
+}
+
+pub fn money_mint_process_update(
+    cid: ContractId,
+    update: MoneyMintUpdate,
+) -> Result<(), ContractError> {
+    let info_db = db_lookup(cid, MONEY_CONTRACT_INFO_TREE)?;
+    let coin_roots_db = db_lookup(cid, MONEY_CONTRACT_COIN_ROOTS_TREE)?;
+
+    let coins = vec![MerkleNode::from(update.coin.inner())];
+
+    msg!("[Mint] Adding new coin to Merkle tree");
+    merkle_add(info_db, coin_roots_db, &serialize(&MONEY_CONTRACT_COIN_MERKLE_TREE), &coins)?;
+
+    Ok(())
+}

+ 11 - 0
src/contract/money/src/model.rs

@@ -21,6 +21,17 @@ use darkfi_sdk::crypto::{
 };
 use darkfi_serial::{SerialDecodable, SerialEncodable};
 
+#[derive(Clone, Debug, SerialEncodable, SerialDecodable)]
+pub struct MoneyMintParams {
+    pub input: ClearInput,
+    pub output: Output,
+}
+
+#[derive(Clone, Debug, SerialEncodable, SerialDecodable)]
+pub struct MoneyMintUpdate {
+    pub coin: Coin,
+}
+
 /// Inputs and outputs for staking coins
 #[derive(Clone, Debug, SerialEncodable, SerialDecodable)]
 pub struct MoneyStakeParams {

+ 150 - 0
src/contract/money/src/swap.rs

@@ -0,0 +1,150 @@
+/* This file is part of DarkFi (https://dark.fi)
+ *
+ * Copyright (C) 2020-2023 Dyne.org foundation
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use darkfi_sdk::{
+    crypto::{Coin, ContractId},
+    db::{db_contains_key, db_lookup},
+    error::ContractError,
+    msg,
+    pasta::pallas,
+    ContractCall,
+};
+use darkfi_serial::{deserialize, serialize, Encodable, WriteExt};
+
+use crate::{
+    money_transfer_get_metadata, money_transfer_process_update, MoneyFunction, MoneyTransferParams,
+    MoneyTransferUpdate, MONEY_CONTRACT_COIN_ROOTS_TREE, MONEY_CONTRACT_NULLIFIERS_TREE,
+};
+
+pub fn money_otcswap_get_metadata(
+    cid: ContractId,
+    call_idx: u32,
+    calls: Vec<ContractCall>,
+) -> Result<Vec<u8>, ContractError> {
+    Ok(money_transfer_get_metadata(cid, call_idx, calls)?)
+}
+
+pub fn money_otcswap_process_instruction(
+    cid: ContractId,
+    call_idx: u32,
+    calls: Vec<ContractCall>,
+) -> Result<Vec<u8>, ContractError> {
+    let self_ = &calls[call_idx as usize];
+    let params: MoneyTransferParams = deserialize(&self_.data[1..])?;
+
+    // State transition for OTC atomic swaps.
+    // We enforce 2 inputs and 2 outputs so every atomic swap looks the same.
+    if !params.clear_inputs.is_empty() {
+        msg!("[OtcSwap] Error: Clear inputs are not empty");
+        return Err(ContractError::Custom(12))
+    }
+
+    if params.inputs.len() != 2 {
+        msg!("[OtcSwap] Error: Expected 2 inputs");
+        return Err(ContractError::Custom(13))
+    }
+
+    if params.outputs.len() != 2 {
+        msg!("[OtcSwap] Error: Expected 2 outputs");
+        return Err(ContractError::Custom(14))
+    }
+
+    let nullifiers_db = db_lookup(cid, MONEY_CONTRACT_NULLIFIERS_TREE)?;
+    let coin_roots_db = db_lookup(cid, MONEY_CONTRACT_COIN_ROOTS_TREE)?;
+
+    let mut new_nullifiers = Vec::with_capacity(2);
+
+    // inputs[0] is being swapped to outputs[1]
+    // inputs[1] is being swapped to outputs[0]
+    // So that's how we check the value and token commitments
+    if params.inputs[0].value_commit != params.outputs[1].value_commit {
+        msg!("[OtcSwap] Error: Value commitments for input 0 and output 1 do not match");
+        return Err(ContractError::Custom(10))
+    }
+
+    if params.inputs[1].value_commit != params.outputs[0].value_commit {
+        msg!("[OtcSwap] Error: Value commitments for input 1 and output 0 do not match");
+        return Err(ContractError::Custom(10))
+    }
+
+    if params.inputs[0].token_commit != params.outputs[1].token_commit {
+        msg!("[OtcSwap] Error: Token commitments for input 0 and output 1 do not match");
+        return Err(ContractError::Custom(11))
+    }
+
+    if params.inputs[1].token_commit != params.outputs[0].token_commit {
+        msg!("[OtcSwap] Error: Token commitments for input 1 and output 0 do not match");
+        return Err(ContractError::Custom(11))
+    }
+
+    msg!("[OtcSwap] Iternating over anonymous inputs");
+    for (i, input) in params.inputs.iter().enumerate() {
+        // For now, make sure that the inputs' spend hooks are zero.
+        // This should however be allowed to some extent.
+        if input.spend_hook != pallas::Base::zero() {
+            msg!("[OtcSwap] Error: Unable to swap coins with spend_hook != 0 (input {})", i);
+            return Err(ContractError::Custom(17))
+        }
+
+        // The Merkle root is used to know whether this coin existed
+        // in a previous state.
+        if !db_contains_key(coin_roots_db, &serialize(&input.merkle_root))? {
+            msg!("[OtcSwap] Error: Merkle root not found in previous state (input {})", i);
+            return Err(ContractError::Custom(5))
+        }
+
+        // The nullifiers should not already exist. It is the double-spend protection.
+        if new_nullifiers.contains(&input.nullifier) ||
+            db_contains_key(nullifiers_db, &serialize(&input.nullifier))?
+        {
+            msg!("[OtcSwap] Error: Duplicate nullifier found in input {}", i);
+            return Err(ContractError::Custom(6))
+        }
+
+        new_nullifiers.push(input.nullifier);
+    }
+
+    // Newly created coins for this transaction are in the outputs.
+    let mut new_coins = Vec::with_capacity(2);
+    for (i, output) in params.outputs.iter().enumerate() {
+        // TODO: Coins should exist in a sled tree in order to check dupes.
+        if new_coins.contains(&Coin::from(output.coin)) {
+            msg!("[OtcSwap] Error: Duplicate coin found in output {}", i);
+            return Err(ContractError::Custom(9))
+        }
+
+        new_coins.push(Coin::from(output.coin));
+    }
+
+    // Create a state update. We also use the `MoneyTransferUpdate` because
+    // they're essentially the same thing, just with a different transition
+    // rule set.
+    let update = MoneyTransferUpdate { nullifiers: new_nullifiers, coins: new_coins };
+    let mut update_data = vec![];
+    update_data.write_u8(MoneyFunction::OtcSwap as u8)?;
+    update.encode(&mut update_data)?;
+
+    Ok(update_data)
+}
+
+pub fn money_otcswap_process_update(
+    cid: ContractId,
+    update: MoneyTransferUpdate,
+) -> Result<(), ContractError> {
+    Ok(money_transfer_process_update(cid, update)?)
+}

+ 251 - 0
src/contract/money/src/transfer.rs

@@ -0,0 +1,251 @@
+/* This file is part of DarkFi (https://dark.fi)
+ *
+ * Copyright (C) 2020-2023 Dyne.org foundation
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use darkfi_sdk::{
+    crypto::{
+        pasta_prelude::*, pedersen_commitment_base, pedersen_commitment_u64, Coin, ContractId,
+        MerkleNode, PublicKey, DARK_TOKEN_ID,
+    },
+    db::{db_contains_key, db_get, db_lookup, db_set},
+    error::ContractError,
+    merkle_add, msg,
+    pasta::pallas,
+    ContractCall,
+};
+use darkfi_serial::{deserialize, serialize, Encodable, WriteExt};
+
+use crate::{
+    MoneyFunction, MoneyTransferParams, MoneyTransferUpdate, MONEY_CONTRACT_COIN_MERKLE_TREE,
+    MONEY_CONTRACT_COIN_ROOTS_TREE, MONEY_CONTRACT_FAUCET_PUBKEYS, MONEY_CONTRACT_INFO_TREE,
+    MONEY_CONTRACT_NULLIFIERS_TREE, MONEY_CONTRACT_ZKAS_BURN_NS_V1, MONEY_CONTRACT_ZKAS_MINT_NS_V1,
+};
+
+pub fn money_transfer_get_metadata(
+    _cid: ContractId,
+    call_idx: u32,
+    calls: Vec<ContractCall>,
+) -> Result<Vec<u8>, ContractError> {
+    let self_ = &calls[call_idx as usize];
+    let params: MoneyTransferParams = deserialize(&self_.data[1..])?;
+
+    let mut zk_public_values: Vec<(String, Vec<pallas::Base>)> = vec![];
+    let mut signature_pubkeys: Vec<PublicKey> = vec![];
+
+    for input in &params.clear_inputs {
+        signature_pubkeys.push(input.signature_public);
+    }
+
+    for input in &params.inputs {
+        let value_coords = input.value_commit.to_affine().coordinates().unwrap();
+        let token_coords = input.token_commit.to_affine().coordinates().unwrap();
+        let (sig_x, sig_y) = input.signature_public.xy();
+
+        zk_public_values.push((
+            MONEY_CONTRACT_ZKAS_BURN_NS_V1.to_string(),
+            vec![
+                input.nullifier.inner(),
+                *value_coords.x(),
+                *value_coords.y(),
+                *token_coords.x(),
+                *token_coords.y(),
+                input.merkle_root.inner(),
+                input.user_data_enc,
+                sig_x,
+                sig_y,
+            ],
+        ));
+
+        signature_pubkeys.push(input.signature_public);
+    }
+
+    for output in &params.outputs {
+        let value_coords = output.value_commit.to_affine().coordinates().unwrap();
+        let token_coords = output.token_commit.to_affine().coordinates().unwrap();
+
+        zk_public_values.push((
+            MONEY_CONTRACT_ZKAS_MINT_NS_V1.to_string(),
+            vec![
+                output.coin,
+                *value_coords.x(),
+                *value_coords.y(),
+                *token_coords.x(),
+                *token_coords.y(),
+            ],
+        ));
+    }
+
+    let mut metadata = vec![];
+    zk_public_values.encode(&mut metadata)?;
+    signature_pubkeys.encode(&mut metadata)?;
+
+    Ok(metadata)
+}
+
+pub fn money_transfer_process_instruction(
+    cid: ContractId,
+    call_idx: u32,
+    calls: Vec<ContractCall>,
+) -> Result<Vec<u8>, ContractError> {
+    let self_ = &calls[call_idx as usize];
+    let params: MoneyTransferParams = deserialize(&self_.data[1..])?;
+
+    if params.clear_inputs.len() + params.inputs.len() < 1 {
+        msg!("[Transfer] Error: No inputs in the call");
+        return Err(ContractError::Custom(1))
+    }
+
+    if params.outputs.is_empty() {
+        msg!("[Transfer] Error: No outputs in the call");
+        return Err(ContractError::Custom(2))
+    }
+
+    let info_db = db_lookup(cid, MONEY_CONTRACT_INFO_TREE)?;
+    let nullifiers_db = db_lookup(cid, MONEY_CONTRACT_NULLIFIERS_TREE)?;
+    let coin_roots_db = db_lookup(cid, MONEY_CONTRACT_COIN_ROOTS_TREE)?;
+
+    let Some(faucet_pubkeys) = db_get(info_db, &serialize(&MONEY_CONTRACT_FAUCET_PUBKEYS))? else {
+        msg!("[Transfer] Error: Missing faucet pubkeys from info db");
+        return Err(ContractError::Internal)
+    };
+    let faucet_pubkeys: Vec<PublicKey> = deserialize(&faucet_pubkeys)?;
+
+    // Accumulator for the value commitments
+    let mut valcom_total = pallas::Point::identity();
+
+    // State transition for payments
+    msg!("[Transfer] Iterating over clear inputs");
+    for (i, input) in params.clear_inputs.iter().enumerate() {
+        if input.token_id != *DARK_TOKEN_ID {
+            msg!("[Transfer] Error: Clear input {} used non-native token", i);
+            return Err(ContractError::Custom(3))
+        }
+
+        if !faucet_pubkeys.contains(&input.signature_public) {
+            msg!("[Transfer] Error: Clear input {} used unauthorised pubkey", i);
+            return Err(ContractError::Custom(4))
+        }
+
+        valcom_total += pedersen_commitment_u64(input.value, input.value_blind);
+    }
+
+    let mut new_nullifiers = Vec::with_capacity(params.inputs.len());
+    msg!("[Transfer] Iterating over anonymous inputs");
+    for (i, input) in params.inputs.iter().enumerate() {
+        // The Merkle root is used to know whether this is a coin that
+        // existed in a previous state.
+        if !db_contains_key(coin_roots_db, &serialize(&input.merkle_root))? {
+            msg!("[Transfer] Error: Merkle root not found in previous state (input {})", i);
+            return Err(ContractError::Custom(5))
+        }
+
+        // The nullifiers should not already exist. It is the double-spend protection.
+        if new_nullifiers.contains(&input.nullifier) ||
+            db_contains_key(nullifiers_db, &serialize(&input.nullifier))?
+        {
+            msg!("[Transfer] Error: Duplicate nullifier found in input {}", i);
+            return Err(ContractError::Custom(6))
+        }
+
+        // Check the invoked contract if spend hook is set
+        if input.spend_hook != pallas::Base::zero() {
+            let next_call_idx = call_idx + 1;
+            if next_call_idx >= calls.len() as u32 {
+                msg!(
+                    "[Transfer] Error: next_call_idx={} but len(calls)={} (input {})",
+                    next_call_idx,
+                    calls.len(),
+                    i
+                );
+                return Err(ContractError::Custom(7))
+            }
+
+            let next = &calls[next_call_idx as usize];
+            if next.contract_id.inner() != input.spend_hook {
+                msg!("[Transfer] Error: Invoking contract call does not match spend hook in input {}", i);
+                return Err(ContractError::Custom(8))
+            }
+        }
+
+        new_nullifiers.push(input.nullifier);
+        valcom_total += input.value_commit;
+    }
+
+    // Newly created coins for this transaction are in the outputs.
+    let mut new_coins = Vec::with_capacity(params.outputs.len());
+    for (i, output) in params.outputs.iter().enumerate() {
+        // TODO: Coins should exist in a sled tree in order to check dupes.
+        if new_coins.contains(&Coin::from(output.coin)) {
+            msg!("[Transfer] Error: Duplicate coin found in output {}", i);
+            return Err(ContractError::Custom(9))
+        }
+
+        // FIXME: Needs some work on types and their place within all these libraries
+        new_coins.push(Coin::from(output.coin));
+        valcom_total -= output.value_commit;
+    }
+
+    // If the accumulator is not back in its initial state, there's a value mismatch.
+    if valcom_total != pallas::Point::identity() {
+        msg!("[Transfer] Error: Value commitments do not result in identity");
+        return Err(ContractError::Custom(10))
+    }
+
+    // Verify that the token commitments are all for the same token
+    let tokcom = params.outputs[0].token_commit;
+    let mut failed_tokcom = params.inputs.iter().any(|input| input.token_commit != tokcom);
+    failed_tokcom =
+        failed_tokcom || params.outputs.iter().any(|output| output.token_commit != tokcom);
+    failed_tokcom = failed_tokcom ||
+        params.clear_inputs.iter().any(|input| {
+            pedersen_commitment_base(input.token_id.inner(), input.token_blind) != tokcom
+        });
+
+    if failed_tokcom {
+        msg!("[Transfer] Error: Token commitments do not match");
+        return Err(ContractError::Custom(11))
+    }
+
+    // Create a state update
+    let update = MoneyTransferUpdate { nullifiers: new_nullifiers, coins: new_coins };
+    let mut update_data = vec![];
+    update_data.write_u8(MoneyFunction::Transfer as u8)?;
+    update.encode(&mut update_data)?;
+
+    Ok(update_data)
+}
+
+pub fn money_transfer_process_update(
+    cid: ContractId,
+    update: MoneyTransferUpdate,
+) -> Result<(), ContractError> {
+    let info_db = db_lookup(cid, MONEY_CONTRACT_INFO_TREE)?;
+    let nullifiers_db = db_lookup(cid, MONEY_CONTRACT_NULLIFIERS_TREE)?;
+    let coin_roots_db = db_lookup(cid, MONEY_CONTRACT_COIN_ROOTS_TREE)?;
+
+    msg!("[Transfer] Adding new nullifiers to the set");
+    for nullifier in update.nullifiers {
+        db_set(nullifiers_db, &serialize(&nullifier), &[])?;
+    }
+
+    let coins: Vec<_> = update.coins.iter().map(|x| MerkleNode::from(x.inner())).collect();
+
+    msg!("[Transfer] Adding new coins to Merkle tree");
+    merkle_add(info_db, coin_roots_db, &serialize(&MONEY_CONTRACT_COIN_MERKLE_TREE), &coins)?;
+
+    Ok(())
+}