/*
* 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)
},
},
}
}