Skip to content

Commit

Permalink
feat(listing): add fulfill listing & tests (#184)
Browse files Browse the repository at this point in the history
## Description

This pull request introduces significant updates to the `Orderbook`
contract, focusing on the implementation of listing order fulfillment
and the enhancement of testing scenarios.

#### Key Additions:

1. **Fulfilling Listing Order Functionality:**
- A new function, `fulfill_order`, has been added to handle the process
of fulfilling orders. This function takes in `execution_info` and
`signer` as parameters and follows these steps:
     - Verifies the signer.
     - Validates the existence of the order and its status.
- Checks if the order is open and not fulfilled by the same offerer.
- Distinguishes the order type and proceeds with the fulfillment process
for listing orders.

2. **Private Function for Listing Order Fulfillment:**
- The `_fulfill_listing_order` private function has been implemented to
specifically handle listing orders. It checks the order's expiration
date and updates the order status to 'Fulfilled' upon successful
execution.

3. **Enhanced Test Coverage:**
   - New test scenarios have been added to validate the functionality:
- `test_create_listing_order_and_fulfill_non_existing_order`: Ensures
the system correctly handles attempts to fulfill non-existing orders.
- `test_create_listing_order_and_fulfill`: Tests the successful creation
and fulfillment of a listing order.
- `test_create_listing_order_and_fulfill_with_same_fulfiller`: Validates
that an order cannot be fulfilled by the same entity that created it.

These updates aim to streamline the order fulfillment process,
particularly for listing orders, while ensuring robustness through
comprehensive testing.

<!-- 
Please do not leave this blank.
Describe the changes in this PR. What does it [add/remove/fix/replace]? 

For crafting a good description, consider using ChatGPT to help
articulate your changes.
-->

## What type of PR is this? (check all applicable)

- [x] 🍕 Feature (`feat:`)
- [ ] 🐛 Bug Fix (`fix:`)
- [ ] 📝 Documentation Update (`docs:`)
- [ ] 🎨 Style (`style:`)
- [ ] 🧑‍💻 Code Refactor (`refactor:`)
- [ ] 🔥 Performance Improvements (`perf:`)
- [ ] ✅ Test (`test:`)
- [ ] 🤖 Build (`build:`)
- [ ] 🔁 CI (`ci:`)
- [ ] 📦 Chore (`chore:`)
- [ ] ⏩ Revert (`revert:`)
- [ ] 🚀 Breaking Changes (`BREAKING CHANGE:`)

## Related Tickets & Documents

<!-- 
Please use this format to link related issues: Fixes #<issue_number>
More info:
https://docs.github.com/en/free-pro-team@latest/github/managing-your-work-on-github/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword
-->

## Added tests?

- [x] 👍 yes
- [ ] 🙅 no, because they aren't needed
- [ ] 🙋 no, because I need help

## Added to documentation?

- [ ] 📜 README.md
- [ ] 📓 Documentation
- [x] 🙅 no documentation needed

## [optional] Are there any post-deployment tasks we need to perform?

<!-- Describe any additional tasks, if any, and provide steps. -->

## [optional] What gif best describes this PR or how it makes you feel?

<!-- Share a fun gif related to your PR! -->

### PR Title and Description Guidelines:

- Ensure your PR title follows semantic versioning standards. This helps
automate releases and changelogs.
- Use types like `feat:`, `fix:`, `chore:`, `BREAKING CHANGE:` etc. in
your PR title.
- Your PR title will be used as a commit message when merging. Make sure
it adheres to [Conventional Commits
standards](https://www.conventionalcommits.org/).

## Closing Issues

<!-- 
Use keywords to close related issues. This ensures that the associated
issues will automatically close when the PR is merged.

- `Fixes #123` will close issue 123 when the PR is merged.
- `Closes #123` will also close issue 123 when the PR is merged.
- `Resolves #123` will also close issue 123 when the PR is merged.

You can also use multiple keywords in one comment:
- `Fixes #123, Resolves #456`

More info:
https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue
-->
  • Loading branch information
kwiss authored Nov 21, 2023
1 parent 11e1f69 commit 7e6feee
Show file tree
Hide file tree
Showing 12 changed files with 450 additions and 80 deletions.
1 change: 1 addition & 0 deletions crates/ark-contracts/arkchain/src/order/order_v1.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use starknet::ContractAddress;
use starknet::contract_address_to_felt252;
use arkchain::order::types::{OrderTrait, OrderValidationError, OrderType, RouteType};
use arkchain::crypto::hash::starknet_keccak;
use arkchain::order::types::FulfillInfo;

const ORDER_VERSION_V1: felt252 = 'v1';
// Auction -> end_amount (reserve price) > start_amount (starting price).
Expand Down
23 changes: 19 additions & 4 deletions crates/ark-contracts/arkchain/src/order/types.cairo
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
//! Order generic variables.
use starknet::ContractAddress;
use arkchain::crypto::hash::starknet_keccak;

/// Order types.
#[derive(Serde, Drop, PartialEq, Copy)]
Expand Down Expand Up @@ -149,19 +150,33 @@ impl Felt252TryIntoOrderStatus of TryInto<felt252, OrderStatus> {
}
}

/// The info related to the execution of an order.
/// The info related to the fulfill of an order.
#[derive(starknet::Store, Serde, Copy, Drop)]
struct ExecutionInfo {
// The hash of the order to execute.
struct FulfillInfo {
// The hash of the order to fulfill.
order_hash: felt252,
// Related order hash in case of an auction for exemple.
related_order_hash: Option<felt252>,
// Address of the fulfiller of the order.
fulfiller: ContractAddress,
// The token chain id.
token_chain_id: felt252,
// The token contract address.
token_address: ContractAddress,
// Token token id.
token_id: felt252,
token_id: Option<u256>,
}

trait FulfillInfoTrait {
fn hash(self: FulfillInfo) -> felt252;
}

impl FulfillInfoImpl of FulfillInfoTrait {
fn hash(self: FulfillInfo) -> felt252 {
let mut buf = array![];
self.serialize(ref buf);
starknet_keccak(buf.span())
}
}

/// The info related to the fulfillment an order.
Expand Down
93 changes: 62 additions & 31 deletions crates/ark-contracts/arkchain/src/orderbook.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
//! This module defines the structure and functionalities of an orderbook contract. It includes
//! trait definitions, error handling, contract storage, events, constructors, L1 handlers, external functions,
//! and internal functions. The primary functionalities include broker whitelisting, order management
//! (creation, cancellation, execution), and order queries.
//! (creation, cancellation, fulfillment), and order queries.

use arkchain::order::types::{ExecutionInfo, OrderType, OrderStatus};
use arkchain::order::types::{FulfillInfo, OrderType, OrderStatus};
use arkchain::order::order_v1::OrderV1;
use arkchain::crypto::signer::{SignInfo, Signer, SignerValidator};

Expand Down Expand Up @@ -42,9 +42,7 @@ trait Orderbook<T> {
///
/// * `order_hash` - The order to be fulfil.
/// * `sign_info` - The signing information associated with the order fulfillment.
fn fullfil_order(
ref self: T, order_hash: felt252, execution_info: ExecutionInfo, signer: Signer
);
fn fulfill_order(ref self: T, fulfill_info: FulfillInfo, signer: Signer);

/// Retrieves the type of an order using its hash.
///
Expand Down Expand Up @@ -81,15 +79,19 @@ mod orderbook_errors {
const ORDER_INVALID_DATA: felt252 = 'OB: order invalid data';
const ORDER_ALREADY_EXISTS: felt252 = 'OB: order already exists';
const ORDER_ALREADY_EXEC: felt252 = 'OB: order already executed';
const ORDER_NOT_FULFILLABLE: felt252 = 'OB: order not fulfillable';
const ORDER_NOT_FOUND: felt252 = 'OB: order not found';
const ORDER_FULFILLED: felt252 = 'OB: order fulfilled';
const ORDER_NOT_CANCELLABLE: felt252 = 'OB: order not cancellable';
const ORDER_EXPIRED: felt252 = 'OB: order expired';
const ORDER_SAME_OFFERER: felt252 = 'OB: order has same offerer';
const OFFER_ALREADY_EXISTS: felt252 = 'OB: offer already exists';
}

/// StarkNet smart contract module for an order book.
#[starknet::contract]
mod orderbook {
use arkchain::order::types::FulfillInfoTrait;
use core::traits::TryInto;
use core::result::ResultTrait;
use core::zeroable::Zeroable;
Expand All @@ -98,13 +100,15 @@ mod orderbook {
use core::traits::Into;
use super::{orderbook_errors, Orderbook};
use starknet::ContractAddress;
use arkchain::order::types::{OrderTrait, OrderType, ExecutionInfo, FulfillmentInfo};
use arkchain::order::types::{OrderTrait, OrderType, FulfillInfo, FulfillmentInfo};
use arkchain::order::order_v1::OrderV1;
use arkchain::order::database::{
order_read, order_status_read, order_write, order_status_write, order_type_read
};
use arkchain::crypto::signer::{SignInfo, Signer, SignerValidator};
use arkchain::order::types::OrderStatus;
use arkchain::crypto::hash::starknet_keccak;
use debug::PrintTrait;

const EXTENSION_TIME_IN_SECONDS: u64 = 600;

Expand Down Expand Up @@ -165,7 +169,7 @@ mod orderbook {
struct OrderExecuted {
#[key]
order_hash: felt252,
info: ExecutionInfo,
// info: ExecutionInfo,
}

/// Event for when an order is cancelled.
Expand All @@ -183,7 +187,7 @@ mod orderbook {
#[key]
order_hash: felt252,
#[key]
transaction_hash_settlement: felt252,
fulfiller: ContractAddress,
}

// *************************************************************************
Expand Down Expand Up @@ -329,19 +333,61 @@ mod orderbook {
self.emit(OrderCancelled { order_hash, reason: OrderStatus::CancelledUser.into() });
}

fn fullfil_order(
ref self: ContractState,
order_hash: felt252,
execution_info: ExecutionInfo,
signer: Signer
) {}
fn fulfill_order(ref self: ContractState, fulfill_info: FulfillInfo, signer: Signer) {
let order_hash = fulfill_info.order_hash;
let execution_hash = fulfill_info.hash();
SignerValidator::verify(execution_hash, signer);
let order: OrderV1 = match order_read(order_hash) {
Option::Some(o) => o,
Option::None => panic_with_felt252(orderbook_errors::ORDER_NOT_FOUND),
};
let status = match order_status_read(order_hash) {
Option::Some(s) => s,
Option::None => panic_with_felt252(orderbook_errors::ORDER_NOT_FOUND),
};
assert(status == OrderStatus::Open, orderbook_errors::ORDER_NOT_FULFILLABLE);
let order_type = match order_type_read(order_hash) {
Option::Some(s) => s,
Option::None => panic_with_felt252(orderbook_errors::ORDER_NOT_FOUND),
};
match order_type {
OrderType::Listing => { self._fulfill_listing_order(fulfill_info, order); },
OrderType::Auction => { panic_with_felt252('Auction not implemented'); },
OrderType::Offer => { panic_with_felt252('Offer not implemented'); },
OrderType::CollectionOffer => {
panic_with_felt252('CollectionOffer not implemented');
},
}
}
}

// *************************************************************************
// INTERNAL FUNCTIONS
// *************************************************************************
#[generate_trait]
impl InternalFunctions of InternalFunctionsTrait {
/// Fulfill order
///
/// # Arguments
/// * `fulfill_info` - The execution info of the order.
/// * `order_type` - The type of the order.
///
fn _fulfill_listing_order(
ref self: ContractState, fulfill_info: FulfillInfo, order: OrderV1
) {
assert(order.offerer != fulfill_info.fulfiller, orderbook_errors::ORDER_SAME_OFFERER);
assert(
order.end_date > starknet::get_block_timestamp(), orderbook_errors::ORDER_EXPIRED
);
order_status_write(fulfill_info.order_hash, OrderStatus::Fulfilled);
self
.emit(
OrderFulfilled {
order_hash: fulfill_info.order_hash, fulfiller: fulfill_info.fulfiller
}
);
}

/// Get order hash from token hash
///
/// # Arguments
Expand All @@ -351,15 +397,15 @@ mod orderbook {
self.token_listings.read(token_hash)
}

/// get previous order
/// get previous order
///
/// # Arguments
/// * `token_hash` - The token hash of the order.
///
/// # Return option of (order hash: felt252, is_order_expired: bool, order: OrderV1)
/// * order_hash
/// * is_order_expired
/// * order
/// * order
fn _get_previous_order(
self: @ContractState, token_hash: felt252
) -> Option<(felt252, bool, OrderV1)> {
Expand All @@ -371,7 +417,6 @@ mod orderbook {
.auctions
.read(token_hash);
let mut previous_orderhash = 0;

if (previous_listing_orderhash.is_non_zero()) {
previous_orderhash = previous_listing_orderhash;
let previous_order: Option<OrderV1> = order_read(previous_orderhash);
Expand Down Expand Up @@ -412,29 +457,20 @@ mod orderbook {
ref self: ContractState, token_hash: felt252, offerer: ContractAddress
) -> Option<felt252> {
let previous_order = self._get_previous_order(token_hash);

// Check of previous order exists
if (previous_order.is_some()) {
let (previous_orderhash, previous_order_is_expired, previous_order) = previous_order
.unwrap();
let previous_order_status = order_status_read(previous_orderhash)
.expect('Invalid Order status');
let previous_order_type = order_type_read(previous_orderhash)
.expect('Invalid Order type');

// check if previous order is fulfilled
assert(
previous_order_status != OrderStatus::Fulfilled,
orderbook_errors::ORDER_FULFILLED
);

// verify offerer is the same
if (previous_order.offerer == offerer) {
// check previous order is expired
assert(previous_order_is_expired, orderbook_errors::ORDER_NOT_CANCELLABLE);
}

// cancel previous order
order_status_write(previous_orderhash, OrderStatus::CancelledByNewOrder);
return Option::Some(previous_orderhash);
}
Expand All @@ -446,11 +482,8 @@ mod orderbook {
ref self: ContractState, order: OrderV1, order_type: OrderType, order_hash: felt252
) -> Option<felt252> {
let token_hash = order.compute_token_hash();
// Check if there is a previous order and cancel it if needed
let cancelled_order_hash = self._process_previous_order(token_hash, order.offerer);
// Write new order with status open
order_write(order_hash, order_type, order);
// Associate token_hash to order_hash on the storage
self.token_listings.write(token_hash, order_hash);
self
.emit(
Expand All @@ -470,9 +503,7 @@ mod orderbook {
ref self: ContractState, order: OrderV1, order_type: OrderType, order_hash: felt252
) {
let token_hash = order.compute_token_hash();
// Check if there is a previous order and cancel it if needed
let cancelled_order_hash = self._process_previous_order(token_hash, order.offerer);
/// we create an auction on the storage
order_write(order_hash, order_type, order);
self.auctions.write(token_hash, (order_hash, order.end_date, 0));
self
Expand Down
47 changes: 35 additions & 12 deletions crates/ark-contracts/arkchain/tests/common/setup.cairo
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use core::option::OptionTrait;
use core::traits::Into;
use arkchain::order::types::OrderTrait;
use arkchain::order::order_v1::OrderV1;
Expand All @@ -7,6 +8,7 @@ use snforge_std::signature::{
StarkCurveKeyPair, StarkCurveKeyPairTrait, Signer as SNSigner, Verifier
};

use super::super::common::signer::sign_mock;
/// Utility function to setup orders for test environment.
///
/// # Returns a tuple of the different orders
Expand Down Expand Up @@ -277,8 +279,13 @@ fn setup_auction_order(
(order_listing, signer, order_hash, token_hash)
}

fn setup(block_timestamp: u64) -> (OrderV1, arkchain::crypto::signer::Signer, felt252, felt252) {
let end_date = block_timestamp + (30 * 24 * 60 * 60);
fn setup(
block_timestamp: u64, is_expired: bool
) -> (OrderV1, arkchain::crypto::signer::Signer, felt252, felt252) {
let mut end_date = block_timestamp + (30 * 24 * 60 * 60);
if (is_expired) {
end_date = block_timestamp;
}
let data = array![];
let data_span = data.span();

Expand Down Expand Up @@ -309,17 +316,33 @@ fn setup(block_timestamp: u64) -> (OrderV1, arkchain::crypto::signer::Signer, fe
let order_hash = order_listing.compute_order_hash();
let token_hash = order_listing.compute_token_hash();
let signer = sign_mock(order_hash);

(order_listing, signer, order_hash, token_hash)
}


fn sign_mock(message_hash: felt252) -> Signer {
let private_key: felt252 = 0x1234567890987654321;
let mut key_pair = StarkCurveKeyPairTrait::from_private_key(private_key);
let (r, s) = key_pair.sign(message_hash).unwrap();

Signer::WEIERSTRESS_STARKNET(
SignInfo { user_pubkey: key_pair.public_key, user_sig_r: r, user_sig_s: s, }
)
fn get_offer_order() -> OrderV1 {
let data = array![];
let data_span = data.span();
OrderV1 {
route: RouteType::Erc20ToErc721.into(),
currency_address: 0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7
.try_into()
.unwrap(),
currency_chain_id: 0x534e5f4d41494e.try_into().unwrap(),
salt: 0,
offerer: 0x00E4769a4d2F7F69C70951A003eBA5c32707Cef3CdfB6B27cA63567f51cdd078
.try_into()
.unwrap(),
token_chain_id: 0x534e5f4d41494e.try_into().unwrap(),
token_address: 0x01435498bf393da86b4733b9264a86b58a42b31f8d8b8ba309593e5c17847672
.try_into()
.unwrap(),
token_id: Option::Some(1),
quantity: 1,
start_amount: 600000000000000000,
end_amount: 0,
start_date: 1699525884797,
end_date: 1702117884797,
broker_id: 123,
additional_data: data_span,
}
}
14 changes: 14 additions & 0 deletions crates/ark-contracts/arkchain/tests/common/signer.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
use arkchain::crypto::signer::{Signer, SignInfo};
use snforge_std::signature::{
StarkCurveKeyPair, StarkCurveKeyPairTrait, Signer as SNSigner, Verifier
};

fn sign_mock(message_hash: felt252) -> Signer {
let private_key: felt252 = 0x1234567890987654321;
let mut key_pair = StarkCurveKeyPairTrait::from_private_key(private_key);
let (r, s) = key_pair.sign(message_hash).unwrap();

Signer::WEIERSTRESS_STARKNET(
SignInfo { user_pubkey: key_pair.public_key, user_sig_r: r, user_sig_s: s, }
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ use super::super::common::setup::{setup, setup_listing_order_with_sign};
#[should_panic(expected: ('OB: order already exists',))]
fn test_create_existing_order() {
let block_timestamp = 1699556828; // starknet::get_block_timestamp();
let (order_listing, signer, _order_hash, token_hash) = setup(block_timestamp);
let (order_listing, signer, _order_hash, token_hash) = setup(block_timestamp, false);
let contract = declare('orderbook');
let contract_data = array![0x00E4769a4d2F7F69C70951A003eBA5c32707Cef3CdfB6B27cA63567f51cdd078];
let contract_address = contract.deploy(@contract_data).unwrap();
Expand All @@ -30,13 +30,13 @@ fn test_create_existing_order() {
#[test]
fn test_create_listing_order() {
let block_timestamp = 1699556828; // starknet::get_block_timestamp();
let (order_listing, signer, _order_hash, token_hash) = setup(block_timestamp);
let (order_listing, signer, _order_hash, token_hash) = setup(block_timestamp, false);
let contract = declare('orderbook');
let contract_data = array![0x00E4769a4d2F7F69C70951A003eBA5c32707Cef3CdfB6B27cA63567f51cdd078];
let contract_address = contract.deploy(@contract_data).unwrap();
let dispatcher = OrderbookDispatcher { contract_address };

let order = dispatcher.create_order(order: order_listing, signer: signer);
dispatcher.create_order(order: order_listing, signer: signer);
let order = dispatcher.get_order(_order_hash);
let order_status = dispatcher.get_order_status(_order_hash);
let order_type = dispatcher.get_order_type(_order_hash);
Expand Down
Loading

0 comments on commit 7e6feee

Please sign in to comment.