# Perp DEXes investigation ## Kwenta ![](https://hackmd.io/_uploads/HJjnKZzT3.png) - To open a leveraged position, one needs at least 50 sUSD - Here I tried the trade function to get sUSD from ETH (both tokens are on Optimism) ![](https://hackmd.io/_uploads/H1m99bfT3.png) ![](https://hackmd.io/_uploads/rkXa5-z62.png) ![](https://hackmd.io/_uploads/BkmC9-zah.png)a - Trade tx hash: 0x5c353a865c18be4aeead77c0c57687ec98f211c22867eaf9a069bb099bf8e358 ![](https://hackmd.io/_uploads/SyzepWzp2.png) - Making a long position on ETH/sUSD - At first we need to create a smart margin account - ![](https://hackmd.io/_uploads/rJMMTWzp3.png) - The smart margin account creation tx hash: 0x471c35e859f3b491cd17accf1ad4fc594e0e59a09dea69cf6eaf40c0540144a4 - This tx calls the function `newAccount()` in the `Factory` contract. ![](https://hackmd.io/_uploads/HyUwyfMT3.png) - The function `newAccount()` deploys a new contract called `AccountProxy` (name in Github, but `BakedBeanManager` on Optimism mainnet) taking as constructor the factory. In each `AccountProxy` contract the corresponding factory address is stored in a special slot hardcoded as ```solidity bytes32 internal constant _BEACON_STORAGE_SLOT = bytes32(uint256(keccak256("eip1967.proxy.beacon")) - 1); ``` - The new `AccountProxy` address is then registered inside `Factory` and added to the list of of accounts associating to the user addresses. This means one user has have more than one `AccountProxy` - We will need a database to store these account proxies for each user. - All account proxies use the same implementation stored as a public variable in the `Factory` contract. This implementation is the contract called `Account`. - When the account is created the following functions are called: - `setInitialOwnership`: parameter is the user address - `VERSION`: to get the version number of the current Account implementation - An event is emitted with three parameters: - user address - proxy address - implementation version - This event can be listened by the backend to update the database - Current Account version on Github main branch is 2.0.2, whereas on Optimism mainnet it is 2.1.0 - Opening a long position. What is the fee? ![](https://hackmd.io/_uploads/SyCIAUXT2.png) ![](https://hackmd.io/_uploads/HkgYRIX63.png) - First we need to approve the Account proxy so it can use sUSD from user address - Afterwards we create the long order, tx hash 0x816bfa633935ec066f7e8f3376777bc820cd6d9f490dce065a2dc4e7986e2a1f - Creating the long order calls the function `execute` in `Account`. The function input consists of two lists: one list for command types and second list for command inputs - There are 14 command types ```solidity enum Command { ACCOUNT_MODIFY_MARGIN, // 0 ACCOUNT_WITHDRAW_ETH, PERPS_V2_MODIFY_MARGIN, PERPS_V2_WITHDRAW_ALL_MARGIN, PERPS_V2_SUBMIT_ATOMIC_ORDER, PERPS_V2_SUBMIT_DELAYED_ORDER, // 5 PERPS_V2_SUBMIT_OFFCHAIN_DELAYED_ORDER, PERPS_V2_CLOSE_POSITION, PERPS_V2_SUBMIT_CLOSE_DELAYED_ORDER, PERPS_V2_SUBMIT_CLOSE_OFFCHAIN_DELAYED_ORDER, PERPS_V2_CANCEL_DELAYED_ORDER, // 10 PERPS_V2_CANCEL_OFFCHAIN_DELAYED_ORDER, GELATO_PLACE_CONDITIONAL_ORDER, GELATO_CANCEL_CONDITIONAL_ORDER } ``` - Only the owner can executes the first two types of commands, i.e. `ACCOUNT_MODIFY_MARGIN` and `ACCOUNT_WITHDRAW_ETH` - The first command `ACCOUNT_MODIFY_MARGIN` is used to deposit/withdraw margin to/from the smart margin account. It requires only one input of length 32 bytes, which is then translated to one `int256` variable, called `amount`. If the `amount` is positive, it means deposit sUSD from owner address to the smart margin account SC, i.e. it needs user's approval to transfer the fund. Otherwise if the `amount` is negative, it means withdraw sUSD from the smart margin account SC to user's address, i.e. it will check that there is enough free margin to withdraw. Free margin is the margin account SC's sUSD balance minus the `committedMargin`, where commitedMargin is sUSD used in positions. - The second command `ACCOUNT_WITHDRAW_ETH` allows users to withdraw ETH deposited for keepers fee, i.e. keepers fee is stored in the smart margin account SC. It requires only one input of length 32 bytes, which is then translated to one `uint256` variable, which denotes the amount of ETH to withdraw. - Other commands can be executed by the owners and delegates. - What is a Synthetix PerpsV2 Market? - The 3rd command `PERPS_V2_MODIFY_MARGIN` is used to deposit/withdraw fund from/to the margin account to/from the Synthetix PerpsV2 Market. It requires two inputs, each spans 32 bytes, the first one is the market address, the second one is the amount. - The 4th command `PERPS_V2_WITHDRAW_ALL_MARGIN` is used to (close?) withdraw all margin back to the smart margin contract. It requires only one input, which is the market address. - The 5th command `PERPS_V2_SUBMIT_ATOMIC_ORDER` is used to submit an atomic order to a Synthetix V2 Market. Atomic orders are executed immediately and incur a significant fee. It requires three inputs, the market address, size delta of the order, desired fill price of order. - The 6th command `PERPS_V2_SUBMIT_DELAYED_ORDER` is used to submit an order with a certain waiting time. It requires four inputs, the address of the market, the size delta of the order, the desired time delta, the desired fill price. - The 7th command `PERPS_V2_SUBMIT_OFFCHAIN_DELAYED_ORDER` is used to submit an offchain delayed order to a Synthetix PerpsV2 Market. It requires three inputs, the address of the market, the size delta of the order, the desired fill price. - The 8th command `PERPS_V2_CLOSE_POSITION` is used close Synthetix PerpsV2 Market position via an atomic order. It requires two inputs, the address of the market and desired fill price. - How is the tracking code used? - The 9th command `PERPS_V2_SUBMIT_CLOSE_DELAYED_ORDER` is used to close Synthetix PerpsV2 Market position via a delayed order. It requires three inputs, the market address, the desired time delta, the desired fill price. - The 10th command `PERPS_V2_SUBMIT_CLOSE_OFFCHAIN_DELAYED_ORDER` is used to close Synthetix PerpsV2 Market position via an offchain delayed order. IT requires two inputs, the market address, the desired fill price. - The 11th command `PERPS_V2_CANCEL_DELAYED_ORDER` is used to cancel a pending delayed order from a Synthetix PerpsV2 Market. It requires one ipnut, the market address. - The 12th command `PERPS_V2_CANCEL_OFFCHAIN_DELAYED_ORDER` is used to cancel a pending off-chain delayed order from a Synthetix PerpsV2 Market ## Gainz Network - A lot of important addresses are stored in a smart contract `storageT`, which is an immutable variable - `storageT` has implementation at address `0xaee4d11a16B2bc65EDD6416Fb626EB404a6D65BD` - `GNSPriceAggregatorV6_4` at address `0x126F32723c5FC8DFEB17c46b7B7dD3dCd458A816` - pairsStorage at address `0x6e5326e944F528c243B9Ca5d14fe5C9269a8c922` with implementation `GNSPairsStorageV6` - pool at address `0x151757c2E830C467B28Fe6C09c3174b6c76aA0c5` with implementation `GNSPoolV5` - trading logics at address `0x6d91EDb04166251345071998Cf0Ce546Ae810E17` with implementation `GNSTradingV6_4`, but it seems like there can be more accepted trading contracts as denoted in the mapping `isTradingContract` - pairInfo logics at address `0xEe7442aCcC1C27f2C69423576d3b1D25b563E977` with implementation `GNSPairInfosV6_1` - callbacks logics at address `0x82e59334da8C667797009BBe82473B55c7A6b311` with implementation `GNSTradingCallbacksV6_4` - vault at address `0x91993f2101cc758d0deb7279d41e880f7defe827` with implementation `GTokenV6_3_2` - tokenDaiRouter at address `0xefe16bba1457c219b0575573ada4dc2aacfc5214` with implementation `OvercollatMigrationRouter` - token at address `0xE5417Af564e4bFDA1c483642db72007871397896` with implementation `GainsNetworkToken` - nftRewards at address `0x8103C0665A544201BBF606d90845d1B2D8005F1c` with implementation `GNSNftRewardsV6_3_1` - referrals at address `0x0F9498b1206Bf9FfDE2a2321fDB56F573A052425` with implementation `GNSReferralsV6_2` - borrowingFees at address `0x83AE6fADB88872D77299550Dc746b95D0196FE15` with implementation `GNSBorrowingFeesV6_4` - staking at address `0xFb06a737f549Eb2512Eb6082A808fc7F16C0819D` with implementation `GNSStakingV6_2` ### GFarmTradingStorageV5 - for each trader we store the following info ```solidity struct Trader{ uint leverageUnlocked; address referral; uint referralRewardsTotal; // 1e18 } ``` - Trade info for submission: ```solidity struct Trade{ address trader; uint pairIndex; uint index; uint initialPosToken; // 1e18 uint positionSizeDai; // 1e18 uint openPrice; // PRECISION bool buy; uint leverage; uint tp; // PRECISION uint sl; // PRECISION } ``` - the `openTrades` mapping has the structure `trader -> pairIndex -> index -> trade`, similar for `openTradesInfo` mapping. - function `updateToken()` only allows GNS update when both trading and callbacks contracts are paused. - function `updateNFTs()` only checks if the first NFT address is nonzero. - function `addTradingContract()` only allows adding a new trading contract when it has minter role in the token/GNS contract - function `storeTrade()`: - onlyTrading modifier - assigns the index so that it is the first where the leverage turned 0. - if for the certain pair it's the trader's first open trade then we also add the trader address to the `pairTraders` array. - the parameter `beingMarketClosed` in `TradeInfo` is set to false - infos are stored in the two mappings `openTrades` and `openTradesInfo` - calls `updateOpenInterestDai()` - function `unregisterTrade()`: - doing almost everything in reverse to `storeTrade()` - only the `tradesPerBlock` is also increased by 1. - function `storePendingMarketOrder()`: - onlyTrading modifier - an input is `_id`, seems like the one assigned in `storeTrade()` - the id is pushed to the array corresponding to the trader address in the mapping `pendingOrderIds` - an input is `_open` and when it is false then the field `beingMarketClosed` in `TradeInfo` with the corresponding index turned to `true` - What if we call this function without calling `storeTrade()`? - function `unregisterPendingMarketOrder()`: - doing everything in reverse to `storePendingMarketOrder()` - function `updateOpenInterestDai()`: - update the mapping `openInterestDai` from `storeTrade()` and `unregisterTrade()` - function `storeOpenLimitOrder()`: - onlyTrading modifier - updates the array `openLimitOrders` and two mappings `openLimitOrderIds` and `openLimitOrdersCount` accordingly - function `updateOpenLimitOrder()`: - onlyTrading modifier - update the parameters in the corresponding limit order in the mapping `openLimitOrders` - function `unregisterOpenLimitOrder()`: - onlyTrading modifier - removes the correponding open limit order in the array `openLimitOrders` by copying the last one to its place then deleting the last one. - update the id of the last limit order in the `openLimitOrderIds` mapping to the new index in the `openLimitOrders` array. - decrease the corresponding `openLimitOrdersCount` by 1. - functions `storePendingNftOrder()` and `unregisterPendingNftOrder()` to manage `NFT` orders. ### GNSTradingV6_1 ```solidity function openTrade( StorageInterfaceV5.Trade memory t, NftRewardsInterfaceV6.OpenLimitOrderType _type, uint _spreadReductionId, uint _slippageP, // for market orders address _referral ) external notContract notDone ``` - function `openTrade()`: - first checks that the trading contract is not paused. - it retrieves the aggregator from storage, then pairsStorage from aggregator. - retrieves the spreadReductionP value from storage - checks that for that pairIndex the sum of `openTrades`, `pendingMarketOpenCount` and `openLimitOrdersCount` is less than `maxTradesPerPair`. - checks that the `pendingOrderIdsCount` is less than `maxPendingMarketOrders` - checks position size - checks corresponding NFT ownership to apply spreadReductionFee - checks take profit and stop loss values - retrieves the price impact from pairInfos, then checks that if the price impact is not too high for that leverage - transfers `t.positionSizeDai` from trader to storage - If the `orderType` is not `NftRewardsInterfaceV6_3_1.OpenLimitOrderType.LEGACY` then we enter the logics for open limit order, which is weird, if not we enter the logics for market order - If it is an open limit order we store the order in both `storageT` and `nftRewards` and `callbacks` - If it is a market order then we request the price from the oracle network then store the order in the `storageT` - Updates the revelant referrals data ### GNSPriceAggregatorV6_4 - function `fulfill`: - does it wait for sufficient number of price answers and only then calls the callback contract? ### GNSPairsStorageV6 - For each pair `spreadP` is a constant set when the pair is added ### GNSTradingCallbacksV6_4 - function `_openTradePrep`: - first we take the price supplied by the oracle - changed it by the spread (including spread reduction) - then we change it further based on price impact depending on the asset utilization and `onePercentDepth` - `onePercentDepth` is set by manager in pairsInfo - checks that the price shift does not exceed the `maxSlippage` - checks that sl and tp prices are not reached by the `priceAfterImpact` - checks that the exposure is within limits, i.e. the new total leverage trading volume is less than pair max OI and less than group max OI. - checks that the `priceImpactP` multiplied by the leverage is less than `maxNegativePnlOnOpenP` - checks that the leverage is less than the maximal possible leverage - function `registertrade()`: - Charge referral fee (if applicable) and send DAI amount to vault - And also mints some GNS? - Charge opening fee - referral fee (if applicable) - Charge NFT / SSS fee - Distribute NFT fee and send DAI amount to vault (if applicable) - Set trade final details - ... - function `currentPercentProfit`: - there is a threshold on the profit percentage!!! ### GNSBorrowingFeesV6_4 ### Gains Network fees ### Things to discuss - Oracle fee is based on the leveraged size #### Oracle fee - Paid whenever the order is executed - Paid in function `getPrice()` in `GNSPriceAggregatorV6_4.sol` - Calculated by helper function `linkFee`, then distributed evenly amongst all participating oracle nodes ![](https://hackmd.io/_uploads/SkI8VolJ6.png) - Each pair has an oracle fee coefficient stored in `GNSPairsStorageV6.sol`, then multiplied of the leveraged position of the order, divided by the LINK price to get the total amount of LINK to pay. ![](https://hackmd.io/_uploads/SJ6Ursxka.png) - We cannot remove this fee, but maybe we can consider a fixed amount of LINK instead of the current dynamic one depending on the leveraged volume. #### Fixed spread - Paid for open market/limit orders - When the most recent price feed is submitted by the oracles through the function `fulfill()` in `GNSPriceAggregatorV6_4.sol`, the aggregator contract calls the function `openTradeMarketCallback` (for open market order) or the function `executeNftOpenOrderCallback` (for open limit order) in `GNSTradingCallbacksV6_4`. - Calculated by the function `marketExecutionPrice` in `GNSTradingCallbacksV6_4`. ![](https://hackmd.io/_uploads/BJsUHW-kT.png) - The input `price` is the open price submitted by the oracles - The input `spreadP` is stored in `GNSPairsStorageV6` and configured when the pair is added to the system by the governance. - The input `spreadReductionP` is baded on NFT tier ownership. - The price diff the then a percentage (`spreadP - spreadReductionP`) of the `price`, which is added to the `price` if the order is long or subtracted if the order is short. In both cases user's profit after closing the position is less by this amount. #### Dynamic spread - Taken on top of the fixed spread so it appears in the same places. - Calculated by the function `getTradePriceImpactPure()` in `GNSPairInfosV6_1`. ![](https://hackmd.io/_uploads/ryjgqZZJp.png) - The input `openPrice` is the price after considering fixed spread. - The input `long` is true for long orders and false for short orders. - The input `startOpenInterest` is the volume of all long (or short) trades for the pair. It is stored in `GFarmTradingStorage5`. - The input `tradeOpenInterest` is the leveraged volume of the current order. - The input `onePercentDepth` is from the trading activities from Binance and imported onchain to `GNSPairInfosV6_1` by the manager. - The bigger the current direction volume, i.e. asset utilization, the higher the price impact. The lower the `onePercentDepth`, i.e. illiquid pair, the higher the price impact. - Again the price impact is added to the `price` if the order is long or subtracted if the order is short. In both cases user's profit after closing the position is less by this amount. #### DevGovFee - Paid for open market/limit orders - Calculated in the function `registerTrade()` in `GNSTradingCallbacksV6_4`. ![](https://hackmd.io/_uploads/rkoE2sWya.png) - Calculated by the function `handleDevGoveFees` in `GFarmTradingStorage5`. ![](https://hackmd.io/_uploads/Syq8hoWJa.png) - Taken as a percentage of the leveraged position. The percentage is pair specific and stored in `GNSPairsStorageV6`. - The fees are claimed by the function `claimFees()` in `GFarmTradingStorage5`. #### Referral fee - Paid for open market/limit orders - Calculated in the function `registerTrade()` in `GNSTradingCallbacksV6_4`. ![](https://hackmd.io/_uploads/H1r9JfW1p.png) - The referral fee is a percentage of the open fee. This percentage is calculated by `getPercentOfOpenFeeP_calc()` in `GNSReferralsV6_2`. ![](https://hackmd.io/_uploads/B1bKis-ya.png) - Where `openFeeP` and `startReferrerFeeP` are two fixed constant - Note that this is taken from DevGovFee #### NFT fee - Paid for limit orders with spread reduction NFT - Calculated in the function `registerTrade(), executeNftCloseOrderCallback()` in `GNSTradingCallbacksV6_4`. ![](https://hackmd.io/_uploads/rJT4yh-ya.png) #### Staking fee - Paid for market orders or limit orders without spread reduction NFT - Calculated in the function `registerTrade(), unregisterTrade()` in `GNSTradingCallbacksV6_4`. ![](https://hackmd.io/_uploads/r11d1hZ16.png) #### Borrowing fee - Paid when closing a trade - Called