Money Contract Source Code

BZHKGQ26bzmBithTQYTJtjo2QdCqpkR9tjSBopT4yf4o
entrypoint/swap_v1.rs
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
/* This file is part of DarkFi (https://dark.fi)
 *
 * Copyright (C) 2020-2025 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::*,
        smt::{
            wasmdb::{SmtWasmDbStorage, SmtWasmFp},
            PoseidonFp, EMPTY_NODES_FP,
        },
        ContractId,
    },
    dark_tree::DarkLeaf,
    error::{ContractError, ContractResult},
    msg,
    pasta::pallas,
    wasm, ContractCall,
};
use darkfi_serial::{deserialize, serialize};

use super::transfer_v1::{money_transfer_get_metadata_v1, money_transfer_process_update_v1};
use crate::{
    error::MoneyError,
    model::{MoneyTransferParamsV1, MoneyTransferUpdateV1},
    MONEY_CONTRACT_COINS_TREE, MONEY_CONTRACT_COIN_ROOTS_TREE, MONEY_CONTRACT_NULLIFIERS_TREE,
};

/// `get_metadata` function for `Money::OtcSwapV1`
pub(crate) fn money_otcswap_get_metadata_v1(
    cid: ContractId,
    call_idx: usize,
    calls: Vec<DarkLeaf<ContractCall>>,
) -> Result<Vec<u8>, ContractError> {
    // In here we can use the same function as we use in `TransferV1`.
    money_transfer_get_metadata_v1(cid, call_idx, calls)
}

/// `process_instruction` function for `Money::OtcSwapV1`
pub(crate) fn money_otcswap_process_instruction_v1(
    cid: ContractId,
    call_idx: usize,
    calls: Vec<DarkLeaf<ContractCall>>,
) -> Result<Vec<u8>, ContractError> {
    let self_ = &calls[call_idx].data;
    let params: MoneyTransferParamsV1 = deserialize(&self_.data[1..])?;

    // The atomic swap is able to use the same parameters as `TransferV1`.
    // In here we just have a different state transition where we enforce
    // 2 anonymous inputs and 2 anonymous outputs. This is enforced so that
    // every atomic swap looks the same on the network, therefore there is
    // no special anonymity leak for different swaps that are being done,
    // at least in the scope of this contract call.
    if params.inputs.len() != 2 {
        msg!("[OtcSwapV1] Error: Expected 2 inputs");
        return Err(MoneyError::InvalidNumberOfInputs.into())
    }

    if params.outputs.len() != 2 {
        msg!("[OtcSwapV1] Error: Expected 2 outputs");
        return Err(MoneyError::InvalidNumberOfOutputs.into())
    }

    // Grab the db handles we'll be using here
    let coins_db = wasm::db::db_lookup(cid, MONEY_CONTRACT_COINS_TREE)?;
    let nullifiers_db = wasm::db::db_lookup(cid, MONEY_CONTRACT_NULLIFIERS_TREE)?;
    let coin_roots_db = wasm::db::db_lookup(cid, MONEY_CONTRACT_COIN_ROOTS_TREE)?;

    // We expect two new nullifiers and two new coins
    let mut new_nullifiers = Vec::with_capacity(2);
    let mut new_coins = 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!("[OtcSwapV1] Error: Value commitments for input 0 and output 1 mismatch");
        return Err(MoneyError::ValueMismatch.into())
    }

    if params.inputs[1].value_commit != params.outputs[0].value_commit {
        msg!("[OtcSwapV1] Error: Value commitments for input 1 and ouptut 0 mismatch");
        return Err(MoneyError::ValueMismatch.into())
    }

    if params.inputs[0].token_commit != params.outputs[1].token_commit {
        msg!("[OtcSwapV1] Error: Token commitments for input 0 and output 1 mismatch");
        return Err(MoneyError::TokenMismatch.into())
    }

    if params.inputs[1].token_commit != params.outputs[0].token_commit {
        msg!("[OtcSwapV1] Error: Token commitments for input 1 and output 0 mismatch");
        return Err(MoneyError::TokenMismatch.into())
    }

    let hasher = PoseidonFp::new();
    let empty_leaf = pallas::Base::ZERO;
    let smt_store = SmtWasmDbStorage::new(nullifiers_db);
    let smt = SmtWasmFp::new(smt_store, hasher, &EMPTY_NODES_FP);

    msg!("[OtcSwapV1] Iterating over anonymous inputs");
    for (i, input) in params.inputs.iter().enumerate() {
        // The Merkle root is used to know whether this coin
        // has existed in a previous state.
        if !wasm::db::db_contains_key(coin_roots_db, &serialize(&input.merkle_root))? {
            msg!("[OtcSwapV1] Error: Merkle root not found in previous state (input {})", i);
            return Err(MoneyError::SwapMerkleRootNotFound.into())
        }

        // The nullifiers should not already exist. It is the double-spend protection.
        if new_nullifiers.contains(&input.nullifier) ||
            smt.get_leaf(&input.nullifier.inner()) != empty_leaf
        {
            msg!("[OtcSwapV1] Error: Duplicate nullifier found in input {}", i);
            return Err(MoneyError::DuplicateNullifier.into())
        }

        new_nullifiers.push(input.nullifier);
    }

    // Newly created coins for this call are in the outputs
    for (i, output) in params.outputs.iter().enumerate() {
        if new_coins.contains(&output.coin) ||
            wasm::db::db_contains_key(coins_db, &serialize(&output.coin))?
        {
            msg!("[OtcSwapV1] Error: Duplicate coin found in output {}", i);
            return Err(MoneyError::DuplicateCoin.into())
        }

        new_coins.push(output.coin);
    }

    // Create a state update. We also use `MoneyTransferUpdateV1` because
    // they're essentially the same thing, just with a different transition
    // ruleset.
    let update = MoneyTransferUpdateV1 { nullifiers: new_nullifiers, coins: new_coins };
    Ok(serialize(&update))
}

/// `process_update` function for `Money::OtcSwapV1`
pub(crate) fn money_otcswap_process_update_v1(
    cid: ContractId,
    update: MoneyTransferUpdateV1,
) -> ContractResult {
    // In here we can use the same function as we use in `TransferV1`.
    money_transfer_process_update_v1(cid, update)
}