Skip to content
This repository was archived by the owner on Jan 18, 2023. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
root = true

[*]
charset = utf-8
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true

[*.move]
indent_size = 4

[*.json]
indent_size = 4
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
sui.log.*
.env
build
output.bpl
.coverage_*
.trace
node_modules
package-lock.json
yarn.lock
42 changes: 42 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a
Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.4.0] - 2022-09-29

### Changed

- Using Movemate's implementation of critbit (sorted binary tree) instead of
the naive implementation with two sorted arrays.
- Renamed package to `Syrup`

## [0.3.0] - 2022-09-27

### Changed

- The creation of an orderbook is a witness protected function. The downstream
contract has the liberty of deciding when and how a new instance of an
orderbook should be created.

### Added

- Draft implementation for fees. At the moment, only the permission concept is
figured out and data model. To collect fees, the downstream collection
contract calls witness protected `collect_fees` function.
- Logic for optionally witness protecting trading entry functions.

## [0.2.0] - 2022-09-21

### Added

- Endpoint to buy a specific listed NFT.
- Getter for OB collection id.
- Getters for ask and bid fields.

### Changed

- Using `nft-protocol` v0.2.0 and Sui v0.9.0
11 changes: 11 additions & 0 deletions Move.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[package]
name = "Syrup"
version = "0.4.0"

[dependencies]
NftProtocol = { git = "https://github.com/Origin-Byte/nft-protocol", rev = "0.3.0" }
Sui = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework", rev = "devnet-0.9.0" }

[addresses]
syrup = "0x0"
# nft_protocol = "0xe6d6f01725d469b7b04d9905e6b3151e8c4264cb"
84 changes: 82 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,82 @@
# syrup
NFT Liquidity Layer
- Sui v0.9.0

NFT liquidity layer is a suite of modules which aim to bring NFT trading across
marketplaces to a single point, thereby concentrating the liquidity into a
common interface.

# Build

```
$ sui move build
```

# Orderbook

Orderbook implementation where bids are fungible tokens and asks are NFTs.
A bid is a request to buy one NFT from a specific collection.
An ask is one NFT with a min price condition.

`Ask` is an object which is associated with a single NFT.
When `Ask` is created, we transfer the ownership of the NFT to this
new object.
When an `Ask` is matched with a bid, we transfer the ownership of the
`ask` object to the bid owner (buyer).
The buyer can then claim the NFT via `claim_nft` endpoint.

One can:

- create a new orderbook between a given collection and a BID token (witness
pattern protected)
- collect fees (witness pattern protected)
- set publicly accessible actions to be witness protected
- open a new BID
- cancel an existing BID they own
- offer an NFT if a collection matches OB collection
- cancel an existing NFT offer
- instantly buy a specific NFT

# Witness protected actions

The contract which creates the orderbook can restrict specific actions to be
called only with a witness pattern and not via the entry point function.
This means others can build contracts on top of the orderbook with their own
custom logic based on their requirements or they can just use the entry point
functions that cover other use cases.

If a method is protected, clients will need to call a standard endpoint in the
witness-owning smart contract instead of the relevant endpoint in the orderbook.
Another way to think about this from a marketplace or wallet POV:
if I see that an action is protected, I can decide to either call the downstream
implementation in the collection smart contract, or simply disable that specific
action.

An example of this would be an NFT which has an expiry date like a name service
which requires an additional check before an ask can be placed on the orderbook.
Marketplaces can choose to support this additional logic or simply use the
standard orderbook and enable bids but asks would be disabled until they decide
to support the additional checks.

The setting is stored on the orderbook object:

```move
struct WitnessProtectedActions has store {
buy_nft: bool,
cancel_ask: bool,
cancel_bid: bool,
create_ask: bool,
create_bid: bool,
}
```

This means that the additional complexity is _(i)_ opt-in by the collection and
_(ii)_ reserved only to the particular action which warrants that complexity.

To reiterate, a marketplace can list NFTs from collections that have all
actions unprotected, ie. no special logic. Or they can just disable that
particular action that is disabled in the UI.

Additionally, if we created a standard interface for custom implementations of
the disabled actions, then the added opt-in complexity could be abstracted away
from the clients by our SDK.

TBD: Can this be misused by removing the option to cancel an ask/bid?
59 changes: 59 additions & 0 deletions examples/custom_collection.move
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
module syrup::custom_collection {
//! A simple showcase how to participate in the liquidity layer as a custom
//! collection implementor.
//!
//! The common use case to trade art NFTs would be implemented by the
//! `StdCollection` of the OriginByte nft protocol (will be a standalone
//! contract so no circular dependencies.)
//!
//! In a nutshell, the `StdCollection` implements a default desired behavior
//! for creators to list their collections in the liquidity layer.
//!
//! It would implements royalty distribution based on a business logic which
//! makes most sense for art NFTs.

use syrup::orderbook::{Self, Orderbook};
use std::fixed_point32;
use sui::object::{Self, UID};
use sui::balance;
use sui::coin;
use sui::transfer::{Self, transfer};
use sui::sui::SUI;
use sui::tx_context::{Self, TxContext};

// TODO: remove `store` once new version of nft-protocol lands
struct Witness has drop, store {}

struct MyCollection has key, store {
id: UID,
admin: address,
}

public entry fun create_orderbook<FT>(
my_collection: &MyCollection,
ctx: &mut TxContext,
) {
assert!(tx_context::sender(ctx) == my_collection.admin, 0);

let royalty = fixed_point32::create_from_rational(1, 100); // 1%

transfer::share_object(orderbook::create<Witness, SUI>(
Witness {},
object::id(my_collection),
royalty,
ctx,
));
}

public entry fun collect_royalties(
my_collection: &MyCollection,
ob: &mut Orderbook<Witness, SUI>,
ctx: &mut TxContext,
) {
assert!(tx_context::sender(ctx) == my_collection.admin, 0);

let balance = orderbook::fees_balance(Witness {}, ob);
let total = balance::value(balance);
transfer(coin::take(balance, total, ctx), my_collection.admin);
}
}
82 changes: 82 additions & 0 deletions examples/name_service.move
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
module syrup::name_service {
//! This example showcases custom implementation for logic creating an ask.
//!
//! Now, marketplaces and wallets have two options: implement creating ask
//! for Sui NS (via the standard e.g.) or decide to only enable bids and
//! buying of name addreses on their platform, but skip selling.
//!
//! We name this "name_service" because one of the teams building a NS on
//! Sui wanted to make trading of expired NFTs impossible.

use nft_protocol::collection::{Self, Collection};
use nft_protocol::nft::NftOwned;
use syrup::orderbook::{Self, Orderbook};
use std::fixed_point32;
use std::option::Option;
use std::string::String;
use sui::object::{Self, ID, UID};
use sui::transfer;
use sui::tx_context::{Self, TxContext};

// witness pattern
// TODO: remove `store` once new version of nft-protocol lands
struct NS has drop, store {}

// user NFT data
struct NSNftMetadata has key, store {
id: UID,
// points to `NSDomain`
domain: ID,
// some expiry info which will be used in `fun is_expired`
expires_at: u64,
}

// holds global information about the NS collection, such as tld, registered
// domains etc
struct NSCollMetadata has store {
admin: address,
registered_domains: vector<String>,
}

// info specific to a domain that's global, owned e.g. by the collection
// object or shared and only accessible by some authority
struct NSDomain has key {
id: UID,
name: String,
current_nft: Option<ID>,
}

fun is_expired(_nft: &NftOwned<NS, NSNftMetadata>): bool {
// TODO: implement logic for determining if the domain is expired

false
}

/// Joins the NFT liquidity layer but restricts asks such that they can only
/// be called via this contract.
public entry fun create_orderbook<FT>(
col: &Collection<NS, NSCollMetadata>,
ctx: &mut TxContext,
) {
let sender = tx_context::sender(ctx);
assert!(collection::metadata(col).admin == sender, 0);

let fee = fixed_point32::create_from_rational(1, 100); // 1%

let ob = orderbook::create<NS, FT>(NS {}, object::id(col), fee, ctx);
orderbook::toggle_protection_on_create_ask(NS {}, &mut ob);

transfer::share_object(ob);
}

/// The NFT can only be listed for sale if the domain is not expired.
public entry fun create_ask<FT>(
book: &mut Orderbook<NS, FT>,
requsted_tokens: u64,
nft: NftOwned<NS, NSNftMetadata>,
ctx: &mut TxContext,
) {
assert!(!is_expired(&nft), 0);
orderbook::create_ask_protected(NS {}, book, requsted_tokens, nft, ctx)
}
}
Loading