smplx-std 0.0.5

A blazingly-fast, ux-first simplicity development framework
Documentation
/*
 * Options
 * 
 * Important: Currently only the LBTC collateral is supported.
 *
 * Based on the https://blockstream.com/assets/downloads/pdf/options-whitepaper.pdf
 *
 * This contract implements cash-settled European-style options using covenant-locked collateral.
 *
 * Room for optimization:
 * - https://github.com/BlockstreamResearch/simplicity-contracts/issues/2 (Use input asset to determine option covenent type)
 * - https://github.com/BlockstreamResearch/simplicity-contracts/issues/3 (Simplify match token_branch in funding_path.)
 * - https://github.com/BlockstreamResearch/simplicity-contracts/issues/4 (why batching is hard to implement)
 * - https://github.com/BlockstreamResearch/simplicity-contracts/issues/5 (Reduce Contract Parameters)
 * - https://github.com/BlockstreamResearch/simplicity-contracts/issues/21 (explains why funding is limited)
 */

/// Assert: a == b * expected_q, via divmod
fn divmod_eq(a: u64, b: u64, expected_q: u64) {   
    let (q, r): (u64, u64) = jet::div_mod_64(a, b);
    assert!(jet::eq_64(q, expected_q));
    assert!(jet::eq_64(r, 0));
}

fn get_output_script_hash(index: u32) -> u256 {
    unwrap(jet::output_script_hash(index))
}

fn get_input_script_hash(index: u32) -> u256 {
    unwrap(jet::input_script_hash(index))
}

fn get_output_explicit_asset_amount(index: u32) -> (u256, u64) {
    let pair: (Asset1, Amount1) = unwrap(jet::output_amount(index));
    let (asset, amount): (Asset1, Amount1) = pair;
    let asset_bits: u256 = unwrap_right::<(u1, u256)>(asset);
    let amount: u64 = unwrap_right::<(u1, u256)>(amount);
    (asset_bits, amount)
}

fn get_input_explicit_asset_amount(index: u32) -> (u256, u64) {
    let pair: (Asset1, Amount1) = unwrap(jet::input_amount(index));
    let (asset, amount): (Asset1, Amount1) = pair;
    let asset_bits: u256 = unwrap_right::<(u1, u256)>(asset);
    let amount: u64 = unwrap_right::<(u1, u256)>(amount);
    (asset_bits, amount)
}

fn ensure_one_bit(bit: bool) { assert!(jet::eq_1(<bool>::into(bit), 1)); }
fn ensure_zero_bit(bit: bool) { assert!(jet::eq_1(<bool>::into(bit), 0)); }

fn increment_by(index: u32, amount: u32) -> u32 {
    let (carry, result): (bool, u32) = jet::add_32(index, amount);
    ensure_zero_bit(carry);
    result
}

fn ensure_input_and_output_script_hash_eq(index: u32) {
    assert!(jet::eq_256(unwrap(jet::input_script_hash(index)), unwrap(jet::output_script_hash(index))));
}

fn ensure_output_is_op_return(index: u32) {
    match jet::output_null_datum(index, 0) {
        Some(entry: Option<Either<(u2, u256), Either<u1, u4>>>) => (),
        None => panic!(),
    }
}

fn ensure_input_asset_eq(index: u32, expected_bits: u256) {
    let asset: Asset1 = unwrap(jet::input_asset(index));
    let asset_bits: u256 = unwrap_right::<(u1, u256)>(asset);
    assert!(jet::eq_256(asset_bits, expected_bits));
}

fn ensure_output_asset_eq(index: u32, expected_bits: u256) {
    let asset: Asset1 = unwrap(jet::output_asset(index));
    let asset_bits: u256 = unwrap_right::<(u1, u256)>(asset);
    assert!(jet::eq_256(asset_bits, expected_bits));
}

fn ensure_output_asset_with_amount_eq(index: u32, expected_bits: u256, expected_amount: u64) {
    let (asset, amount): (u256, u64) = dbg!(get_output_explicit_asset_amount(index));
    assert!(jet::eq_256(asset, expected_bits));
    assert!(jet::eq_64(amount, expected_amount));
}

fn ensure_input_script_hash_eq(index: u32, expected: u256) {
    assert!(jet::eq_256(unwrap(jet::input_script_hash(index)), expected));
}

fn ensure_output_script_hash_eq(index: u32, expected: u256) {
    assert!(jet::eq_256(unwrap(jet::output_script_hash(index)), expected));
}

fn ensure_correct_change_at_index(index: u32, asset_id: u256, asset_amount_to_spend: u64, contract_script_hash: u256, is_change_needed: bool) {
    let (asset_bits, available_asset_amount): (u256, u64) = get_input_explicit_asset_amount(index);
    assert!(jet::eq_256(unwrap(jet::input_script_hash(index)), contract_script_hash));
    assert!(jet::eq_32(jet::current_index(), index));
    
    match is_change_needed {
        true => {
            ensure_input_and_output_script_hash_eq(index);

            let (carry, collateral_change): (bool, u64) = jet::subtract_64(available_asset_amount, asset_amount_to_spend);
            ensure_zero_bit(carry);
            ensure_output_asset_with_amount_eq(index, asset_id, collateral_change);
        },
        false => assert!(jet::eq_64(asset_amount_to_spend, available_asset_amount)),
    }
}

fn check_y(expected_y: Fe, actual_y: Fe) {
    match jet::eq_256(expected_y, actual_y) {
        true => {},
        false => {
            assert!(jet::eq_256(expected_y, jet::fe_negate(actual_y)));
        }
    };
}

fn ensure_input_and_output_reissuance_token_eq(index: u32) {
    let (input_asset, input_amount): (Asset1, Amount1) = unwrap(jet::input_amount(index));
    let (output_asset, output_amount): (Asset1, Amount1) = unwrap(jet::output_amount(index));

    match (input_asset) {
        Left(in_conf: Point) => {
            let (input_asset_parity, input_asset_x): (u1, u256) = in_conf;
            let (output_asset_parity, output_asset_x): (u1, u256) = unwrap_left::<u256>(output_asset);
            
            assert!(jet::eq_1(input_asset_parity, output_asset_parity));
            assert!(jet::eq_256(input_asset_x, output_asset_x));
        },
        Right(in_expl: u256) => {
            let out_expl: u256 = unwrap_right::<Point>(output_asset);
            assert!(jet::eq_256(in_expl, out_expl));
        }
    };

    match (input_amount) {
        Left(in_conf: Point) => {
            let (input_amount_parity, input_amount_x): (u1, u256) = in_conf;
            let (output_amount_parity, output_amount_x): (u1, u256) = unwrap_left::<u64>(output_amount);

            assert!(jet::eq_1(input_amount_parity, output_amount_parity));
            assert!(jet::eq_256(input_amount_x, output_amount_x));
        },
        Right(in_expl: u64) => {
            let out_expl: u64 = unwrap_right::<Point>(output_amount);
            assert!(jet::eq_64(in_expl, out_expl));
        }
    };
}

// Verify that a reissuance token commitment matches the expected token ID using provided blinding factors.
// Reissuance tokens are confidential because, in Elements,
// the asset must be provided in blinded form in order to reissue tokens.
// https://github.com/BlockstreamResearch/simplicity-contracts/issues/21#issuecomment-3691599583
fn verify_token_commitment(actual_asset: Asset1, actual_amount: Amount1, expected_token_id: u256, abf: u256, vbf: u256) {
    match actual_asset {
        Left(conf_token: Point) => {
            let amount_scalar: u256 = 1;
            let (actual_ax, actual_ay): Ge = unwrap(jet::decompress(conf_token));

            let gej_point: Gej = (jet::hash_to_curve(expected_token_id), 1);
            let asset_blind_point: Gej = jet::generate(abf);

            let asset_generator: Gej = jet::gej_add(gej_point, asset_blind_point);
            let (ax, ay): Ge = unwrap(jet::gej_normalize(asset_generator));

            assert!(jet::eq_256(actual_ax, ax));
            check_y(actual_ay, ay);

            // Check amount
            let conf_val: Point = unwrap_left::<u64>(actual_amount);
            let (actual_vx, actual_vy): Ge = unwrap(jet::decompress(conf_val));

            let amount_part: Gej = jet::scale(amount_scalar, asset_generator);
            let vbf_part: Gej = jet::generate(vbf);

            let value_generator: Gej = jet::gej_add(amount_part, vbf_part);
            let (vx, vy): Ge = unwrap(jet::gej_normalize(value_generator));

            assert!(jet::eq_256(actual_vx, vx));
            check_y(actual_vy, vy);
        },
        Right(reissuance_token: u256) => {
            let expected_amount: u64 = 1;
            let actual_amount: u64 = unwrap_right::<Point>(actual_amount);
            
            assert!(jet::eq_64(expected_amount, actual_amount));
            assert!(jet::eq_256(reissuance_token, expected_token_id));
        }
    };
}

fn verify_output_reissuance_token(index: u32, expected_token_id: u256, abf: u256, vbf: u256) {
    let (asset, amount): (Asset1, Amount1) = unwrap(jet::output_amount(index));
    verify_token_commitment(asset, amount, expected_token_id, abf, vbf);
}

fn verify_input_reissuance_token(index: u32, expected_token_id: u256, abf: u256, vbf: u256) {
    let (asset, amount): (Asset1, Amount1) = unwrap(jet::input_amount(index));
    verify_token_commitment(asset, amount, expected_token_id, abf, vbf);
}

/*
 * Funding Path
 */
fn funding_path(
    expected_asset_amount: u64,
    input_option_abf: u256,
    input_option_vbf: u256,
    input_grantor_abf: u256,
    input_grantor_vbf: u256,
    output_option_abf: u256,
    output_option_vbf: u256,
    output_grantor_abf: u256,
    output_grantor_vbf: u256
) {
    ensure_input_and_output_script_hash_eq(0);
    ensure_input_and_output_script_hash_eq(1);

    verify_input_reissuance_token(0, param::OPTION_REISSUANCE_TOKEN_ASSET, input_option_abf, input_option_vbf);
    verify_input_reissuance_token(1, param::GRANTOR_REISSUANCE_TOKEN_ASSET, input_grantor_abf, input_grantor_vbf);

    verify_output_reissuance_token(0, param::OPTION_REISSUANCE_TOKEN_ASSET, output_option_abf, output_option_vbf);
    verify_output_reissuance_token(1, param::GRANTOR_REISSUANCE_TOKEN_ASSET, output_grantor_abf, output_grantor_vbf);

    assert!(dbg!(jet::eq_256(get_output_script_hash(0), get_output_script_hash(1))));

    assert!(jet::le_32(jet::current_index(), 1));

    ensure_output_script_hash_eq(2, get_output_script_hash(0));

    let (collateral_asset_bits, collateral_amount): (u256, u64) = get_output_explicit_asset_amount(2);
    let option_token_amount: u64 = unwrap_right::<(u1, u256)>(unwrap(unwrap(jet::issuance_asset_amount(0))));
    let grantor_token_amount: u64 = unwrap_right::<(u1, u256)>(unwrap(unwrap(jet::issuance_asset_amount(1))));
    assert!(jet::eq_64(option_token_amount, grantor_token_amount));

    divmod_eq(collateral_amount, param::COLLATERAL_PER_CONTRACT, option_token_amount);
    divmod_eq(expected_asset_amount, param::SETTLEMENT_PER_CONTRACT, option_token_amount);

    ensure_output_asset_with_amount_eq(2, param::COLLATERAL_ASSET_ID, collateral_amount);
    ensure_output_asset_with_amount_eq(3, param::OPTION_TOKEN_ASSET, option_token_amount);
    ensure_output_asset_with_amount_eq(4, param::GRANTOR_TOKEN_ASSET, grantor_token_amount);
}

/*
 * Cancellation Path
 */
fn cancellation_path(amount_to_burn: u64, collateral_amount_to_withdraw: u64, is_change_needed: bool) {
    let collateral_input_index: u32 = 0;
    let option_input_index: u32 = 1;
    let grantor_input_index: u32 = 2;

    let (burn_option_output_index, burn_grantor_output_index): (u32, u32) = match is_change_needed {
        true => (1, 2),
        false => (0, 1),
    };
    
    let expected_current_script_hash: u256 = get_input_script_hash(collateral_input_index);

    // Check and ensure collateral change
    ensure_correct_change_at_index(0, param::COLLATERAL_ASSET_ID, collateral_amount_to_withdraw, expected_current_script_hash, is_change_needed);

    // Burn option and grantor tokens
    ensure_output_is_op_return(burn_option_output_index);
    ensure_output_is_op_return(burn_grantor_output_index);

    ensure_output_asset_with_amount_eq(burn_option_output_index, param::OPTION_TOKEN_ASSET, amount_to_burn);
    ensure_output_asset_with_amount_eq(burn_grantor_output_index, param::GRANTOR_TOKEN_ASSET, amount_to_burn);

    // Ensure returned collateral amount is correct
    divmod_eq(collateral_amount_to_withdraw, param::COLLATERAL_PER_CONTRACT, amount_to_burn);
}

/*
 * Exercise Path
 */
fn exercise_path(option_amount_to_burn: u64, collateral_amount_to_get: u64, asset_amount_to_pay: u64, is_change_needed: bool) {
    jet::check_lock_time(param::START_TIME);

    let collateral_input_index: u32 = 0;

    let (burn_option_output_index, asset_to_covenant_output_index): (u32, u32) = match is_change_needed {
        true => (1, 2),
        false => (0, 1),
    };

    let expected_current_script_hash: u256 = get_input_script_hash(collateral_input_index);

    // Check and ensure collateral change
    ensure_correct_change_at_index(0, param::COLLATERAL_ASSET_ID, collateral_amount_to_get, expected_current_script_hash, is_change_needed);

    // Ensure collateral and asset amounts are correct
    divmod_eq(collateral_amount_to_get, param::COLLATERAL_PER_CONTRACT, option_amount_to_burn);
    divmod_eq(asset_amount_to_pay, param::SETTLEMENT_PER_CONTRACT, option_amount_to_burn);

    // Burn option token
    ensure_output_is_op_return(burn_option_output_index);
    ensure_output_asset_with_amount_eq(burn_option_output_index, param::OPTION_TOKEN_ASSET, option_amount_to_burn);

    // Ensure settlement asset and script hash are correct
    ensure_output_asset_with_amount_eq(asset_to_covenant_output_index, param::SETTLEMENT_ASSET_ID, asset_amount_to_pay);
    ensure_output_script_hash_eq(asset_to_covenant_output_index, expected_current_script_hash);
}

/*
 * Settlement Path
 */
fn settlement_path(grantor_token_amount_to_burn: u64, asset_amount: u64, is_change_needed: bool) {
    jet::check_lock_time(param::START_TIME);

    let target_asset_input_index: u32 = 0;

    let burn_grantor_output_index: u32 = match is_change_needed {
        true => 1,
        false => 0,
    };

    let expected_current_script_hash: u256 = get_input_script_hash(target_asset_input_index);

    // Check and ensure settlement asset change
    ensure_correct_change_at_index(0, param::SETTLEMENT_ASSET_ID, asset_amount, expected_current_script_hash, is_change_needed);
    
    // Ensure settlement asset and grantor token amounts are correct
    divmod_eq(asset_amount, param::SETTLEMENT_PER_CONTRACT, grantor_token_amount_to_burn);

    // Burn grantor token
    ensure_output_is_op_return(burn_grantor_output_index);
    ensure_output_asset_with_amount_eq(burn_grantor_output_index, param::GRANTOR_TOKEN_ASSET, grantor_token_amount_to_burn);
}

/*
 * Expiry Path
 */
fn expiry_path(grantor_token_amount_to_burn: u64, collateral_amount: u64, is_change_needed: bool) {
    jet::check_lock_time(param::EXPIRY_TIME);

    let collateral_input_index: u32 = 0;

    let burn_grantor_output_index: u32 = match is_change_needed {
        true => 1,
        false => 0,
    };

    let expected_current_script_hash: u256 = get_input_script_hash(collateral_input_index);

    // Check and ensure collateral change
    ensure_correct_change_at_index(0, param::COLLATERAL_ASSET_ID, collateral_amount, expected_current_script_hash, is_change_needed);

    // Ensure collateral amount is correct
    divmod_eq(collateral_amount, param::COLLATERAL_PER_CONTRACT, grantor_token_amount_to_burn);

    // Burn grantor token
    ensure_output_is_op_return(burn_grantor_output_index);
    ensure_output_asset_with_amount_eq(burn_grantor_output_index, param::GRANTOR_TOKEN_ASSET, grantor_token_amount_to_burn);
}

fn main() {
    match witness::PATH {
        Left(left_or_right: Either<(u64, u256, u256, u256, u256, u256, u256, u256, u256), Either<(bool, u64, u64, u64), (bool, u64, u64)>>) => match left_or_right {
            Left(params: (u64, u256, u256, u256, u256, u256, u256, u256, u256)) => {
                let (expected_asset_amount, input_option_abf, input_option_vbf, input_grantor_abf, input_grantor_vbf, output_option_abf, output_option_vbf, output_grantor_abf, output_grantor_vbf): (u64, u256, u256, u256, u256, u256, u256, u256, u256) = params;
                funding_path(
                    expected_asset_amount,
                    input_option_abf, input_option_vbf,
                    input_grantor_abf, input_grantor_vbf,
                    output_option_abf, output_option_vbf,
                    output_grantor_abf, output_grantor_vbf
                );
            },
            Right(exercise_or_settlement: Either<(bool, u64, u64, u64), (bool, u64, u64)>) => match exercise_or_settlement {
                Left(params: (bool, u64, u64, u64)) => {
                    let (is_change_needed, amount_to_burn, collateral_amount, asset_amount): (bool, u64, u64, u64) = dbg!(params);
                    exercise_path(amount_to_burn, collateral_amount, asset_amount, is_change_needed)
                },
                Right(params: (bool, u64, u64)) => {
                    let (is_change_needed, amount_to_burn, asset_amount): (bool, u64, u64) = dbg!(params);
                    settlement_path(amount_to_burn, asset_amount, is_change_needed)
                },
            },
        },
        Right(left_or_right: Either<(bool, u64, u64), (bool, u64, u64)>) => match left_or_right {
            Left(params: (bool, u64, u64)) => {
                let (is_change_needed, grantor_token_amount_to_burn, collateral_amount): (bool, u64, u64) = params;
                expiry_path(grantor_token_amount_to_burn, collateral_amount, is_change_needed)
            },
            Right(params: (bool, u64, u64)) => {
                let (is_change_needed, amount_to_burn, collateral_amount): (bool, u64, u64) = params;
                cancellation_path(amount_to_burn, collateral_amount, is_change_needed)
            },
        },
    }
}