Skip to content

Instantly share code, notes, and snippets.

@SilentCicero
Created January 31, 2025 17:10
Show Gist options
  • Save SilentCicero/99a5cf0621104a174e564ce194304762 to your computer and use it in GitHub Desktop.
Save SilentCicero/99a5cf0621104a174e564ce194304762 to your computer and use it in GitHub Desktop.
An OTC order as a Sway Predicate that allows for partial fill.
predicate;
use std::{
inputs::*,
outputs::*,
hash::*,
auth::predicate_address
};
configurable {
ORDER_HASH: b256 = 0x09c0b2d1a486c439a87bcba6b46a7a1a23f3897cc83a94521a96da5c23bc58db,
}
fn main(minimal_output_amount: u64, out_index: u64, cancel: Option<(u64, u64)>) -> bool {
let in_count = input_count().as_u64();
let out_count = output_count().as_u64();
let this_predicate = predicate_address().unwrap();
let out_asset = output_asset_id(out_index).unwrap();
let mut in_asset = AssetId::default();
let mut input_index = 0;
let mut output_index = 0;
let mut total_in = 0;
let mut total_out_recipient_in: u64 = 0;
let mut total_out_recipient_out: u64 = 0;
let mut total_out_predicate_in: u64 = 0;
let mut will_cancel: bool = false;
let recipient_address = match input_coin_owner(out_index) {
Some(recipient) => recipient,
None => Address::zero(),
};
while input_index < in_count {
let is_predicate = match input_coin_owner(input_index) {
Some(predicate_address) => predicate_address == this_predicate,
None => false,
};
let is_coin = match input_type(input_index).unwrap() {
Input::Coin => true,
_ => false,
};
let is_recipient = match input_coin_owner(input_index) {
Some(recipient) => recipient == recipient_address,
None => false,
};
let is_in_asset = match input_asset_id(input_index) {
Some(asset_id) => {
if in_asset == AssetId::default() && is_predicate && is_coin {
in_asset = asset_id;
}
return in_asset == asset_id;
},
None => false,
};
if is_coin && is_predicate && is_in_asset {
let amount = input_amount(input_index).unwrap();
total_in = total_in + amount;
}
match cancel {
Some(_) => {
if is_recipient {
will_cancel = true;
}
},
None => {},
}
input_index = input_index + 1;
}
while output_index < out_count {
let is_coin = match output_type(output_index).unwrap() {
Output::Coin => true,
_ => false,
};
let is_predicate = match output_asset_to(output_index) {
Some(owner_address) => owner_address == this_predicate,
None => false,
};
let is_recipient = match output_asset_to(output_index) {
Some(recipient) => recipient == recipient_address,
None => false,
};
let is_in_asset = match output_asset_id(output_index) {
Some(asset_id) => asset_id == in_asset,
None => false,
};
let is_out_asset = match output_asset_id(output_index) {
Some(asset_id) => asset_id == out_asset,
None => false,
};
if (is_coin) {
let amount = output_amount(output_index).unwrap();
if is_predicate && is_in_asset {
total_out_predicate_in = total_out_predicate_in + amount;
}
if is_recipient && is_in_asset {
total_out_recipient_in = total_out_recipient_in + amount;
}
if is_recipient && is_out_asset {
assert(amount >= minimal_output_amount);
total_out_recipient_out = total_out_recipient_out + amount;
}
}
output_index = output_index + 1;
}
let mut numerator = 0;
let mut denominator = 0;
match cancel {
Some(values) => {
numerator = values.0;
denominator = values.1;
},
None => {
let mut gcd = total_in;
let mut b = total_out_recipient_out;
let mut remainder = 0;
while (b != 0) {
remainder = gcd % b;
gcd = b;
b = remainder;
}
numerator = total_in / gcd;
denominator = total_out_recipient_out / gcd;
}
}
let remainder_is_correct = u256::from(total_in) - (u256::from(total_in) * u256::from(numerator)) / u256::from(denominator) == u256::from(total_out_predicate_in);
sha256((
(numerator, denominator, minimal_output_amount),
(recipient_address, in_asset, out_asset)
)) == ORDER_HASH && ((will_cancel && total_in >= total_out_recipient_in) || remainder_is_correct)
}
/*
Details:
- 157 LOC
- Compressed using an order hash (meaning minimal bytes over the wire aside from the blob setup)
- Supports orders from any wallet (EVM, Bako, etc.)
- Supports paid cancellation
- Supports partial fill
- Multiple different orders from different parties and assets can be compressed into the same transaction
- No signature checking required (reduces execution overhead per order)
- Supports swapping between any native asset
- Dust order prevention with a minimum output amount threshold
- Uses the transaction data as a way to rehydrate key values such as swap/return amount, recipient address and asset ids
- Can support multiple inputs of same kind and multiple outputs of same kind
- The order hash enforces that the price is correct, the asset ids are what is requested for the pair and the proper recipient id is used
How it Works:
- Instead of specifying the price you want, you specify the ratio between your asset and the ask asset (think uniswap pools)
- So long as the ratio is followed and the right amount is returned to the order predicate, the user can spend/complete the order
- In the event of cancellation, all funds must be returned to the original recipient address
- Some information will need to be communicated either to the off chain server or in the witness data to make sense of this, namely, the ratio / price, the recipient, the ask asset
Future Considerations:
- Once we can do transaction expiry policies, we can start considering an intent based version of this
- For now, doing things with this swap predicate will cover a majority of cases (yes cancellations are paid, but that can go away in the future with some intent / sequencer system)
*/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment