From 4804b5ab0e307963d4549bd28531a37e3d8da7b3 Mon Sep 17 00:00:00 2001 From: Matej Poklemba Date: Wed, 13 Mar 2024 16:41:35 +0100 Subject: [PATCH 01/20] PRC-4 draft --- PRCS/prc-4.md | 139 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 PRCS/prc-4.md diff --git a/PRCS/prc-4.md b/PRCS/prc-4.md new file mode 100644 index 0000000..18e086d --- /dev/null +++ b/PRCS/prc-4.md @@ -0,0 +1,139 @@ +--- +title: Paima Orderbook DEX +description: Interface for facilitating trading of a game asset (living on a game chain) on different base chain. +author: sebastiengllmt, matejos (@matejos) +status: Draft +created: 2024-03-13 +--- + +## Abstract + +Allowing tradability of game assets directly on popular networks helps achieve a lot more composability and liquidity than would otherwise be possible. This standard helps define how to define an orderbook-like DEX smart contract for trading game assets on different chains without introducing centralization, lowered security or wait times for finality. + +## Motivation + +Many games, due to being data and computation heavy applications, run on sidechains, L2s and appchains as opposed to popular L1 blockchains. This is problematic because liquidity for trading assets live primarily on the L1s (different environments). A common solution to this problem is building a bridge, but bridges have a bad reputation, often require a long delay (especially for optimistic bridges which often require 1 week), and bridging also makes upgrading the game harder as any update to the game state may now also require you to update the data associated with all the bridged state. + +Instead of bridging, this standard allows trading the game assets on base chain via creating and filling sell orders in an orderbook DEX smart contract. + +## Specification + +Every PRC-4 compliant contract must implement the `IOrderbookDex` interface and events: + +```solidity +/// @notice Facilitates base-chain trading of an asset that is living on a different app-chain. +/// @dev The contract should never hold any ETH itself. +interface IOrderbookDex is IERC165 { + struct Order { + uint256 assetAmount; + uint256 price; + address payable seller; + bool active; + } + + event OrderCreated( + uint256 indexed orderId, + address indexed seller, + uint256 assetAmount, + uint256 price + ); + event OrderFilled( + uint256 indexed orderId, + address indexed seller, + address indexed buyer, + uint256 assetAmount, + uint256 price + ); + event OrderCancelled(uint256 indexed orderId); + + /// @notice Returns the current index of orders (index that a new sell order will be mapped to). + function getOrdersIndex() external view returns (uint256); + + /// @notice Returns the Order struct information about order of specified `orderId`. + function getOrder(uint256 orderId) external view returns (Order memory); + + /// @notice Creates a sell order for the specified `assetAmount` at specified `price`. + /// @dev The order is saved in a mapping from incremental ID to Order struct. + /// MUST emit `OrderCreated` event. + function createSellOrder(uint256 assetAmount, uint256 price) external; + + /// @notice Fills an array of orders specified by `orderIds`, transferring portion of msg.value + /// to the orders' sellers according to the price. + /// @dev MUST revert if `active` parameter is `false` for any of the orders. + /// MUST change the `active` parameter for the specified order to `false`. + /// MUST emit `OrderFilled` event for each order. + /// If msg.value is more than the sum of orders' prices, it SHOULD refund the difference back to msg.sender. + function fillSellOrders(uint256[] memory orderIds) external payable; + + /// @notice Cancels the sell order specified by `orderId`, making it unfillable. + /// @dev Reverts if the msg.sender is not the order's seller. + /// MUST change the `active` parameter for the specified order to `false`. + /// MUST emit `OrderCancelled` event. + function cancelSellOrder(uint256 orderId) external; +} +``` + +## Rationale + +It's not an AMM. In a typical AMM dex, there are liquidity pools and you trade against these pools. This is unsuitable for this use-case because a contract on base chain has no means of knowing the state of the game chain so the contract cannot automate market making. Instead, buyers have to be sending tokens directly to the sellers. + +### The idea: + +1. Smart contract on base chain is created to facilitate trading game asset. This contract allows the following: + * Function to create a sell order. A sell order persists and has the following properties: + * `assetAmount` - amount of the game asset the seller is selling, + * `price` - the price in native gas tokens of the base chain the seller is requesting, + * `seller` - the address of the seller, + * `active` - signalizes if the sell order is active or not (meaning it has been cancelled or filled) + * Function to cancel a sell order. + * Function to fill a sell order (in other words - buy). There is no "buy order" that persists, and rather the buy function directly transfers the value specified by price defined in the orders to the sellers. Buying marks all the sell orders it fulfills as inactive by changing the value of `active` flag to `false`. +2. The game chain monitors this contract using CDEs and exposes an API to query its state. + +That is to say, the contract on the base chain has no way of knowing if somebody who makes a sell order really has that amount of game assets in their account. Rather, + +1. When somebody creates a sell order: The game chain sees it and checks if the user has enough of the asset in their account. + * If yes, the amount is locked so they cannot spend it in-game (unless they cancel the order). + * If not, the order is marked as invalid (to differentiate it from the case where it doesn't know the order exists yet). +2. When somebody wants to buy, they specify the orders they wish to purchase by ID. + * The responsibility of querying the API of game chain to check the validity of the sell order is left up to the front-end providers, as well as the presentation of the possible sell orders. +3. The user then makes a smart contract call that fulfills all the orders and sends the right amount to the corresponding accounts in a single transaction (atomic). +4. The game chain monitors the successfully fulfilled orders on the base chain and transfers the assets between accounts. + +**Note:** +All actions (sell, buy, cancel) are done on the base chain (and not on the game chain). This allows the base chain to be the source of truth for the state of which orders are valid which avoids double-spends. That is to say, if you try and perform a buy order, you are guaranteed by the base chain itself that the order hasn't been fulfilled by anybody else yet. + +## Reference Implementation + +You can find all the contracts and interfaces in the Paima Engine codebase [here](https://github.com/PaimaStudios/paima-engine/blob/master/packages/contracts/evm-contracts/contracts/orderbook/). + +## Considerations + +### Avoiding many failed txs in this model + +The problem with this model is people will naturally all try to buy the order with the most favorable sell price. Chosen base chain should have fast blocks so collisions are not so likely if the UI updates fast enough, but if bots start trading good orders as soon as they appear it may cause a rush with many failed transactions. + +**Option 1) A L3 for all games** + +However, the key observation is that solving this concurrency issue does not need any knowledge of the game itself (it doesn't matter if orders are valid or not from the dApp perspective. It just assumes buyers are not making bad purchases). + +This is an important note because it means we could have a decentralized L3 on top of the base chain whose goal it is to match orders properly. Notably, there is a model that provides exactly what we want with no tx fee if concurrent actions are attempted like this: the UTxO model. That is to say, implementing this system on top of Cardano (Aiken) or Fuel is actually much easier than the account model of EVM. However, e.g. Arbitrum users of course cannot be using Cardano, so this would most likely require running a Fuel L3 for Arbitrum (or maybe some ZK-ified UTxO platform if one exists) where the validators are decided by the Paima ecosystem token. + +It falls short on a few key points: + +* It would require adding Fuel support to Paima Engine to properly monitor it. +* It would require the Paima ecosystem to be released (not released yet). +* It means we lose some composability with other Arbitrum dApps (unless Fuel adds a wrapped smart contract system like Milkomeda). +* Users may be hesitant to bridge to this L3 just to trade, so we would have to abstract this away from them (again, wrapped smart contracts may help in the optimistic case where there isn't a conflicting order so they can buy game assets right away). + +**Option 2) Stylus** + +This option assumes the base chain being Arbitrum. +Arbitrum recently introduced a new programming language called Stylus which is much faster & cheaper than EVM. Additionally, it's composable with EVM contracts so you do not run into the same composability tradeoffs as with L3s. However, it does not appear to be able to solve the concurrent issue entirely like the UTxO model. Rather, it might just make the gas cost of failing cheaper. Additionally, it's not live on mainnet at the moment. + +**Option 3) Frontend-driven concurrency management** + +This option is perhaps the easiest if there is only a single website for the DEX, because the website itself can keep track of orders people are attempting to make and thus avoid conflicting orders being placed. However, this can quickly fall apart if another website appears or if people start making trades by directly interacting with the contract. + +## Copyright + +Copyright and related rights waived via [CC0](../LICENSE.md). From f2d40e3b753b9e70c06510b2f4e02bea6dc9d10e Mon Sep 17 00:00:00 2001 From: Matej Poklemba Date: Thu, 14 Mar 2024 13:35:23 +0100 Subject: [PATCH 02/20] Apply suggestions from code review Co-authored-by: Sebastien Guillemot --- PRCS/prc-4.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/PRCS/prc-4.md b/PRCS/prc-4.md index 18e086d..abc9762 100644 --- a/PRCS/prc-4.md +++ b/PRCS/prc-4.md @@ -84,12 +84,12 @@ It's not an AMM. In a typical AMM dex, there are liquidity pools and you trade a * `assetAmount` - amount of the game asset the seller is selling, * `price` - the price in native gas tokens of the base chain the seller is requesting, * `seller` - the address of the seller, - * `active` - signalizes if the sell order is active or not (meaning it has been cancelled or filled) + * `active` - signals whether or not the sell order is active (meaning it has been cancelled or filled) * Function to cancel a sell order. * Function to fill a sell order (in other words - buy). There is no "buy order" that persists, and rather the buy function directly transfers the value specified by price defined in the orders to the sellers. Buying marks all the sell orders it fulfills as inactive by changing the value of `active` flag to `false`. -2. The game chain monitors this contract using CDEs and exposes an API to query its state. +2. The game chain monitors this contract using Paima Primitives and exposes an API to query its state. -That is to say, the contract on the base chain has no way of knowing if somebody who makes a sell order really has that amount of game assets in their account. Rather, +The contract on the base chain has no way of knowing if somebody who makes a sell order really has that amount of game assets in their account. Rather, 1. When somebody creates a sell order: The game chain sees it and checks if the user has enough of the asset in their account. * If yes, the amount is locked so they cannot spend it in-game (unless they cancel the order). @@ -110,18 +110,18 @@ You can find all the contracts and interfaces in the Paima Engine codebase [here ### Avoiding many failed txs in this model -The problem with this model is people will naturally all try to buy the order with the most favorable sell price. Chosen base chain should have fast blocks so collisions are not so likely if the UI updates fast enough, but if bots start trading good orders as soon as they appear it may cause a rush with many failed transactions. +The problem with this model is people will naturally all try to buy the order with the most favorable sell price. The chosen base chain should have fast blocks so that collisions are not too likely if the UI updates fast enough, but if bots start trading good orders as soon as they appear it may cause a rush with many failed transactions. **Option 1) A L3 for all games** -However, the key observation is that solving this concurrency issue does not need any knowledge of the game itself (it doesn't matter if orders are valid or not from the dApp perspective. It just assumes buyers are not making bad purchases). +The key observation is that solving this concurrency issue does not need any knowledge of the game itself (it doesn't matter if orders are valid or not from the dApp perspective. It just assumes buyers are not making bad purchases). -This is an important note because it means we could have a decentralized L3 on top of the base chain whose goal it is to match orders properly. Notably, there is a model that provides exactly what we want with no tx fee if concurrent actions are attempted like this: the UTxO model. That is to say, implementing this system on top of Cardano (Aiken) or Fuel is actually much easier than the account model of EVM. However, e.g. Arbitrum users of course cannot be using Cardano, so this would most likely require running a Fuel L3 for Arbitrum (or maybe some ZK-ified UTxO platform if one exists) where the validators are decided by the Paima ecosystem token. +This is an important note because it means we could have a decentralized L3 on top of the base chain whose goal it is to match orders properly. Notably, there is a model that provides exactly what we want with no tx fee if concurrent actions are attempted like this: the UTxO model. That is to say, implementing this system on top of Cardano (Aiken) or Fuel is actually much easier than the account model of EVM. However, e.g. Arbitrum users of course cannot be using Cardano, so this would most likely require running a Fuel L3 for Arbitrum (or maybe some ZK-ified UTxO platform) where the validators are decided by the Paima ecosystem token. -It falls short on a few key points: +However, this falls short on a few key points: * It would require adding Fuel support to Paima Engine to properly monitor it. -* It would require the Paima ecosystem to be released (not released yet). +* It would require the Paima ecosystem token to be released (not released yet). * It means we lose some composability with other Arbitrum dApps (unless Fuel adds a wrapped smart contract system like Milkomeda). * Users may be hesitant to bridge to this L3 just to trade, so we would have to abstract this away from them (again, wrapped smart contracts may help in the optimistic case where there isn't a conflicting order so they can buy game assets right away). From 4acb39df261254c74b6e6b2a48d6dcabcc8c7f9d Mon Sep 17 00:00:00 2001 From: Matej Poklemba Date: Thu, 14 Mar 2024 13:37:06 +0100 Subject: [PATCH 03/20] Remove Stylus option --- PRCS/prc-4.md | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/PRCS/prc-4.md b/PRCS/prc-4.md index abc9762..0439b8e 100644 --- a/PRCS/prc-4.md +++ b/PRCS/prc-4.md @@ -125,12 +125,7 @@ However, this falls short on a few key points: * It means we lose some composability with other Arbitrum dApps (unless Fuel adds a wrapped smart contract system like Milkomeda). * Users may be hesitant to bridge to this L3 just to trade, so we would have to abstract this away from them (again, wrapped smart contracts may help in the optimistic case where there isn't a conflicting order so they can buy game assets right away). -**Option 2) Stylus** - -This option assumes the base chain being Arbitrum. -Arbitrum recently introduced a new programming language called Stylus which is much faster & cheaper than EVM. Additionally, it's composable with EVM contracts so you do not run into the same composability tradeoffs as with L3s. However, it does not appear to be able to solve the concurrent issue entirely like the UTxO model. Rather, it might just make the gas cost of failing cheaper. Additionally, it's not live on mainnet at the moment. - -**Option 3) Frontend-driven concurrency management** +**Option 2) Frontend-driven concurrency management** This option is perhaps the easiest if there is only a single website for the DEX, because the website itself can keep track of orders people are attempting to make and thus avoid conflicting orders being placed. However, this can quickly fall apart if another website appears or if people start making trades by directly interacting with the contract. From 606fc1695b786421b23a997796f161278f13c3c7 Mon Sep 17 00:00:00 2001 From: Matej Poklemba Date: Thu, 14 Mar 2024 13:46:26 +0100 Subject: [PATCH 04/20] Add Security Considerations section --- PRCS/prc-4.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/PRCS/prc-4.md b/PRCS/prc-4.md index 0439b8e..d6d125c 100644 --- a/PRCS/prc-4.md +++ b/PRCS/prc-4.md @@ -129,6 +129,10 @@ However, this falls short on a few key points: This option is perhaps the easiest if there is only a single website for the DEX, because the website itself can keep track of orders people are attempting to make and thus avoid conflicting orders being placed. However, this can quickly fall apart if another website appears or if people start making trades by directly interacting with the contract. +## Security Considerations + +**Honest RPC**: This standard relies on the default RPC being honestly operated. This is, however, not really a new trust assumption because this is a required assumption in nearly all dApps at the moment (including those like OpenSea where you have to trust them to be operating their website honestly). Just like somebody can run their own Ethereum fullnode to verify the data they see in an NFT marketplace, they can also sync fullnode for a Paima app and use their own RPC to fetch the state. + ## Copyright Copyright and related rights waived via [CC0](../LICENSE.md). From 6bce2c21ef8046dbe8db866612239d2336ca93bc Mon Sep 17 00:00:00 2001 From: Matej Poklemba Date: Tue, 19 Mar 2024 10:21:19 +0100 Subject: [PATCH 05/20] Design changes --- PRCS/prc-4.md | 103 ++++++++++++++++++++++++++++---------------------- 1 file changed, 58 insertions(+), 45 deletions(-) diff --git a/PRCS/prc-4.md b/PRCS/prc-4.md index d6d125c..552bd74 100644 --- a/PRCS/prc-4.md +++ b/PRCS/prc-4.md @@ -26,48 +26,57 @@ Every PRC-4 compliant contract must implement the `IOrderbookDex` interface and interface IOrderbookDex is IERC165 { struct Order { uint256 assetAmount; - uint256 price; - address payable seller; - bool active; + uint256 pricePerAsset; + bool cancelled; } - event OrderCreated( - uint256 indexed orderId, - address indexed seller, - uint256 assetAmount, - uint256 price - ); + event OrderCreated(address indexed seller, uint256 indexed orderId, uint256 assetAmount, uint256 pricePerAsset); event OrderFilled( - uint256 indexed orderId, address indexed seller, + uint256 indexed orderId, address indexed buyer, uint256 assetAmount, - uint256 price + uint256 pricePerAsset ); - event OrderCancelled(uint256 indexed orderId); + event OrderCancelled(address indexed seller, uint256 indexed orderId); /// @notice Returns the current index of orders (index that a new sell order will be mapped to). function getOrdersIndex() external view returns (uint256); - /// @notice Returns the Order struct information about order of specified `orderId`. - function getOrder(uint256 orderId) external view returns (Order memory); + /// @notice Returns the Order struct information about an order identified by the combination ``. + function getOrder(address seller, uint256 orderId) external view returns (Order memory); - /// @notice Creates a sell order for the specified `assetAmount` at specified `price`. - /// @dev The order is saved in a mapping from incremental ID to Order struct. + /// @notice Creates a sell order with incremental seller-specific `orderId` for the specified `assetAmount` at specified `pricePerAsset`. + /// @dev The order information is saved in a nested mapping `seller address -> orderId -> Order`. /// MUST emit `OrderCreated` event. - function createSellOrder(uint256 assetAmount, uint256 price) external; - - /// @notice Fills an array of orders specified by `orderIds`, transferring portion of msg.value - /// to the orders' sellers according to the price. - /// @dev MUST revert if `active` parameter is `false` for any of the orders. - /// MUST change the `active` parameter for the specified order to `false`. - /// MUST emit `OrderFilled` event for each order. - /// If msg.value is more than the sum of orders' prices, it SHOULD refund the difference back to msg.sender. - function fillSellOrders(uint256[] memory orderIds) external payable; - - /// @notice Cancels the sell order specified by `orderId`, making it unfillable. - /// @dev Reverts if the msg.sender is not the order's seller. - /// MUST change the `active` parameter for the specified order to `false`. + function createSellOrder(uint256 assetAmount, uint256 pricePerAsset) external; + + /// @notice Consecutively fills an array of orders identified by the combination ``, + /// by providing an exact amount of ETH and requesting a specific minimum amount of asset to receive. + /// @dev Transfers portions of msg.value to the orders' sellers according to the price. + /// The sum of asset amounts of filled orders MUST be at least `minimumAsset`. + /// If msg.value is more than the sum of orders' prices, it MUST refund the excess back to msg.sender. + /// An order whose `cancelled` parameter has value `true` MUST NOT be filled. + /// MUST change the `assetAmount` parameter for the specified order according to how much of it was filled. + /// MUST emit `OrderFilled` event for each order accordingly. + function fillOrdersExactEth(uint256 minimumAsset, address[] memory sellers, uint256[] memory orderIds) + external + payable; + + /// @notice Consecutively fills an array of orders identified by the combination ``, + /// by providing a possibly surplus amount of ETH and requesting an exact amount of asset to receive. + /// @dev Transfers portions of msg.value to the orders' sellers according to the price. + /// The sum of asset amounts of filled orders MUST be exactly `assetAmount`. Excess ETH MUST be returned back to `msg.sender`. + /// An order whose `cancelled` parameter has value `true` MUST NOT be filled. + /// MUST change the `assetAmount` parameter for the specified order according to how much of it was filled. + /// MUST emit `OrderFilled` event for each order accordingly. + /// If msg.value is more than the sum of orders' prices, it MUST refund the difference back to msg.sender. + function fillOrdersExactAsset(uint256 assetAmount, address[] memory sellers, uint256[] memory orderIds) + external + payable; + + /// @notice Cancels the sell order identified by combination ``, making it unfillable. + /// @dev MUST change the `cancelled` parameter for the specified order to `true`. /// MUST emit `OrderCancelled` event. function cancelSellOrder(uint256 orderId) external; } @@ -79,27 +88,31 @@ It's not an AMM. In a typical AMM dex, there are liquidity pools and you trade a ### The idea: -1. Smart contract on base chain is created to facilitate trading game asset. This contract allows the following: - * Function to create a sell order. A sell order persists and has the following properties: - * `assetAmount` - amount of the game asset the seller is selling, - * `price` - the price in native gas tokens of the base chain the seller is requesting, - * `seller` - the address of the seller, - * `active` - signals whether or not the sell order is active (meaning it has been cancelled or filled) - * Function to cancel a sell order. - * Function to fill a sell order (in other words - buy). There is no "buy order" that persists, and rather the buy function directly transfers the value specified by price defined in the orders to the sellers. Buying marks all the sell orders it fulfills as inactive by changing the value of `active` flag to `false`. -2. The game chain monitors this contract using Paima Primitives and exposes an API to query its state. +1. User initiates the sell order on the game chain - they lock the appropriate amount of the asset and the game chain assigns it the address-specific incremental `orderId`. +2. Smart contract on base chain facilitates trading game asset. This contract allows the following: + * Function to create a sell order of specified amount of asset and price per one unit of said asset. A sell order persists and has the following properties: + * `uint256 assetAmount` - amount of the game asset the seller is selling, + * `uint256 pricePerAsset` - the price in native gas tokens of the base chain the seller is requesting, + * `bool cancelled` - signals whether or not the sell order has been cancelled. + * Function to cancel a sell order of specified `orderId`. + * Function to fill a sell order (in other words - buy). There is no "buy order" that persists, and rather the buy function directly transfers the value specified by price defined in the orders to the sellers. There are 2 variations of fill function: + * `fillOrdersExactEth` - consecutively fills the array of specified orders until all provided ETH is spent and a specified minimum amount of asset has been achieved. Example: You want to buy as much asset for 1 ETH. + * `fillOrdersExactAsset` - consecutively fills the array of specified orders until specified exact amount of asset has been achieved, and returns the excess ETH back to the sender. Example: You want to buy 1000 units of asset as cheaply as possible. +3. The game chain monitors this contract using Paima Primitives and exposes an API to query its state. The contract on the base chain has no way of knowing if somebody who makes a sell order really has that amount of game assets in their account. Rather, -1. When somebody creates a sell order: The game chain sees it and checks if the user has enough of the asset in their account. - * If yes, the amount is locked so they cannot spend it in-game (unless they cancel the order). - * If not, the order is marked as invalid (to differentiate it from the case where it doesn't know the order exists yet). -2. When somebody wants to buy, they specify the orders they wish to purchase by ID. +1. When somebody creates a sell order on the game chain and on the base chain, they will have the same address-specific ID that symbolically links them together. +2. When somebody wants to buy, they specify the orders they wish to purchase by combination of seller address and address-specific ID. * The responsibility of querying the API of game chain to check the validity of the sell order is left up to the front-end providers, as well as the presentation of the possible sell orders. -3. The user then makes a smart contract call that fulfills all the orders and sends the right amount to the corresponding accounts in a single transaction (atomic). +3. The user then makes a smart contract call that fulfills the orders and sends the right amount to the corresponding accounts in a single transaction (atomic). 4. The game chain monitors the successfully fulfilled orders on the base chain and transfers the assets between accounts. -**Note:** +#### Cancelling an asset lock on game chain: + +To maintain base chain as the source of truth for transacting, it is important for the game chain to allow the user to unlock their game asset (cancel the sell order lock) ONLY IF a corresponding sell order on base chain side exists AND it has been cancelled. That means if a user initiates a sell order on game chain and changes their mind, they must proceed to the base chain to create and cancel a sell order with the corresponding ID. + +### **Note:** All actions (sell, buy, cancel) are done on the base chain (and not on the game chain). This allows the base chain to be the source of truth for the state of which orders are valid which avoids double-spends. That is to say, if you try and perform a buy order, you are guaranteed by the base chain itself that the order hasn't been fulfilled by anybody else yet. ## Reference Implementation @@ -127,7 +140,7 @@ However, this falls short on a few key points: **Option 2) Frontend-driven concurrency management** -This option is perhaps the easiest if there is only a single website for the DEX, because the website itself can keep track of orders people are attempting to make and thus avoid conflicting orders being placed. However, this can quickly fall apart if another website appears or if people start making trades by directly interacting with the contract. +Since there are 2 different buy functions - each with clear one-way intent and slippage mechanism - it is possible to submit a fill order specifying surplus of suboptimal orders. This surplus would automatically get used the best orders are quickly filled in the periods of high activity, and would result in slightly suboptimal trade for the user, but non-reverting transaction nonetheless. However, it might be tricky to find the right balance of providing enough surplus orders to the transaction and not providing needlessly too much. ## Security Considerations From 985f3949f61ef5f55cca059dd007c1490d4e892d Mon Sep 17 00:00:00 2001 From: Matej Poklemba Date: Tue, 19 Mar 2024 10:46:38 +0100 Subject: [PATCH 06/20] Minor fix --- PRCS/prc-4.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/PRCS/prc-4.md b/PRCS/prc-4.md index 552bd74..12338b1 100644 --- a/PRCS/prc-4.md +++ b/PRCS/prc-4.md @@ -40,8 +40,8 @@ interface IOrderbookDex is IERC165 { ); event OrderCancelled(address indexed seller, uint256 indexed orderId); - /// @notice Returns the current index of orders (index that a new sell order will be mapped to). - function getOrdersIndex() external view returns (uint256); + /// @notice Returns the current sellers orderId (index that a new sell order will be mapped to). + function getSellersOrderId() external view returns (uint256); /// @notice Returns the Order struct information about an order identified by the combination ``. function getOrder(address seller, uint256 orderId) external view returns (Order memory); From 566f891fa979d3224645a1060c5e42aae160eda6 Mon Sep 17 00:00:00 2001 From: Matej Poklemba Date: Tue, 19 Mar 2024 10:48:33 +0100 Subject: [PATCH 07/20] Minor fix --- PRCS/prc-4.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/PRCS/prc-4.md b/PRCS/prc-4.md index 12338b1..66935b8 100644 --- a/PRCS/prc-4.md +++ b/PRCS/prc-4.md @@ -40,8 +40,8 @@ interface IOrderbookDex is IERC165 { ); event OrderCancelled(address indexed seller, uint256 indexed orderId); - /// @notice Returns the current sellers orderId (index that a new sell order will be mapped to). - function getSellersOrderId() external view returns (uint256); + /// @notice Returns the seller's current `orderId` (index that their new sell order will be mapped to). + function getSellerOrderId(address seller) external view returns (uint256); /// @notice Returns the Order struct information about an order identified by the combination ``. function getOrder(address seller, uint256 orderId) external view returns (Order memory); From 20d37170b081ad7f6fd174d32b2c568e71fb2f01 Mon Sep 17 00:00:00 2001 From: Matej Poklemba Date: Tue, 19 Mar 2024 11:21:21 +0100 Subject: [PATCH 08/20] Sellers must be payable addresses --- PRCS/prc-4.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/PRCS/prc-4.md b/PRCS/prc-4.md index 66935b8..4bd6556 100644 --- a/PRCS/prc-4.md +++ b/PRCS/prc-4.md @@ -59,7 +59,7 @@ interface IOrderbookDex is IERC165 { /// An order whose `cancelled` parameter has value `true` MUST NOT be filled. /// MUST change the `assetAmount` parameter for the specified order according to how much of it was filled. /// MUST emit `OrderFilled` event for each order accordingly. - function fillOrdersExactEth(uint256 minimumAsset, address[] memory sellers, uint256[] memory orderIds) + function fillOrdersExactEth(uint256 minimumAsset, address payable[] memory sellers, uint256[] memory orderIds) external payable; @@ -71,7 +71,7 @@ interface IOrderbookDex is IERC165 { /// MUST change the `assetAmount` parameter for the specified order according to how much of it was filled. /// MUST emit `OrderFilled` event for each order accordingly. /// If msg.value is more than the sum of orders' prices, it MUST refund the difference back to msg.sender. - function fillOrdersExactAsset(uint256 assetAmount, address[] memory sellers, uint256[] memory orderIds) + function fillOrdersExactAsset(uint256 assetAmount, address payable[] memory sellers, uint256[] memory orderIds) external payable; From 736bcc9f652a224001960f0bea94a1c180f3cdea Mon Sep 17 00:00:00 2001 From: Matej Poklemba Date: Mon, 8 Apr 2024 10:23:33 +0200 Subject: [PATCH 09/20] Latest design updates --- PRCS/prc-4.md | 150 +++++++++++++++++++++++++++++++------------------- 1 file changed, 94 insertions(+), 56 deletions(-) diff --git a/PRCS/prc-4.md b/PRCS/prc-4.md index 4bd6556..f46519b 100644 --- a/PRCS/prc-4.md +++ b/PRCS/prc-4.md @@ -8,7 +8,7 @@ created: 2024-03-13 ## Abstract -Allowing tradability of game assets directly on popular networks helps achieve a lot more composability and liquidity than would otherwise be possible. This standard helps define how to define an orderbook-like DEX smart contract for trading game assets on different chains without introducing centralization, lowered security or wait times for finality. +Allowing tradability of game assets directly on popular networks helps achieve a lot more composability and liquidity than would otherwise be possible. This standard helps define how to define an orderbook-like DEX smart contract for trading game assets on different chains without introducing centralization, lowered security or wait times for finality. The game asset is represented by an IERC1155 compliant token, utilizing the PRC-5 standard (Inverse Projection Interface for ERC1155). ## Motivation @@ -22,15 +22,37 @@ Every PRC-4 compliant contract must implement the `IOrderbookDex` interface and ```solidity /// @notice Facilitates base-chain trading of an asset that is living on a different app-chain. -/// @dev The contract should never hold any ETH itself. -interface IOrderbookDex is IERC165 { +/// @dev Orders are identified by a unique incremental `orderId`. +interface IOrderbookDex is IERC1155Receiver { struct Order { + /// @dev The asset's unique token identifier. + uint256 assetId; + /// @dev The amount of the asset that is available to be sold. uint256 assetAmount; + /// @dev The price per one unit of asset. uint256 pricePerAsset; - bool cancelled; + /// @dev The seller's address. + address payable seller; } - event OrderCreated(address indexed seller, uint256 indexed orderId, uint256 assetAmount, uint256 pricePerAsset); + /// @param seller The seller's address. + /// @param orderId The order's unique identifier. + /// @param assetId The asset's unique token identifier. + /// @param assetAmount The amount of the asset that has been put for sale. + /// @param pricePerAsset The requested price per one unit of asset. + event OrderCreated( + address indexed seller, + uint256 indexed orderId, + uint256 indexed assetId, + uint256 assetAmount, + uint256 pricePerAsset + ); + + /// @param seller The seller's address. + /// @param orderId The order's unique identifier. + /// @param buyer The buyer's address. + /// @param assetAmount The amount of the asset that was traded. + /// @param pricePerAsset The price per one unit of asset that was paid. event OrderFilled( address indexed seller, uint256 indexed orderId, @@ -38,47 +60,70 @@ interface IOrderbookDex is IERC165 { uint256 assetAmount, uint256 pricePerAsset ); - event OrderCancelled(address indexed seller, uint256 indexed orderId); - /// @notice Returns the seller's current `orderId` (index that their new sell order will be mapped to). - function getSellerOrderId(address seller) external view returns (uint256); + /// @param seller The seller's address. + /// @param id The order's unique identifier. + event OrderCancelled(address indexed seller, uint256 indexed id); - /// @notice Returns the Order struct information about an order identified by the combination ``. - function getOrder(address seller, uint256 orderId) external view returns (Order memory); + /// @notice Returns the address of the asset that is being traded in this DEX contract. + function getAsset() external view returns (address); - /// @notice Creates a sell order with incremental seller-specific `orderId` for the specified `assetAmount` at specified `pricePerAsset`. - /// @dev The order information is saved in a nested mapping `seller address -> orderId -> Order`. - /// MUST emit `OrderCreated` event. - function createSellOrder(uint256 assetAmount, uint256 pricePerAsset) external; + /// @notice Returns the `orderId` of the next sell order. + function getCurrentOrderId() external view returns (uint256); + + /// @notice Returns the Order struct information about an order identified by the `orderId`. + function getOrder(uint256 orderId) external view returns (Order memory); - /// @notice Consecutively fills an array of orders identified by the combination ``, + /// @notice Creates a sell order for the `assetAmount` of `assetId` at `pricePerAsset`. + /// @dev The order information is saved in a mapping `orderId -> Order`, with `orderId` being a unique incremental identifier. + /// MUST transfer the `assetAmount` of `assetId` from the seller to the contract. + /// MUST emit `OrderCreated` event. + /// @return The unique identifier of the created order. + function createSellOrder( + uint256 assetId, + uint256 assetAmount, + uint256 pricePerAsset + ) external returns (uint256); + + /// @notice Creates a batch of sell orders for the `assetAmount` of `assetId` at `pricePerAsset`. + /// @dev This is a batched version of `createSellOrder` that simply iterates through the arrays to call said function. + /// @return The unique identifiers of the created orders. + function createBatchSellOrder( + uint256[] memory assetIds, + uint256[] memory assetAmounts, + uint256[] memory pricesPerAssets + ) external returns (uint256[] memory); + + /// @notice Consecutively fills an array of orders identified by the `orderId` of each order, /// by providing an exact amount of ETH and requesting a specific minimum amount of asset to receive. /// @dev Transfers portions of msg.value to the orders' sellers according to the price. /// The sum of asset amounts of filled orders MUST be at least `minimumAsset`. - /// If msg.value is more than the sum of orders' prices, it MUST refund the excess back to msg.sender. - /// An order whose `cancelled` parameter has value `true` MUST NOT be filled. - /// MUST change the `assetAmount` parameter for the specified order according to how much of it was filled. + /// If msg.value is more than the sum of orders' prices, it MUST refund the excess back to `msg.sender`. + /// MUST decrease the `assetAmount` parameter for the specified order according to how much of it was filled, + /// and transfer that amount of the order's `assetId` to the buyer. /// MUST emit `OrderFilled` event for each order accordingly. - function fillOrdersExactEth(uint256 minimumAsset, address payable[] memory sellers, uint256[] memory orderIds) - external - payable; + function fillOrdersExactEth(uint256 minimumAsset, uint256[] memory orderIds) external payable; - /// @notice Consecutively fills an array of orders identified by the combination ``, + /// @notice Consecutively fills an array of orders identified by the `orderId` of each order, /// by providing a possibly surplus amount of ETH and requesting an exact amount of asset to receive. /// @dev Transfers portions of msg.value to the orders' sellers according to the price. /// The sum of asset amounts of filled orders MUST be exactly `assetAmount`. Excess ETH MUST be returned back to `msg.sender`. - /// An order whose `cancelled` parameter has value `true` MUST NOT be filled. - /// MUST change the `assetAmount` parameter for the specified order according to how much of it was filled. + /// MUST decrease the `assetAmount` parameter for the specified order according to how much of it was filled, + /// and transfer that amount of the order's `assetId` to the buyer. /// MUST emit `OrderFilled` event for each order accordingly. - /// If msg.value is more than the sum of orders' prices, it MUST refund the difference back to msg.sender. - function fillOrdersExactAsset(uint256 assetAmount, address payable[] memory sellers, uint256[] memory orderIds) - external - payable; + /// If msg.value is more than the sum of orders' prices, it MUST refund the difference back to `msg.sender`. + function fillOrdersExactAsset(uint256 assetAmount, uint256[] memory orderIds) external payable; - /// @notice Cancels the sell order identified by combination ``, making it unfillable. - /// @dev MUST change the `cancelled` parameter for the specified order to `true`. + /// @notice Cancels the sell order identified by the `orderId`, transferring the order's assets back to the seller. + /// @dev MUST revert if the order's seller is not `msg.sender`. + /// MUST change the `assetAmount` parameter for the specified order to `0`. /// MUST emit `OrderCancelled` event. + /// MUST transfer the `assetAmount` of `assetId` back to the seller. function cancelSellOrder(uint256 orderId) external; + + /// @notice Cancels a batch of sell orders identified by the `orderIds`, transferring the orders' assets back to the seller. + /// @dev This is a batched version of `cancelSellOrder` that simply iterates through the array to call said function. + function cancelBatchSellOrder(uint256[] memory orderIds) external; } ``` @@ -88,32 +133,25 @@ It's not an AMM. In a typical AMM dex, there are liquidity pools and you trade a ### The idea: -1. User initiates the sell order on the game chain - they lock the appropriate amount of the asset and the game chain assigns it the address-specific incremental `orderId`. +1. User withdraws the game asset to the base chain by using the PRC-5 Inverse Projection system - they lock the appropriate amount of the asset and the game chain assigns it the address-specific incremental `orderId`, for which they mint InverseProjected1155 tokens on the base chain. 2. Smart contract on base chain facilitates trading game asset. This contract allows the following: - * Function to create a sell order of specified amount of asset and price per one unit of said asset. A sell order persists and has the following properties: - * `uint256 assetAmount` - amount of the game asset the seller is selling, - * `uint256 pricePerAsset` - the price in native gas tokens of the base chain the seller is requesting, - * `bool cancelled` - signals whether or not the sell order has been cancelled. - * Function to cancel a sell order of specified `orderId`. - * Function to fill a sell order (in other words - buy). There is no "buy order" that persists, and rather the buy function directly transfers the value specified by price defined in the orders to the sellers. There are 2 variations of fill function: - * `fillOrdersExactEth` - consecutively fills the array of specified orders until all provided ETH is spent and a specified minimum amount of asset has been achieved. Example: You want to buy as much asset for 1 ETH. - * `fillOrdersExactAsset` - consecutively fills the array of specified orders until specified exact amount of asset has been achieved, and returns the excess ETH back to the sender. Example: You want to buy 1000 units of asset as cheaply as possible. -3. The game chain monitors this contract using Paima Primitives and exposes an API to query its state. + - Function to create a sell order of specified amount of asset and price per one unit of said asset. A sell order persists and has the following properties: + - `uint256 assetId` - token ID of the ERC1155 asset being sold, + - `uint256 assetAmount` - amount of the game asset the seller is selling, + - `uint256 pricePerAsset` - the price in native gas tokens of the base chain the seller is requesting, + - `address seller` - the seller's address. + - Function to cancel a sell order of specified `orderId` + - Batch functions `createBatchSellOrder` and `cancelBatchSellOrder` of the abovementioned functions + - Function to fill sell orders (in other words - buy). There is no "buy order" that persists, and rather the buy function directly transfers the value specified by price defined in the orders to the sellers. There are 2 variations of fill function: + - `fillOrdersExactEth` - consecutively fills the array of specified orders until all provided ETH is spent and a specified minimum amount of asset has been achieved. Example: You want to buy as much asset as possible for 1 ETH. + - `fillOrdersExactAsset` - consecutively fills the array of specified orders until specified exact amount of asset has been achieved, and returns the excess ETH back to the sender. Example: You want to buy 1000 units of asset as cheaply as possible. The contract on the base chain has no way of knowing if somebody who makes a sell order really has that amount of game assets in their account. Rather, -1. When somebody creates a sell order on the game chain and on the base chain, they will have the same address-specific ID that symbolically links them together. -2. When somebody wants to buy, they specify the orders they wish to purchase by combination of seller address and address-specific ID. - * The responsibility of querying the API of game chain to check the validity of the sell order is left up to the front-end providers, as well as the presentation of the possible sell orders. -3. The user then makes a smart contract call that fulfills the orders and sends the right amount to the corresponding accounts in a single transaction (atomic). -4. The game chain monitors the successfully fulfilled orders on the base chain and transfers the assets between accounts. - -#### Cancelling an asset lock on game chain: - -To maintain base chain as the source of truth for transacting, it is important for the game chain to allow the user to unlock their game asset (cancel the sell order lock) ONLY IF a corresponding sell order on base chain side exists AND it has been cancelled. That means if a user initiates a sell order on game chain and changes their mind, they must proceed to the base chain to create and cancel a sell order with the corresponding ID. - -### **Note:** -All actions (sell, buy, cancel) are done on the base chain (and not on the game chain). This allows the base chain to be the source of truth for the state of which orders are valid which avoids double-spends. That is to say, if you try and perform a buy order, you are guaranteed by the base chain itself that the order hasn't been fulfilled by anybody else yet. +1. When somebody creates a sell order for a specific asset ID on the game chain, its validity can be checked via the API in the token's `uri` function. +2. When somebody wants to buy, they specify the orders they wish to purchase by their unique ID. + - The responsibility of querying the API of game chain to check the validity of the sell order is left up to the front-end providers, as well as the presentation of the possible sell orders. +3. The user then makes a smart contract call that fulfills the orders and transfers the right amounts of assets to the corresponding accounts in a single transaction (atomic). ## Reference Implementation @@ -133,10 +171,10 @@ This is an important note because it means we could have a decentralized L3 on t However, this falls short on a few key points: -* It would require adding Fuel support to Paima Engine to properly monitor it. -* It would require the Paima ecosystem token to be released (not released yet). -* It means we lose some composability with other Arbitrum dApps (unless Fuel adds a wrapped smart contract system like Milkomeda). -* Users may be hesitant to bridge to this L3 just to trade, so we would have to abstract this away from them (again, wrapped smart contracts may help in the optimistic case where there isn't a conflicting order so they can buy game assets right away). +- It would require adding Fuel support to Paima Engine to properly monitor it. +- It would require the Paima ecosystem token to be released (not released yet). +- It means we lose some composability with other Arbitrum dApps (unless Fuel adds a wrapped smart contract system like Milkomeda). +- Users may be hesitant to bridge to this L3 just to trade, so we would have to abstract this away from them (again, wrapped smart contracts may help in the optimistic case where there isn't a conflicting order so they can buy game assets right away). **Option 2) Frontend-driven concurrency management** From 1294bc4118993aae6bc9ce9a289c81e42af3655e Mon Sep 17 00:00:00 2001 From: Matej Poklemba Date: Tue, 9 Apr 2024 09:49:49 +0200 Subject: [PATCH 10/20] Reformulate a section --- PRCS/prc-4.md | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/PRCS/prc-4.md b/PRCS/prc-4.md index f46519b..f0d04e1 100644 --- a/PRCS/prc-4.md +++ b/PRCS/prc-4.md @@ -21,6 +21,8 @@ Instead of bridging, this standard allows trading the game assets on base chain Every PRC-4 compliant contract must implement the `IOrderbookDex` interface and events: ```solidity +import {IERC1155Receiver} from "@openzeppelin/contracts/token/ERC1155/IERC1155Receiver.sol"; + /// @notice Facilitates base-chain trading of an asset that is living on a different app-chain. /// @dev Orders are identified by a unique incremental `orderId`. interface IOrderbookDex is IERC1155Receiver { @@ -146,12 +148,7 @@ It's not an AMM. In a typical AMM dex, there are liquidity pools and you trade a - `fillOrdersExactEth` - consecutively fills the array of specified orders until all provided ETH is spent and a specified minimum amount of asset has been achieved. Example: You want to buy as much asset as possible for 1 ETH. - `fillOrdersExactAsset` - consecutively fills the array of specified orders until specified exact amount of asset has been achieved, and returns the excess ETH back to the sender. Example: You want to buy 1000 units of asset as cheaply as possible. -The contract on the base chain has no way of knowing if somebody who makes a sell order really has that amount of game assets in their account. Rather, - -1. When somebody creates a sell order for a specific asset ID on the game chain, its validity can be checked via the API in the token's `uri` function. -2. When somebody wants to buy, they specify the orders they wish to purchase by their unique ID. - - The responsibility of querying the API of game chain to check the validity of the sell order is left up to the front-end providers, as well as the presentation of the possible sell orders. -3. The user then makes a smart contract call that fulfills the orders and transfers the right amounts of assets to the corresponding accounts in a single transaction (atomic). +The DEX contract on the base chain only facilitates the trading (transferring) of existing Inverse Projected ERC1155 assets, it does not make any assurances about validity of such assets. That aspect is handled by the feature set of the Inverse Projected ERC1155 standard itself. The responsibility of querying the API of game chain to check the validity of the sell order is left up to the front-end providers, as well as the presentation of the possible sell orders. ## Reference Implementation From 5792b4c51291512782ec39b157378a0b9a4ded9e Mon Sep 17 00:00:00 2001 From: Matej Poklemba Date: Thu, 11 Apr 2024 16:22:48 +0200 Subject: [PATCH 11/20] Simplify getters --- PRCS/prc-4.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/PRCS/prc-4.md b/PRCS/prc-4.md index f0d04e1..5092e6d 100644 --- a/PRCS/prc-4.md +++ b/PRCS/prc-4.md @@ -67,11 +67,11 @@ interface IOrderbookDex is IERC1155Receiver { /// @param id The order's unique identifier. event OrderCancelled(address indexed seller, uint256 indexed id); - /// @notice Returns the address of the asset that is being traded in this DEX contract. - function getAsset() external view returns (address); + /// @notice The address of the asset that is being traded in this DEX contract. + function asset() external view returns (address); - /// @notice Returns the `orderId` of the next sell order. - function getCurrentOrderId() external view returns (uint256); + /// @notice The `orderId` of the next sell order. + function currentOrderId() external view returns (uint256); /// @notice Returns the Order struct information about an order identified by the `orderId`. function getOrder(uint256 orderId) external view returns (Order memory); From 1e37cd4e77c1cf6a516076416ff567fb1218b251 Mon Sep 17 00:00:00 2001 From: Edward Alvarado Date: Mon, 27 May 2024 12:46:03 -0400 Subject: [PATCH 12/20] prc4 game node api proposal --- PRCS/prc-4.md | 113 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 113 insertions(+) diff --git a/PRCS/prc-4.md b/PRCS/prc-4.md index 5092e6d..81bbf0e 100644 --- a/PRCS/prc-4.md +++ b/PRCS/prc-4.md @@ -150,6 +150,119 @@ It's not an AMM. In a typical AMM dex, there are liquidity pools and you trade a The DEX contract on the base chain only facilitates the trading (transferring) of existing Inverse Projected ERC1155 assets, it does not make any assurances about validity of such assets. That aspect is handled by the feature set of the Inverse Projected ERC1155 standard itself. The responsibility of querying the API of game chain to check the validity of the sell order is left up to the front-end providers, as well as the presentation of the possible sell orders. +## Game Node Dex API (OPTIONAL) + +These endpoints are provided by the game node to allow external sites generate a frontend for the DEX. + +1. Get game assets and metadata. + + `GET /dex/` + + RESPONSE + ```js + { + assets: { + code: string; // Asset Code + description: string; // Asset Description + fromSym: string; // Name of base Asset + toSym: string; // Name of unit to convert + }[], + game: { + id: string // Game ID + name?: string // Optional Game Name + version?: string // Optional Game Version + } + } + ``` + +4. Returns Asset Information. + + `GET /dex/{asset}` + + * asset: valid name for specific game asset token. + + RESPONSE + ```js + { + totalSupply: number; // Total number of assets + } + ``` + + +2. Get array of ERC1155 tokens of `asset` for specified `user` that have been minted or owned in a valid way +-- meaning the `amount` and `tokenId`/`userTokenId` combination matches. This is used by the DEX to get the list of valid assets that user is able to create sell orders for. + + `GET /dex/{asset}/user_valid_minted_assets/{wallet}` + + * asset: valid name for specific game asset token. + * wallet_address: wallet to query for asset + + RESPONSE + ```js + { + total: number // Total number of assets owned + stats: { + tokenId: number; // ERC1155 Token ID + amount: number; // Number of assets owned + }[]; + } + ``` + +3. Gets array of created sell orders that are valid -- meaning they have been created with valid minted assets. +This is used by the DEX to get the list of valid sell orders to display to users wanting to buy. Ordered by lowest price. + + `GET /dex/{asset}/orders?seller=wallet&page=number&limit=number` + + * asset: valid name for specific game asset token. + * seller (OPTIONAL): fetch where wallet address matches wallet + * page (OPTIONAL): results page number, default = 1 + * limit (OPTIONAL): 10, 25, 50, 100, default = 25 + + RESPONSE + ```js + { + stats: { + orderId: number; // Order unique ID + seller: string; // Seller wallet + tokenId: number; // ERC1155 TokenID + amount: number; // Number of assets for sale + price: string; // Price per asset + }[]; + } + ``` + +5. Historical data. Allows the UI to draw a chart with historical values. + + `Get /dex/{asset}/historical_price?freq=string&start=number&end=number` + + * asset: valid name for specific game asset token. + * freq (OPTIONAL): hour | day | month - range for specific (default: hour) + * start (OPTIONAL): start range unix time + * end (OPTIONAL): end range unix time + + If start is not defined, 5, 30, 365 days ago are used as defaults. + If end is not defined, now is used. + NOTES: + Data is limited to 170 data points per query (1 week of data per hour) + If data points miss then previous data point is still valid (no changes) + + RESPONSE + ```js + { + timeFrom: number; // First data point date + timeTo: number; // Last data point date + data: { + time: number; // Time start date for data point + high: number; // Max price for range + low: number; // Min price for range + open: number; // Start price for range + close: number; // End price for range + volumeFrom: number; // Total Supply of Assets: Unit fromSym + volumeTo: number; // Total Supply of Assets / open: Unit toSym + }[]; + } + ``` + ## Reference Implementation You can find all the contracts and interfaces in the Paima Engine codebase [here](https://github.com/PaimaStudios/paima-engine/blob/master/packages/contracts/evm-contracts/contracts/orderbook/). From 48bc93006337c14bf3bef24bfb875a9defdce07a Mon Sep 17 00:00:00 2001 From: Edward Alvarado Date: Mon, 27 May 2024 12:50:14 -0400 Subject: [PATCH 13/20] minor fixes --- PRCS/prc-4.md | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/PRCS/prc-4.md b/PRCS/prc-4.md index 81bbf0e..7f97f4f 100644 --- a/PRCS/prc-4.md +++ b/PRCS/prc-4.md @@ -154,7 +154,7 @@ The DEX contract on the base chain only facilitates the trading (transferring) o These endpoints are provided by the game node to allow external sites generate a frontend for the DEX. -1. Get game assets and metadata. +1. Get Game Assets and Metadata. `GET /dex/` @@ -175,7 +175,7 @@ These endpoints are provided by the game node to allow external sites generate a } ``` -4. Returns Asset Information. +2. Get Asset information. `GET /dex/{asset}` @@ -189,10 +189,9 @@ These endpoints are provided by the game node to allow external sites generate a ``` -2. Get array of ERC1155 tokens of `asset` for specified `user` that have been minted or owned in a valid way --- meaning the `amount` and `tokenId`/`userTokenId` combination matches. This is used by the DEX to get the list of valid assets that user is able to create sell orders for. +3. Get ERC1155 tokens of `asset` for the specified `wallet` that have been minted or owned in a valid way. This is used by the DEX to get the list of valid assets that user is able to create sell orders for. - `GET /dex/{asset}/user_valid_minted_assets/{wallet}` + `GET /dex/{asset}/wallet/{wallet}` * asset: valid name for specific game asset token. * wallet_address: wallet to query for asset @@ -208,8 +207,7 @@ These endpoints are provided by the game node to allow external sites generate a } ``` -3. Gets array of created sell orders that are valid -- meaning they have been created with valid minted assets. -This is used by the DEX to get the list of valid sell orders to display to users wanting to buy. Ordered by lowest price. +4. Gets valid created Sell Orders. This is used by the DEX to get the list of valid Sell Orders to display to users wanting to buy. Ordered by lowest price. `GET /dex/{asset}/orders?seller=wallet&page=number&limit=number` @@ -231,7 +229,7 @@ This is used by the DEX to get the list of valid sell orders to display to users } ``` -5. Historical data. Allows the UI to draw a chart with historical values. +5. Get Asset Historical data. Allows the UI to draw a chart with historical values. `Get /dex/{asset}/historical_price?freq=string&start=number&end=number` From 58324307a8f73a31a8932e766e31240e538b5db5 Mon Sep 17 00:00:00 2001 From: Edward Alvarado Date: Mon, 27 May 2024 13:06:42 -0400 Subject: [PATCH 14/20] volumeFrom/To --- PRCS/prc-4.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/PRCS/prc-4.md b/PRCS/prc-4.md index 7f97f4f..47490c8 100644 --- a/PRCS/prc-4.md +++ b/PRCS/prc-4.md @@ -231,7 +231,7 @@ These endpoints are provided by the game node to allow external sites generate a 5. Get Asset Historical data. Allows the UI to draw a chart with historical values. - `Get /dex/{asset}/historical_price?freq=string&start=number&end=number` + `GET /dex/{asset}/historical_price?freq=string&start=number&end=number` * asset: valid name for specific game asset token. * freq (OPTIONAL): hour | day | month - range for specific (default: hour) @@ -255,8 +255,8 @@ These endpoints are provided by the game node to allow external sites generate a low: number; // Min price for range open: number; // Start price for range close: number; // End price for range - volumeFrom: number; // Total Supply of Assets: Unit fromSym - volumeTo: number; // Total Supply of Assets / open: Unit toSym + volumeFrom: number; // Total Supply of Assets (at `time`) in fromSym Units + volumeTo: number; // Total Supply of Assets (at `time`) in toSym Units }[]; } ``` From e493f6cb13b5f1b4d8fca319f6d09dadfebf0b38 Mon Sep 17 00:00:00 2001 From: Edward Alvarado Date: Tue, 28 May 2024 10:40:32 -0400 Subject: [PATCH 15/20] Update PRCS/prc-4.md Co-authored-by: Matej Poklemba --- PRCS/prc-4.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PRCS/prc-4.md b/PRCS/prc-4.md index 47490c8..7404db2 100644 --- a/PRCS/prc-4.md +++ b/PRCS/prc-4.md @@ -194,7 +194,7 @@ These endpoints are provided by the game node to allow external sites generate a `GET /dex/{asset}/wallet/{wallet}` * asset: valid name for specific game asset token. - * wallet_address: wallet to query for asset + * wallet: wallet to query for asset RESPONSE ```js From ce28f693857e60fe272e28f0e9d3b430443f9013 Mon Sep 17 00:00:00 2001 From: Edward Alvarado Date: Mon, 3 Jun 2024 13:21:00 -0400 Subject: [PATCH 16/20] Updated PRC4 Assets --- PRCS/prc-4.md | 63 +++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 54 insertions(+), 9 deletions(-) diff --git a/PRCS/prc-4.md b/PRCS/prc-4.md index 7404db2..b54a7e6 100644 --- a/PRCS/prc-4.md +++ b/PRCS/prc-4.md @@ -162,19 +162,55 @@ These endpoints are provided by the game node to allow external sites generate a ```js { assets: { - code: string; // Asset Code - description: string; // Asset Description - fromSym: string; // Name of base Asset - toSym: string; // Name of unit to convert - }[], + asset: string; // Asset Code + name?: string; // OPTIONAL Asset Name + description?: string; // OPTIONAL Asset Description + fromSym: string; // Name of base Asset + toSym: string; // Name of unit to convert + contractAsset: string; // Contract Address for Asset (IERC1155) + contractDex: string; // Contract Address for Dex (OrderbookDex) + contractChain: string; // CAIP2 Chain Identifier + image?: string; // OPTIONAL Asset URL Image (1:1 200px Image) + }[], game: { - id: string // Game ID - name?: string // Optional Game Name - version?: string // Optional Game Version + id: string; // Game ID + name?: string; // Optional Game Name + version?: string; // Optional Game Version } } ``` + RESPONSE Example + ```js + { + assets: [{ + asset: 'gold', + name: 'Game Gold', + description: 'Purchase Items with GG', + fromSym: 'GG', + toSym: 'ETH', + contractAsset: '0x1111', + contractDex: '0xaaaa', + contractChain: 'eip155:1', + image: 'https://game-assets/gg.png' + }, { + asset: 'silver', + name 'Game Silver', + description: 'Purchase Magic with GS', + fromSym: 'GS', + toSym: 'ETH', + contractAsset: '0x2222', + contractDex: '0xbbbb', + contractChain: 'eip155:42161', + image: 'https://game-assets/gs.png' + }], + game: { + id: 'my-game', + name: 'My Game', + version: '1.0.0' + } + } + 2. Get Asset information. `GET /dex/{asset}` @@ -184,7 +220,16 @@ These endpoints are provided by the game node to allow external sites generate a RESPONSE ```js { - totalSupply: number; // Total number of assets + asset: string; // Asset Code + name?: string; // OPTIONAL Asset Name + description?: string; // OPTIONAL Asset Description + fromSym: string; // Name of base Asset + toSym: string; // Name of unit to convert + contractAsset: string; // Contract Address for Asset (IERC1155) + contractDex: string; // Contract Address for Dex (OrderbookDex) + contractChain: string; // CAIP2 Chain Identifier + image?: string; // OPTIONAL Asset URL Image (1:1 200px Image) + totalSupply: number; // Total number of assets } ``` From 7daaa6a520edbc05c76ee301c8e1580889fbb67e Mon Sep 17 00:00:00 2001 From: Matej Poklemba Date: Mon, 10 Jun 2024 16:07:53 +0200 Subject: [PATCH 17/20] Format --- PRCS/prc-4.md | 284 ++++++++++++++++++++++++++------------------------ 1 file changed, 147 insertions(+), 137 deletions(-) diff --git a/PRCS/prc-4.md b/PRCS/prc-4.md index b54a7e6..907837f 100644 --- a/PRCS/prc-4.md +++ b/PRCS/prc-4.md @@ -156,155 +156,165 @@ These endpoints are provided by the game node to allow external sites generate a 1. Get Game Assets and Metadata. - `GET /dex/` - - RESPONSE - ```js - { - assets: { - asset: string; // Asset Code - name?: string; // OPTIONAL Asset Name - description?: string; // OPTIONAL Asset Description - fromSym: string; // Name of base Asset - toSym: string; // Name of unit to convert - contractAsset: string; // Contract Address for Asset (IERC1155) - contractDex: string; // Contract Address for Dex (OrderbookDex) - contractChain: string; // CAIP2 Chain Identifier - image?: string; // OPTIONAL Asset URL Image (1:1 200px Image) - }[], - game: { - id: string; // Game ID - name?: string; // Optional Game Name - version?: string; // Optional Game Version - } - } - ``` - - RESPONSE Example - ```js - { - assets: [{ - asset: 'gold', - name: 'Game Gold', - description: 'Purchase Items with GG', - fromSym: 'GG', - toSym: 'ETH', - contractAsset: '0x1111', - contractDex: '0xaaaa', - contractChain: 'eip155:1', - image: 'https://game-assets/gg.png' - }, { - asset: 'silver', - name 'Game Silver', - description: 'Purchase Magic with GS', - fromSym: 'GS', - toSym: 'ETH', - contractAsset: '0x2222', - contractDex: '0xbbbb', - contractChain: 'eip155:42161', - image: 'https://game-assets/gs.png' - }], - game: { - id: 'my-game', - name: 'My Game', - version: '1.0.0' - } - } + `GET /dex/` + + RESPONSE + + ```js + { + assets: { + asset: string; // Asset Code + name?: string; // OPTIONAL Asset Name + description?: string; // OPTIONAL Asset Description + fromSym: string; // Name of base Asset + toSym: string; // Name of unit to convert + contractAsset: string; // Contract Address for Asset (IERC1155) + contractDex: string; // Contract Address for Dex (OrderbookDex) + contractChain: string; // CAIP2 Chain Identifier + image?: string; // OPTIONAL Asset URL Image (1:1 200px Image) + }[], + game: { + id: string; // Game ID + name?: string; // Optional Game Name + version?: string; // Optional Game Version + } + } + ``` + + RESPONSE Example + + ```js + { + assets: [{ + asset: 'gold', + name: 'Game Gold', + description: 'Purchase Items with GG', + fromSym: 'GG', + toSym: 'ETH', + contractAsset: '0x1111', + contractDex: '0xaaaa', + contractChain: 'eip155:1', + image: 'https://game-assets/gg.png' + }, { + asset: 'silver', + name 'Game Silver', + description: 'Purchase Magic with GS', + fromSym: 'GS', + toSym: 'ETH', + contractAsset: '0x2222', + contractDex: '0xbbbb', + contractChain: 'eip155:42161', + image: 'https://game-assets/gs.png' + }], + game: { + id: 'my-game', + name: 'My Game', + version: '1.0.0' + } + } + + ``` 2. Get Asset information. - `GET /dex/{asset}` - - * asset: valid name for specific game asset token. - - RESPONSE - ```js - { - asset: string; // Asset Code - name?: string; // OPTIONAL Asset Name - description?: string; // OPTIONAL Asset Description - fromSym: string; // Name of base Asset - toSym: string; // Name of unit to convert - contractAsset: string; // Contract Address for Asset (IERC1155) - contractDex: string; // Contract Address for Dex (OrderbookDex) - contractChain: string; // CAIP2 Chain Identifier - image?: string; // OPTIONAL Asset URL Image (1:1 200px Image) - totalSupply: number; // Total number of assets - } - ``` + `GET /dex/{asset}` + + - asset: valid name for specific game asset token. + RESPONSE + + ```js + { + asset: string; // Asset Code + name?: string; // OPTIONAL Asset Name + description?: string; // OPTIONAL Asset Description + fromSym: string; // Name of base Asset + toSym: string; // Name of unit to convert + contractAsset: string; // Contract Address for Asset (IERC1155) + contractDex: string; // Contract Address for Dex (OrderbookDex) + contractChain: string; // CAIP2 Chain Identifier + image?: string; // OPTIONAL Asset URL Image (1:1 200px Image) + totalSupply: number; // Total number of assets + } + ``` 3. Get ERC1155 tokens of `asset` for the specified `wallet` that have been minted or owned in a valid way. This is used by the DEX to get the list of valid assets that user is able to create sell orders for. - `GET /dex/{asset}/wallet/{wallet}` + `GET /dex/{asset}/wallet/{wallet}` - * asset: valid name for specific game asset token. - * wallet: wallet to query for asset + - asset: valid name for specific game asset token. + - wallet: wallet to query for asset - RESPONSE - ```js - { - total: number // Total number of assets owned - stats: { - tokenId: number; // ERC1155 Token ID - amount: number; // Number of assets owned - }[]; - } - ``` - -4. Gets valid created Sell Orders. This is used by the DEX to get the list of valid Sell Orders to display to users wanting to buy. Ordered by lowest price. - - `GET /dex/{asset}/orders?seller=wallet&page=number&limit=number` - - * asset: valid name for specific game asset token. - * seller (OPTIONAL): fetch where wallet address matches wallet - * page (OPTIONAL): results page number, default = 1 - * limit (OPTIONAL): 10, 25, 50, 100, default = 25 - - RESPONSE - ```js - { - stats: { - orderId: number; // Order unique ID - seller: string; // Seller wallet - tokenId: number; // ERC1155 TokenID - amount: number; // Number of assets for sale - price: string; // Price per asset - }[]; - } - ``` + RESPONSE + + ```js + { + total: number; // Total number of assets owned + stats: { + tokenId: number; // ERC1155 Token ID + amount: number; // Number of assets owned + } + []; + } + ``` + +4. Gets valid created Sell Orders. This is used by the DEX to get the list of valid Sell Orders to display to users wanting to buy. Ordered by lowest price. + + `GET /dex/{asset}/orders?seller=wallet&page=number&limit=number` + + - asset: valid name for specific game asset token. + - seller (OPTIONAL): fetch where wallet address matches wallet + - page (OPTIONAL): results page number, default = 1 + - limit (OPTIONAL): 10, 25, 50, 100, default = 25 + + RESPONSE + + ```js + { + stats: { + orderId: number; // Order unique ID + seller: string; // Seller wallet + tokenId: number; // ERC1155 TokenID + amount: number; // Number of assets for sale + price: string; // Price per asset + } + []; + } + ``` 5. Get Asset Historical data. Allows the UI to draw a chart with historical values. - `GET /dex/{asset}/historical_price?freq=string&start=number&end=number` - - * asset: valid name for specific game asset token. - * freq (OPTIONAL): hour | day | month - range for specific (default: hour) - * start (OPTIONAL): start range unix time - * end (OPTIONAL): end range unix time - - If start is not defined, 5, 30, 365 days ago are used as defaults. - If end is not defined, now is used. - NOTES: - Data is limited to 170 data points per query (1 week of data per hour) - If data points miss then previous data point is still valid (no changes) - - RESPONSE - ```js - { - timeFrom: number; // First data point date - timeTo: number; // Last data point date - data: { - time: number; // Time start date for data point - high: number; // Max price for range - low: number; // Min price for range - open: number; // Start price for range - close: number; // End price for range - volumeFrom: number; // Total Supply of Assets (at `time`) in fromSym Units - volumeTo: number; // Total Supply of Assets (at `time`) in toSym Units - }[]; - } - ``` + `GET /dex/{asset}/historical_price?freq=string&start=number&end=number` + + - asset: valid name for specific game asset token. + - freq (OPTIONAL): hour | day | month - range for specific (default: hour) + - start (OPTIONAL): start range unix time + - end (OPTIONAL): end range unix time + + If start is not defined, 5, 30, 365 days ago are used as defaults. + If end is not defined, now is used. + NOTES: + Data is limited to 170 data points per query (1 week of data per hour) + If data points miss then previous data point is still valid (no changes) + + RESPONSE + + ```js + { + timeFrom: number; // First data point date + timeTo: number; // Last data point date + data: { + time: number; // Time start date for data point + high: number; // Max price for range + low: number; // Min price for range + open: number; // Start price for range + close: number; // End price for range + volumeFrom: number; // Total Supply of Assets (at `time`) in fromSym Units + volumeTo: number; // Total Supply of Assets (at `time`) in toSym Units + } + []; + } + ``` ## Reference Implementation From a7b6f3c8ec88c00de595827524372fd17214622c Mon Sep 17 00:00:00 2001 From: Matej Poklemba Date: Mon, 10 Jun 2024 16:08:27 +0200 Subject: [PATCH 18/20] Add support for multiple assets and fee system --- PRCS/prc-4.md | 148 +++++++++++++++++++++++++++++++++++++------------- 1 file changed, 111 insertions(+), 37 deletions(-) diff --git a/PRCS/prc-4.md b/PRCS/prc-4.md index 907837f..b3c5f91 100644 --- a/PRCS/prc-4.md +++ b/PRCS/prc-4.md @@ -23,9 +23,18 @@ Every PRC-4 compliant contract must implement the `IOrderbookDex` interface and ```solidity import {IERC1155Receiver} from "@openzeppelin/contracts/token/ERC1155/IERC1155Receiver.sol"; -/// @notice Facilitates base-chain trading of an asset that is living on a different app-chain. -/// @dev Orders are identified by a unique incremental `orderId`. +/// @notice Facilitates base-chain trading of assets that are living on a different app-chain. +/// @dev Orders are identified by an asset-specific unique incremental `orderId`. interface IOrderbookDex is IERC1155Receiver { + struct FeeInfo { + /// @dev The maker fee collected from the seller when a sell order is filled. Expressed in basis points. + uint256 makerFee; + /// @dev The taker fee collected from the buyer when a sell order is filled. Expressed in basis points. + uint256 takerFee; + /// @dev Flag indicating whether the fees are set. + bool set; + } + struct Order { /// @dev The asset's unique token identifier. uint256 assetId; @@ -35,97 +44,160 @@ interface IOrderbookDex is IERC1155Receiver { uint256 pricePerAsset; /// @dev The seller's address. address payable seller; + /// @dev The maker fee in basis points, set when order is created, defined by the asset's fee info. + uint256 makerFee; + /// @dev The taker fee in basis points, set when order is created, defined by the asset's fee info. + uint256 takerFee; } - /// @param seller The seller's address. - /// @param orderId The order's unique identifier. + /// @param asset The asset's address. /// @param assetId The asset's unique token identifier. + /// @param orderId The order's asset-specific unique identifier. + /// @param seller The seller's address. /// @param assetAmount The amount of the asset that has been put for sale. /// @param pricePerAsset The requested price per one unit of asset. + /// @param makerFee The maker fee in basis points. + /// @param takerFee The taker fee in basis points. event OrderCreated( - address indexed seller, - uint256 indexed orderId, + address indexed asset, uint256 indexed assetId, + uint256 indexed orderId, + address seller, uint256 assetAmount, - uint256 pricePerAsset + uint256 pricePerAsset, + uint256 makerFee, + uint256 takerFee ); + /// @param asset The asset's address. + /// @param assetId The asset's unique token identifier. + /// @param orderId The order's asset-specific unique identifier. /// @param seller The seller's address. - /// @param orderId The order's unique identifier. /// @param buyer The buyer's address. /// @param assetAmount The amount of the asset that was traded. /// @param pricePerAsset The price per one unit of asset that was paid. + /// @param makerFeeCollected The maker fee in native tokens that was collected. + /// @param takerFeeCollected The taker fee in native tokens that was collected. event OrderFilled( - address indexed seller, + address indexed asset, + uint256 indexed assetId, uint256 indexed orderId, - address indexed buyer, + address seller, + address buyer, uint256 assetAmount, - uint256 pricePerAsset + uint256 pricePerAsset, + uint256 makerFeeCollected, + uint256 takerFeeCollected ); - /// @param seller The seller's address. - /// @param id The order's unique identifier. - event OrderCancelled(address indexed seller, uint256 indexed id); + /// @param asset The asset's address. + /// @param assetId The asset's unique token identifier. + /// @param orderId The order's asset-specific unique identifier. + event OrderCancelled(address indexed asset, uint256 indexed assetId, uint256 indexed orderId); + + /// @param receiver The address that received the fees. + /// @param amount The amount of fees that were withdrawn. + event FeesWithdrawn(address indexed receiver, uint256 amount); + + /// @notice The `orderId` of the next sell order for specific `asset`. + function currentOrderId(address asset) external view returns (uint256); + + /// @notice The default maker fee, used if fee information for asset is not set. + function defaultMakerFee() external view returns (uint256); + + /// @notice The default taker fee, used if fee information for asset is not set. + function defaultTakerFee() external view returns (uint256); - /// @notice The address of the asset that is being traded in this DEX contract. - function asset() external view returns (address); + /// @notice The maximum fee, maker/taker fees cannot be set to exceed this amount. + function maxFee() external view returns (uint256); - /// @notice The `orderId` of the next sell order. - function currentOrderId() external view returns (uint256); + /// @notice The fee information of `asset`. + function getAssetFeeInfo(address asset) external view returns (FeeInfo memory); - /// @notice Returns the Order struct information about an order identified by the `orderId`. - function getOrder(uint256 orderId) external view returns (Order memory); + /// @notice Returns the asset fees if set, otherwise returns the default fees. + function getAssetAppliedFees( + address asset + ) external view returns (uint256 makerFee, uint256 takerFee); - /// @notice Creates a sell order for the `assetAmount` of `assetId` at `pricePerAsset`. - /// @dev The order information is saved in a mapping `orderId -> Order`, with `orderId` being a unique incremental identifier. - /// MUST transfer the `assetAmount` of `assetId` from the seller to the contract. + /// @notice Set the fee information of `asset`. Executable only by the owner. + /// @dev MUST revert if `makerFee` or `takerFee` exceeds `maxFee`. + /// MUST revert if called by unauthorized account. + function setAssetFeeInfo(address asset, uint256 makerFee, uint256 takerFee) external; + + /// @notice Set the default fee information that is used if fee information for asset is not set. Executable only by the owner. + /// @dev MUST revert if `makerFee` or `takerFee` exceeds `maxFee`. + /// MUST revert if called by unauthorized account. + function setDefaultFeeInfo(uint256 makerFee, uint256 takerFee) external; + + /// @notice Returns the Order struct information about an order identified by the `orderId` for specific `asset`. + function getOrder(address asset, uint256 orderId) external view returns (Order memory); + + /// @notice Creates a sell order for the `assetAmount` of `asset` with ID `assetId` at `pricePerAsset`. + /// @dev The order information is saved in a mapping `asset -> orderId -> Order`, with `orderId` being an asset-specific unique incremental identifier. + /// MUST transfer the `assetAmount` of `asset` with ID `assetId` from the seller to the contract. /// MUST emit `OrderCreated` event. - /// @return The unique identifier of the created order. + /// @return The asset-specific unique identifier of the created order. function createSellOrder( + address asset, uint256 assetId, uint256 assetAmount, uint256 pricePerAsset ) external returns (uint256); - /// @notice Creates a batch of sell orders for the `assetAmount` of `assetId` at `pricePerAsset`. + /// @notice Creates a batch of sell orders for the `assetAmount` of `asset` with ID `assetId` at `pricePerAsset`. /// @dev This is a batched version of `createSellOrder` that simply iterates through the arrays to call said function. - /// @return The unique identifiers of the created orders. + /// @return The asset-specific unique identifiers of the created orders. function createBatchSellOrder( + address asset, uint256[] memory assetIds, uint256[] memory assetAmounts, uint256[] memory pricesPerAssets ) external returns (uint256[] memory); - /// @notice Consecutively fills an array of orders identified by the `orderId` of each order, + /// @notice Consecutively fills an array of orders of `asset` identified by the asset-specific `orderId` of each order, /// by providing an exact amount of ETH and requesting a specific minimum amount of asset to receive. /// @dev Transfers portions of msg.value to the orders' sellers according to the price. /// The sum of asset amounts of filled orders MUST be at least `minimumAsset`. /// If msg.value is more than the sum of orders' prices, it MUST refund the excess back to `msg.sender`. /// MUST decrease the `assetAmount` parameter for the specified order according to how much of it was filled, - /// and transfer that amount of the order's `assetId` to the buyer. + /// and transfer that amount of the order's `asset` with ID `assetId` to the buyer. /// MUST emit `OrderFilled` event for each order accordingly. - function fillOrdersExactEth(uint256 minimumAsset, uint256[] memory orderIds) external payable; + function fillOrdersExactEth( + address asset, + uint256 minimumAsset, + uint256[] memory orderIds + ) external payable; - /// @notice Consecutively fills an array of orders identified by the `orderId` of each order, + /// @notice Consecutively fills an array of orders identified by the asset-specific `orderId` of each order, /// by providing a possibly surplus amount of ETH and requesting an exact amount of asset to receive. /// @dev Transfers portions of msg.value to the orders' sellers according to the price. /// The sum of asset amounts of filled orders MUST be exactly `assetAmount`. Excess ETH MUST be returned back to `msg.sender`. /// MUST decrease the `assetAmount` parameter for the specified order according to how much of it was filled, - /// and transfer that amount of the order's `assetId` to the buyer. + /// and transfer that amount of the order's `asset` with ID `assetId` to the buyer. /// MUST emit `OrderFilled` event for each order accordingly. /// If msg.value is more than the sum of orders' prices, it MUST refund the difference back to `msg.sender`. - function fillOrdersExactAsset(uint256 assetAmount, uint256[] memory orderIds) external payable; + function fillOrdersExactAsset( + address asset, + uint256 assetAmount, + uint256[] memory orderIds + ) external payable; - /// @notice Cancels the sell order identified by the `orderId`, transferring the order's assets back to the seller. + /// @notice Cancels the sell order of `asset` with asset-specific `orderId`, transferring the order's assets back to the seller. /// @dev MUST revert if the order's seller is not `msg.sender`. /// MUST change the `assetAmount` parameter for the specified order to `0`. /// MUST emit `OrderCancelled` event. - /// MUST transfer the `assetAmount` of `assetId` back to the seller. - function cancelSellOrder(uint256 orderId) external; + /// MUST transfer the order's `assetAmount` of `asset` with `assetId` back to the seller. + function cancelSellOrder(address asset, uint256 orderId) external; - /// @notice Cancels a batch of sell orders identified by the `orderIds`, transferring the orders' assets back to the seller. + /// @notice Cancels a batch of sell orders of `asset` with asset-specific `orderIds`, transferring the orders' assets back to the seller. /// @dev This is a batched version of `cancelSellOrder` that simply iterates through the array to call said function. - function cancelBatchSellOrder(uint256[] memory orderIds) external; + function cancelBatchSellOrder(address asset, uint256[] memory orderIds) external; + + /// @notice Withdraws the contract balance (containing collected fees) to the owner. Executable only by the owner. + /// @dev MUST transfer the entire contract balance to the owner. + /// MUST revert if called by unauthorized account. + /// MUST emit `FeesWithdrawn` event. + function withdrawFees() external; } ``` @@ -142,6 +214,8 @@ It's not an AMM. In a typical AMM dex, there are liquidity pools and you trade a - `uint256 assetAmount` - amount of the game asset the seller is selling, - `uint256 pricePerAsset` - the price in native gas tokens of the base chain the seller is requesting, - `address seller` - the seller's address. + - `uint256 makerFee` - maker fee expressed in basis points, depicting the ratio of payment tokens that will be deducted from the payment to the seller + - `uint256 takerFee` - taker fee expressed in basis points, depicting the ratio of payment tokens that are added to the purchase cost of the order - Function to cancel a sell order of specified `orderId` - Batch functions `createBatchSellOrder` and `cancelBatchSellOrder` of the abovementioned functions - Function to fill sell orders (in other words - buy). There is no "buy order" that persists, and rather the buy function directly transfers the value specified by price defined in the orders to the sellers. There are 2 variations of fill function: From e87f5552876610a3bc1e6518e2ef6507f8b5512c Mon Sep 17 00:00:00 2001 From: Matej Poklemba Date: Tue, 11 Jun 2024 11:23:22 +0200 Subject: [PATCH 19/20] Update PRCS/prc-4.md Co-authored-by: Edward Alvarado --- PRCS/prc-4.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/PRCS/prc-4.md b/PRCS/prc-4.md index b3c5f91..ceba101 100644 --- a/PRCS/prc-4.md +++ b/PRCS/prc-4.md @@ -351,6 +351,9 @@ These endpoints are provided by the game node to allow external sites generate a tokenId: number; // ERC1155 TokenID amount: number; // Number of assets for sale price: string; // Price per asset + asset: string; // Asset address + makerFee: number; // Maker Fee + takerFee: number; // Taker Fee } []; } From f4a2e38cdb66286930a1d9af02bb7d182635182f Mon Sep 17 00:00:00 2001 From: Matej Poklemba Date: Mon, 24 Jun 2024 09:24:17 +0200 Subject: [PATCH 20/20] Claim-based system etc. --- PRCS/prc-4.md | 73 +++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 65 insertions(+), 8 deletions(-) diff --git a/PRCS/prc-4.md b/PRCS/prc-4.md index ceba101..b35cf21 100644 --- a/PRCS/prc-4.md +++ b/PRCS/prc-4.md @@ -48,8 +48,19 @@ interface IOrderbookDex is IERC1155Receiver { uint256 makerFee; /// @dev The taker fee in basis points, set when order is created, defined by the asset's fee info. uint256 takerFee; + /// @dev The order creation fee paid by the seller when creating the order, refunded if sell order is cancelled. + uint256 creationFeePaid; } + /// @param user The address that claimed the balance. + /// @param amount The amount that was claimed. + event BalanceClaimed(address indexed user, uint256 amount); + + /// @param asset The asset's address (zero address if changing default fees). + /// @param makerFee The new maker fee in basis points. + /// @param takerFee The new taker fee in basis points. + event FeeInfoChanged(address indexed asset, uint256 makerFee, uint256 takerFee); + /// @param asset The asset's address. /// @param assetId The asset's unique token identifier. /// @param orderId The order's asset-specific unique identifier. @@ -95,19 +106,35 @@ interface IOrderbookDex is IERC1155Receiver { /// @param orderId The order's asset-specific unique identifier. event OrderCancelled(address indexed asset, uint256 indexed assetId, uint256 indexed orderId); + /// @param oldFee The old fee value. + /// @param newFee The new fee value. + event OrderCreationFeeChanged(uint256 oldFee, uint256 newFee); + /// @param receiver The address that received the fees. /// @param amount The amount of fees that were withdrawn. event FeesWithdrawn(address indexed receiver, uint256 amount); + /// @notice The balance of `user` that's claimable by `claim` function. + function balances(address user) external view returns (uint256); + + /// @notice Withdraw the claimable balance of the caller. + function claim() external; + /// @notice The `orderId` of the next sell order for specific `asset`. function currentOrderId(address asset) external view returns (uint256); + /// @notice The total amount of fees collected by the contract. + function collectedFees() external view returns (uint256); + /// @notice The default maker fee, used if fee information for asset is not set. function defaultMakerFee() external view returns (uint256); /// @notice The default taker fee, used if fee information for asset is not set. function defaultTakerFee() external view returns (uint256); + /// @notice The flat fee paid by the seller when creating a sell order, to prevent spam. + function orderCreationFee() external view returns (uint256); + /// @notice The maximum fee, maker/taker fees cannot be set to exceed this amount. function maxFee() external view returns (uint256); @@ -129,30 +156,36 @@ interface IOrderbookDex is IERC1155Receiver { /// MUST revert if called by unauthorized account. function setDefaultFeeInfo(uint256 makerFee, uint256 takerFee) external; + /// @notice Set the flat fee paid by the seller when creating a sell order, to prevent spam. Executable only by the owner. + /// @dev MUST revert if called by unauthorized account. + function setOrderCreationFee(uint256 fee) external; + /// @notice Returns the Order struct information about an order identified by the `orderId` for specific `asset`. function getOrder(address asset, uint256 orderId) external view returns (Order memory); - /// @notice Creates a sell order for the `assetAmount` of `asset` with ID `assetId` at `pricePerAsset`. + /// @notice Creates a sell order for the `assetAmount` of `asset` with ID `assetId` at `pricePerAsset`. Requires payment of `orderCreationFee`. /// @dev The order information is saved in a mapping `asset -> orderId -> Order`, with `orderId` being an asset-specific unique incremental identifier. /// MUST transfer the `assetAmount` of `asset` with ID `assetId` from the seller to the contract. /// MUST emit `OrderCreated` event. + /// MUST revert if `msg.value` is less than `orderCreationFee`. /// @return The asset-specific unique identifier of the created order. function createSellOrder( address asset, uint256 assetId, uint256 assetAmount, uint256 pricePerAsset - ) external returns (uint256); + ) external payable returns (uint256); - /// @notice Creates a batch of sell orders for the `assetAmount` of `asset` with ID `assetId` at `pricePerAsset`. + /// @notice Creates a batch of sell orders for the `assetAmount` of `asset` with ID `assetId` at `pricePerAsset`. Requires payment of `orderCreationFee` times the amount of orders. /// @dev This is a batched version of `createSellOrder` that simply iterates through the arrays to call said function. + /// MUST revert if `msg.value` is less than `orderCreationFee * assetIds.length`. /// @return The asset-specific unique identifiers of the created orders. function createBatchSellOrder( address asset, uint256[] memory assetIds, uint256[] memory assetAmounts, uint256[] memory pricesPerAssets - ) external returns (uint256[] memory); + ) external payable returns (uint256[] memory); /// @notice Consecutively fills an array of orders of `asset` identified by the asset-specific `orderId` of each order, /// by providing an exact amount of ETH and requesting a specific minimum amount of asset to receive. @@ -203,7 +236,7 @@ interface IOrderbookDex is IERC1155Receiver { ## Rationale -It's not an AMM. In a typical AMM dex, there are liquidity pools and you trade against these pools. This is unsuitable for this use-case because a contract on base chain has no means of knowing the state of the game chain so the contract cannot automate market making. Instead, buyers have to be sending tokens directly to the sellers. +It's not an AMM. In a typical AMM dex, there are liquidity pools and you trade against these pools. This is unsuitable for this use-case because a contract on base chain has no means of knowing the state of the game chain so the contract cannot automate market making. Instead, buyers have to be explicitely buying only those tokens that they deem to be valid. ### The idea: @@ -216,13 +249,37 @@ It's not an AMM. In a typical AMM dex, there are liquidity pools and you trade a - `address seller` - the seller's address. - `uint256 makerFee` - maker fee expressed in basis points, depicting the ratio of payment tokens that will be deducted from the payment to the seller - `uint256 takerFee` - taker fee expressed in basis points, depicting the ratio of payment tokens that are added to the purchase cost of the order + - `uint256 creationFeePaid` - fee paid by the seller when creating the order, refunded if sell order is cancelled + - This fee exists to deter from an attack of creating thousands of extremely small sell orders, which would cost large amounts of gas to fill. - Function to cancel a sell order of specified `orderId` - Batch functions `createBatchSellOrder` and `cancelBatchSellOrder` of the abovementioned functions - - Function to fill sell orders (in other words - buy). There is no "buy order" that persists, and rather the buy function directly transfers the value specified by price defined in the orders to the sellers. There are 2 variations of fill function: + - Function to fill sell orders (in other words - buy). There is no "buy order" that persists, and rather the buy function directly executes the sell order fill and transfers the value specified by price defined in the orders to the contract and attributing it to the sellers balance mapping. There are 2 variations of fill function: - `fillOrdersExactEth` - consecutively fills the array of specified orders until all provided ETH is spent and a specified minimum amount of asset has been achieved. Example: You want to buy as much asset as possible for 1 ETH. - `fillOrdersExactAsset` - consecutively fills the array of specified orders until specified exact amount of asset has been achieved, and returns the excess ETH back to the sender. Example: You want to buy 1000 units of asset as cheaply as possible. -The DEX contract on the base chain only facilitates the trading (transferring) of existing Inverse Projected ERC1155 assets, it does not make any assurances about validity of such assets. That aspect is handled by the feature set of the Inverse Projected ERC1155 standard itself. The responsibility of querying the API of game chain to check the validity of the sell order is left up to the front-end providers, as well as the presentation of the possible sell orders. +The DEX contract on the base chain only facilitates the trading (transferring) of existing Inverse Projected ERC1155 assets, it does not make any assurances about validity of such assets. That aspect is handled by the feature set of the Inverse Projected ERC1155 standard itself. The responsibility of querying the API of game chain to check the validity of the sell order is left up to the front-end providers, as well as the presentation of the valid sell orders. + +### Claim-based system of payments + +When sell orders are filled, payments attributed to the sellers are **not** transferred directly in the fill transaction, but rather a balance value in the contract is updated for each seller. A seller can then claim their balance via the `claim` function and get their balance transferred to them. + +This was done because if there were direct transfers to the sellers in the fill transaction, there would be two problems with that: + +1. When transferring eth to the seller, all gas is forwarded. A malicious smart contract seller could be consuming large amounts of gas for arbitrary execution when receiving eth. +2. Any one of those transfers can fail, and that would revert the transaction. Why the transfer would fail - for example somebody might make a malicious smart contract that always fails on receiving eth and create sell orders with that contract. + +There are some other options on how to fix the: + +- first issue: + - Set gas limit to some value, possibly calculated to result in the sell order creation fee + - Not great because there might be a legit reason why seller should consume gas over the limit (smart contract wallet, any smart contracts built on top of the DEX) +- second issue: + - Don't revert on transfer fail, ignore sell order, return payment to buyer + - Not great because an always-reverting (but good priced) sell order would end up being tried to fill over and over by multiple fill requests, unnecessarily wasting gas of buyers + - Don't revert on transfer fail, cancel the sell order, return payment to buyer + - Might potentially be annoying to have sell order cancelled because of a transfer fail + +These fixes are suboptimal in comparison with the fix of adopting a claim-based system. Admittedly, it results in a slightly worse UX for the sellers because they are forced to do one more transaction to get their payment, but the pros dramatically outweight this one con. ## Game Node Dex API (OPTIONAL) @@ -352,7 +409,7 @@ These endpoints are provided by the game node to allow external sites generate a amount: number; // Number of assets for sale price: string; // Price per asset asset: string; // Asset address - makerFee: number; // Maker Fee + makerFee: number; // Maker Fee takerFee: number; // Taker Fee } [];