From 22ed93f2164233e373e403140bbb043fd777db6c Mon Sep 17 00:00:00 2001 From: Hendobox <50964581+Hendobox@users.noreply.github.com> Date: Sat, 24 Dec 2022 02:42:50 +0100 Subject: [PATCH 1/2] Done treasury fee todo I added a new method in the Store.sol file named collectTreasuryFee. This method will enable the gov address to collect available fees in the base cryptocurrency. After this happens, the fee balance in the contract gets updated. --- README.md | 7 +- script/DeployLocal.s.sol | 56 +-- src/CLP.sol | 6 +- src/Chainlink.sol | 84 ++--- src/Pool.sol | 261 +++++++------- src/Store.sol | 331 ++++++++++------- src/Trade.sol | 684 ++++++++++++++++++++---------------- src/interfaces/ITrade.sol | 102 +++--- src/mocks/MockChainlink.sol | 9 +- src/mocks/MockToken.sol | 10 +- 10 files changed, 841 insertions(+), 709 deletions(-) diff --git a/README.md b/README.md index 62fc461..3ece842 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,7 @@ For details on how the margin / trading system works, please check the [Whitepaper](https://www.cap.finance/whitepaper.pdf), specifically sections 4 and 4.4. Liquidation Rebates and Interest Rate no longer apply. The items below are listed in priority order. All milestones are **ASAP**, with a target production launch date of **early January** on Arbitrum. The driving factor is high quality, speed, and code simplicity. - -- [ ] Treasury fees should be paid out to a treasury address directly (set by gov) + - [ ] If submitOrder margin exceeds freeMargin, set it to the max freeMargin available - [ ] Add MAX_FEE and other constants in Store to curtail gov powers in methods marked with onlyGov. The goal is to prevent gov from having too much power over system function, like setting a fee share too high and siphoning all the funds. - [ ] Add automated tests, including fuzzy, to achieve > 90% coverage @@ -25,7 +24,7 @@ The items below are listed in priority order. All milestones are **ASAP**, with - [x] Flat fee - [x] Allow submitting TP/SL with an order - [x] Contracts: Trade, Pool, Store, Chainlink - +- [x] Treasury fees should be paid out to a treasury address directly (set by gov) ## Compiling @@ -38,4 +37,4 @@ forge build --via-ir ``` anvil forge script DeployLocalScript --rpc-url http://127.0.0.1:8545 --broadcast --via-ir -vvvv -``` \ No newline at end of file +``` diff --git a/script/DeployLocal.s.sol b/script/DeployLocal.s.sol index ddc16cf..ed17272 100644 --- a/script/DeployLocal.s.sol +++ b/script/DeployLocal.s.sol @@ -11,7 +11,6 @@ import "../src/mocks/MockChainlink.sol"; import "../src/mocks/MockToken.sol"; contract DeployLocalScript is Script { - uint256 public constant CURRENCY_UNIT = 10**6; Trade public trade; @@ -25,10 +24,10 @@ contract DeployLocalScript is Script { function setUp() public {} function run() public { - // this is the default mnemonic anvil uses - string memory mnemonic = "test test test test test test test test test test test junk"; - (address deployer,) = deriveRememberKey(mnemonic, 0); + string + memory mnemonic = "test test test test test test test test test test test junk"; + (address deployer, ) = deriveRememberKey(mnemonic, 0); console.log("Deploying contracts with address", deployer); vm.startBroadcast(deployer); @@ -58,26 +57,32 @@ contract DeployLocalScript is Script { console.log("Contracts linked"); // Setup markets - store.setMarket("ETH-USD", Store.Market({ - symbol: "ETH-USD", - feed: address(0), - maxLeverage: 50, - maxOI: 5000000 * CURRENCY_UNIT, - fee: 100, - fundingFactor: 5000, - minSize: 20 * CURRENCY_UNIT, - minSettlementTime: 1 minutes - })); - store.setMarket("BTC-USD", Store.Market({ - symbol: "BTC-USD", - feed: address(0), - maxLeverage: 50, - maxOI: 5000000 * CURRENCY_UNIT, - fee: 100, - fundingFactor: 5000, - minSize: 20 * CURRENCY_UNIT, - minSettlementTime: 1 minutes - })); + store.setMarket( + "ETH-USD", + Store.Market({ + symbol: "ETH-USD", + feed: address(0), + maxLeverage: 50, + maxOI: 5000000 * CURRENCY_UNIT, + fee: 100, + fundingFactor: 5000, + minSize: 20 * CURRENCY_UNIT, + minSettlementTime: 1 minutes + }) + ); + store.setMarket( + "BTC-USD", + Store.Market({ + symbol: "BTC-USD", + feed: address(0), + maxLeverage: 50, + maxOI: 5000000 * CURRENCY_UNIT, + fee: 100, + fundingFactor: 5000, + minSize: 20 * CURRENCY_UNIT, + minSettlementTime: 1 minutes + }) + ); console.log("Markets set up."); @@ -90,7 +95,7 @@ contract DeployLocalScript is Script { vm.stopBroadcast(); - (address user,) = deriveRememberKey(mnemonic, 2); + (address user, ) = deriveRememberKey(mnemonic, 2); console.log("Minting tokens with account", user); vm.startBroadcast(user); @@ -101,6 +106,5 @@ contract DeployLocalScript is Script { console.log("Minted mock tokens for secondary account."); vm.stopBroadcast(); - } } diff --git a/src/CLP.sol b/src/CLP.sol index a6bd7ad..d0de760 100644 --- a/src/CLP.sol +++ b/src/CLP.sol @@ -4,7 +4,6 @@ pragma solidity ^0.8.13; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; contract CLP is ERC20 { - address public store; constructor(address _store) ERC20("CLP", "CLP") { @@ -16,9 +15,8 @@ contract CLP is ERC20 { _mint(to, amount); } - function burn(address from, uint256 amount) public { + function burn(address from, uint256 amount) public { require(msg.sender == store, "!authorized"); _burn(from, amount); } - -} \ No newline at end of file +} diff --git a/src/Chainlink.sol b/src/Chainlink.sol index 8975514..eeba334 100644 --- a/src/Chainlink.sol +++ b/src/Chainlink.sol @@ -4,15 +4,13 @@ pragma solidity ^0.8.7; import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol"; contract Chainlink { - // -- Constants -- // uint256 public constant UNIT = 10**18; uint256 public constant GRACE_PERIOD_TIME = 3600; - // -- Variables -- // - + AggregatorV3Interface internal sequencerUptimeFeed; // -- Errors -- // @@ -21,56 +19,62 @@ contract Chainlink { error GracePeriodNotOver(); /** - * For a list of available sequencer proxy addresses, see: - * https://docs.chain.link/docs/l2-sequencer-flag/#available-networks - */ + * For a list of available sequencer proxy addresses, see: + * https://docs.chain.link/docs/l2-sequencer-flag/#available-networks + */ // -- Constructor -- // constructor() { - sequencerUptimeFeed = AggregatorV3Interface(0xFdB631F5EE196F0ed6FAa767959853A9F217697D); + sequencerUptimeFeed = AggregatorV3Interface( + 0xFdB631F5EE196F0ed6FAa767959853A9F217697D + ); } function getPrice(address feed) public view returns (uint256) { - if (feed == address(0)) return 0; - ( - /*uint80 roundId*/, - int256 answer, - uint256 startedAt, - /*uint256 updatedAt*/, - /*uint80 answeredInRound*/ - ) = sequencerUptimeFeed.latestRoundData(); - - // Answer == 0: Sequencer is up - // Answer == 1: Sequencer is down - bool isSequencerUp = answer == 0; - if (!isSequencerUp) { - revert SequencerDown(); - } - - // Make sure the grace period has passed after the sequencer is back up. - uint256 timeSinceUp = block.timestamp - startedAt; - - if (timeSinceUp <= GRACE_PERIOD_TIME) { - revert GracePeriodNotOver(); - } - - AggregatorV3Interface priceFeed = AggregatorV3Interface(feed); ( - /*uint80 roundID*/, - int price, - /*uint startedAt*/, - /*uint timeStamp*/, + , + /*uint80 roundId*/ + int256 answer, + uint256 startedAt, + , + + ) = /*uint256 updatedAt*/ /*uint80 answeredInRound*/ - ) = priceFeed.latestRoundData(); + sequencerUptimeFeed.latestRoundData(); + + // Answer == 0: Sequencer is up + // Answer == 1: Sequencer is down + bool isSequencerUp = answer == 0; + if (!isSequencerUp) { + revert SequencerDown(); + } + + // Make sure the grace period has passed after the sequencer is back up. + uint256 timeSinceUp = block.timestamp - startedAt; + + if (timeSinceUp <= GRACE_PERIOD_TIME) { + revert GracePeriodNotOver(); + } + + AggregatorV3Interface priceFeed = AggregatorV3Interface(feed); + ( + , + /*uint80 roundID*/ + int256 price, + , + , + + ) = /*uint startedAt*/ + /*uint timeStamp*/ + /*uint80 answeredInRound*/ + priceFeed.latestRoundData(); uint8 decimals = priceFeed.decimals(); // Return 18 decimals standard - return uint256(price) * UNIT / 10**decimals; - + return (uint256(price) * UNIT) / 10**decimals; } - -} \ No newline at end of file +} diff --git a/src/Pool.sol b/src/Pool.sol index 735b027..d14f375 100644 --- a/src/Pool.sol +++ b/src/Pool.sol @@ -5,35 +5,34 @@ import "./Chainlink.sol"; import "./Store.sol"; contract Pool { - - uint256 public constant UNIT = 10**18; + uint256 public constant UNIT = 10**18; uint256 public constant BPS_DIVIDER = 10000; address public gov; address public trade; - Chainlink public chainlink; + Chainlink public chainlink; Store public store; - // Events + // Events - event AddLiquidity( - address indexed user, - uint256 amount, + event AddLiquidity( + address indexed user, + uint256 amount, uint256 clpAmount, uint256 poolBalance ); event RemoveLiquidity( - address indexed user, - uint256 amount, - uint256 feeAmount, + address indexed user, + uint256 amount, + uint256 feeAmount, uint256 clpAmount, uint256 poolBalance ); event PoolPayIn( - address indexed user, + address indexed user, string market, uint256 amount, uint256 bufferToPoolAmount, @@ -42,24 +41,24 @@ contract Pool { ); event PoolPayOut( - address indexed user, + address indexed user, string market, uint256 amount, uint256 poolBalance, uint256 bufferBalance ); - event FeePaid( - address indexed user, - string market, - uint256 fee, - uint256 poolFee, - bool isLiquidation - ); + event FeePaid( + address indexed user, + string market, + uint256 fee, + uint256 poolFee, + bool isLiquidation + ); - // Methods + // Methods - constructor() { + constructor() { gov = msg.sender; } @@ -68,7 +67,7 @@ contract Pool { store = Store(_store); } - function addLiquidity(uint256 amount) external { + function addLiquidity(uint256 amount) external { require(amount > 0, "!amount"); uint256 balance = store.poolBalance(); address user = msg.sender; @@ -76,157 +75,145 @@ contract Pool { uint256 clpSupply = store.getCLPSupply(); - uint256 clpAmount = balance == 0 || clpSupply == 0 ? amount : amount * clpSupply / balance; - - store.mintCLP(user, clpAmount); - store.incrementPoolBalance(amount); + uint256 clpAmount = balance == 0 || clpSupply == 0 + ? amount + : (amount * clpSupply) / balance; - emit AddLiquidity( - user, - amount, - clpAmount, - store.poolBalance() - ); + store.mintCLP(user, clpAmount); + store.incrementPoolBalance(amount); + emit AddLiquidity(user, amount, clpAmount, store.poolBalance()); } function removeLiquidity(uint256 amount) external { - require(amount > 0, "!amount"); - address user = msg.sender; - uint256 balance = store.poolBalance(); - uint256 clpSupply = store.getCLPSupply(); - require(balance > 0 && clpSupply > 0, "!empty"); - - uint256 userBalance = store.getUserPoolBalance(user); - if (amount > userBalance) amount = userBalance; + address user = msg.sender; + uint256 balance = store.poolBalance(); + uint256 clpSupply = store.getCLPSupply(); + require(balance > 0 && clpSupply > 0, "!empty"); - uint256 feeAmount = amount * store.poolWithdrawalFee() / BPS_DIVIDER; - uint256 amountMinusFee = amount - feeAmount; + uint256 userBalance = store.getUserPoolBalance(user); + if (amount > userBalance) amount = userBalance; - // CLP amount - uint256 clpAmount = amountMinusFee * clpSupply / balance; + uint256 feeAmount = (amount * store.poolWithdrawalFee()) / BPS_DIVIDER; + uint256 amountMinusFee = amount - feeAmount; - store.burnCLP(user, clpAmount); - store.decrementPoolBalance(amountMinusFee); + // CLP amount + uint256 clpAmount = (amountMinusFee * clpSupply) / balance; - store.transferOut(user, amountMinusFee); + store.burnCLP(user, clpAmount); + store.decrementPoolBalance(amountMinusFee); - emit RemoveLiquidity( - user, - amount, - feeAmount, - clpAmount, - store.poolBalance() - ); + store.transferOut(user, amountMinusFee); + emit RemoveLiquidity( + user, + amount, + feeAmount, + clpAmount, + store.poolBalance() + ); } - function creditTraderLoss( - address user, - string memory market, - uint256 amount - ) external onlyTrade { - - store.incrementBufferBalance(amount); - - uint256 lastPaid = store.poolLastPaid(); - uint256 _now = block.timestamp; - - if (lastPaid == 0) { - store.setPoolLastPaid(_now); - return; - } - - uint256 bufferBalance = store.bufferBalance(); - uint256 bufferPayoutPeriod = store.bufferPayoutPeriod(); - - uint256 amountToSendPool = bufferBalance * (block.timestamp - lastPaid) / bufferPayoutPeriod; - - if (amountToSendPool > bufferBalance) amountToSendPool = bufferBalance; - - store.incrementPoolBalance(amountToSendPool); - store.decrementBufferBalance(amountToSendPool); - store.setPoolLastPaid(_now); + function creditTraderLoss( + address user, + string memory market, + uint256 amount + ) external onlyTrade { + store.incrementBufferBalance(amount); - store.decrementBalance(user, amount); + uint256 lastPaid = store.poolLastPaid(); + uint256 _now = block.timestamp; - emit PoolPayIn( - user, - market, - amount, - amountToSendPool, - store.poolBalance(), - store.bufferBalance() - ); + if (lastPaid == 0) { + store.setPoolLastPaid(_now); + return; + } - } + uint256 bufferBalance = store.bufferBalance(); + uint256 bufferPayoutPeriod = store.bufferPayoutPeriod(); + + uint256 amountToSendPool = (bufferBalance * + (block.timestamp - lastPaid)) / bufferPayoutPeriod; + + if (amountToSendPool > bufferBalance) amountToSendPool = bufferBalance; + + store.incrementPoolBalance(amountToSendPool); + store.decrementBufferBalance(amountToSendPool); + store.setPoolLastPaid(_now); + + store.decrementBalance(user, amount); + + emit PoolPayIn( + user, + market, + amount, + amountToSendPool, + store.poolBalance(), + store.bufferBalance() + ); + } function debitTraderProfit( - address user, - string memory market, - uint256 amount - ) external onlyTrade { + address user, + string memory market, + uint256 amount + ) external onlyTrade { + if (amount == 0) return; - if (amount == 0) return; - - uint256 bufferBalance = store.bufferBalance(); + uint256 bufferBalance = store.bufferBalance(); - store.decrementBufferBalance(amount); + store.decrementBufferBalance(amount); - if (amount > bufferBalance) { - uint256 diffToPayFromPool = amount - bufferBalance; - uint256 poolBalance = store.poolBalance(); - require(diffToPayFromPool < poolBalance, "!pool-balance"); - store.decrementPoolBalance(diffToPayFromPool); - } + if (amount > bufferBalance) { + uint256 diffToPayFromPool = amount - bufferBalance; + uint256 poolBalance = store.poolBalance(); + require(diffToPayFromPool < poolBalance, "!pool-balance"); + store.decrementPoolBalance(diffToPayFromPool); + } store.incrementBalance(user, amount); - - emit PoolPayOut( - user, - market, - amount, - store.poolBalance(), - store.bufferBalance() - ); - } + emit PoolPayOut( + user, + market, + amount, + store.poolBalance(), + store.bufferBalance() + ); + } function creditFee( - address user, - string memory market, - uint256 fee, - bool isLiquidation + address user, + string memory market, + uint256 fee, + bool isLiquidation ) external onlyTrade { + if (fee == 0) return; - if (fee == 0) return; + uint256 poolFee = (fee * store.poolFeeShare()) / BPS_DIVIDER; + uint256 treasuryFee = fee - poolFee; - uint256 poolFee = fee * store.poolFeeShare() / BPS_DIVIDER; - uint256 treasuryFee = fee - poolFee; - - store.incrementPoolBalance(poolFee); - store.incrementTreasuryBalance(treasuryFee); - - emit FeePaid( - user, - market, - fee, // paid by user // - poolFee, - isLiquidation - ); + store.incrementPoolBalance(poolFee); + store.incrementTreasuryBalance(treasuryFee); + emit FeePaid( + user, + market, + fee, // paid by user // + poolFee, + isLiquidation + ); } - modifier onlyTrade() { - require(msg.sender == trade, '!trade'); + modifier onlyTrade() { + require(msg.sender == trade, "!trade"); _; } - modifier onlyGov() { - require(msg.sender == gov, '!gov'); + modifier onlyGov() { + require(msg.sender == gov, "!gov"); _; } - -} \ No newline at end of file +} diff --git a/src/Store.sol b/src/Store.sol index ee081ee..075a57f 100644 --- a/src/Store.sol +++ b/src/Store.sol @@ -7,7 +7,6 @@ import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; import "./CLP.sol"; contract Store { - // TODO: send balance to treasury address using EnumerableSet for EnumerableSet.UintSet; @@ -30,7 +29,7 @@ contract Store { uint256 public poolWithdrawalFee = 10; // in bps uint256 public minimumMarginLevel = 2000; // 20% in bps, at which account is liquidated - // Structs + // Structs struct Market { string symbol; @@ -62,10 +61,10 @@ contract Store { string market; bool isLong; uint256 size; - uint256 margin; - int256 fundingTracker; - uint256 price; - uint256 timestamp; + uint256 margin; + int256 fundingTracker; + uint256 price; + uint256 timestamp; } // Variables @@ -90,7 +89,7 @@ contract Store { mapping(bytes32 => Position) private positions; // key = user,market EnumerableSet.Bytes32Set private positionKeys; // [position keys..] - mapping(address => EnumerableSet.Bytes32Set) private positionKeysForUser; // user => [position keys..] + mapping(address => EnumerableSet.Bytes32Set) private positionKeysForUser; // user => [position keys..] mapping(string => uint256) private OILong; mapping(string => uint256) private OIShort; @@ -100,10 +99,12 @@ contract Store { EnumerableSet.AddressSet private usersWithLockedMargin; // [users...] // Funding - uint256 public constant fundingInterval = 1 hours; // In seconds. + uint256 public constant fundingInterval = 1 hours; // In seconds. - mapping(string => int256) private fundingTrackers; // market => funding tracker (long) (short is opposite) // in UNIT * bps - mapping(string => uint256) private fundingLastUpdated; // market => last time fundingTracker was updated. In seconds. + mapping(string => int256) private fundingTrackers; // market => funding tracker (long) (short is opposite) // in UNIT * bps + mapping(string => uint256) private fundingLastUpdated; // market => last time fundingTracker was updated. In seconds. + + event FeeCollected(address indexed treasuryAddress, uint256 amount); constructor() { gov = msg.sender; @@ -123,6 +124,19 @@ contract Store { // Gov methods + function collectTreasuryFee(address payable treasuryAddress, uint256 amount) + external + onlyGov + { + uint256 bal = treasuryBalance; + require(bal >= amount, "NOTHING_AVAILABLE"); + require(treasuryAddress != address(0), "INVALID_ADDRESS"); + treasuryBalance -= amount; + (bool success, ) = treasuryAddress.call{value: amount}(""); + require(success, "TRANSFER_ERROR"); + emit FeeCollected(treasuryAddress, amount); + } + function setPoolFeeShare(uint256 amount) external onlyGov { poolFeeShare = amount; } @@ -143,26 +157,32 @@ contract Store { bufferPayoutPeriod = amount; } - function setMarket(string memory market, Market memory marketInfo) external onlyGov { - require(marketInfo.fee <= MAX_FEE, "!max-fee"); - markets[market] = marketInfo; - for (uint256 i = 0; i < marketList.length; i++) { - if (keccak256(abi.encodePacked(marketList[i])) == keccak256(abi.encodePacked(market))) return; - } - marketList.push(market); - } + function setMarket(string memory market, Market memory marketInfo) + external + onlyGov + { + require(marketInfo.fee <= MAX_FEE, "!max-fee"); + markets[market] = marketInfo; + for (uint256 i = 0; i < marketList.length; i++) { + if ( + keccak256(abi.encodePacked(marketList[i])) == + keccak256(abi.encodePacked(market)) + ) return; + } + marketList.push(market); + } // Methods function transferIn(address user, uint256 amount) external onlyContract { - IERC20(currency).safeTransferFrom(user, address(this), amount); - } + IERC20(currency).safeTransferFrom(user, address(this), amount); + } function transferOut(address user, uint256 amount) external onlyContract { IERC20(currency).safeTransfer(user, amount); - } + } - function getCLPSupply() external view onlyContract returns(uint256) { + function getCLPSupply() external view onlyContract returns (uint256) { return IERC20(clp).totalSupply(); } @@ -174,16 +194,22 @@ contract Store { CLP(clp).mint(user, amount); } - function incrementBalance(address user, uint256 amount) external onlyContract { + function incrementBalance(address user, uint256 amount) + external + onlyContract + { balances[user] += amount; } - function decrementBalance(address user, uint256 amount) external onlyContract { + function decrementBalance(address user, uint256 amount) + external + onlyContract + { require(amount <= balances[user], "!balance"); balances[user] -= amount; } - function getBalance(address user) external view returns(uint256) { + function getBalance(address user) external view returns (uint256) { return balances[user]; } @@ -195,11 +221,11 @@ contract Store { poolBalance -= amount; } - function getUserPoolBalance(address user) public view returns(uint256) { + function getUserPoolBalance(address user) public view returns (uint256) { uint256 clpSupply = IERC20(clp).totalSupply(); if (clpSupply == 0) return 0; - return IERC20(clp).balanceOf(user) * poolBalance / clpSupply; - } + return (IERC20(clp).balanceOf(user) * poolBalance) / clpSupply; + } function incrementBufferBalance(uint256 amount) external onlyContract { bufferBalance += amount; @@ -221,7 +247,7 @@ contract Store { treasuryBalance -= amount; } - function lockMargin(address user, uint256 amount) external onlyContract { + function lockMargin(address user, uint256 amount) external onlyContract { lockedMargins[user] += amount; usersWithLockedMargin.add(user); } @@ -237,19 +263,27 @@ contract Store { } } - function getLockedMargin(address user) external view returns(uint256) { + function getLockedMargin(address user) external view returns (uint256) { return lockedMargins[user]; } - function getUsersWithLockedMarginLength() external view returns(uint256) { + function getUsersWithLockedMarginLength() external view returns (uint256) { return usersWithLockedMargin.length(); } - function getUserWithLockedMargin(uint256 i) external view returns(address) { + function getUserWithLockedMargin(uint256 i) + external + view + returns (address) + { return usersWithLockedMargin.at(i); } - function incrementOI(string memory market, uint256 size, bool isLong) external onlyContract { + function incrementOI( + string memory market, + uint256 size, + bool isLong + ) external onlyContract { if (isLong) { OILong[market] += size; require(markets[market].maxOI >= OILong[market], "!max-oi"); @@ -259,7 +293,11 @@ contract Store { } } - function decrementOI(string memory market, uint256 size, bool isLong) external onlyContract { + function decrementOI( + string memory market, + uint256 size, + bool isLong + ) external onlyContract { if (isLong) { if (size > OILong[market]) { OILong[market] = 0; @@ -275,125 +313,172 @@ contract Store { } } - function getOILong(string memory market) external view returns(uint256) { - return OILong[market]; - } + function getOILong(string memory market) external view returns (uint256) { + return OILong[market]; + } - function getOIShort(string memory market) external view returns(uint256) { - return OIShort[market]; - } + function getOIShort(string memory market) external view returns (uint256) { + return OIShort[market]; + } function getOrder(uint256 id) external view returns (Order memory _order) { return orders[id]; } - function addOrder(Order memory order) external onlyContract returns(uint256) { - uint256 nextOrderId = ++orderId; + function addOrder(Order memory order) + external + onlyContract + returns (uint256) + { + uint256 nextOrderId = ++orderId; order.orderId = nextOrderId; - orders[nextOrderId] = order; - userOrderIds[order.user].add(nextOrderId); + orders[nextOrderId] = order; + userOrderIds[order.user].add(nextOrderId); orderIds.add(nextOrderId); - return nextOrderId; - } + return nextOrderId; + } function updateOrder(Order memory order) external onlyContract { - orders[order.orderId] = order; - } + orders[order.orderId] = order; + } function removeOrder(uint256 _orderId) external onlyContract { - Order memory order = orders[_orderId]; - if (order.size == 0) return; - userOrderIds[order.user].remove(_orderId); + Order memory order = orders[_orderId]; + if (order.size == 0) return; + userOrderIds[order.user].remove(_orderId); orderIds.remove(orderId); - delete orders[_orderId]; - } - - function getOrders() external view returns(Order[] memory _orders) { - uint256 length = orderIds.length(); - _orders = new Order[](length); - for (uint256 i = 0; i < length; i++) { - _orders[i] = orders[orderIds.at(i)]; - } - return _orders; - } - - function getUserOrders(address user) external view returns(Order[] memory _orders) { - uint256 length = userOrderIds[user].length(); - _orders = new Order[](length); - for (uint256 i = 0; i < length; i++) { - _orders[i] = orders[userOrderIds[user].at(i)]; - } - return _orders; - } - - function addOrUpdatePosition(Position memory position) external onlyContract { - bytes32 key = _getPositionKey(position.user, position.market); - positions[key] = position; - positionKeysForUser[position.user].add(key); - positionKeys.add(key); - } - - function removePosition(address user, string memory market) external onlyContract { - bytes32 key = _getPositionKey(user, market); - positionKeysForUser[user].remove(key); - positionKeys.remove(key); - delete positions[key]; - } - - function getPosition(address user, string memory market) public view returns(Position memory position) { - bytes32 key = _getPositionKey(user, market); - return positions[key]; - } - - function getUserPositions(address user) external view returns(Position[] memory _positions) { - uint256 length = positionKeysForUser[user].length(); - _positions = new Position[](length); - for (uint256 i = 0; i < length; i++) { - _positions[i] = positions[positionKeysForUser[user].at(i)]; - } - return _positions; - } - - function _getPositionKey(address user, string memory market) internal pure returns (bytes32) { + delete orders[_orderId]; + } + + function getOrders() external view returns (Order[] memory _orders) { + uint256 length = orderIds.length(); + _orders = new Order[](length); + for (uint256 i = 0; i < length; i++) { + _orders[i] = orders[orderIds.at(i)]; + } + return _orders; + } + + function getUserOrders(address user) + external + view + returns (Order[] memory _orders) + { + uint256 length = userOrderIds[user].length(); + _orders = new Order[](length); + for (uint256 i = 0; i < length; i++) { + _orders[i] = orders[userOrderIds[user].at(i)]; + } + return _orders; + } + + function addOrUpdatePosition(Position memory position) + external + onlyContract + { + bytes32 key = _getPositionKey(position.user, position.market); + positions[key] = position; + positionKeysForUser[position.user].add(key); + positionKeys.add(key); + } + + function removePosition(address user, string memory market) + external + onlyContract + { + bytes32 key = _getPositionKey(user, market); + positionKeysForUser[user].remove(key); + positionKeys.remove(key); + delete positions[key]; + } + + function getPosition(address user, string memory market) + public + view + returns (Position memory position) + { + bytes32 key = _getPositionKey(user, market); + return positions[key]; + } + + function getUserPositions(address user) + external + view + returns (Position[] memory _positions) + { + uint256 length = positionKeysForUser[user].length(); + _positions = new Position[](length); + for (uint256 i = 0; i < length; i++) { + _positions[i] = positions[positionKeysForUser[user].at(i)]; + } + return _positions; + } + + function _getPositionKey(address user, string memory market) + internal + pure + returns (bytes32) + { return keccak256(abi.encodePacked(user, market)); } - function getMarket(string memory market) external view returns (Market memory _market) { + function getMarket(string memory market) + external + view + returns (Market memory _market) + { return markets[market]; } - function getMarketList() external view returns(string[] memory) { - return marketList; - } + function getMarketList() external view returns (string[] memory) { + return marketList; + } - function getFundingLastUpdated(string memory market) external view returns(uint256) { - return fundingLastUpdated[market]; - } + function getFundingLastUpdated(string memory market) + external + view + returns (uint256) + { + return fundingLastUpdated[market]; + } - function getFundingFactor(string memory market) external view returns(uint256) { + function getFundingFactor(string memory market) + external + view + returns (uint256) + { return markets[market].fundingFactor; - } + } - function getFundingTracker(string memory market) external view returns(int256) { - return fundingTrackers[market]; - } + function getFundingTracker(string memory market) + external + view + returns (int256) + { + return fundingTrackers[market]; + } - function setFundingLastUpdated(string memory market, uint256 timestamp) external onlyContract { - fundingLastUpdated[market] = timestamp; - } + function setFundingLastUpdated(string memory market, uint256 timestamp) + external + onlyContract + { + fundingLastUpdated[market] = timestamp; + } - function updateFundingTracker(string memory market, int256 fundingIncrement) external onlyContract { - fundingTrackers[market] += fundingIncrement; - } + function updateFundingTracker(string memory market, int256 fundingIncrement) + external + onlyContract + { + fundingTrackers[market] += fundingIncrement; + } modifier onlyContract() { - require(msg.sender == trade || msg.sender == pool, '!contract'); + require(msg.sender == trade || msg.sender == pool, "!contract"); _; } modifier onlyGov() { - require(msg.sender == gov, '!gov'); + require(msg.sender == gov, "!gov"); _; } - -} \ No newline at end of file +} diff --git a/src/Trade.sol b/src/Trade.sol index bad2254..2200bbf 100644 --- a/src/Trade.sol +++ b/src/Trade.sol @@ -7,35 +7,38 @@ import "./Pool.sol"; import "./interfaces/ITrade.sol"; contract Trade is ITrade { - - uint256 public constant UNIT = 10**18; + uint256 public constant UNIT = 10**18; uint256 public constant BPS_DIVIDER = 10000; address public gov; - Chainlink public chainlink; + Chainlink public chainlink; Pool public pool; Store public store; - // Methods + // Methods - constructor() { + constructor() { gov = msg.sender; } - function link(address _chainlink, address _pool, address _store) external onlyGov { + function link( + address _chainlink, + address _pool, + address _store + ) external onlyGov { chainlink = Chainlink(_chainlink); pool = Pool(_pool); store = Store(_store); } - function deposit(uint256 amount) external { + function deposit(uint256 amount) external { require(amount > 0, "!amount"); store.transferIn(msg.sender, amount); store.incrementBalance(msg.sender, amount); emit Deposit(msg.sender, amount); } - + function withdraw(uint256 amount) external { require(amount > 0, "!amount"); address user = msg.sender; @@ -53,10 +56,13 @@ contract Trade is ITrade { emit Withdraw(msg.sender, amount); } - function submitOrder(Store.Order memory params, uint256 tpPrice, uint256 slPrice) external { - + function submitOrder( + Store.Order memory params, + uint256 tpPrice, + uint256 slPrice + ) external { address user = msg.sender; - + Store.Market memory market = store.getMarket(params.market); require(market.maxLeverage > 0, "!market"); require(market.minSize <= params.size, "!min-size"); @@ -65,12 +71,11 @@ contract Trade is ITrade { params.margin = 0; } else { require(params.margin > 0, "!margin"); - uint256 leverage = UNIT * params.size / params.margin; + uint256 leverage = (UNIT * params.size) / params.margin; require(leverage >= UNIT, "!min-leverage"); require(leverage <= market.maxLeverage * UNIT, "!max-leverage"); store.lockMargin(user, params.margin); - } // check equity @@ -82,7 +87,7 @@ contract Trade is ITrade { require(int256(lockedMargin) <= equity, "!equity"); // fee - uint256 fee = market.fee * params.size / BPS_DIVIDER; + uint256 fee = (market.fee * params.size) / BPS_DIVIDER; store.decrementBalance(user, fee); // Get chainlink price @@ -91,23 +96,31 @@ contract Trade is ITrade { // Check chainlink price vs order price for trigger orders if ( - params.orderType == 1 && params.isLong && chainlinkPrice <= params.price || - params.orderType == 1 && !params.isLong && chainlinkPrice >= params.price || - params.orderType == 2 && params.isLong && chainlinkPrice >= params.price || - params.orderType == 2 && !params.isLong && chainlinkPrice <= params.price + (params.orderType == 1 && + params.isLong && + chainlinkPrice <= params.price) || + (params.orderType == 1 && + !params.isLong && + chainlinkPrice >= params.price) || + (params.orderType == 2 && + params.isLong && + chainlinkPrice >= params.price) || + (params.orderType == 2 && + !params.isLong && + chainlinkPrice <= params.price) ) { revert("!orderType"); } // Assign current chainlink price to market orders if (params.orderType == 0) { - params.price = chainlinkPrice; + params.price = chainlinkPrice; } // Save order to store params.user = user; - params.fee = fee; - params.timestamp = block.timestamp; + params.fee = fee; + params.timestamp = block.timestamp; uint256 orderId = store.addOrder(params); emit OrderCreated( @@ -182,7 +195,6 @@ contract Trade is ITrade { slOrder.isReduceOnly ); } - } function updateOrder(uint256 orderId, uint256 price) external { @@ -194,10 +206,12 @@ contract Trade is ITrade { uint256 chainlinkPrice = chainlink.getPrice(market.feed); require(chainlinkPrice > 0, "!chainlink"); if ( - order.orderType == 1 && order.isLong && chainlinkPrice <= price || - order.orderType == 1 && !order.isLong && chainlinkPrice >= price || - order.orderType == 2 && order.isLong && chainlinkPrice >= price || - order.orderType == 2 && !order.isLong && chainlinkPrice <= price + (order.orderType == 1 && order.isLong && chainlinkPrice <= price) || + (order.orderType == 1 && + !order.isLong && + chainlinkPrice >= price) || + (order.orderType == 2 && order.isLong && chainlinkPrice >= price) || + (order.orderType == 2 && !order.isLong && chainlinkPrice <= price) ) { if (order.orderType == 1) order.orderType = 2; if (order.orderType == 2) order.orderType = 1; @@ -217,10 +231,7 @@ contract Trade is ITrade { store.incrementBalance(order.user, order.fee); store.transferOut(order.user, order.margin + order.fee); store.removeOrder(orderId); - emit OrderCancelled( - orderId, - order.user - ); + emit OrderCancelled(orderId, order.user); } function cancelOrders(uint256[] calldata orderIds) external { @@ -229,13 +240,15 @@ contract Trade is ITrade { } } - function getExecutableOrderIds() public view returns(uint256[] memory orderIdsToExecute){ - + function getExecutableOrderIds() + public + view + returns (uint256[] memory orderIdsToExecute) + { Store.Order[] memory orders = store.getOrders(); uint256[] memory _orderIds = new uint256[](orders.length); uint256 j; for (uint256 i = 0; i < orders.length; i++) { - Store.Order memory order = orders[i]; Store.Market memory market = store.getMarket(order.market); @@ -245,18 +258,28 @@ contract Trade is ITrade { // Can this order be executed? if ( order.orderType == 0 || - order.orderType == 1 && order.isLong && chainlinkPrice <= order.price || - order.orderType == 1 && !order.isLong && chainlinkPrice >= order.price || - order.orderType == 2 && order.isLong && chainlinkPrice >= order.price || - order.orderType == 2 && !order.isLong && chainlinkPrice <= order.price + (order.orderType == 1 && + order.isLong && + chainlinkPrice <= order.price) || + (order.orderType == 1 && + !order.isLong && + chainlinkPrice >= order.price) || + (order.orderType == 2 && + order.isLong && + chainlinkPrice >= order.price) || + (order.orderType == 2 && + !order.isLong && + chainlinkPrice <= order.price) ) { // Check settlement time has passed, or chainlinkPrice is different for market order - if (order.orderType == 0 && chainlinkPrice != order.price || block.timestamp - order.timestamp > market.minSettlementTime) { + if ( + (order.orderType == 0 && chainlinkPrice != order.price) || + block.timestamp - order.timestamp > market.minSettlementTime + ) { _orderIds[j] = order.orderId; j++; } } - } // Return trimmed result containing only executable order ids @@ -266,12 +289,11 @@ contract Trade is ITrade { } return orderIdsToExecute; - } function executeOrders() external { uint256[] memory orderIds = getExecutableOrderIds(); - for (uint256 i = 0; i < orderIds.length; i++) { + for (uint256 i = 0; i < orderIds.length; i++) { uint256 orderId = orderIds[i]; Store.Order memory order = store.getOrder(orderId); if (order.size == 0 || order.price == 0) continue; @@ -282,28 +304,40 @@ contract Trade is ITrade { } } - function _executeOrder(Store.Order memory order, uint256 price, address keeper) internal { - + function _executeOrder( + Store.Order memory order, + uint256 price, + address keeper + ) internal { // Check for existing position - Store.Position memory position = store.getPosition(order.user, order.market); + Store.Position memory position = store.getPosition( + order.user, + order.market + ); - bool doAdd = !order.isReduceOnly && (position.size == 0 || order.isLong == position.isLong); - bool doReduce = position.size > 0 && order.isLong != position.isLong; + bool doAdd = !order.isReduceOnly && + (position.size == 0 || order.isLong == position.isLong); + bool doReduce = position.size > 0 && order.isLong != position.isLong; if (doAdd) { _increasePosition(order, price, keeper); } else if (doReduce) { _decreasePosition(order, price, keeper); } - } - function _increasePosition(Store.Order memory order, uint256 price, address keeper) internal { - - Store.Position memory position = store.getPosition(order.user, order.market); + function _increasePosition( + Store.Order memory order, + uint256 price, + address keeper + ) internal { + Store.Position memory position = store.getPosition( + order.user, + order.market + ); uint256 fee = order.fee; - uint256 keeperFee = fee * store.keeperFeeShare() / BPS_DIVIDER; + uint256 keeperFee = (fee * store.keeperFeeShare()) / BPS_DIVIDER; fee -= keeperFee; pool.creditFee(order.user, order.market, fee, false); @@ -314,19 +348,22 @@ contract Trade is ITrade { _updateFundingTracker(order.market); - uint256 averagePrice = (position.size * position.price + order.size * price) / (position.size + order.size); - - if (position.size == 0) { - position.user = order.user; - position.market = order.market; - position.timestamp = block.timestamp; - position.isLong = order.isLong; - position.fundingTracker = store.getFundingTracker(order.market); - } + uint256 averagePrice = (position.size * + position.price + + order.size * + price) / (position.size + order.size); + + if (position.size == 0) { + position.user = order.user; + position.market = order.market; + position.timestamp = block.timestamp; + position.isLong = order.isLong; + position.fundingTracker = store.getFundingTracker(order.market); + } position.size += order.size; - position.margin += order.margin; - position.price = averagePrice; + position.margin += order.margin; + position.price = averagePrice; store.addOrUpdatePosition(position); @@ -336,150 +373,157 @@ contract Trade is ITrade { emit PositionIncreased( order.orderId, - order.user, - order.market, - order.isLong, - order.size, - order.margin, - price, - position.margin, - position.size, - position.price, - position.fundingTracker, + order.user, + order.market, + order.isLong, + order.size, + order.margin, + price, + position.margin, + position.size, + position.price, + position.fundingTracker, fee, keeperFee - ); - + ); } - function _decreasePosition(Store.Order memory order, uint256 price, address keeper) internal { - - Store.Position memory position = store.getPosition(order.user, order.market); + function _decreasePosition( + Store.Order memory order, + uint256 price, + address keeper + ) internal { + Store.Position memory position = store.getPosition( + order.user, + order.market + ); - uint256 executedOrderSize = position.size > order.size ? order.size : position.size; - uint256 remainingOrderSize = order.size - executedOrderSize; + uint256 executedOrderSize = position.size > order.size + ? order.size + : position.size; + uint256 remainingOrderSize = order.size - executedOrderSize; - uint256 remainingOrderMargin; - uint256 amountToReturnToUser; + uint256 remainingOrderMargin; + uint256 amountToReturnToUser; - if (order.isReduceOnly) { - // order.margin = 0 - // A fee (order.fee) corresponding to order.size was taken from balance on submit. Only fee corresponding to executedOrderSize should be charged, rest should be returned, if any - store.incrementBalance(order.user, order.fee * remainingOrderSize / order.size); - } else { - // User submitted order.margin when sending the order. Refund the portion of order.margin that executes against the position - uint256 executedOrderMargin = order.margin * executedOrderSize / order.size; - amountToReturnToUser += executedOrderMargin; - remainingOrderMargin = order.margin - executedOrderMargin; - } + if (order.isReduceOnly) { + // order.margin = 0 + // A fee (order.fee) corresponding to order.size was taken from balance on submit. Only fee corresponding to executedOrderSize should be charged, rest should be returned, if any + store.incrementBalance( + order.user, + (order.fee * remainingOrderSize) / order.size + ); + } else { + // User submitted order.margin when sending the order. Refund the portion of order.margin that executes against the position + uint256 executedOrderMargin = (order.margin * executedOrderSize) / + order.size; + amountToReturnToUser += executedOrderMargin; + remainingOrderMargin = order.margin - executedOrderMargin; + } uint256 fee = order.fee; - uint256 keeperFee = fee * store.keeperFeeShare() / BPS_DIVIDER; + uint256 keeperFee = (fee * store.keeperFeeShare()) / BPS_DIVIDER; fee -= keeperFee; pool.creditFee(order.user, order.market, fee, false); store.transferOut(keeper, keeperFee); - // Funding update + // Funding update + + store.decrementOI(order.market, order.size, position.isLong); - store.decrementOI(order.market, order.size, position.isLong); - _updateFundingTracker(order.market); - // P/L - - (int256 pnl, int256 fundingFee) = _getPnL( - order.market, - position.isLong, - price, - position.price, - executedOrderSize, - position.fundingTracker - ); - - uint256 executedPositionMargin = position.margin * executedOrderSize / position.size; - - if (pnl <= -1 * int256(position.margin)) { - pnl = -1 * int256(position.margin); - executedPositionMargin = position.margin; - executedOrderSize = position.size; - position.size = 0; - } else { - position.margin -= executedPositionMargin; - position.size -= executedOrderSize; - position.fundingTracker = store.getFundingTracker(order.market); - } - - if (pnl < 0) { - uint256 absPnl = uint256(-1 * pnl); + // P/L - // credit trader loss to pool - pool.creditTraderLoss(order.user, order.market, absPnl); + (int256 pnl, int256 fundingFee) = _getPnL( + order.market, + position.isLong, + price, + position.price, + executedOrderSize, + position.fundingTracker + ); - if (absPnl < executedPositionMargin) { - amountToReturnToUser += executedPositionMargin - absPnl; - } + uint256 executedPositionMargin = (position.margin * executedOrderSize) / + position.size; - } else { - pool.debitTraderProfit(order.user, order.market, uint256(pnl)); - amountToReturnToUser += executedPositionMargin; - } + if (pnl <= -1 * int256(position.margin)) { + pnl = -1 * int256(position.margin); + executedPositionMargin = position.margin; + executedOrderSize = position.size; + position.size = 0; + } else { + position.margin -= executedPositionMargin; + position.size -= executedOrderSize; + position.fundingTracker = store.getFundingTracker(order.market); + } + + if (pnl < 0) { + uint256 absPnl = uint256(-1 * pnl); + + // credit trader loss to pool + pool.creditTraderLoss(order.user, order.market, absPnl); + + if (absPnl < executedPositionMargin) { + amountToReturnToUser += executedPositionMargin - absPnl; + } + } else { + pool.debitTraderProfit(order.user, order.market, uint256(pnl)); + amountToReturnToUser += executedPositionMargin; + } store.unlockMargin(order.user, amountToReturnToUser); - if (position.size == 0) { - store.removePosition(order.user, order.market); - } else { - store.addOrUpdatePosition(position); - } - - store.removeOrder(order.orderId); - - emit PositionDecreased( - order.orderId, - order.user, - order.market, - order.isLong, - executedOrderSize, - executedPositionMargin, - price, - position.margin, - position.size, - position.price, - position.fundingTracker, - fee, + if (position.size == 0) { + store.removePosition(order.user, order.market); + } else { + store.addOrUpdatePosition(position); + } + + store.removeOrder(order.orderId); + + emit PositionDecreased( + order.orderId, + order.user, + order.market, + order.isLong, + executedOrderSize, + executedPositionMargin, + price, + position.margin, + position.size, + position.price, + position.fundingTracker, + fee, keeperFee, - pnl, - fundingFee - ); - - // Open position in opposite direction if size remains - - if (!order.isReduceOnly && remainingOrderSize > 0) { - - Store.Order memory nextOrder = Store.Order({ - orderId: 0, - user: order.user, - market: order.market, - margin: remainingOrderMargin, - size: remainingOrderSize, - price: 0, - isLong: order.isLong, - orderType: 0, - fee: order.fee * remainingOrderSize / order.size, - isReduceOnly: false, - timestamp: block.timestamp - }); + pnl, + fundingFee + ); - _increasePosition(nextOrder, price, keeper); + // Open position in opposite direction if size remains - } + if (!order.isReduceOnly && remainingOrderSize > 0) { + Store.Order memory nextOrder = Store.Order({ + orderId: 0, + user: order.user, + market: order.market, + margin: remainingOrderMargin, + size: remainingOrderSize, + price: 0, + isLong: order.isLong, + orderType: 0, + fee: (order.fee * remainingOrderSize) / order.size, + isReduceOnly: false, + timestamp: block.timestamp + }); + _increasePosition(nextOrder, price, keeper); + } } function closePositionWithoutProfit(string memory _market) external { - address user = msg.sender; Store.Position memory position = store.getPosition(user, _market); @@ -487,27 +531,27 @@ contract Trade is ITrade { Store.Market memory market = store.getMarket(_market); - uint256 fee = position.size * market.fee / BPS_DIVIDER; + uint256 fee = (position.size * market.fee) / BPS_DIVIDER; pool.creditFee(user, _market, fee, false); - store.decrementOI(_market, position.size, position.isLong); - + store.decrementOI(_market, position.size, position.isLong); + _updateFundingTracker(_market); uint256 chainlinkPrice = chainlink.getPrice(market.feed); require(chainlinkPrice > 0, "!price"); - // P/L + // P/L - (int256 pnl, ) = _getPnL( - _market, - position.isLong, - chainlinkPrice, - position.price, - position.size, - position.fundingTracker - ); + (int256 pnl, ) = _getPnL( + _market, + position.isLong, + chainlinkPrice, + position.price, + position.size, + position.fundingTracker + ); // Only profitable positions can be closed this way require(pnl >= 0, "!pnl"); @@ -515,27 +559,30 @@ contract Trade is ITrade { store.unlockMargin(user, position.margin); store.removePosition(user, _market); - emit PositionDecreased( - 0, - user, - _market, - !position.isLong, - position.size, - position.margin, - chainlinkPrice, - position.margin, - position.size, - position.price, - position.fundingTracker, - fee, + emit PositionDecreased( 0, - 0, - 0 - ); - + user, + _market, + !position.isLong, + position.size, + position.margin, + chainlinkPrice, + position.margin, + position.size, + position.price, + position.fundingTracker, + fee, + 0, + 0, + 0 + ); } - function getLiquidatableUsers() public view returns(address[] memory usersToLiquidate) { + function getLiquidatableUsers() + public + view + returns (address[] memory usersToLiquidate) + { uint256 length = store.getUsersWithLockedMarginLength(); address[] memory _users = new address[](length); uint256 j = 0; @@ -547,13 +594,13 @@ contract Trade is ITrade { if (equity <= 0) { marginLevel = 0; } else { - marginLevel = BPS_DIVIDER * uint256(equity) / lockedMargin; + marginLevel = (BPS_DIVIDER * uint256(equity)) / lockedMargin; } if (marginLevel < store.minimumMarginLevel()) { _users[j] = user; j++; } - } + } // Return trimmed result containing only users to be liquidated usersToLiquidate = new address[](j); for (uint256 i = 0; i < j; i++) { @@ -563,7 +610,6 @@ contract Trade is ITrade { } function liquidateUsers() external { - address[] memory usersToLiquidate = getLiquidatableUsers(); uint256 liquidatorFees; @@ -571,18 +617,26 @@ contract Trade is ITrade { address user = usersToLiquidate[i]; Store.Position[] memory positions = store.getUserPositions(user); for (uint256 j = 0; j < positions.length; j++) { - Store.Position memory position = positions[j]; Store.Market memory market = store.getMarket(position.market); - uint256 fee = position.size * market.fee / BPS_DIVIDER; - uint256 liquidatorFee = fee * store.keeperFeeShare() / BPS_DIVIDER; + uint256 fee = (position.size * market.fee) / BPS_DIVIDER; + uint256 liquidatorFee = (fee * store.keeperFeeShare()) / + BPS_DIVIDER; fee -= liquidatorFee; liquidatorFees += liquidatorFee; - pool.creditTraderLoss(user, position.market, position.margin - fee - liquidatorFee); + pool.creditTraderLoss( + user, + position.market, + position.margin - fee - liquidatorFee + ); pool.creditFee(user, position.market, fee, true); - store.decrementOI(position.market, position.size, position.isLong); + store.decrementOI( + position.market, + position.size, + position.isLong + ); _updateFundingTracker(position.market); store.removePosition(user, position.market); @@ -600,16 +654,18 @@ contract Trade is ITrade { fee, liquidatorFee ); - - } + } } // credit liquidator fees store.transferOut(msg.sender, liquidatorFees); - } - function getUserPositionsWithUpls(address user) external view returns(Store.Position[] memory _positions, int256[] memory _upls) { + function getUserPositionsWithUpls(address user) + external + view + returns (Store.Position[] memory _positions, int256[] memory _upls) + { _positions = store.getUserPositions(user); uint256 length = _positions.length; _upls = new int256[](length); @@ -622,24 +678,25 @@ contract Trade is ITrade { if (chainlinkPrice == 0) continue; (int256 pnl, ) = _getPnL( - position.market, - position.isLong, - chainlinkPrice, - position.price, - position.size, + position.market, + position.isLong, + chainlinkPrice, + position.price, + position.size, position.fundingTracker ); _upls[i] = pnl; - } return (_positions, _upls); - } - - function getMarketsWithPrices() external view returns(Store.Market[] memory _markets, uint256[] memory _prices) { - + + function getMarketsWithPrices() + external + view + returns (Store.Market[] memory _markets, uint256[] memory _prices) + { string[] memory marketList = store.getMarketList(); uint256 length = marketList.length; _markets = new Store.Market[](length); @@ -653,44 +710,45 @@ contract Trade is ITrade { } return (_markets, _prices); - } function _getPnL( - string memory market, - bool isLong, - uint256 price, - uint256 positionPrice, - uint256 size, - int256 fundingTracker - ) internal view returns(int256 pnl, int256 fundingFee) { - - if (price == 0 || positionPrice == 0 || size == 0) return (0,0); - - if (isLong) { - pnl = int256(size) * (int256(price) - int256(positionPrice)) / int256(positionPrice); - } else { - pnl = int256(size) * (int256(positionPrice) - int256(price)) / int256(positionPrice); - } - - int256 currentFundingTracker = store.getFundingTracker(market); - fundingFee = int256(size) * (currentFundingTracker - fundingTracker) / (int256(BPS_DIVIDER) * int256(UNIT)); // funding tracker is in UNIT * bps - - if (isLong) { - pnl -= fundingFee; // positive = longs pay, negative = longs receive - } else { - pnl += fundingFee; // positive = shorts receive, negative = shorts pay - } + string memory market, + bool isLong, + uint256 price, + uint256 positionPrice, + uint256 size, + int256 fundingTracker + ) internal view returns (int256 pnl, int256 fundingFee) { + if (price == 0 || positionPrice == 0 || size == 0) return (0, 0); + + if (isLong) { + pnl = + (int256(size) * (int256(price) - int256(positionPrice))) / + int256(positionPrice); + } else { + pnl = + (int256(size) * (int256(positionPrice) - int256(price))) / + int256(positionPrice); + } - return (pnl, fundingFee); + int256 currentFundingTracker = store.getFundingTracker(market); + fundingFee = + (int256(size) * (currentFundingTracker - fundingTracker)) / + (int256(BPS_DIVIDER) * int256(UNIT)); // funding tracker is in UNIT * bps - } + if (isLong) { + pnl -= fundingFee; // positive = longs pay, negative = longs receive + } else { + pnl += fundingFee; // positive = shorts receive, negative = shorts pay + } - function getUpl(address user) public view returns(int256 upl) { + return (pnl, fundingFee); + } + function getUpl(address user) public view returns (int256 upl) { Store.Position[] memory positions = store.getUserPositions(user); for (uint256 j = 0; j < positions.length; j++) { - Store.Position memory position = positions[j]; Store.Market memory market = store.getMarket(position.market); @@ -698,80 +756,82 @@ contract Trade is ITrade { if (chainlinkPrice == 0) continue; (int256 pnl, ) = _getPnL( - position.market, - position.isLong, - chainlinkPrice, - position.price, - position.size, + position.market, + position.isLong, + chainlinkPrice, + position.price, + position.size, position.fundingTracker ); upl += pnl; - } return upl; - } function _updateFundingTracker(string memory market) internal { - uint256 lastUpdated = store.getFundingLastUpdated(market); - uint256 _now = block.timestamp; - - if (lastUpdated == 0) { - store.setFundingLastUpdated(market, _now); - return; - } - - if (lastUpdated + store.fundingInterval() > _now) return; - - int256 fundingIncrement = getAccruedFunding(market, 0); // in UNIT * bps - - if (fundingIncrement == 0) return; - - store.updateFundingTracker(market, fundingIncrement); - store.setFundingLastUpdated(market, _now); - - emit FundingUpdated( - market, - store.getFundingTracker(market), - fundingIncrement - ); + uint256 _now = block.timestamp; - } + if (lastUpdated == 0) { + store.setFundingLastUpdated(market, _now); + return; + } + + if (lastUpdated + store.fundingInterval() > _now) return; - function getAccruedFunding(string memory market, uint256 intervals) public view returns (int256) { + int256 fundingIncrement = getAccruedFunding(market, 0); // in UNIT * bps + if (fundingIncrement == 0) return; + + store.updateFundingTracker(market, fundingIncrement); + store.setFundingLastUpdated(market, _now); + + emit FundingUpdated( + market, + store.getFundingTracker(market), + fundingIncrement + ); + } + + function getAccruedFunding(string memory market, uint256 intervals) + public + view + returns (int256) + { if (intervals == 0) { - intervals = (block.timestamp - store.getFundingLastUpdated(market)) / store.fundingInterval(); - } - - if (intervals == 0) return 0; - - uint256 OILong = store.getOILong(market); - uint256 OIShort = store.getOIShort(market); - - if (OIShort == 0 && OILong == 0) return 0; - - uint256 OIDiff = OIShort > OILong ? OIShort - OILong : OILong - OIShort; - uint256 yearlyFundingFactor = store.getFundingFactor(market); // in bps - // intervals = hours since fundingInterval = 1 hour - uint256 accruedFunding = UNIT * yearlyFundingFactor * OIDiff * intervals / (24 * 365 * (OILong + OIShort)); // in UNIT * bps + intervals = + (block.timestamp - store.getFundingLastUpdated(market)) / + store.fundingInterval(); + } + + if (intervals == 0) return 0; - if (OILong > OIShort) { - // Longs pay shorts. Increase funding tracker. - return int256(accruedFunding); - } else { - // Shorts pay longs. Decrease funding tracker. - return -1 * int256(accruedFunding); - } + uint256 OILong = store.getOILong(market); + uint256 OIShort = store.getOIShort(market); + if (OIShort == 0 && OILong == 0) return 0; + + uint256 OIDiff = OIShort > OILong ? OIShort - OILong : OILong - OIShort; + uint256 yearlyFundingFactor = store.getFundingFactor(market); // in bps + // intervals = hours since fundingInterval = 1 hour + uint256 accruedFunding = (UNIT * + yearlyFundingFactor * + OIDiff * + intervals) / (24 * 365 * (OILong + OIShort)); // in UNIT * bps + + if (OILong > OIShort) { + // Longs pay shorts. Increase funding tracker. + return int256(accruedFunding); + } else { + // Shorts pay longs. Decrease funding tracker. + return -1 * int256(accruedFunding); + } } - modifier onlyGov() { - require(msg.sender == gov, '!gov'); + modifier onlyGov() { + require(msg.sender == gov, "!gov"); _; } - -} \ No newline at end of file +} diff --git a/src/interfaces/ITrade.sol b/src/interfaces/ITrade.sol index 0db1aa9..0dc11bb 100644 --- a/src/interfaces/ITrade.sol +++ b/src/interfaces/ITrade.sol @@ -2,76 +2,72 @@ pragma solidity ^0.8.13; interface ITrade { - event Deposit(address indexed user, uint256 amount); + event Deposit(address indexed user, uint256 amount); event Withdraw(address indexed user, uint256 amount); event OrderCreated( uint256 indexed orderId, - address indexed user, - string market, - bool isLong, - uint256 margin, - uint256 size, - uint256 price, - uint256 fee, - uint8 orderType, - bool isReduceOnly + address indexed user, + string market, + bool isLong, + uint256 margin, + uint256 size, + uint256 price, + uint256 fee, + uint8 orderType, + bool isReduceOnly ); - event OrderCancelled( - uint256 indexed orderId, - address indexed user - ); + event OrderCancelled(uint256 indexed orderId, address indexed user); event PositionIncreased( uint256 indexed orderId, - address indexed user, - string market, - bool isLong, - uint256 size, - uint256 margin, - uint256 price, - uint256 positionMargin, - uint256 positionSize, - uint256 positionPrice, - int256 fundingTracker, - uint256 fee, - uint256 keeperFee - ); + address indexed user, + string market, + bool isLong, + uint256 size, + uint256 margin, + uint256 price, + uint256 positionMargin, + uint256 positionSize, + uint256 positionPrice, + int256 fundingTracker, + uint256 fee, + uint256 keeperFee + ); - event PositionDecreased( + event PositionDecreased( uint256 indexed orderId, - address indexed user, - string market, - bool isLong, - uint256 size, - uint256 margin, - uint256 price, - uint256 positionMargin, - uint256 positionSize, - uint256 positionPrice, - int256 fundingTracker, - uint256 fee, + address indexed user, + string market, + bool isLong, + uint256 size, + uint256 margin, + uint256 price, + uint256 positionMargin, + uint256 positionSize, + uint256 positionPrice, + int256 fundingTracker, + uint256 fee, uint256 keeperFee, - int256 pnl, - int256 fundingFee - ); + int256 pnl, + int256 fundingFee + ); event PositionLiquidated( - address indexed user, - string market, - bool isLong, - uint256 size, - uint256 margin, - uint256 price, - uint256 fee, + address indexed user, + string market, + bool isLong, + uint256 size, + uint256 margin, + uint256 price, + uint256 fee, uint256 liquidatorFee - ); + ); event FundingUpdated( string market, int256 fundingTracker, - int256 fundingIncrement + int256 fundingIncrement ); - -} \ No newline at end of file +} diff --git a/src/mocks/MockChainlink.sol b/src/mocks/MockChainlink.sol index 3d2f173..17b1b75 100644 --- a/src/mocks/MockChainlink.sol +++ b/src/mocks/MockChainlink.sol @@ -2,18 +2,15 @@ pragma solidity ^0.8.13; contract MockChainlink { - mapping(address => uint256) prices; - constructor() { - } + constructor() {} function setPrice(address feed, uint256 price) external { prices[feed] = price; } - function getPrice(address feed) external view returns(uint256) { + function getPrice(address feed) external view returns (uint256) { return prices[feed]; } - -} \ No newline at end of file +} diff --git a/src/mocks/MockToken.sol b/src/mocks/MockToken.sol index 93fffa9..371aea9 100644 --- a/src/mocks/MockToken.sol +++ b/src/mocks/MockToken.sol @@ -4,10 +4,13 @@ pragma solidity ^0.8.13; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; contract MockToken is ERC20 { - uint8 _decimals; - constructor(string memory name, string memory symbol, uint8 __decimals) ERC20(name, symbol) { + constructor( + string memory name, + string memory symbol, + uint8 __decimals + ) ERC20(name, symbol) { _decimals = __decimals; } @@ -19,5 +22,4 @@ contract MockToken is ERC20 { function mint(uint256 amount) public { _mint(msg.sender, amount); } - -} \ No newline at end of file +} From 86eb7a4fef284a55e83030f414fb0d70f5018bdc Mon Sep 17 00:00:00 2001 From: Hendobox <50964581+Hendobox@users.noreply.github.com> Date: Sat, 24 Dec 2022 19:20:06 +0100 Subject: [PATCH 2/2] Refactored, Tightly Packed, and Getter Functionalities rearranged the state variables in the Store.sol file to optimize storage. Secondly, created IStore, ITrade, ICLP, and IPool interfaces to eliminate redundancy in the codebase, thus optimizing our storage. Thirdly, since the contracts now import interfaces, I added getter functions for state variables in the Store.sol file for accessibility getPoolBalance, getPoolWithdrawalFee, getPoolLastPaid, getBufferBalance, getBufferPayoutPeriod, getPoolFeeShare, getKeeperFeeShare, getMinimumMarginLevel, and getFundingInterval. Fourthly, since the treasury fee will be sent out to the treasury wallet address on the go, I decided to set the value for the treasury wallet during deployment because we do not want a case where fees are sent to the Lastly, updated the README file. I discovered that MAX_FEE was already set in the contract. I also ticked the treasury fees and refactoring tasks. --- README.md | 6 +- script/DeployLocal.s.sol | 2 +- src/CLP.sol | 9 +- src/Chainlink.sol | 10 +- src/Pool.sol | 42 ++++---- src/Store.sol | 123 ++++++++++++--------- src/Trade.sol | 108 +++++++++---------- src/interfaces/ICLP.sol | 8 ++ src/interfaces/IChainlink.sol | 8 ++ src/interfaces/IPool.sol | 71 ++++++++++++ src/interfaces/IStore.sol | 197 ++++++++++++++++++++++++++++++++++ 11 files changed, 445 insertions(+), 139 deletions(-) create mode 100644 src/interfaces/ICLP.sol create mode 100644 src/interfaces/IChainlink.sol create mode 100644 src/interfaces/IPool.sol create mode 100644 src/interfaces/IStore.sol diff --git a/README.md b/README.md index 3ece842..ed881c5 100644 --- a/README.md +++ b/README.md @@ -7,11 +7,9 @@ For details on how the margin / trading system works, please check the [Whitepap The items below are listed in priority order. All milestones are **ASAP**, with a target production launch date of **early January** on Arbitrum. The driving factor is high quality, speed, and code simplicity. - [ ] If submitOrder margin exceeds freeMargin, set it to the max freeMargin available -- [ ] Add MAX_FEE and other constants in Store to curtail gov powers in methods marked with onlyGov. The goal is to prevent gov from having too much power over system function, like setting a fee share too high and siphoning all the funds. - [ ] Add automated tests, including fuzzy, to achieve > 90% coverage - [ ] Verify Chainlink contract works as expected for Arbitrum and its sequencer. Support all other Chainlink networks (or have a custom Chainlink contract for each chain) - [ ] Add methods "depositThroughUniswap" and "addLiquidityThroughUniswap" to allow deposits from a contract like Uniswap Router, to allow people to deposit any asset which is then automatically converted into the Store-supported currency. Potentially support other DEXes like 1inch. -- [ ] Refactor code while maintaining readability - [ ] Run auditing tools, get more eyes on the contracts - [ ] Deploy and test locally with the [UI](https://github.com/capofficial/ui) to make sure everything is working as expected - [ ] Create production deploy scripts @@ -24,9 +22,11 @@ The items below are listed in priority order. All milestones are **ASAP**, with - [x] Flat fee - [x] Allow submitting TP/SL with an order - [x] Contracts: Trade, Pool, Store, Chainlink +- [x] Add MAX_FEE and other constants in Store to curtail gov powers in methods marked with onlyGov. The goal is to prevent gov from having too much power over system function, like setting a fee share too high and siphoning all the funds. - [x] Treasury fees should be paid out to a treasury address directly (set by gov) +- [x] Refactor code while maintaining readability -## Compiling +## Compilings ``` forge build --via-ir diff --git a/script/DeployLocal.s.sol b/script/DeployLocal.s.sol index ed17272..02dc1b3 100644 --- a/script/DeployLocal.s.sol +++ b/script/DeployLocal.s.sol @@ -38,7 +38,7 @@ contract DeployLocalScript is Script { chainlink = new MockChainlink(); console.log("Chainlink deployed to", address(chainlink)); - store = new Store(); + store = new Store(payable(deployer)); console.log("Store deployed to", address(store)); trade = new Trade(); diff --git a/src/CLP.sol b/src/CLP.sol index d0de760..6a9e1bb 100644 --- a/src/CLP.sol +++ b/src/CLP.sol @@ -11,12 +11,17 @@ contract CLP is ERC20 { } function mint(address to, uint256 amount) public { - require(msg.sender == store, "!authorized"); + _storeOnly(); _mint(to, amount); } function burn(address from, uint256 amount) public { - require(msg.sender == store, "!authorized"); + _storeOnly(); _burn(from, amount); } + + function _storeOnly() private view { + address store_ = store; + require(msg.sender == store_, "!authorized"); + } } diff --git a/src/Chainlink.sol b/src/Chainlink.sol index eeba334..6abfc42 100644 --- a/src/Chainlink.sol +++ b/src/Chainlink.sol @@ -38,11 +38,10 @@ contract Chainlink { , /*uint80 roundId*/ int256 answer, - uint256 startedAt, + uint256 startedAt, /*uint256 updatedAt*/ , - ) = /*uint256 updatedAt*/ - /*uint80 answeredInRound*/ + ) = /*uint80 answeredInRound*/ sequencerUptimeFeed.latestRoundData(); // Answer == 0: Sequencer is up @@ -63,12 +62,11 @@ contract Chainlink { ( , /*uint80 roundID*/ - int256 price, + int256 price, /*uint startedAt*/ , , - ) = /*uint startedAt*/ - /*uint timeStamp*/ + ) = /*uint timeStamp*/ /*uint80 answeredInRound*/ priceFeed.latestRoundData(); diff --git a/src/Pool.sol b/src/Pool.sol index d14f375..9ee2bc1 100644 --- a/src/Pool.sol +++ b/src/Pool.sol @@ -1,8 +1,7 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity ^0.8.13; -import "./Chainlink.sol"; -import "./Store.sol"; +import "./interfaces/IStore.sol"; contract Pool { uint256 public constant UNIT = 10**18; @@ -10,9 +9,7 @@ contract Pool { address public gov; address public trade; - - Chainlink public chainlink; - Store public store; + IStore public store; // Events @@ -64,12 +61,12 @@ contract Pool { function link(address _trade, address _store) external onlyGov { trade = _trade; - store = Store(_store); + store = IStore(_store); } function addLiquidity(uint256 amount) external { require(amount > 0, "!amount"); - uint256 balance = store.poolBalance(); + uint256 balance = store.getPoolBalance(); address user = msg.sender; store.transferIn(user, amount); @@ -82,21 +79,22 @@ contract Pool { store.mintCLP(user, clpAmount); store.incrementPoolBalance(amount); - emit AddLiquidity(user, amount, clpAmount, store.poolBalance()); + emit AddLiquidity(user, amount, clpAmount, store.getPoolBalance()); } function removeLiquidity(uint256 amount) external { require(amount > 0, "!amount"); address user = msg.sender; - uint256 balance = store.poolBalance(); + uint256 balance = store.getPoolBalance(); uint256 clpSupply = store.getCLPSupply(); require(balance > 0 && clpSupply > 0, "!empty"); uint256 userBalance = store.getUserPoolBalance(user); if (amount > userBalance) amount = userBalance; - uint256 feeAmount = (amount * store.poolWithdrawalFee()) / BPS_DIVIDER; + uint256 feeAmount = (amount * store.getPoolWithdrawalFee()) / + BPS_DIVIDER; uint256 amountMinusFee = amount - feeAmount; // CLP amount @@ -112,7 +110,7 @@ contract Pool { amount, feeAmount, clpAmount, - store.poolBalance() + store.getPoolBalance() ); } @@ -123,7 +121,7 @@ contract Pool { ) external onlyTrade { store.incrementBufferBalance(amount); - uint256 lastPaid = store.poolLastPaid(); + uint256 lastPaid = store.getPoolLastPaid(); uint256 _now = block.timestamp; if (lastPaid == 0) { @@ -131,8 +129,8 @@ contract Pool { return; } - uint256 bufferBalance = store.bufferBalance(); - uint256 bufferPayoutPeriod = store.bufferPayoutPeriod(); + uint256 bufferBalance = store.getBufferBalance(); + uint256 bufferPayoutPeriod = store.getBufferPayoutPeriod(); uint256 amountToSendPool = (bufferBalance * (block.timestamp - lastPaid)) / bufferPayoutPeriod; @@ -150,8 +148,8 @@ contract Pool { market, amount, amountToSendPool, - store.poolBalance(), - store.bufferBalance() + store.getPoolBalance(), + store.getBufferBalance() ); } @@ -162,13 +160,13 @@ contract Pool { ) external onlyTrade { if (amount == 0) return; - uint256 bufferBalance = store.bufferBalance(); + uint256 bufferBalance = store.getBufferBalance(); store.decrementBufferBalance(amount); if (amount > bufferBalance) { uint256 diffToPayFromPool = amount - bufferBalance; - uint256 poolBalance = store.poolBalance(); + uint256 poolBalance = store.getPoolBalance(); require(diffToPayFromPool < poolBalance, "!pool-balance"); store.decrementPoolBalance(diffToPayFromPool); } @@ -179,8 +177,8 @@ contract Pool { user, market, amount, - store.poolBalance(), - store.bufferBalance() + store.getPoolBalance(), + store.getBufferBalance() ); } @@ -192,11 +190,11 @@ contract Pool { ) external onlyTrade { if (fee == 0) return; - uint256 poolFee = (fee * store.poolFeeShare()) / BPS_DIVIDER; + uint256 poolFee = (fee * store.getPoolFeeShare()) / BPS_DIVIDER; uint256 treasuryFee = fee - poolFee; store.incrementPoolBalance(poolFee); - store.incrementTreasuryBalance(treasuryFee); + store.payTreasuryFee(treasuryFee); emit FeePaid( user, diff --git a/src/Store.sol b/src/Store.sol index 075a57f..0b02534 100644 --- a/src/Store.sol +++ b/src/Store.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.13; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; -import "./CLP.sol"; +import "./interfaces/ICLP.sol"; contract Store { // TODO: send balance to treasury address @@ -14,23 +14,35 @@ contract Store { using EnumerableSet for EnumerableSet.AddressSet; using SafeERC20 for IERC20; - uint256 public constant BPS_DIVIDER = 10000; - address public gov; address public currency; address public clp; + address payable public treasuryAddress; // contracts address public trade; address public pool; - uint256 public poolFeeShare = 5000; // in bps - uint256 public keeperFeeShare = 1000; // in bps - uint256 public poolWithdrawalFee = 10; // in bps - uint256 public minimumMarginLevel = 2000; // 20% in bps, at which account is liquidated + uint256 public constant BPS_DIVIDER = 10000; + uint256 public constant MAX_FEE = 1000; // 10% + + uint256 private poolFeeShare = 5000; // in bps + uint256 private keeperFeeShare = 1000; // in bps + uint256 private poolWithdrawalFee = 10; // in bps + uint256 private minimumMarginLevel = 2000; // 20% in bps, at which account is liquidated - // Structs + // Variables + uint256 public orderId; + uint256 private bufferBalance; + uint256 private poolBalance; + uint256 private poolLastPaid; + + uint256 private bufferPayoutPeriod = 7 days; + + string[] public marketList; // "ETH-USD", "BTC-USD", etc + + // Structs struct Market { string symbol; address feed; @@ -45,11 +57,11 @@ contract Store { struct Order { uint256 orderId; address user; - string market; - uint256 price; bool isLong; bool isReduceOnly; uint8 orderType; // 0 = market, 1 = limit, 2 = stop + string market; + uint256 price; uint256 margin; uint256 size; uint256 fee; @@ -58,8 +70,8 @@ contract Store { struct Position { address user; - string market; bool isLong; + string market; uint256 size; uint256 margin; int256 fundingTracker; @@ -67,24 +79,9 @@ contract Store { uint256 timestamp; } - // Variables - - uint256 public orderId; - - uint256 public bufferBalance; - uint256 public poolBalance; - uint256 public poolLastPaid; - uint256 public treasuryBalance; - - uint256 public bufferPayoutPeriod = 7 days; - mapping(uint256 => Order) private orders; mapping(address => EnumerableSet.UintSet) private userOrderIds; // user => [order ids..] EnumerableSet.UintSet private orderIds; // [order ids..] - - uint256 public constant MAX_FEE = 1000; // 10% - - string[] public marketList; // "ETH-USD", "BTC-USD", etc mapping(string => Market) private markets; mapping(bytes32 => Position) private positions; // key = user,market @@ -98,16 +95,12 @@ contract Store { mapping(address => uint256) private lockedMargins; // user => amount EnumerableSet.AddressSet private usersWithLockedMargin; // [users...] - // Funding - uint256 public constant fundingInterval = 1 hours; // In seconds. - mapping(string => int256) private fundingTrackers; // market => funding tracker (long) (short is opposite) // in UNIT * bps mapping(string => uint256) private fundingLastUpdated; // market => last time fundingTracker was updated. In seconds. - event FeeCollected(address indexed treasuryAddress, uint256 amount); - - constructor() { + constructor(address payable _treasuryAddress) { gov = msg.sender; + setTreasuryAddress(_treasuryAddress); } function link( @@ -124,17 +117,12 @@ contract Store { // Gov methods - function collectTreasuryFee(address payable treasuryAddress, uint256 amount) - external + function setTreasuryAddress(address payable _treasuryAddress) + public onlyGov { - uint256 bal = treasuryBalance; - require(bal >= amount, "NOTHING_AVAILABLE"); - require(treasuryAddress != address(0), "INVALID_ADDRESS"); - treasuryBalance -= amount; - (bool success, ) = treasuryAddress.call{value: amount}(""); - require(success, "TRANSFER_ERROR"); - emit FeeCollected(treasuryAddress, amount); + require(treasuryAddress != address(0), "!address"); + treasuryAddress = _treasuryAddress; } function setPoolFeeShare(uint256 amount) external onlyGov { @@ -187,11 +175,16 @@ contract Store { } function mintCLP(address user, uint256 amount) external onlyContract { - CLP(clp).mint(user, amount); + ICLP(clp).mint(user, amount); } function burnCLP(address user, uint256 amount) external onlyContract { - CLP(clp).mint(user, amount); + ICLP(clp).mint(user, amount); + } + + function payTreasuryFee(uint256 amount) external onlyContract { + (bool success, ) = treasuryAddress.call{value: amount}(""); + require(success, "!transferred"); } function incrementBalance(address user, uint256 amount) @@ -227,6 +220,10 @@ contract Store { return (IERC20(clp).balanceOf(user) * poolBalance) / clpSupply; } + function getPoolBalance() external view returns (uint256) { + return poolBalance; + } + function incrementBufferBalance(uint256 amount) external onlyContract { bufferBalance += amount; } @@ -239,14 +236,6 @@ contract Store { poolLastPaid = timestamp; } - function incrementTreasuryBalance(uint256 amount) external onlyContract { - treasuryBalance += amount; - } - - function decrementTreasuryBalance(uint256 amount) external onlyContract { - treasuryBalance -= amount; - } - function lockMargin(address user, uint256 amount) external onlyContract { lockedMargins[user] += amount; usersWithLockedMargin.add(user); @@ -325,6 +314,10 @@ contract Store { return orders[id]; } + function getPoolLastPaid() external view returns (uint256) { + return poolLastPaid; + } + function addOrder(Order memory order) external onlyContract @@ -458,6 +451,34 @@ contract Store { return fundingTrackers[market]; } + function getPoolWithdrawalFee() external view returns (uint256) { + return poolWithdrawalFee; + } + + function getBufferBalance() external view returns (uint256) { + return bufferBalance; + } + + function getBufferPayoutPeriod() external view returns (uint256) { + return bufferPayoutPeriod; + } + + function getPoolFeeShare() external view returns (uint256) { + return poolFeeShare; + } + + function getKeeperFeeShare() external view returns (uint256) { + return keeperFeeShare; + } + + function getMinimumMarginLevel() external view returns (uint256) { + return minimumMarginLevel; + } + + function getFundingInterval() external pure returns (uint256) { + return 1 hours; + } + function setFundingLastUpdated(string memory market, uint256 timestamp) external onlyContract diff --git a/src/Trade.sol b/src/Trade.sol index 2200bbf..42a0e65 100644 --- a/src/Trade.sol +++ b/src/Trade.sol @@ -1,10 +1,10 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity ^0.8.13; -import "./Chainlink.sol"; -import "./Store.sol"; -import "./Pool.sol"; +import "./interfaces/IPool.sol"; +import "./interfaces/IStore.sol"; import "./interfaces/ITrade.sol"; +import "./interfaces/IChainlink.sol"; contract Trade is ITrade { uint256 public constant UNIT = 10**18; @@ -12,9 +12,9 @@ contract Trade is ITrade { address public gov; - Chainlink public chainlink; - Pool public pool; - Store public store; + IChainlink public chainlink; + IPool public pool; + IStore public store; // Methods @@ -27,9 +27,9 @@ contract Trade is ITrade { address _pool, address _store ) external onlyGov { - chainlink = Chainlink(_chainlink); - pool = Pool(_pool); - store = Store(_store); + chainlink = IChainlink(_chainlink); + pool = IPool(_pool); + store = IStore(_store); } function deposit(uint256 amount) external { @@ -57,13 +57,13 @@ contract Trade is ITrade { } function submitOrder( - Store.Order memory params, + IStore.Order memory params, uint256 tpPrice, uint256 slPrice ) external { address user = msg.sender; - Store.Market memory market = store.getMarket(params.market); + IStore.Market memory market = store.getMarket(params.market); require(market.maxLeverage > 0, "!market"); require(market.minSize <= params.size, "!min-size"); @@ -137,14 +137,14 @@ contract Trade is ITrade { ); if (tpPrice > 0) { - Store.Order memory tpOrder = Store.Order({ + IStore.Order memory tpOrder = IStore.Order({ orderId: 0, user: user, - market: params.market, - price: tpPrice, isLong: !params.isLong, isReduceOnly: true, orderType: 1, + market: params.market, + price: tpPrice, margin: 0, size: params.size, fee: params.fee, @@ -167,14 +167,14 @@ contract Trade is ITrade { } if (slPrice > 0) { - Store.Order memory slOrder = Store.Order({ + IStore.Order memory slOrder = IStore.Order({ orderId: 0, user: user, - market: params.market, - price: slPrice, isLong: !params.isLong, isReduceOnly: true, orderType: 2, + market: params.market, + price: slPrice, margin: 0, size: params.size, fee: params.fee, @@ -198,11 +198,11 @@ contract Trade is ITrade { } function updateOrder(uint256 orderId, uint256 price) external { - Store.Order memory order = store.getOrder(orderId); + IStore.Order memory order = store.getOrder(orderId); require(order.user == msg.sender, "!user"); require(order.size > 0, "!order"); require(order.orderType != 0, "!market-order"); - Store.Market memory market = store.getMarket(order.market); + IStore.Market memory market = store.getMarket(order.market); uint256 chainlinkPrice = chainlink.getPrice(market.feed); require(chainlinkPrice > 0, "!chainlink"); if ( @@ -221,7 +221,7 @@ contract Trade is ITrade { } function cancelOrder(uint256 orderId) public { - Store.Order memory order = store.getOrder(orderId); + IStore.Order memory order = store.getOrder(orderId); require(order.user == msg.sender, "!user"); require(order.size > 0, "!order"); require(order.orderType != 0, "!market-order"); @@ -245,12 +245,12 @@ contract Trade is ITrade { view returns (uint256[] memory orderIdsToExecute) { - Store.Order[] memory orders = store.getOrders(); + IStore.Order[] memory orders = store.getOrders(); uint256[] memory _orderIds = new uint256[](orders.length); uint256 j; for (uint256 i = 0; i < orders.length; i++) { - Store.Order memory order = orders[i]; - Store.Market memory market = store.getMarket(order.market); + IStore.Order memory order = orders[i]; + IStore.Market memory market = store.getMarket(order.market); uint256 chainlinkPrice = chainlink.getPrice(market.feed); if (chainlinkPrice == 0) continue; @@ -295,9 +295,9 @@ contract Trade is ITrade { uint256[] memory orderIds = getExecutableOrderIds(); for (uint256 i = 0; i < orderIds.length; i++) { uint256 orderId = orderIds[i]; - Store.Order memory order = store.getOrder(orderId); + IStore.Order memory order = store.getOrder(orderId); if (order.size == 0 || order.price == 0) continue; - Store.Market memory market = store.getMarket(order.market); + IStore.Market memory market = store.getMarket(order.market); uint256 chainlinkPrice = chainlink.getPrice(market.feed); if (chainlinkPrice == 0) continue; _executeOrder(order, chainlinkPrice, msg.sender); @@ -305,12 +305,12 @@ contract Trade is ITrade { } function _executeOrder( - Store.Order memory order, + IStore.Order memory order, uint256 price, address keeper ) internal { // Check for existing position - Store.Position memory position = store.getPosition( + IStore.Position memory position = store.getPosition( order.user, order.market ); @@ -327,17 +327,17 @@ contract Trade is ITrade { } function _increasePosition( - Store.Order memory order, + IStore.Order memory order, uint256 price, address keeper ) internal { - Store.Position memory position = store.getPosition( + IStore.Position memory position = store.getPosition( order.user, order.market ); uint256 fee = order.fee; - uint256 keeperFee = (fee * store.keeperFeeShare()) / BPS_DIVIDER; + uint256 keeperFee = (fee * store.getKeeperFeeShare()) / BPS_DIVIDER; fee -= keeperFee; pool.creditFee(order.user, order.market, fee, false); @@ -389,11 +389,11 @@ contract Trade is ITrade { } function _decreasePosition( - Store.Order memory order, + IStore.Order memory order, uint256 price, address keeper ) internal { - Store.Position memory position = store.getPosition( + IStore.Position memory position = store.getPosition( order.user, order.market ); @@ -422,7 +422,7 @@ contract Trade is ITrade { } uint256 fee = order.fee; - uint256 keeperFee = (fee * store.keeperFeeShare()) / BPS_DIVIDER; + uint256 keeperFee = (fee * store.getKeeperFeeShare()) / BPS_DIVIDER; fee -= keeperFee; pool.creditFee(order.user, order.market, fee, false); @@ -505,15 +505,15 @@ contract Trade is ITrade { // Open position in opposite direction if size remains if (!order.isReduceOnly && remainingOrderSize > 0) { - Store.Order memory nextOrder = Store.Order({ + IStore.Order memory nextOrder = IStore.Order({ orderId: 0, user: order.user, + isLong: order.isLong, + orderType: 0, market: order.market, margin: remainingOrderMargin, size: remainingOrderSize, price: 0, - isLong: order.isLong, - orderType: 0, fee: (order.fee * remainingOrderSize) / order.size, isReduceOnly: false, timestamp: block.timestamp @@ -526,10 +526,10 @@ contract Trade is ITrade { function closePositionWithoutProfit(string memory _market) external { address user = msg.sender; - Store.Position memory position = store.getPosition(user, _market); + IStore.Position memory position = store.getPosition(user, _market); require(position.size > 0, "!position"); - Store.Market memory market = store.getMarket(_market); + IStore.Market memory market = store.getMarket(_market); uint256 fee = (position.size * market.fee) / BPS_DIVIDER; @@ -596,7 +596,7 @@ contract Trade is ITrade { } else { marginLevel = (BPS_DIVIDER * uint256(equity)) / lockedMargin; } - if (marginLevel < store.minimumMarginLevel()) { + if (marginLevel < store.getMinimumMarginLevel()) { _users[j] = user; j++; } @@ -615,13 +615,13 @@ contract Trade is ITrade { for (uint256 i = 0; i < usersToLiquidate.length; i++) { address user = usersToLiquidate[i]; - Store.Position[] memory positions = store.getUserPositions(user); + IStore.Position[] memory positions = store.getUserPositions(user); for (uint256 j = 0; j < positions.length; j++) { - Store.Position memory position = positions[j]; - Store.Market memory market = store.getMarket(position.market); + IStore.Position memory position = positions[j]; + IStore.Market memory market = store.getMarket(position.market); uint256 fee = (position.size * market.fee) / BPS_DIVIDER; - uint256 liquidatorFee = (fee * store.keeperFeeShare()) / + uint256 liquidatorFee = (fee * store.getKeeperFeeShare()) / BPS_DIVIDER; fee -= liquidatorFee; liquidatorFees += liquidatorFee; @@ -664,15 +664,15 @@ contract Trade is ITrade { function getUserPositionsWithUpls(address user) external view - returns (Store.Position[] memory _positions, int256[] memory _upls) + returns (IStore.Position[] memory _positions, int256[] memory _upls) { _positions = store.getUserPositions(user); uint256 length = _positions.length; _upls = new int256[](length); for (uint256 i = 0; i < length; i++) { - Store.Position memory position = _positions[i]; + IStore.Position memory position = _positions[i]; - Store.Market memory market = store.getMarket(position.market); + IStore.Market memory market = store.getMarket(position.market); uint256 chainlinkPrice = chainlink.getPrice(market.feed); if (chainlinkPrice == 0) continue; @@ -695,15 +695,15 @@ contract Trade is ITrade { function getMarketsWithPrices() external view - returns (Store.Market[] memory _markets, uint256[] memory _prices) + returns (IStore.Market[] memory _markets, uint256[] memory _prices) { string[] memory marketList = store.getMarketList(); uint256 length = marketList.length; - _markets = new Store.Market[](length); + _markets = new IStore.Market[](length); _prices = new uint256[](length); for (uint256 i = 0; i < length; i++) { - Store.Market memory market = store.getMarket(marketList[i]); + IStore.Market memory market = store.getMarket(marketList[i]); uint256 chainlinkPrice = chainlink.getPrice(market.feed); _markets[i] = market; _prices[i] = chainlinkPrice; @@ -747,10 +747,10 @@ contract Trade is ITrade { } function getUpl(address user) public view returns (int256 upl) { - Store.Position[] memory positions = store.getUserPositions(user); + IStore.Position[] memory positions = store.getUserPositions(user); for (uint256 j = 0; j < positions.length; j++) { - Store.Position memory position = positions[j]; - Store.Market memory market = store.getMarket(position.market); + IStore.Position memory position = positions[j]; + IStore.Market memory market = store.getMarket(position.market); uint256 chainlinkPrice = chainlink.getPrice(market.feed); if (chainlinkPrice == 0) continue; @@ -779,7 +779,7 @@ contract Trade is ITrade { return; } - if (lastUpdated + store.fundingInterval() > _now) return; + if (lastUpdated + store.getFundingInterval() > _now) return; int256 fundingIncrement = getAccruedFunding(market, 0); // in UNIT * bps @@ -803,7 +803,7 @@ contract Trade is ITrade { if (intervals == 0) { intervals = (block.timestamp - store.getFundingLastUpdated(market)) / - store.fundingInterval(); + store.getFundingInterval(); } if (intervals == 0) return 0; diff --git a/src/interfaces/ICLP.sol b/src/interfaces/ICLP.sol new file mode 100644 index 0000000..0f9ee47 --- /dev/null +++ b/src/interfaces/ICLP.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +interface ICLP { + function mint(address to, uint256 amount) external; + + function burn(address from, uint256 amount) external; +} diff --git a/src/interfaces/IChainlink.sol b/src/interfaces/IChainlink.sol new file mode 100644 index 0000000..0c645fb --- /dev/null +++ b/src/interfaces/IChainlink.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.13; + +interface IChainlink { + function setPrice(address feed, uint256 price) external; + + function getPrice(address feed) external view returns (uint256); +} diff --git a/src/interfaces/IPool.sol b/src/interfaces/IPool.sol new file mode 100644 index 0000000..8f3ea35 --- /dev/null +++ b/src/interfaces/IPool.sol @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.13; + +interface IPool { + // Events + + event AddLiquidity( + address indexed user, + uint256 amount, + uint256 clpAmount, + uint256 poolBalance + ); + + event RemoveLiquidity( + address indexed user, + uint256 amount, + uint256 feeAmount, + uint256 clpAmount, + uint256 poolBalance + ); + + event PoolPayIn( + address indexed user, + string market, + uint256 amount, + uint256 bufferToPoolAmount, + uint256 poolBalance, + uint256 bufferBalance + ); + + event PoolPayOut( + address indexed user, + string market, + uint256 amount, + uint256 poolBalance, + uint256 bufferBalance + ); + + event FeePaid( + address indexed user, + string market, + uint256 fee, + uint256 poolFee, + bool isLiquidation + ); + + function link(address _trade, address _store) external; + + function addLiquidity(uint256 amount) external; + + function removeLiquidity(uint256 amount) external; + + function creditTraderLoss( + address user, + string memory market, + uint256 amount + ) external; + + function debitTraderProfit( + address user, + string memory market, + uint256 amount + ) external; + + function creditFee( + address user, + string memory market, + uint256 fee, + bool isLiquidation + ) external; +} diff --git a/src/interfaces/IStore.sol b/src/interfaces/IStore.sol new file mode 100644 index 0000000..7f5ecca --- /dev/null +++ b/src/interfaces/IStore.sol @@ -0,0 +1,197 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.13; + +interface IStore { + // Structs + struct Market { + string symbol; + address feed; + uint256 maxLeverage; + uint256 maxOI; + uint256 fee; // in bps + uint256 fundingFactor; // Yearly funding rate if OI is completely skewed to one side. In bps. + uint256 minSize; + uint256 minSettlementTime; // time before keepers can execute order (price finality) if chainlink price didn't change + } + + struct Order { + uint256 orderId; + address user; + bool isLong; + bool isReduceOnly; + uint8 orderType; // 0 = market, 1 = limit, 2 = stop + string market; + uint256 price; + uint256 margin; + uint256 size; + uint256 fee; + uint256 timestamp; + } + + struct Position { + address user; + bool isLong; + string market; + uint256 size; + uint256 margin; + int256 fundingTracker; + uint256 price; + uint256 timestamp; + } + + function link( + address _trade, + address _pool, + address _currency, + address _clp + ) external; + + // Gov methods + + function setTreasuryAddress(address payable _treasuryAddress) external; + + function setPoolFeeShare(uint256 amount) external; + + function setKeeperFeeShare(uint256 amount) external; + + function setPoolWithdrawalFee(uint256 amount) external; + + function setMinimumMarginLevel(uint256 amount) external; + + function setBufferPayoutPeriod(uint256 amount) external; + + function setMarket(string memory market, Market memory marketInfo) external; + + // Methods + + function transferIn(address user, uint256 amount) external; + + function transferOut(address user, uint256 amount) external; + + function getCLPSupply() external view returns (uint256); + + function mintCLP(address user, uint256 amount) external; + + function burnCLP(address user, uint256 amount) external; + + function payTreasuryFee(uint256 amount) external; + + function incrementBalance(address user, uint256 amount) external; + + function decrementBalance(address user, uint256 amount) external; + + function getBalance(address user) external view returns (uint256); + + function incrementPoolBalance(uint256 amount) external; + + function decrementPoolBalance(uint256 amount) external; + + function getUserPoolBalance(address user) external view returns (uint256); + + function incrementBufferBalance(uint256 amount) external; + + function decrementBufferBalance(uint256 amount) external; + + function setPoolLastPaid(uint256 timestamp) external; + + function lockMargin(address user, uint256 amount) external; + + function unlockMargin(address user, uint256 amount) external; + + function getLockedMargin(address user) external view returns (uint256); + + function getUsersWithLockedMarginLength() external view returns (uint256); + + function getUserWithLockedMargin(uint256 i) external view returns (address); + + function incrementOI( + string memory market, + uint256 size, + bool isLong + ) external; + + function decrementOI( + string memory market, + uint256 size, + bool isLong + ) external; + + function getOILong(string memory market) external view returns (uint256); + + function getOIShort(string memory market) external view returns (uint256); + + function getOrder(uint256 id) external view returns (Order memory _order); + + function addOrder(Order memory order) external returns (uint256); + + function updateOrder(Order memory order) external; + + function removeOrder(uint256 _orderId) external; + + function getOrders() external view returns (Order[] memory _orders); + + function getUserOrders(address user) + external + view + returns (Order[] memory _orders); + + function addOrUpdatePosition(Position memory position) external; + + function removePosition(address user, string memory market) external; + + function getPosition(address user, string memory market) + external + view + returns (Position memory position); + + function getUserPositions(address user) + external + view + returns (Position[] memory _positions); + + function getMarket(string memory market) + external + view + returns (Market memory _market); + + function getMarketList() external view returns (string[] memory); + + function getFundingLastUpdated(string memory market) + external + view + returns (uint256); + + function getFundingFactor(string memory market) + external + view + returns (uint256); + + function getFundingTracker(string memory market) + external + view + returns (int256); + + function setFundingLastUpdated(string memory market, uint256 timestamp) + external; + + function updateFundingTracker(string memory market, int256 fundingIncrement) + external; + + function getPoolBalance() external view returns (uint256); + + function getPoolWithdrawalFee() external view returns (uint256); + + function getPoolLastPaid() external view returns (uint256); + + function getBufferBalance() external view returns (uint256); + + function getBufferPayoutPeriod() external view returns (uint256); + + function getPoolFeeShare() external view returns (uint256); + + function getKeeperFeeShare() external view returns (uint256); + + function getMinimumMarginLevel() external view returns (uint256); + + function getFundingInterval() external view returns (uint256); +}