diff --git a/README.md b/README.md index 5cf14fa5..d8444051 100644 --- a/README.md +++ b/README.md @@ -42,5 +42,6 @@ We would like to thank the following projects. - [Aave](https://github.com/aave/aave-v3-core) - [Uniswap](https://github.com/Uniswap/v3-core) - [Seaport](https://github.com/ProjectOpenSea/seaport) +- [Vertex](https://github.com/vertex-protocol) - [BendDAO](https://github.com/BendDAO) - [Blur.io](https://blur.io) diff --git a/contracts/dependencies/vertex/ArbAirdrop.sol b/contracts/dependencies/vertex/ArbAirdrop.sol new file mode 100644 index 00000000..d491ecd5 --- /dev/null +++ b/contracts/dependencies/vertex/ArbAirdrop.sol @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +import "./interfaces/IArbAirdrop.sol"; +import "./Endpoint.sol"; +import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol"; +import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +contract ArbAirdrop is OwnableUpgradeable, IArbAirdrop { + address token; + address sanctions; + uint32 pastWeeks; + + mapping(uint32 => bytes32) merkleRoots; + mapping(uint32 => mapping(address => uint256)) claimed; + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + function initialize(address _token, address _sanctions) + external + initializer + { + __Ownable_init(); + token = _token; + sanctions = _sanctions; + } + + function registerMerkleRoot(uint32 week, bytes32 merkleRoot) + external + onlyOwner + { + pastWeeks += 1; + require(week == pastWeeks, "Invalid week provided."); + merkleRoots[week] = merkleRoot; + } + + function _verifyProof( + uint32 week, + address sender, + uint256 totalAmount, + bytes32[] calldata proof + ) internal { + require(claimed[week][sender] == 0, "Already claimed."); + require( + merkleRoots[week] != bytes32(0), + "Week hasn't been registered." + ); + require( + !ISanctionsList(sanctions).isSanctioned(sender), + "address is sanctioned." + ); + bytes32 leaf = keccak256( + bytes.concat(keccak256(abi.encode(sender, totalAmount))) + ); + bool isValidLeaf = MerkleProof.verify(proof, merkleRoots[week], leaf); + require(isValidLeaf, "Invalid proof."); + claimed[week][sender] = totalAmount; + } + + function _claim( + uint32 week, + uint256 totalAmount, + bytes32[] calldata proof + ) internal { + _verifyProof(week, msg.sender, totalAmount, proof); + SafeERC20.safeTransfer(IERC20(token), msg.sender, totalAmount); + emit ClaimArb(msg.sender, week, totalAmount); + } + + function claim(ClaimProof[] calldata claimProofs) external { + for (uint32 i = 0; i < claimProofs.length; i++) { + _claim( + claimProofs[i].week, + claimProofs[i].totalAmount, + claimProofs[i].proof + ); + } + } + + function getClaimed(address account) + external + view + returns (uint256[] memory) + { + uint256[] memory result = new uint256[](pastWeeks + 1); + for (uint32 week = 1; week <= pastWeeks; week++) { + result[week] = claimed[week][account]; + } + return result; + } +} diff --git a/contracts/dependencies/vertex/BaseEngine.sol b/contracts/dependencies/vertex/BaseEngine.sol new file mode 100644 index 00000000..a7f0ed41 --- /dev/null +++ b/contracts/dependencies/vertex/BaseEngine.sol @@ -0,0 +1,119 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import "hardhat/console.sol"; + +import "./common/Constants.sol"; +import "./common/Errors.sol"; +import "./libraries/MathHelper.sol"; +import "./libraries/MathSD21x18.sol"; +import "./interfaces/clearinghouse/IClearinghouse.sol"; +import "./interfaces/engine/IProductEngine.sol"; +import "./interfaces/IOffchainBook.sol"; +import "./interfaces/IFeeCalculator.sol"; +import "./interfaces/IEndpoint.sol"; +import "./EndpointGated.sol"; +import "./interfaces/clearinghouse/IClearinghouseState.sol"; + +abstract contract BaseEngine is IProductEngine, EndpointGated { + using MathSD21x18 for int128; + + IClearinghouse internal _clearinghouse; + IFeeCalculator internal _fees; + uint32[] internal productIds; + + // productId => orderbook + mapping(uint32 => IOffchainBook) public markets; + + // Whether an address can apply deltas - all orderbooks and clearinghouse is whitelisted + mapping(address => bool) internal canApplyDeltas; + + event BalanceUpdate(uint32 productId, bytes32 subaccount); + event ProductUpdate(uint32 productId); + + function _productUpdate(uint32 productId) internal virtual {} + + function _balanceUpdate(uint32 productId, bytes32 subaccount) + internal + virtual + {} + + function checkCanApplyDeltas() internal view virtual { + require(canApplyDeltas[msg.sender], ERR_UNAUTHORIZED); + } + + function _initialize( + address _clearinghouseAddr, + address, /* _quoteAddr */ + address _endpointAddr, + address _admin, + address _feeAddr + ) internal initializer { + __Ownable_init(); + setEndpoint(_endpointAddr); + transferOwnership(_admin); + + _clearinghouse = IClearinghouse(_clearinghouseAddr); + _fees = IFeeCalculator(_feeAddr); + + canApplyDeltas[_endpointAddr] = true; + canApplyDeltas[_clearinghouseAddr] = true; + } + + function getClearinghouse() external view returns (address) { + return address(_clearinghouse); + } + + function getProductIds() external view returns (uint32[] memory) { + return productIds; + } + + function getOrderbook(uint32 productId) public view returns (address) { + return address(markets[productId]); + } + + function _addProductForId( + uint32 healthGroup, + IClearinghouseState.RiskStore memory riskStore, + address book, + int128 sizeIncrement, + int128 priceIncrementX18, + int128 minSize, + int128 lpSpreadX18 + ) internal returns (uint32 productId) { + require(book != address(0)); + require( + riskStore.longWeightInitial <= riskStore.longWeightMaintenance && + riskStore.shortWeightInitial >= + riskStore.shortWeightMaintenance, + ERR_BAD_PRODUCT_CONFIG + ); + + // register product with clearinghouse + productId = _clearinghouse.registerProductForId( + book, + riskStore, + healthGroup + ); + + productIds.push(productId); + canApplyDeltas[book] = true; + + markets[productId] = IOffchainBook(book); + markets[productId].initialize( + _clearinghouse, + this, + getEndpoint(), + owner(), + _fees, + productId, + sizeIncrement, + priceIncrementX18, + minSize, + lpSpreadX18 + ); + + emit AddProduct(productId); + } +} diff --git a/contracts/dependencies/vertex/Clearinghouse.sol b/contracts/dependencies/vertex/Clearinghouse.sol new file mode 100644 index 00000000..9405cd0f --- /dev/null +++ b/contracts/dependencies/vertex/Clearinghouse.sol @@ -0,0 +1,740 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; + +import "./common/Constants.sol"; +import "./interfaces/clearinghouse/IClearinghouse.sol"; +import "./interfaces/engine/IProductEngine.sol"; +import "./interfaces/engine/ISpotEngine.sol"; +import "./interfaces/IOffchainBook.sol"; +import "./libraries/ERC20Helper.sol"; +import "./libraries/MathHelper.sol"; +import "./libraries/MathSD21x18.sol"; +import "./interfaces/engine/IPerpEngine.sol"; +import "./EndpointGated.sol"; +import "./interfaces/IEndpoint.sol"; +import "./ClearinghouseRisk.sol"; +import "./ClearinghouseStorage.sol"; +import "./Version.sol"; + +contract Clearinghouse is + ClearinghouseRisk, + ClearinghouseStorage, + IClearinghouse, + Version +{ + using MathSD21x18 for int128; + using ERC20Helper for IERC20Base; + + function initialize( + address _endpoint, + address _quote, + address _fees, + address _clearinghouseLiq + ) external initializer { + __Ownable_init(); + setEndpoint(_endpoint); + quote = _quote; + fees = IFeeCalculator(_fees); + clearinghouse = address(this); + clearinghouseLiq = _clearinghouseLiq; + numProducts = 1; + + // fees subaccount will be subaccount max int + + risks[QUOTE_PRODUCT_ID] = RiskStore({ + longWeightInitial: 1e9, + shortWeightInitial: 1e9, + longWeightMaintenance: 1e9, + shortWeightMaintenance: 1e9, + largePositionPenalty: 0 + }); + + emit ClearinghouseInitialized(_endpoint, _quote, _fees); + } + + /** + * View + */ + + function getQuote() external view returns (address) { + return quote; + } + + function getSupportedEngines() + external + view + returns (IProductEngine.EngineType[] memory) + { + return supportedEngines; + } + + function getEngineByType(IProductEngine.EngineType engineType) + external + view + returns (address) + { + return address(engineByType[engineType]); + } + + function getEngineByProduct(uint32 productId) + external + view + returns (address) + { + return address(productToEngine[productId]); + } + + function getOrderbook(uint32 productId) external view returns (address) { + return address(productToEngine[productId].getOrderbook(productId)); + } + + function getNumProducts() external view returns (uint32) { + return numProducts; + } + + function getInsurance() external view returns (int128) { + return insurance; + } + + /// @notice grab total subaccount health + function getHealth(bytes32 subaccount, IProductEngine.HealthType healthType) + public + view + returns (int128 health) + { + ISpotEngine spotEngine = ISpotEngine( + address(engineByType[IProductEngine.EngineType.SPOT]) + ); + IPerpEngine perpEngine = IPerpEngine( + address(engineByType[IProductEngine.EngineType.PERP]) + ); + + { + ISpotEngine.Balance memory balance = spotEngine.getBalance( + QUOTE_PRODUCT_ID, + subaccount + ); + health = balance.amount; + } + + for (uint32 i = 0; i <= maxHealthGroup; ++i) { + HealthGroup memory group = HealthGroup(i * 2 + 1, i * 2 + 2); + HealthVars memory healthVars; + healthVars.pricesX18 = getOraclePricesX18(i); + + if (spotEngine.hasBalance(group.spotId, subaccount)) { + ( + ISpotEngine.LpBalance memory lpBalance, + ISpotEngine.Balance memory balance + ) = spotEngine.getBalances(group.spotId, subaccount); + + if (group.spotId == VRTX_PRODUCT_ID) { + // health will be negative as long as VRTX balance is negative, + // so that nobody can borrow it. + if (balance.amount < 0) { + return -ONE; + } + } + + if (lpBalance.amount != 0) { + ISpotEngine.LpState memory lpState = spotEngine.getLpState( + group.spotId + ); + (int128 ammBase, int128 ammQuote) = MathHelper + .ammEquilibrium( + lpState.base.amount, + lpState.quote.amount, + healthVars.pricesX18.spotPriceX18 + ); + + health += ammQuote.mul(lpBalance.amount).div( + lpState.supply + ); + healthVars.spotInLpAmount = ammBase + .mul(lpBalance.amount) + .div(lpState.supply); + } + + healthVars.spotAmount = balance.amount; + healthVars.spotRisk = getRisk(group.spotId); + } + if ( + group.perpId != 0 && + perpEngine.hasBalance(group.perpId, subaccount) + ) { + ( + IPerpEngine.LpBalance memory lpBalance, + IPerpEngine.Balance memory balance + ) = perpEngine.getBalances(group.perpId, subaccount); + + if (lpBalance.amount != 0) { + IPerpEngine.LpState memory lpState = perpEngine.getLpState( + group.perpId + ); + (int128 ammBase, int128 ammQuote) = MathHelper + .ammEquilibrium( + lpState.base, + lpState.quote, + healthVars.pricesX18.perpPriceX18 + ); + + health += ammQuote.mul(lpBalance.amount).div( + lpState.supply + ); + healthVars.perpInLpAmount = ammBase + .mul(lpBalance.amount) + .div(lpState.supply); + } + + health += balance.vQuoteBalance; + healthVars.perpAmount = balance.amount; + healthVars.perpRisk = getRisk(group.perpId); + + if ( + (healthVars.spotAmount > 0) != (healthVars.perpAmount > 0) + ) { + if (healthVars.spotAmount > 0) { + healthVars.basisAmount = MathHelper.min( + healthVars.spotAmount, + -healthVars.perpAmount + ); + } else { + healthVars.basisAmount = MathHelper.max( + healthVars.spotAmount, + -healthVars.perpAmount + ); + } + healthVars.spotAmount -= healthVars.basisAmount; + healthVars.perpAmount += healthVars.basisAmount; + } + } + + // risk for the basis trade, discounted + if (healthVars.basisAmount != 0) { + // add the actual value of the basis (PNL) + health += (healthVars.pricesX18.spotPriceX18 - + healthVars.pricesX18.perpPriceX18).mul( + healthVars.basisAmount + ); + + int128 posAmount = MathHelper.abs(healthVars.basisAmount); + + // compute a penalty% on the notional size of the basis trade + // this is equivalent to a long weight, i.e. long weight 0.95 == 0.05 penalty + // we take the square of the penalties on the spot and the perp positions + health -= RiskHelper + ._getSpreadPenaltyX18( + healthVars.spotRisk, + healthVars.perpRisk, + posAmount, + healthType + ) + .mul(posAmount) + .mul( + healthVars.pricesX18.spotPriceX18 + + healthVars.pricesX18.perpPriceX18 + ); + } + + // apply risk for spot and perp positions + int128 combinedSpot = healthVars.spotAmount + + healthVars.spotInLpAmount; + + if (combinedSpot != 0) { + health += RiskHelper + ._getWeightX18( + healthVars.spotRisk, + combinedSpot, + healthType + ) + .mul(combinedSpot) + .mul(healthVars.pricesX18.spotPriceX18); + } + + int128 combinedPerp = healthVars.perpAmount + + healthVars.perpInLpAmount; + + if (combinedPerp != 0) { + health += RiskHelper + ._getWeightX18( + healthVars.perpRisk, + combinedPerp, + healthType + ) + .mul(combinedPerp) + .mul(healthVars.pricesX18.perpPriceX18); + } + + if (healthVars.spotInLpAmount != 0) { + // apply penalties on amount in LPs + health -= (ONE - + RiskHelper._getWeightX18( + healthVars.spotRisk, + healthVars.spotInLpAmount, + healthType + )).mul(healthVars.spotInLpAmount).mul( + healthVars.pricesX18.spotPriceX18 + ); + } + + if (healthVars.perpInLpAmount != 0) { + health -= (ONE - + RiskHelper._getWeightX18( + healthVars.perpRisk, + healthVars.perpInLpAmount, + healthType + )).mul(healthVars.perpInLpAmount).mul( + healthVars.pricesX18.perpPriceX18 + ); + } + } + } + + /** + * Actions + */ + + function addEngine(address engine, IProductEngine.EngineType engineType) + external + onlyOwner + { + require(address(engineByType[engineType]) == address(0)); + require(engine != address(0)); + IProductEngine productEngine = IProductEngine(engine); + // Register + supportedEngines.push(engineType); + engineByType[engineType] = productEngine; + + // add quote to product mapping + if (engineType == IProductEngine.EngineType.SPOT) { + productToEngine[QUOTE_PRODUCT_ID] = productEngine; + } + + // Initialize engine + productEngine.initialize( + address(this), + quote, + getEndpoint(), + owner(), + address(fees) + ); + } + + function modifyProductConfig(uint32 productId, RiskStore memory riskStore) + external + { + IProductEngine engine = IProductEngine(msg.sender); + IProductEngine.EngineType engineType = engine.getEngineType(); + require( + address(engineByType[engineType]) == msg.sender, + ERR_UNAUTHORIZED + ); + risks[productId] = riskStore; + } + + /// @notice registers product id and returns + function registerProductForId( + address book, + RiskStore memory riskStore, + uint32 healthGroup + ) external returns (uint32 productId) { + IProductEngine engine = IProductEngine(msg.sender); + IProductEngine.EngineType engineType = engine.getEngineType(); + require( + address(engineByType[engineType]) == msg.sender, + ERR_UNAUTHORIZED + ); + + numProducts += 1; + + // So for a given productId except quote, its healthGroup + // is (productId + 1) / 2 + productId = healthGroup * 2 + 1; + if (engineType == IProductEngine.EngineType.PERP) { + productId += 1; + } + risks[productId] = riskStore; + + if (healthGroup > maxHealthGroup) { + require( + healthGroup == maxHealthGroup + 1, + ERR_INVALID_HEALTH_GROUP + ); + maxHealthGroup = healthGroup; + } + + productToEngine[productId] = engine; + IEndpoint(getEndpoint()).setBook(productId, book); + return productId; + } + + function handleDepositTransfer( + IERC20Base token, + address from, + uint128 amount + ) internal virtual { + token.safeTransferFrom(from, address(this), uint256(amount)); + } + + function depositCollateral(IEndpoint.DepositCollateral calldata txn) + external + virtual + onlyEndpoint + { + require(txn.amount <= INT128_MAX, ERR_CONVERSION_OVERFLOW); + ISpotEngine spotEngine = ISpotEngine( + address(engineByType[IProductEngine.EngineType.SPOT]) + ); + IERC20Base token = IERC20Base( + spotEngine.getConfig(txn.productId).token + ); + require(address(token) != address(0)); + // transfer from the endpoint + handleDepositTransfer(token, msg.sender, uint128(txn.amount)); + + require(token.decimals() <= MAX_DECIMALS); + int256 multiplier = int256(10**(MAX_DECIMALS - token.decimals())); + int128 amountRealized = int128(txn.amount) * int128(multiplier); + + IProductEngine.ProductDelta[] + memory deltas = new IProductEngine.ProductDelta[](1); + + deltas[0] = IProductEngine.ProductDelta({ + productId: txn.productId, + subaccount: txn.sender, + amountDelta: amountRealized, + vQuoteDelta: 0 + }); + + spotEngine.applyDeltas(deltas); + + emit ModifyCollateral(amountRealized, txn.sender, txn.productId); + } + + /// @notice control insurance balance, only callable by owner + function depositInsurance(IEndpoint.DepositInsurance calldata txn) + external + virtual + onlyEndpoint + { + require(txn.amount <= INT128_MAX, ERR_CONVERSION_OVERFLOW); + IERC20Base token = IERC20Base(quote); + int256 multiplier = int256(10**(MAX_DECIMALS - token.decimals())); + int128 amount = int128(txn.amount) * int128(multiplier); + + insurance += amount; + // facilitate transfer + handleDepositTransfer(token, msg.sender, uint128(txn.amount)); + } + + function handleWithdrawTransfer( + IERC20Base token, + address to, + uint128 amount + ) internal virtual { + token.safeTransfer(to, uint256(amount)); + } + + function withdrawCollateral(IEndpoint.WithdrawCollateral calldata txn) + external + virtual + onlyEndpoint + { + require(txn.amount <= INT128_MAX, ERR_CONVERSION_OVERFLOW); + ISpotEngine spotEngine = ISpotEngine( + address(engineByType[IProductEngine.EngineType.SPOT]) + ); + IERC20Base token = IERC20Base( + spotEngine.getConfig(txn.productId).token + ); + require(address(token) != address(0)); + handleWithdrawTransfer( + token, + address(uint160(bytes20(txn.sender))), + txn.amount + ); + + int256 multiplier = int256(10**(MAX_DECIMALS - token.decimals())); + int128 amountRealized = -int128(txn.amount) * int128(multiplier); + + IProductEngine.ProductDelta[] + memory deltas = new IProductEngine.ProductDelta[](1); + + deltas[0] = IProductEngine.ProductDelta({ + productId: txn.productId, + subaccount: txn.sender, + amountDelta: amountRealized, + vQuoteDelta: 0 + }); + + spotEngine.applyDeltas(deltas); + require(!_isUnderInitial(txn.sender), ERR_SUBACCT_HEALTH); + + emit ModifyCollateral(amountRealized, txn.sender, txn.productId); + } + + function mintLp(IEndpoint.MintLp calldata txn) + external + virtual + onlyEndpoint + { + productToEngine[txn.productId].mintLp( + txn.productId, + txn.sender, + int128(txn.amountBase), + int128(txn.quoteAmountLow), + int128(txn.quoteAmountHigh) + ); + require(!_isUnderInitial(txn.sender), ERR_SUBACCT_HEALTH); + } + + function mintLpSlowMode(IEndpoint.MintLp calldata txn) + external + virtual + onlyEndpoint + { + require( + txn.productId != QUOTE_PRODUCT_ID && + productToEngine[txn.productId].getEngineType() == + IProductEngine.EngineType.SPOT + ); + productToEngine[txn.productId].mintLp( + txn.productId, + txn.sender, + int128(txn.amountBase), + int128(txn.quoteAmountLow), + int128(txn.quoteAmountHigh) + ); + require(!_isUnderInitial(txn.sender), ERR_SUBACCT_HEALTH); + } + + function burnLp(IEndpoint.BurnLp calldata txn) + external + virtual + onlyEndpoint + { + productToEngine[txn.productId].burnLp( + txn.productId, + txn.sender, + int128(txn.amount) + ); + } + + function burnLpAndTransfer(IEndpoint.BurnLpAndTransfer calldata txn) + external + virtual + onlyEndpoint + { + (int128 amountBase, int128 amountQuote) = productToEngine[txn.productId] + .burnLp(txn.productId, txn.sender, int128(txn.amount)); + + IProductEngine.ProductDelta[] + memory deltas = new IProductEngine.ProductDelta[](4); + + deltas[0] = IProductEngine.ProductDelta({ + productId: QUOTE_PRODUCT_ID, + subaccount: txn.sender, + amountDelta: -amountQuote, + vQuoteDelta: 0 + }); + + deltas[1] = IProductEngine.ProductDelta({ + productId: QUOTE_PRODUCT_ID, + subaccount: txn.recipient, + amountDelta: amountQuote, + vQuoteDelta: 0 + }); + + deltas[2] = IProductEngine.ProductDelta({ + productId: txn.productId, + subaccount: txn.sender, + amountDelta: -amountBase, + vQuoteDelta: -amountQuote + }); + + deltas[3] = IProductEngine.ProductDelta({ + productId: txn.productId, + subaccount: txn.recipient, + amountDelta: amountBase, + vQuoteDelta: amountQuote + }); + + productToEngine[txn.productId].applyDeltas(deltas); + require(!_isUnderInitial(txn.sender), ERR_SUBACCT_HEALTH); + } + + function updateFeeRates(IEndpoint.UpdateFeeRates calldata txn) + external + virtual + onlyEndpoint + { + fees.updateFeeRates( + txn.user, + txn.productId, + txn.makerRateX18, + txn.takerRateX18 + ); + } + + function claimSequencerFees( + IEndpoint.ClaimSequencerFees calldata txn, + int128[] calldata fees + ) external virtual onlyEndpoint { + ISpotEngine spotEngine = ISpotEngine( + address(engineByType[IProductEngine.EngineType.SPOT]) + ); + + IPerpEngine perpEngine = IPerpEngine( + address(engineByType[IProductEngine.EngineType.PERP]) + ); + + uint32[] memory spotIds = spotEngine.getProductIds(); + uint32[] memory perpIds = perpEngine.getProductIds(); + + IProductEngine.ProductDelta[] + memory spotDeltas = new IProductEngine.ProductDelta[]( + spotIds.length * 2 + ); + + IProductEngine.ProductDelta[] + memory perpDeltas = new IProductEngine.ProductDelta[]( + perpIds.length * 2 + ); + + require(spotIds[0] == QUOTE_PRODUCT_ID, ERR_INVALID_PRODUCT); + + for (uint32 i = 1; i < spotIds.length; i++) { + spotDeltas[0].amountDelta += IOffchainBook( + productToEngine[spotIds[i]].getOrderbook(spotIds[i]) + ).claimSequencerFee(); + } + + for (uint256 i = 0; i < spotIds.length; i++) { + (uint256 subaccountIdx, uint256 feesIdx) = (2 * i, 2 * i + 1); + + spotDeltas[subaccountIdx].productId = spotIds[i]; + spotDeltas[subaccountIdx].subaccount = txn.subaccount; + spotDeltas[subaccountIdx].amountDelta += fees[i]; + + ISpotEngine.Balance memory feeBalance = spotEngine.getBalance( + spotIds[i], + FEES_ACCOUNT + ); + + spotDeltas[subaccountIdx].amountDelta += feeBalance.amount; + + spotDeltas[feesIdx].productId = spotIds[i]; + spotDeltas[feesIdx].subaccount = FEES_ACCOUNT; + spotDeltas[feesIdx].amountDelta -= feeBalance.amount; + } + + for (uint256 i = 0; i < perpIds.length; i++) { + (uint256 subaccountIdx, uint256 feesIdx) = (2 * i, 2 * i + 1); + + perpDeltas[subaccountIdx].productId = perpIds[i]; + perpDeltas[subaccountIdx].subaccount = txn.subaccount; + perpDeltas[subaccountIdx].vQuoteDelta += IOffchainBook( + productToEngine[perpIds[i]].getOrderbook(perpIds[i]) + ).claimSequencerFee(); + + IPerpEngine.Balance memory feeBalance = perpEngine.getBalance( + perpIds[i], + FEES_ACCOUNT + ); + + perpDeltas[subaccountIdx].amountDelta += feeBalance.amount; + perpDeltas[subaccountIdx].vQuoteDelta += feeBalance.vQuoteBalance; + + perpDeltas[feesIdx].productId = perpIds[i]; + perpDeltas[feesIdx].subaccount = FEES_ACCOUNT; + perpDeltas[feesIdx].amountDelta -= feeBalance.amount; + perpDeltas[feesIdx].vQuoteDelta -= feeBalance.vQuoteBalance; + } + + spotEngine.applyDeltas(spotDeltas); + perpEngine.applyDeltas(perpDeltas); + } + + function _settlePnl(bytes32 subaccount, uint256 productIds) internal { + IPerpEngine perpEngine = IPerpEngine( + address(engineByType[IProductEngine.EngineType.PERP]) + ); + IProductEngine.ProductDelta[] + memory deltas = new IProductEngine.ProductDelta[](1); + + int128 amountSettled = perpEngine.settlePnl(subaccount, productIds); + deltas[0] = IProductEngine.ProductDelta({ + productId: QUOTE_PRODUCT_ID, + subaccount: subaccount, + amountDelta: amountSettled, + vQuoteDelta: 0 + }); + + ISpotEngine spotEngine = ISpotEngine( + address(engineByType[IProductEngine.EngineType.SPOT]) + ); + spotEngine.applyDeltas(deltas); + } + + function settlePnl(IEndpoint.SettlePnl calldata txn) external onlyEndpoint { + for (uint128 i = 0; i < txn.subaccounts.length; ++i) { + _settlePnl(txn.subaccounts[i], txn.productIds[i]); + } + } + + function _isUnderInitial(bytes32 subaccount) public view returns (bool) { + // Weighted initial health with limit orders < 0 + return getHealth(subaccount, IProductEngine.HealthType.INITIAL) < 0; + } + + function _isAboveInitial(bytes32 subaccount) public view returns (bool) { + // Weighted initial health with limit orders < 0 + return getHealth(subaccount, IProductEngine.HealthType.INITIAL) > 0; + } + + function _isUnderMaintenance(bytes32 subaccount) + internal + view + returns (bool) + { + // Weighted maintenance health < 0 + return getHealth(subaccount, IProductEngine.HealthType.MAINTENANCE) < 0; + } + + function liquidateSubaccount(IEndpoint.LiquidateSubaccount calldata txn) + external + virtual + onlyEndpoint + { + bytes4 liquidateSubaccountSelector = bytes4( + keccak256( + "liquidateSubaccountImpl((bytes32,bytes32,uint8,uint32,int128,uint64))" + ) + ); + bytes memory liquidateSubaccountCall = abi.encodeWithSelector( + liquidateSubaccountSelector, + txn + ); + (bool success, bytes memory result) = clearinghouseLiq.delegatecall( + liquidateSubaccountCall + ); + require(success, string(result)); + } + + function upgradeClearinghouseLiq(address _clearinghouseLiq) + external + onlyOwner + { + clearinghouseLiq = _clearinghouseLiq; + } + + function getAllBooks() external view returns (address[] memory) { + address[] memory allBooks = new address[](numProducts); + for (uint32 productId = 0; productId < numProducts; productId++) { + allBooks[productId] = IEndpoint(getEndpoint()).getBook(productId); + } + return allBooks; + } +} diff --git a/contracts/dependencies/vertex/ClearinghouseLiq.sol b/contracts/dependencies/vertex/ClearinghouseLiq.sol new file mode 100644 index 00000000..808d1441 --- /dev/null +++ b/contracts/dependencies/vertex/ClearinghouseLiq.sol @@ -0,0 +1,707 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import "hardhat/console.sol"; + +import "./common/Constants.sol"; +import "./interfaces/clearinghouse/IClearinghouseLiq.sol"; +import "./interfaces/clearinghouse/IClearinghouse.sol"; +import "./interfaces/engine/IProductEngine.sol"; +import "./interfaces/engine/ISpotEngine.sol"; +import "./interfaces/IOffchainBook.sol"; +import "./libraries/ERC20Helper.sol"; +import "./libraries/MathHelper.sol"; +import "./libraries/MathSD21x18.sol"; +import "./interfaces/engine/IPerpEngine.sol"; +import "./EndpointGated.sol"; +import "./interfaces/IEndpoint.sol"; +import "./ClearinghouseRisk.sol"; +import "./ClearinghouseStorage.sol"; +import "./Version.sol"; + +contract ClearinghouseLiq is + ClearinghouseRisk, + ClearinghouseStorage, + IClearinghouseLiq, + Version +{ + using MathSD21x18 for int128; + + function getHealthFromClearinghouse( + bytes32 subaccount, + IProductEngine.HealthType healthType + ) internal view returns (int128 health) { + return IClearinghouse(clearinghouse).getHealth(subaccount, healthType); + } + + function isUnderInitial(bytes32 subaccount) public view returns (bool) { + // Weighted initial health with limit orders < 0 + return + getHealthFromClearinghouse( + subaccount, + IProductEngine.HealthType.INITIAL + ) < 0; + } + + function isAboveInitial(bytes32 subaccount) public view returns (bool) { + // Weighted initial health with limit orders < 0 + return + getHealthFromClearinghouse( + subaccount, + IProductEngine.HealthType.INITIAL + ) > 0; + } + + function isUnderMaintenance(bytes32 subaccount) + internal + view + returns (bool) + { + // Weighted maintenance health < 0 + return + getHealthFromClearinghouse( + subaccount, + IProductEngine.HealthType.MAINTENANCE + ) < 0; + } + + function _getOrderbook(uint32 productId) internal view returns (address) { + return address(productToEngine[productId].getOrderbook(productId)); + } + + struct HealthGroupSummary { + uint32 perpId; + int128 perpAmount; + int128 perpVQuote; + uint32 spotId; + int128 spotAmount; + int128 basisAmount; + } + + function describeHealthGroup( + ISpotEngine spotEngine, + IPerpEngine perpEngine, + uint32 groupId, + bytes32 subaccount + ) internal view returns (HealthGroupSummary memory summary) { + HealthGroup memory group = HealthGroup( + groupId * 2 + 1, + groupId * 2 + 2 + ); + + summary.spotId = group.spotId; + summary.perpId = group.perpId; + + // we pretend VRTX balance always being 0 to make it not liquidatable. + if (group.spotId != VRTX_PRODUCT_ID) { + (, ISpotEngine.Balance memory balance) = spotEngine + .getStateAndBalance(group.spotId, subaccount); + summary.spotAmount = balance.amount; + } + + { + (, IPerpEngine.Balance memory balance) = perpEngine + .getStateAndBalance(group.perpId, subaccount); + summary.perpAmount = balance.amount; + summary.perpVQuote = balance.vQuoteBalance; + } + + if ((summary.spotAmount > 0) != (summary.perpAmount > 0)) { + if (summary.spotAmount > 0) { + summary.basisAmount = MathHelper.min( + summary.spotAmount, + -summary.perpAmount + ); + } else { + summary.basisAmount = MathHelper.max( + summary.spotAmount, + -summary.perpAmount + ); + } + summary.spotAmount -= summary.basisAmount; + summary.perpAmount += summary.basisAmount; + } + } + + function assertLiquidationAmount( + int128 originalBalance, + int128 liquidationAmount + ) internal pure { + require( + originalBalance != 0 && liquidationAmount != 0, + ERR_NOT_LIQUIDATABLE_AMT + ); + if (liquidationAmount > 0) { + require( + originalBalance >= liquidationAmount, + ERR_NOT_LIQUIDATABLE_AMT + ); + } else { + require( + originalBalance <= liquidationAmount, + ERR_NOT_LIQUIDATABLE_AMT + ); + } + } + + struct LiquidationVars { + int128 liquidationPriceX18; + int128 excessPerpToLiquidate; + int128 liquidationPayment; + int128 insuranceCover; + int128 oraclePriceX18; + int128 liquidationFees; + int128 perpSizeIncrement; + } + + function settlePnlAgainstLiquidator( + ISpotEngine spotEngine, + IPerpEngine perpEngine, + bytes32 liquidator, + bytes32 liquidatee, + uint32 perpId, + int128 positionPnl + ) internal { + IProductEngine.ProductDelta[] memory deltas; + deltas = new IProductEngine.ProductDelta[](2); + deltas[0] = IProductEngine.ProductDelta({ + productId: perpId, + subaccount: liquidatee, + amountDelta: 0, + vQuoteDelta: -positionPnl + }); + deltas[1] = IProductEngine.ProductDelta({ + productId: perpId, + subaccount: liquidator, + amountDelta: 0, + vQuoteDelta: positionPnl + }); + perpEngine.applyDeltas(deltas); + + deltas = new IProductEngine.ProductDelta[](2); + deltas[0] = IProductEngine.ProductDelta({ + productId: QUOTE_PRODUCT_ID, + subaccount: liquidatee, + amountDelta: positionPnl, + vQuoteDelta: 0 + }); + deltas[1] = IProductEngine.ProductDelta({ + productId: QUOTE_PRODUCT_ID, + subaccount: liquidator, + amountDelta: -positionPnl, + vQuoteDelta: 0 + }); + spotEngine.applyDeltas(deltas); + } + + function finalizeSubaccount( + ISpotEngine spotEngine, + IPerpEngine perpEngine, + bytes32 liquidator, + bytes32 liquidatee + ) internal { + // check whether the subaccount can be finalized: + // - all perps positions have closed + // - all spread positions have closed + // - all spot assets have closed + // - all positive pnls have been settled + // - after settling all positive pnls, if (quote + insurance) is positive, + // all spot liabilities have closed + IProductEngine.ProductDelta[] memory deltas; + + for (uint32 i = 0; i <= maxHealthGroup; ++i) { + HealthGroupSummary memory summary = describeHealthGroup( + spotEngine, + perpEngine, + i, + liquidatee + ); + + require( + summary.perpAmount == 0 && + summary.basisAmount == 0 && + summary.spotAmount <= 0, + ERR_NOT_FINALIZABLE_SUBACCOUNT + ); + + // spread positions have been closed so vQuote balance is the pnl + int128 positionPnl = summary.perpVQuote; + if (positionPnl > 0) { + settlePnlAgainstLiquidator( + spotEngine, + perpEngine, + liquidator, + liquidatee, + summary.perpId, + positionPnl + ); + } + } + + (, ISpotEngine.Balance memory quoteBalance) = spotEngine + .getStateAndBalance(QUOTE_PRODUCT_ID, liquidatee); + + insurance -= lastLiquidationFees; + bool canLiquidateMore = (quoteBalance.amount + insurance) > 0; + + // settle negative pnls until quote balance becomes 0 + for (uint32 i = 0; i <= maxHealthGroup; ++i) { + HealthGroupSummary memory summary = describeHealthGroup( + spotEngine, + perpEngine, + i, + liquidatee + ); + if (canLiquidateMore) { + require( + summary.spotAmount == 0, + ERR_NOT_FINALIZABLE_SUBACCOUNT + ); + } + if (quoteBalance.amount > 0) { + int128 positionPnl = summary.perpVQuote; + if (positionPnl < 0) { + int128 canSettle = MathHelper.max( + positionPnl, + -quoteBalance.amount + ); + settlePnlAgainstLiquidator( + spotEngine, + perpEngine, + liquidator, + liquidatee, + summary.perpId, + canSettle + ); + quoteBalance.amount += canSettle; + } + } + } + + insurance = perpEngine.socializeSubaccount(liquidatee, insurance); + + // we can assure that quoteBalance must be non positive + int128 insuranceCover = MathHelper.min(insurance, -quoteBalance.amount); + if (insuranceCover > 0) { + insurance -= insuranceCover; + deltas = new IProductEngine.ProductDelta[](1); + deltas[0] = IProductEngine.ProductDelta({ + productId: QUOTE_PRODUCT_ID, + subaccount: liquidatee, + amountDelta: insuranceCover, + vQuoteDelta: 0 + }); + spotEngine.applyDeltas(deltas); + } + if (insurance == 0) { + spotEngine.socializeSubaccount(liquidatee); + } + insurance += lastLiquidationFees; + } + + function liquidateSubaccountImpl(IEndpoint.LiquidateSubaccount calldata txn) + external + { + require(txn.sender != txn.liquidatee, ERR_UNAUTHORIZED); + + require(isUnderMaintenance(txn.liquidatee), ERR_NOT_LIQUIDATABLE); + + ISpotEngine spotEngine = ISpotEngine( + address(engineByType[IProductEngine.EngineType.SPOT]) + ); + IPerpEngine perpEngine = IPerpEngine( + address(engineByType[IProductEngine.EngineType.PERP]) + ); + insurance += spotEngine.decomposeLps( + txn.liquidatee, + txn.sender, + address(fees) + ); + insurance += perpEngine.decomposeLps( + txn.liquidatee, + txn.sender, + address(fees) + ); + + if ( + getHealthFromClearinghouse( + txn.liquidatee, + IProductEngine.HealthType.INITIAL + ) >= 0 + ) { + return; + } + + if (txn.healthGroup == type(uint32).max) { + finalizeSubaccount( + spotEngine, + perpEngine, + txn.sender, + txn.liquidatee + ); + return; + } + + int128 amountToLiquidate = txn.amount; + bool isLiability = (txn.mode != + uint8(IEndpoint.LiquidationMode.PERP)) && (amountToLiquidate < 0); + + IProductEngine.ProductDelta[] memory deltas; + + if (isLiability) { + // check whether liabilities can be liquidated and settle + // all positive pnls + for (uint32 i = 0; i <= maxHealthGroup; ++i) { + HealthGroupSummary memory groupSummary = describeHealthGroup( + spotEngine, + perpEngine, + i, + txn.liquidatee + ); + + // liabilities can only be liquidated after + // - all perp positions (outside of spreads) have closed + // - no spot nor spread assets exist + require( + groupSummary.perpAmount == 0 && + groupSummary.spotAmount <= 0 && + groupSummary.basisAmount <= 0, + ERR_NOT_LIQUIDATABLE_LIABILITIES + ); + + // settle positive pnl against the liquidator + int128 positionPnl; + if (groupSummary.basisAmount == 0) { + positionPnl = groupSummary.perpVQuote; + } else { + positionPnl = perpEngine.getPositionPnl( + groupSummary.perpId, + txn.liquidatee + ); + } + + if (positionPnl > 0) { + settlePnlAgainstLiquidator( + spotEngine, + perpEngine, + txn.sender, + txn.liquidatee, + groupSummary.perpId, + positionPnl + ); + } + } + } + + HealthGroupSummary memory summary = describeHealthGroup( + spotEngine, + perpEngine, + txn.healthGroup, + txn.liquidatee + ); + LiquidationVars memory vars; + + vars.perpSizeIncrement = IOffchainBook(_getOrderbook(summary.perpId)) + .getMarket() + .sizeIncrement; + + if (summary.basisAmount != 0) { + int128 excessBasisAmount = summary.basisAmount % + vars.perpSizeIncrement; + summary.basisAmount -= excessBasisAmount; + summary.spotAmount += excessBasisAmount; + summary.perpAmount -= excessBasisAmount; + } + + if (txn.mode != uint8(IEndpoint.LiquidationMode.SPOT)) { + require( + amountToLiquidate % vars.perpSizeIncrement == 0, + ERR_INVALID_LIQUIDATION_AMOUNT + ); + } + + if (txn.mode == uint8(IEndpoint.LiquidationMode.SPREAD)) { + assertLiquidationAmount(summary.basisAmount, amountToLiquidate); + require(summary.spotId != 0 && summary.perpId != 0); + + vars.liquidationPriceX18 = getSpreadLiqPriceX18( + HealthGroup(summary.spotId, summary.perpId), + amountToLiquidate + ); + vars.oraclePriceX18 = getOraclePriceX18(summary.spotId); + + // there is a fixed amount of the spot component of the spread + // we can liquidate until the insurance fund runs out of money + // however we can still liquidate the remaining perp component + // at the perp liquidation price. this way the spot liability just remains + // and the spread liability decomposes into a spot liability which is + // handled through socialization + + if (isLiability) { + (, ISpotEngine.Balance memory quoteBalance) = spotEngine + .getStateAndBalance(QUOTE_PRODUCT_ID, txn.liquidatee); + + int128 maximumLiquidatable = MathHelper.ceil( + MathHelper.max( + // liquidate slightly more to not block socialization. + (quoteBalance.amount + insurance).div( + vars.liquidationPriceX18 + ) + 1, + 0 + ), + vars.perpSizeIncrement + ); + + vars.excessPerpToLiquidate = + MathHelper.max(amountToLiquidate, -maximumLiquidatable) - + amountToLiquidate; + amountToLiquidate += vars.excessPerpToLiquidate; + vars.liquidationPayment = vars.liquidationPriceX18.mul( + amountToLiquidate + ); + vars.insuranceCover = MathHelper.min( + insurance, + MathHelper.max( + 0, + -vars.liquidationPayment - quoteBalance.amount + ) + ); + } else { + vars.liquidationPayment = vars.liquidationPriceX18.mul( + amountToLiquidate + ); + } + + vars.liquidationFees = (vars.oraclePriceX18 - + vars.liquidationPriceX18) + .mul( + fees.getLiquidationFeeFractionX18( + txn.sender, + summary.spotId + ) + ) + .mul(amountToLiquidate); + + deltas = new IProductEngine.ProductDelta[](4); + deltas[0] = IProductEngine.ProductDelta({ + productId: summary.spotId, + subaccount: txn.liquidatee, + amountDelta: -amountToLiquidate, + vQuoteDelta: vars.liquidationPayment + }); + deltas[1] = IProductEngine.ProductDelta({ + productId: summary.spotId, + subaccount: txn.sender, + amountDelta: amountToLiquidate, + vQuoteDelta: -vars.liquidationPayment + }); + deltas[2] = IProductEngine.ProductDelta({ + productId: QUOTE_PRODUCT_ID, + subaccount: txn.liquidatee, + amountDelta: vars.liquidationPayment + vars.insuranceCover, + vQuoteDelta: 0 + }); + deltas[3] = IProductEngine.ProductDelta({ + productId: QUOTE_PRODUCT_ID, + subaccount: txn.sender, + amountDelta: -vars.liquidationPayment, + vQuoteDelta: 0 + }); + + insurance -= vars.insuranceCover; + spotEngine.applyDeltas(deltas); + + vars.oraclePriceX18 = getOraclePriceX18(summary.perpId); + // write perp deltas + // in spread liquidation, we do the liquidation payment + // on top of liquidating the spot. for perp we simply + // transfer the balances at 0 pnl + // (ie. vQuoteAmount == amount * perpPrice) + int128 perpQuoteDelta = amountToLiquidate.mul(vars.oraclePriceX18); + + vars.liquidationPriceX18 = getLiqPriceX18( + summary.perpId, + vars.excessPerpToLiquidate + ); + + int128 excessPerpQuoteDelta = vars.liquidationPriceX18.mul( + vars.excessPerpToLiquidate + ); + + vars.liquidationFees += (vars.oraclePriceX18 - + vars.liquidationPriceX18) + .mul( + fees.getLiquidationFeeFractionX18( + txn.sender, + summary.perpId + ) + ) + .mul(vars.excessPerpToLiquidate); + + deltas = new IProductEngine.ProductDelta[](2); + deltas[0] = IProductEngine.ProductDelta({ + productId: summary.perpId, + subaccount: txn.liquidatee, + amountDelta: amountToLiquidate - vars.excessPerpToLiquidate, + vQuoteDelta: -perpQuoteDelta + excessPerpQuoteDelta + }); + deltas[1] = IProductEngine.ProductDelta({ + productId: summary.perpId, + subaccount: txn.sender, + amountDelta: -amountToLiquidate + vars.excessPerpToLiquidate, + vQuoteDelta: perpQuoteDelta - + excessPerpQuoteDelta - + vars.liquidationFees + }); + perpEngine.applyDeltas(deltas); + } else if (txn.mode == uint8(IEndpoint.LiquidationMode.SPOT)) { + uint32 productId = summary.spotId; + require( + productId != QUOTE_PRODUCT_ID, + ERR_INVALID_LIQUIDATION_PARAMS + ); + assertLiquidationAmount(summary.spotAmount, amountToLiquidate); + (, ISpotEngine.Balance memory quoteBalance) = spotEngine + .getStateAndBalance(QUOTE_PRODUCT_ID, txn.liquidatee); + + vars.liquidationPriceX18 = getLiqPriceX18( + productId, + amountToLiquidate + ); + vars.oraclePriceX18 = getOraclePriceX18(productId); + + if (isLiability) { + int128 maximumLiquidatable = MathHelper.max( + // liquidate slightly more to not block socialization. + (quoteBalance.amount + insurance).div( + vars.liquidationPriceX18 + ) + 1, + 0 + ); + amountToLiquidate = MathHelper.max( + amountToLiquidate, + -maximumLiquidatable + ); + } + vars.liquidationPayment = vars.liquidationPriceX18.mul( + amountToLiquidate + ); + + vars.liquidationFees = (vars.oraclePriceX18 - + vars.liquidationPriceX18) + .mul(fees.getLiquidationFeeFractionX18(txn.sender, productId)) + .mul(amountToLiquidate); + + // quoteBalance.amount + liquidationPayment18 + insuranceCover == 0 + vars.insuranceCover = (isLiability) + ? MathHelper.min( + insurance, + MathHelper.max( + 0, + -vars.liquidationPayment - quoteBalance.amount + ) + ) + : int128(0); + + deltas = new IProductEngine.ProductDelta[](4); + deltas[0] = IProductEngine.ProductDelta({ + productId: productId, + subaccount: txn.liquidatee, + amountDelta: -amountToLiquidate, + vQuoteDelta: vars.liquidationPayment + }); + deltas[1] = IProductEngine.ProductDelta({ + productId: productId, + subaccount: txn.sender, + amountDelta: amountToLiquidate, + vQuoteDelta: -vars.liquidationPayment + }); + deltas[2] = IProductEngine.ProductDelta({ + productId: QUOTE_PRODUCT_ID, + subaccount: txn.liquidatee, + amountDelta: vars.liquidationPayment + vars.insuranceCover, + vQuoteDelta: 0 + }); + deltas[3] = IProductEngine.ProductDelta({ + productId: QUOTE_PRODUCT_ID, + subaccount: txn.sender, + amountDelta: -vars.liquidationPayment - vars.liquidationFees, + vQuoteDelta: 0 + }); + + insurance -= vars.insuranceCover; + spotEngine.applyDeltas(deltas); + } else if (txn.mode == uint8(IEndpoint.LiquidationMode.PERP)) { + uint32 productId = summary.perpId; + require( + productId != QUOTE_PRODUCT_ID, + ERR_INVALID_LIQUIDATION_PARAMS + ); + assertLiquidationAmount(summary.perpAmount, amountToLiquidate); + + vars.liquidationPriceX18 = getLiqPriceX18( + productId, + amountToLiquidate + ); + vars.oraclePriceX18 = getOraclePriceX18(productId); + + vars.liquidationPayment = vars.liquidationPriceX18.mul( + amountToLiquidate + ); + vars.liquidationFees = (vars.oraclePriceX18 - + vars.liquidationPriceX18) + .mul(fees.getLiquidationFeeFractionX18(txn.sender, productId)) + .mul(amountToLiquidate); + + deltas = new IProductEngine.ProductDelta[](2); + deltas[0] = IProductEngine.ProductDelta({ + productId: productId, + subaccount: txn.liquidatee, + amountDelta: -amountToLiquidate, + vQuoteDelta: vars.liquidationPayment + }); + deltas[1] = IProductEngine.ProductDelta({ + productId: productId, + subaccount: txn.sender, + amountDelta: amountToLiquidate, + vQuoteDelta: -vars.liquidationPayment - vars.liquidationFees + }); + perpEngine.applyDeltas(deltas); + } else { + revert(ERR_INVALID_LIQUIDATION_PARAMS); + } + + // it's ok to let initial health become 0 + require(!isAboveInitial(txn.liquidatee), ERR_LIQUIDATED_TOO_MUCH); + require(!isUnderInitial(txn.sender), ERR_SUBACCT_HEALTH); + + insurance += vars.liquidationFees; + + // if insurance is not enough for making a subaccount healthy, we should + // - use all insurance to buy its liabilities, then + // - socialize the subaccount + + // however, after the first step, insurance funds will be refilled a little bit + // which blocks the second step, so we keep the fees of the last liquidation and + // do not use this part in socialization to unblock it. + lastLiquidationFees = vars.liquidationFees; + + emit Liquidation( + txn.sender, + txn.liquidatee, + // 0 -> spread, 1 -> spot, 2 -> perp + txn.mode, + txn.healthGroup, + txn.amount, // amount that was liquidated + // this is the amount of product transferred from liquidatee + // to liquidator; this and the following field will have the same sign + // if spread, one unit represents one long spot and one short perp + // i.e. if amount == -1, it means a short spot and a long perp was liquidated + vars.liquidationPayment, // add actual liquidatee quoteDelta + // meaning there was a payment of liquidationPayment + // from liquidator to liquidatee for the liquidated products + vars.insuranceCover + ); + } +} diff --git a/contracts/dependencies/vertex/ClearinghouseRisk.sol b/contracts/dependencies/vertex/ClearinghouseRisk.sol new file mode 100644 index 00000000..cee3ea16 --- /dev/null +++ b/contracts/dependencies/vertex/ClearinghouseRisk.sol @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +import "./interfaces/engine/IProductEngine.sol"; +import "./interfaces/clearinghouse/IClearinghouseState.sol"; +import "./common/Constants.sol"; +import "./common/Errors.sol"; + +import "./libraries/MathHelper.sol"; +import "./libraries/MathSD21x18.sol"; +import "./EndpointGated.sol"; +import "./libraries/RiskHelper.sol"; + +abstract contract ClearinghouseRisk is IClearinghouseState, EndpointGated { + using MathSD21x18 for int128; + + uint32 maxHealthGroup; + mapping(uint32 => HealthGroup) healthGroups; // deprecated + mapping(uint32 => RiskStore) risks; + + function getMaxHealthGroup() external view returns (uint32) { + return maxHealthGroup; + } + + function getRisk(uint32 productId) + public + view + returns (RiskHelper.Risk memory) + { + RiskStore memory risk = risks[productId]; + return + RiskHelper.Risk({ + longWeightInitialX18: int128(risk.longWeightInitial) * 1e9, + shortWeightInitialX18: int128(risk.shortWeightInitial) * 1e9, + longWeightMaintenanceX18: int128(risk.longWeightMaintenance) * + 1e9, + shortWeightMaintenanceX18: int128(risk.shortWeightMaintenance) * + 1e9, + largePositionPenaltyX18: int128(risk.largePositionPenalty) * 1e9 + }); + } + + function getLiqPriceX18(uint32 productId, int128 amount) + internal + view + returns (int128) + { + RiskHelper.Risk memory risk = getRisk(productId); + return + getOraclePriceX18(productId).mul( + ONE + + (RiskHelper._getWeightX18( + risk, + amount, + IProductEngine.HealthType.MAINTENANCE + ) - ONE) / + 5 + ); + } + + function getSpreadLiqPriceX18(HealthGroup memory healthGroup, int128 amount) + internal + view + returns (int128) + { + RiskHelper.Risk memory spotRisk = getRisk(healthGroup.spotId); + RiskHelper.Risk memory perpRisk = getRisk(healthGroup.perpId); + int128 spreadPenaltyX18 = RiskHelper._getSpreadPenaltyX18( + spotRisk, + perpRisk, + MathHelper.abs(amount), + IProductEngine.HealthType.MAINTENANCE + ) / 5; + if (amount > 0) { + return + getOraclePriceX18(healthGroup.spotId).mul( + ONE - spreadPenaltyX18 + ); + } else { + return + getOraclePriceX18(healthGroup.spotId).mul( + ONE + spreadPenaltyX18 + ); + } + } +} diff --git a/contracts/dependencies/vertex/ClearinghouseStorage.sol b/contracts/dependencies/vertex/ClearinghouseStorage.sol new file mode 100644 index 00000000..452f42f9 --- /dev/null +++ b/contracts/dependencies/vertex/ClearinghouseStorage.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +import "./interfaces/engine/IProductEngine.sol"; +import "./interfaces/IFeeCalculator.sol"; + +abstract contract ClearinghouseStorage { + // Each clearinghouse has a quote ERC20 + address quote; + + address clearinghouse; + address clearinghouseLiq; + + // fee calculator + IFeeCalculator fees; + + // Number of products registered across all engines + uint32 numProducts; + + // product ID -> engine address + mapping(uint32 => IProductEngine) productToEngine; + // Type to engine address + mapping(IProductEngine.EngineType => IProductEngine) engineByType; + // Supported engine types + IProductEngine.EngineType[] supportedEngines; + + // insurance stuff, consider making it its own subaccount later + int128 public insurance; + + int128 lastLiquidationFees; +} diff --git a/contracts/dependencies/vertex/Endpoint.sol b/contracts/dependencies/vertex/Endpoint.sol new file mode 100644 index 00000000..60f06d5b --- /dev/null +++ b/contracts/dependencies/vertex/Endpoint.sol @@ -0,0 +1,781 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/utils/cryptography/draft-EIP712Upgradeable.sol"; +import "./interfaces/IEndpoint.sol"; +import "./interfaces/clearinghouse/IClearinghouse.sol"; +import "./interfaces/IOffchainBook.sol"; +import "./EndpointGated.sol"; +import "./common/Errors.sol"; +import "./libraries/ERC20Helper.sol"; +import "./interfaces/IEndpoint.sol"; +import "./libraries/Logger.sol"; +import "./interfaces/engine/ISpotEngine.sol"; +import "./interfaces/engine/IPerpEngine.sol"; +import "./interfaces/IERC20Base.sol"; +import "./Version.sol"; + +interface ISanctionsList { + function isSanctioned(address addr) external view returns (bool); +} + +contract Endpoint is IEndpoint, EIP712Upgradeable, OwnableUpgradeable, Version { + using ERC20Helper for IERC20Base; + + IERC20Base private quote; + IClearinghouse public clearinghouse; + ISpotEngine private spotEngine; + IPerpEngine private perpEngine; + ISanctionsList private sanctions; + + address sequencer; + int128 public sequencerFees; + + mapping(bytes32 => uint64) subaccountIds; + mapping(uint64 => bytes32) subaccounts; + uint64 numSubaccounts; + + // healthGroup -> (spotPriceX18, perpPriceX18) + mapping(uint32 => Prices) pricesX18; + mapping(uint32 => address) books; + mapping(address => uint64) nonces; + + uint64 public nSubmissions; + + SlowModeConfig public slowModeConfig; + mapping(uint64 => SlowModeTx) public slowModeTxs; + + struct Times { + uint128 perpTime; + uint128 spotTime; + } + + Times private times; + + mapping(uint32 => int128) public sequencerFee; + + mapping(bytes32 => address) linkedSigners; + + int128 private slowModeFees; + + // invitee -> referralCode + mapping(address => string) public referralCodes; + + // address -> whether can call `BurnLpAndTransfer`. + mapping(address => bool) transferableWallets; + + string constant LIQUIDATE_SUBACCOUNT_SIGNATURE = + "LiquidateSubaccount(bytes32 sender,bytes32 liquidatee,uint8 mode,uint32 healthGroup,int128 amount,uint64 nonce)"; + string constant WITHDRAW_COLLATERAL_SIGNATURE = + "WithdrawCollateral(bytes32 sender,uint32 productId,uint128 amount,uint64 nonce)"; + string constant MINT_LP_SIGNATURE = + "MintLp(bytes32 sender,uint32 productId,uint128 amountBase,uint128 quoteAmountLow,uint128 quoteAmountHigh,uint64 nonce)"; + string constant BURN_LP_SIGNATURE = + "BurnLp(bytes32 sender,uint32 productId,uint128 amount,uint64 nonce)"; + string constant LINK_SIGNER_SIGNATURE = + "LinkSigner(bytes32 sender,bytes32 signer,uint64 nonce)"; + + function initialize( + address _sanctions, + address _sequencer, + IClearinghouse _clearinghouse, + uint64 slowModeTimeout, + uint128 _time, + int128[] memory _prices + ) external initializer { + __Ownable_init(); + __EIP712_init("Vertex", "0.0.1"); + sequencer = _sequencer; + clearinghouse = _clearinghouse; + sanctions = ISanctionsList(_sanctions); + spotEngine = ISpotEngine( + clearinghouse.getEngineByType(IProductEngine.EngineType.SPOT) + ); + + perpEngine = IPerpEngine( + clearinghouse.getEngineByType(IProductEngine.EngineType.PERP) + ); + + quote = IERC20Base(clearinghouse.getQuote()); + + slowModeConfig = SlowModeConfig({ + timeout: slowModeTimeout, + txCount: 0, + txUpTo: 0 + }); + times = Times({perpTime: _time, spotTime: _time}); + for (uint32 i = 0; i < _prices.length; i += 2) { + pricesX18[i / 2].spotPriceX18 = _prices[i]; + pricesX18[i / 2].perpPriceX18 = _prices[i + 1]; + } + } + + // NOTE: we want DepositCollateral to be the first action anybody takes on Vertex + // so we can record the existence of their subaccount on-chain + // unfortunately, there are some edge cases where an empty account can place an order + // or do an AMM swap that passes health checks without going through a deposit, so + // we block those functions unless there has been a deposit first + function _recordSubaccount(bytes32 subaccount) internal { + if (subaccountIds[subaccount] == 0) { + subaccountIds[subaccount] = ++numSubaccounts; + subaccounts[numSubaccounts] = subaccount; + } + } + + function requireSubaccount(bytes32 subaccount) public view { + require(subaccountIds[subaccount] != 0, ERR_REQUIRES_DEPOSIT); + } + + function validateNonce(bytes32 sender, uint64 nonce) internal virtual { + require( + nonce == nonces[address(uint160(bytes20(sender)))]++, + ERR_WRONG_NONCE + ); + } + + function chargeFee(bytes32 sender, int128 fee) internal { + chargeFee(sender, fee, QUOTE_PRODUCT_ID); + } + + function chargeFee( + bytes32 sender, + int128 fee, + uint32 productId + ) internal { + IProductEngine.ProductDelta[] + memory deltas = IProductEngine.ProductDelta[]( + new IProductEngine.ProductDelta[](1) + ); + + deltas[0] = IProductEngine.ProductDelta({ + productId: productId, + subaccount: sender, + amountDelta: -fee, + vQuoteDelta: 0 + }); + + sequencerFee[productId] += fee; + spotEngine.applyDeltas(deltas); + } + + function validateSignature( + bytes32 sender, + bytes32 digest, + bytes memory signature + ) internal view virtual { + address recovered = ECDSA.recover(digest, signature); + require( + (recovered != address(0)) && + ((recovered == address(uint160(bytes20(sender)))) || + (recovered == linkedSigners[sender])), + ERR_INVALID_SIGNATURE + ); + } + + function increaseAllowance( + IERC20Base token, + address to, + uint256 amount + ) internal virtual { + token.increaseAllowance(to, amount); + } + + function safeTransferFrom( + IERC20Base token, + address from, + uint256 amount + ) internal virtual { + token.safeTransferFrom(from, address(this), amount); + } + + function handleDepositTransfer( + IERC20Base token, + address from, + uint256 amount + ) internal { + increaseAllowance(token, address(clearinghouse), amount); + safeTransferFrom(token, from, amount); + } + + function validateSender(bytes32 txSender, address sender) internal view { + require( + address(uint160(bytes20(txSender))) == sender || + sender == address(this), + ERR_SLOW_MODE_WRONG_SENDER + ); + } + + function setReferralCode(address sender, string memory referralCode) + internal + { + if (bytes(referralCodes[sender]).length == 0) { + referralCodes[sender] = referralCode; + } + } + + function depositCollateral( + bytes12 subaccountName, + uint32 productId, + uint128 amount + ) external { + depositCollateralWithReferral( + bytes32(abi.encodePacked(msg.sender, subaccountName)), + productId, + amount, + DEFAULT_REFERRAL_CODE + ); + } + + function depositCollateralWithReferral( + bytes12 subaccountName, + uint32 productId, + uint128 amount, + string calldata referralCode + ) external { + depositCollateralWithReferral( + bytes32(abi.encodePacked(msg.sender, subaccountName)), + productId, + amount, + referralCode + ); + } + + function depositCollateralWithReferral( + bytes32 subaccount, + uint32 productId, + uint128 amount, + string memory referralCode + ) public { + require(bytes(referralCode).length != 0, ERR_INVALID_REFERRAL_CODE); + + address sender = address(bytes20(subaccount)); + + // depositor / depositee need to be unsanctioned + requireUnsanctioned(msg.sender); + requireUnsanctioned(sender); + + // no referral code allowed for remote deposit + setReferralCode( + sender, + sender == msg.sender ? referralCode : DEFAULT_REFERRAL_CODE + ); + + IERC20Base token = IERC20Base(spotEngine.getConfig(productId).token); + require(address(token) != address(0)); + handleDepositTransfer(token, msg.sender, uint256(amount)); + + // copy from submitSlowModeTransaction + SlowModeConfig memory _slowModeConfig = slowModeConfig; + + // hardcoded to three days + uint64 executableAt = uint64(block.timestamp) + 259200; + slowModeTxs[_slowModeConfig.txCount++] = SlowModeTx({ + executableAt: executableAt, + sender: sender, + tx: abi.encodePacked( + uint8(TransactionType.DepositCollateral), + abi.encode( + DepositCollateral({ + sender: subaccount, + productId: productId, + amount: amount + }) + ) + ) + }); + slowModeConfig = _slowModeConfig; + } + + function requireUnsanctioned(address sender) internal view virtual { + require(!sanctions.isSanctioned(sender), ERR_WALLET_SANCTIONED); + } + + function submitSlowModeTransaction(bytes calldata transaction) external { + TransactionType txType = TransactionType(uint8(transaction[0])); + + // special case for DepositCollateral because upon + // slow mode submission we must take custody of the + // actual funds + + address sender = msg.sender; + + if (txType == TransactionType.DepositCollateral) { + revert(); + } else if (txType == TransactionType.DepositInsurance) { + DepositInsurance memory txn = abi.decode( + transaction[1:], + (DepositInsurance) + ); + IERC20Base token = IERC20Base(clearinghouse.getQuote()); + require(address(token) != address(0)); + handleDepositTransfer(token, sender, uint256(txn.amount)); + } else if (txType == TransactionType.UpdateProduct) { + require(sender == owner()); + } else if (txType == TransactionType.BurnLpAndTransfer) { + require(transferableWallets[sender], ERR_WALLET_NOT_TRANSFERABLE); + } else { + safeTransferFrom(quote, sender, uint256(int256(SLOW_MODE_FEE))); + slowModeFees += SLOW_MODE_FEE; + } + + SlowModeConfig memory _slowModeConfig = slowModeConfig; + // hardcoded to three days + uint64 executableAt = uint64(block.timestamp) + 259200; + requireUnsanctioned(sender); + slowModeTxs[_slowModeConfig.txCount++] = SlowModeTx({ + executableAt: executableAt, + sender: sender, + tx: transaction + }); + // TODO: to save on costs we could potentially just emit something + // for now, we can just create a separate loop in the engine that queries the remote + // sequencer for slow mode transactions, and ignore the possibility of a reorgy attack + slowModeConfig = _slowModeConfig; + } + + function _executeSlowModeTransaction( + SlowModeConfig memory _slowModeConfig, + bool fromSequencer + ) internal { + require( + _slowModeConfig.txUpTo < _slowModeConfig.txCount, + ERR_NO_SLOW_MODE_TXS_REMAINING + ); + SlowModeTx memory txn = slowModeTxs[_slowModeConfig.txUpTo]; + delete slowModeTxs[_slowModeConfig.txUpTo++]; + + require( + fromSequencer || (txn.executableAt <= block.timestamp), + ERR_SLOW_TX_TOO_RECENT + ); + + uint256 gasRemaining = gasleft(); + try this.processSlowModeTransaction(txn.sender, txn.tx) {} catch { + // we need to differentiate between a revert and an out of gas + // the expectation is that because 63/64 * gasRemaining is forwarded + // we should be able to differentiate based on whether + // gasleft() >= gasRemaining / 64. however, experimentally + // even more gas can be remaining, and i don't have a clear + // understanding as to why. as a result we just err on the + // conservative side and provide two conservative + // asserts that should cover all cases at the expense of needing + // to provide a higher gas limit than necessary + + if (gasleft() <= 100000 || gasleft() <= gasRemaining / 16) { + assembly { + invalid() + } + } + + // try return funds now removed + } + } + + function executeSlowModeTransactions(uint32 count) external { + SlowModeConfig memory _slowModeConfig = slowModeConfig; + require( + count <= _slowModeConfig.txCount - _slowModeConfig.txUpTo, + ERR_INVALID_COUNT + ); + + while (count > 0) { + _executeSlowModeTransaction(_slowModeConfig, false); + --count; + } + slowModeConfig = _slowModeConfig; + } + + // TODO: these do not need senders or nonces + // we can save some gas by creating new structs + function processSlowModeTransaction( + address sender, + bytes calldata transaction + ) public { + require(msg.sender == address(this)); + TransactionType txType = TransactionType(uint8(transaction[0])); + if (txType == TransactionType.LiquidateSubaccount) { + LiquidateSubaccount memory txn = abi.decode( + transaction[1:], + (LiquidateSubaccount) + ); + validateSender(txn.sender, sender); + requireSubaccount(txn.sender); + clearinghouse.liquidateSubaccount(txn); + } else if (txType == TransactionType.DepositCollateral) { + DepositCollateral memory txn = abi.decode( + transaction[1:], + (DepositCollateral) + ); + validateSender(txn.sender, sender); + _recordSubaccount(txn.sender); + clearinghouse.depositCollateral(txn); + } else if (txType == TransactionType.WithdrawCollateral) { + WithdrawCollateral memory txn = abi.decode( + transaction[1:], + (WithdrawCollateral) + ); + validateSender(txn.sender, sender); + clearinghouse.withdrawCollateral(txn); + } else if (txType == TransactionType.SettlePnl) { + SettlePnl memory txn = abi.decode(transaction[1:], (SettlePnl)); + clearinghouse.settlePnl(txn); + } else if (txType == TransactionType.DepositInsurance) { + DepositInsurance memory txn = abi.decode( + transaction[1:], + (DepositInsurance) + ); + clearinghouse.depositInsurance(txn); + } else if (txType == TransactionType.MintLp) { + MintLp memory txn = abi.decode(transaction[1:], (MintLp)); + validateSender(txn.sender, sender); + clearinghouse.mintLpSlowMode(txn); + } else if (txType == TransactionType.BurnLp) { + BurnLp memory txn = abi.decode(transaction[1:], (BurnLp)); + validateSender(txn.sender, sender); + clearinghouse.burnLp(txn); + } else if (txType == TransactionType.SwapAMM) { + SwapAMM memory txn = abi.decode(transaction[1:], (SwapAMM)); + validateSender(txn.sender, sender); + requireSubaccount(txn.sender); + IOffchainBook(books[txn.productId]).swapAMM(txn); + } else if (txType == TransactionType.UpdateProduct) { + UpdateProduct memory txn = abi.decode( + transaction[1:], + (UpdateProduct) + ); + IProductEngine(txn.engine).updateProduct(txn.tx); + } else if (txType == TransactionType.LinkSigner) { + LinkSigner memory txn = abi.decode(transaction[1:], (LinkSigner)); + validateSender(txn.sender, sender); + linkedSigners[txn.sender] = address(uint160(bytes20(txn.signer))); + } else if (txType == TransactionType.BurnLpAndTransfer) { + BurnLpAndTransfer memory txn = abi.decode( + transaction[1:], + (BurnLpAndTransfer) + ); + validateSender(txn.sender, sender); + _recordSubaccount(txn.recipient); + clearinghouse.burnLpAndTransfer(txn); + } else { + revert(); + } + } + + function processTransaction(bytes calldata transaction) internal { + TransactionType txType = TransactionType(uint8(transaction[0])); + if (txType == TransactionType.LiquidateSubaccount) { + SignedLiquidateSubaccount memory signedTx = abi.decode( + transaction[1:], + (SignedLiquidateSubaccount) + ); + validateNonce(signedTx.tx.sender, signedTx.tx.nonce); + bytes32 digest = _hashTypedDataV4( + keccak256( + abi.encode( + keccak256(bytes(LIQUIDATE_SUBACCOUNT_SIGNATURE)), + signedTx.tx.sender, + signedTx.tx.liquidatee, + signedTx.tx.mode, + signedTx.tx.healthGroup, + signedTx.tx.amount, + signedTx.tx.nonce + ) + ) + ); + validateSignature(signedTx.tx.sender, digest, signedTx.signature); + requireSubaccount(signedTx.tx.sender); + chargeFee(signedTx.tx.sender, LIQUIDATION_FEE); + clearinghouse.liquidateSubaccount(signedTx.tx); + } else if (txType == TransactionType.WithdrawCollateral) { + SignedWithdrawCollateral memory signedTx = abi.decode( + transaction[1:], + (SignedWithdrawCollateral) + ); + validateNonce(signedTx.tx.sender, signedTx.tx.nonce); + bytes32 digest = _hashTypedDataV4( + keccak256( + abi.encode( + keccak256(bytes(WITHDRAW_COLLATERAL_SIGNATURE)), + signedTx.tx.sender, + signedTx.tx.productId, + signedTx.tx.amount, + signedTx.tx.nonce + ) + ) + ); + validateSignature(signedTx.tx.sender, digest, signedTx.signature); + chargeFee( + signedTx.tx.sender, + spotEngine.getWithdrawFee(signedTx.tx.productId), + signedTx.tx.productId + ); + clearinghouse.withdrawCollateral(signedTx.tx); + } else if (txType == TransactionType.SpotTick) { + SpotTick memory txn = abi.decode(transaction[1:], (SpotTick)); + Times memory t = times; + uint128 dt = txn.time - t.spotTime; + spotEngine.updateStates(dt); + t.spotTime = txn.time; + times = t; + } else if (txType == TransactionType.PerpTick) { + PerpTick memory txn = abi.decode(transaction[1:], (PerpTick)); + Times memory t = times; + uint128 dt = txn.time - t.perpTime; + perpEngine.updateStates(dt, txn.avgPriceDiffs); + t.perpTime = txn.time; + times = t; + } else if (txType == TransactionType.UpdatePrice) { + UpdatePrice memory txn = abi.decode(transaction[1:], (UpdatePrice)); + require(txn.priceX18 > 0, ERR_INVALID_PRICE); + uint32 healthGroup = _getHealthGroup(txn.productId); + if (txn.productId % 2 == 1) { + pricesX18[healthGroup].spotPriceX18 = txn.priceX18; + } else { + pricesX18[healthGroup].perpPriceX18 = txn.priceX18; + } + } else if (txType == TransactionType.SettlePnl) { + SettlePnl memory txn = abi.decode(transaction[1:], (SettlePnl)); + clearinghouse.settlePnl(txn); + } else if (txType == TransactionType.MatchOrders) { + MatchOrders memory txn = abi.decode(transaction[1:], (MatchOrders)); + requireSubaccount(txn.taker.order.sender); + requireSubaccount(txn.maker.order.sender); + MatchOrdersWithSigner memory txnWithSigner = MatchOrdersWithSigner({ + matchOrders: txn, + takerLinkedSigner: linkedSigners[txn.taker.order.sender], + makerLinkedSigner: linkedSigners[txn.maker.order.sender] + }); + IOffchainBook(books[txn.productId]).matchOrders(txnWithSigner); + } else if (txType == TransactionType.MatchOrderAMM) { + MatchOrderAMM memory txn = abi.decode( + transaction[1:], + (MatchOrderAMM) + ); + requireSubaccount(txn.taker.order.sender); + IOffchainBook(books[txn.productId]).matchOrderAMM( + txn, + linkedSigners[txn.taker.order.sender] + ); + } else if (txType == TransactionType.ExecuteSlowMode) { + SlowModeConfig memory _slowModeConfig = slowModeConfig; + _executeSlowModeTransaction(_slowModeConfig, true); + slowModeConfig = _slowModeConfig; + } else if (txType == TransactionType.MintLp) { + SignedMintLp memory signedTx = abi.decode( + transaction[1:], + (SignedMintLp) + ); + validateNonce(signedTx.tx.sender, signedTx.tx.nonce); + bytes32 digest = _hashTypedDataV4( + keccak256( + abi.encode( + keccak256(bytes(MINT_LP_SIGNATURE)), + signedTx.tx.sender, + signedTx.tx.productId, + signedTx.tx.amountBase, + signedTx.tx.quoteAmountLow, + signedTx.tx.quoteAmountHigh, + signedTx.tx.nonce + ) + ) + ); + validateSignature(signedTx.tx.sender, digest, signedTx.signature); + chargeFee(signedTx.tx.sender, HEALTHCHECK_FEE); + clearinghouse.mintLp(signedTx.tx); + } else if (txType == TransactionType.BurnLp) { + SignedBurnLp memory signedTx = abi.decode( + transaction[1:], + (SignedBurnLp) + ); + validateNonce(signedTx.tx.sender, signedTx.tx.nonce); + bytes32 digest = _hashTypedDataV4( + keccak256( + abi.encode( + keccak256(bytes(BURN_LP_SIGNATURE)), + signedTx.tx.sender, + signedTx.tx.productId, + signedTx.tx.amount, + signedTx.tx.nonce + ) + ) + ); + validateSignature(signedTx.tx.sender, digest, signedTx.signature); + chargeFee(signedTx.tx.sender, HEALTHCHECK_FEE); + clearinghouse.burnLp(signedTx.tx); + } else if (txType == TransactionType.DumpFees) { + uint32 numProducts = clearinghouse.getNumProducts(); + for (uint32 i = 1; i < numProducts; i++) { + IOffchainBook(books[i]).dumpFees(); + } + } else if (txType == TransactionType.ClaimSequencerFees) { + ClaimSequencerFees memory txn = abi.decode( + transaction[1:], + (ClaimSequencerFees) + ); + uint32[] memory spotIds = spotEngine.getProductIds(); + int128[] memory fees = new int128[](spotIds.length); + for (uint256 i = 0; i < spotIds.length; i++) { + fees[i] = sequencerFee[spotIds[i]]; + sequencerFee[spotIds[i]] = 0; + } + clearinghouse.claimSequencerFees(txn, fees); + } else if (txType == TransactionType.ManualAssert) { + ManualAssert memory txn = abi.decode( + transaction[1:], + (ManualAssert) + ); + perpEngine.manualAssert(txn.openInterests); + spotEngine.manualAssert(txn.totalDeposits, txn.totalBorrows); + } else if (txType == TransactionType.Rebate) { + // deprecated. + } else if (txType == TransactionType.LinkSigner) { + SignedLinkSigner memory signedTx = abi.decode( + transaction[1:], + (SignedLinkSigner) + ); + validateNonce(signedTx.tx.sender, signedTx.tx.nonce); + bytes32 digest = _hashTypedDataV4( + keccak256( + abi.encode( + keccak256(bytes(LINK_SIGNER_SIGNATURE)), + signedTx.tx.sender, + signedTx.tx.signer, + signedTx.tx.nonce + ) + ) + ); + validateSignature(signedTx.tx.sender, digest, signedTx.signature); + linkedSigners[signedTx.tx.sender] = address( + uint160(bytes20(signedTx.tx.signer)) + ); + } else if (txType == TransactionType.UpdateFeeRates) { + UpdateFeeRates memory txn = abi.decode( + transaction[1:], + (UpdateFeeRates) + ); + clearinghouse.updateFeeRates(txn); + } else { + revert(); + } + } + + function requireSequencer() internal view virtual { + require(msg.sender == sequencer); + } + + function submitTransactions(bytes[] calldata transactions) public { + requireSequencer(); + for (uint128 i = 0; i < transactions.length; i++) { + bytes calldata transaction = transactions[i]; + processTransaction(transaction); + } + nSubmissions += uint64(transactions.length); + emit SubmitTransactions(); + } + + function submitTransactionsChecked( + uint64 idx, + bytes[] calldata transactions + ) external { + requireSequencer(); + require(idx == nSubmissions, ERR_INVALID_SUBMISSION_INDEX); + // TODO: if one of these transactions fails this means the sequencer is in an error state + // we should probably record this, and engage some sort of recovery mode + submitTransactions(transactions); + } + + function submitTransactionsCheckedWithGasLimit( + uint64 idx, + bytes[] calldata transactions, + uint256 gasLimit + ) external returns (uint64, uint256) { + uint256 gasUsed = gasleft(); + requireSequencer(); + require(idx == nSubmissions, ERR_INVALID_SUBMISSION_INDEX); + for (uint128 i = 0; i < transactions.length; i++) { + bytes calldata transaction = transactions[i]; + processTransaction(transaction); + if (gasUsed - gasleft() > gasLimit) { + return (uint64(i), gasUsed - gasleft()); + } + } + return (uint64(transactions.length), gasUsed - gasleft()); + } + + function setBook(uint32 productId, address book) external { + require( + msg.sender == address(clearinghouse), + ERR_ONLY_CLEARINGHOUSE_CAN_SET_BOOK + ); + books[productId] = book; + } + + function getBook(uint32 productId) external view returns (address) { + return books[productId]; + } + + function getSubaccountId(bytes32 subaccount) + external + view + returns (uint64) + { + return subaccountIds[subaccount]; + } + + // this is enforced anywhere in addProduct, we generate productId in + // the following way. + function _getHealthGroup(uint32 productId) internal pure returns (uint32) { + require(productId != 0, ERR_GETTING_ZERO_HEALTH_GROUP); + return (productId - 1) / 2; + } + + function getPriceX18(uint32 productId) + public + view + returns (int128 priceX18) + { + uint32 healthGroup = _getHealthGroup(productId); + if (productId % 2 == 1) { + priceX18 = pricesX18[healthGroup].spotPriceX18; + } else { + priceX18 = pricesX18[healthGroup].perpPriceX18; + } + require(priceX18 != 0, ERR_INVALID_PRODUCT); + } + + function getPricesX18(uint32 healthGroup) + external + view + returns (Prices memory) + { + return pricesX18[healthGroup]; + } + + function getTime() external view returns (uint128) { + Times memory t = times; + uint128 _time = t.spotTime > t.perpTime ? t.spotTime : t.perpTime; + require(_time != 0, ERR_INVALID_TIME); + return _time; + } + + function setSequencer(address _sequencer) external onlyOwner { + sequencer = _sequencer; + } + + function getSequencer() external view returns (address) { + return sequencer; + } + + function getNonce(address sender) external view returns (uint64) { + return nonces[sender]; + } + + function registerTransferableWallet(address wallet, bool transferable) + external + onlyOwner + { + transferableWallets[wallet] = transferable; + } +} diff --git a/contracts/dependencies/vertex/EndpointGated.sol b/contracts/dependencies/vertex/EndpointGated.sol new file mode 100644 index 00000000..53e99f5b --- /dev/null +++ b/contracts/dependencies/vertex/EndpointGated.sol @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import "./interfaces/IEndpoint.sol"; +import "./interfaces/IEndpointGated.sol"; +import "./libraries/MathSD21x18.sol"; +import "./common/Constants.sol"; +import "hardhat/console.sol"; + +abstract contract EndpointGated is OwnableUpgradeable, IEndpointGated { + address private endpoint; + + function setEndpoint(address _endpoint) public onlyOwner { + endpoint = _endpoint; + } + + function getEndpoint() public view returns (address) { + return endpoint; + } + + function getOraclePriceX18(uint32 productId) public view returns (int128) { + if (productId == QUOTE_PRODUCT_ID) { + return MathSD21x18.fromInt(1); + } + return IEndpoint(endpoint).getPriceX18(productId); + } + + function getOraclePricesX18(uint32 healthGroup) + public + view + returns (IEndpoint.Prices memory) + { + return IEndpoint(endpoint).getPricesX18(healthGroup); + } + + function getOracleTime() internal view returns (uint128) { + return IEndpoint(endpoint).getTime(); + } + + modifier onlyEndpoint() { + require( + msg.sender == endpoint, + "SequencerGated: caller is not the endpoint" + ); + _; + } +} diff --git a/contracts/dependencies/vertex/FQuerier.sol b/contracts/dependencies/vertex/FQuerier.sol new file mode 100644 index 00000000..36c28269 --- /dev/null +++ b/contracts/dependencies/vertex/FQuerier.sol @@ -0,0 +1,600 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +import "./interfaces/clearinghouse/IClearinghouse.sol"; +import "./interfaces/IOffchainBook.sol"; +import "./interfaces/engine/ISpotEngine.sol"; +import "./interfaces/engine/IPerpEngine.sol"; +import "./interfaces/engine/IProductEngine.sol"; +import "./libraries/MathSD21x18.sol"; +import "./libraries/RiskHelper.sol"; +import "./common/Constants.sol"; +import "./Version.sol"; + +// NOTE: not related to VertexQuerier +// custom querier contract just for queries with FNode +// VertexQuerier has some issues with abi generation +contract FQuerier is Version { + using MathSD21x18 for int128; + + IClearinghouse private clearinghouse; + IEndpoint private endpoint; + ISpotEngine private spotEngine; + IPerpEngine private perpEngine; + + function initialize(address _clearinghouse) external { + clearinghouse = IClearinghouse(_clearinghouse); + endpoint = IEndpoint(clearinghouse.getEndpoint()); + + spotEngine = ISpotEngine( + clearinghouse.getEngineByType(IProductEngine.EngineType.SPOT) + ); + + perpEngine = IPerpEngine( + clearinghouse.getEngineByType(IProductEngine.EngineType.PERP) + ); + } + + struct SpotBalance { + uint32 productId; + ISpotEngine.LpBalance lpBalance; + ISpotEngine.Balance balance; + } + + struct PerpBalance { + uint32 productId; + IPerpEngine.LpBalance lpBalance; + IPerpEngine.Balance balance; + } + + // for config just go to the chain + struct SpotProduct { + uint32 productId; + int128 oraclePriceX18; + RiskHelper.Risk risk; + ISpotEngine.Config config; + ISpotEngine.State state; + ISpotEngine.LpState lpState; + BookInfo bookInfo; + } + + struct PerpProduct { + uint32 productId; + int128 oraclePriceX18; + RiskHelper.Risk risk; + IPerpEngine.State state; + IPerpEngine.LpState lpState; + BookInfo bookInfo; + } + + struct BookInfo { + int128 sizeIncrement; + int128 priceIncrementX18; + int128 minSize; + int128 collectedFees; + int128 lpSpreadX18; + } + + struct HealthInfo { + int128 assets; + int128 liabilities; + int128 health; + } + + struct SubaccountInfo { + bytes32 subaccount; + bool exists; + HealthInfo[] healths; + int128[][] healthContributions; + uint32 spotCount; + uint32 perpCount; + SpotBalance[] spotBalances; + PerpBalance[] perpBalances; + SpotProduct[] spotProducts; + PerpProduct[] perpProducts; + } + + struct ProductInfo { + SpotProduct[] spotProducts; + PerpProduct[] perpProducts; + } + + function getClearinghouse() external view returns (address) { + return address(clearinghouse); + } + + function _getAllProductIds() + internal + view + returns (uint32[] memory spotIds, uint32[] memory perpIds) + { + spotIds = spotEngine.getProductIds(); + perpIds = perpEngine.getProductIds(); + } + + function getAllProducts() public view returns (ProductInfo memory) { + ( + uint32[] memory spotIds, + uint32[] memory perpIds + ) = _getAllProductIds(); + return + ProductInfo({ + spotProducts: getSpotProducts(spotIds), + perpProducts: getPerpProducts(perpIds) + }); + } + + function getSpotProducts(uint32[] memory productIds) + public + view + returns (SpotProduct[] memory spotProducts) + { + spotProducts = new SpotProduct[](productIds.length); + + for (uint32 i = 0; i < productIds.length; i++) { + uint32 productId = productIds[i]; + spotProducts[i] = getSpotProduct(productId); + } + } + + function getPerpProducts(uint32[] memory productIds) + public + view + returns (PerpProduct[] memory perpProducts) + { + perpProducts = new PerpProduct[](productIds.length); + + for (uint32 i = 0; i < productIds.length; i++) { + uint32 productId = productIds[i]; + perpProducts[i] = getPerpProduct(productId); + } + } + + function getSpotProduct(uint32 productId) + public + view + returns (SpotProduct memory) + { + ( + ISpotEngine.LpState memory lpState, + , + ISpotEngine.State memory state, + + ) = spotEngine.getStatesAndBalances(productId, 0); + int128 oraclePriceX18 = productId == QUOTE_PRODUCT_ID + ? ONE + : endpoint.getPricesX18((productId - 1) / 2).spotPriceX18; + return + SpotProduct({ + productId: productId, + oraclePriceX18: oraclePriceX18, + risk: clearinghouse.getRisk(productId), + config: spotEngine.getConfig(productId), + state: state, + lpState: lpState, + bookInfo: productId != 0 + ? getBookInfo(productId, spotEngine) + : BookInfo(0, 0, 0, 0, 0) + }); + } + + function getPerpProduct(uint32 productId) + public + view + returns (PerpProduct memory) + { + ( + IPerpEngine.LpState memory lpState, + , + IPerpEngine.State memory state, + + ) = perpEngine.getStatesAndBalances(productId, 0); + + return + PerpProduct({ + productId: productId, + oraclePriceX18: endpoint + .getPricesX18((productId - 1) / 2) + .perpPriceX18, + risk: clearinghouse.getRisk(productId), + state: state, + lpState: lpState, + bookInfo: productId != 0 + ? getBookInfo(productId, perpEngine) + : BookInfo(0, 0, 0, 0, 0) + }); + } + + function getSubaccountInfo(bytes32 subaccount) + public + view + returns (SubaccountInfo memory) + { + SubaccountInfo memory subaccountInfo; + + { + ( + uint32[] memory spotIds, + uint32[] memory perpIds + ) = _getAllProductIds(); + + // initial, maintenance, pnl + subaccountInfo.subaccount = subaccount; + subaccountInfo.exists = true; + subaccountInfo.healths = new HealthInfo[](3); + + uint256 productIdsLength = spotIds.length + perpIds.length; + subaccountInfo.healthContributions = new int128[][]( + productIdsLength + ); + for (uint256 i = 0; i < productIdsLength; i++) { + subaccountInfo.healthContributions[i] = new int128[](3); + } + + subaccountInfo.spotBalances = new SpotBalance[](spotIds.length); + subaccountInfo.perpBalances = new PerpBalance[](perpIds.length); + subaccountInfo.spotProducts = new SpotProduct[](spotIds.length); + subaccountInfo.perpProducts = new PerpProduct[](perpIds.length); + } + + uint32 maxHealthGroup = clearinghouse.getMaxHealthGroup(); + for (uint32 i = 0; i <= maxHealthGroup; i++) { + IClearinghouse.HealthGroup memory group; + group.spotId = i * 2 + 1; + group.perpId = i * 2 + 2; + IClearinghouseState.HealthVars memory healthVars; + healthVars.pricesX18 = endpoint.getPricesX18(i); + + { + ( + ISpotEngine.LpState memory lpState, + ISpotEngine.LpBalance memory lpBalance, + ISpotEngine.State memory state, + ISpotEngine.Balance memory balance + ) = spotEngine.getStatesAndBalances(group.spotId, subaccount); + + if (lpBalance.amount != 0) { + (int128 ammBase, int128 ammQuote) = MathHelper + .ammEquilibrium( + lpState.base.amount, + lpState.quote.amount, + healthVars.pricesX18.spotPriceX18 + ); + + for (uint128 j = 0; j < 3; ++j) { + subaccountInfo.healthContributions[group.spotId][ + j + ] += ammQuote.mul(lpBalance.amount).div( + lpState.supply + ); + } + + healthVars.spotInLpAmount = ammBase + .mul(lpBalance.amount) + .div(lpState.supply); + } + + healthVars.spotAmount = balance.amount; + healthVars.spotRisk = clearinghouse.getRisk(group.spotId); + + subaccountInfo.spotBalances[ + subaccountInfo.spotCount + ] = SpotBalance({ + productId: group.spotId, + balance: balance, + lpBalance: lpBalance + }); + subaccountInfo.spotProducts[ + subaccountInfo.spotCount++ + ] = SpotProduct({ + productId: group.spotId, + oraclePriceX18: healthVars.pricesX18.spotPriceX18, + risk: healthVars.spotRisk, + config: spotEngine.getConfig(group.spotId), + state: state, + lpState: lpState, + bookInfo: getBookInfo(group.spotId, spotEngine) + }); + } + { + ( + IPerpEngine.LpState memory lpState, + IPerpEngine.LpBalance memory lpBalance, + IPerpEngine.State memory state, + IPerpEngine.Balance memory balance + ) = perpEngine.getStatesAndBalances(group.perpId, subaccount); + + if (lpBalance.amount != 0) { + (int128 ammBase, int128 ammQuote) = MathHelper + .ammEquilibrium( + lpState.base, + lpState.quote, + healthVars.pricesX18.perpPriceX18 + ); + + for (uint128 j = 0; j < 3; ++j) { + subaccountInfo.healthContributions[group.perpId][ + j + ] += ammQuote.mul(lpBalance.amount).div( + lpState.supply + ); + } + healthVars.perpInLpAmount = ammBase + .mul(lpBalance.amount) + .div(lpState.supply); + } + + for (uint128 j = 0; j < 3; ++j) { + subaccountInfo.healthContributions[group.perpId][ + j + ] += balance.vQuoteBalance; + } + + healthVars.perpAmount = balance.amount; + healthVars.perpRisk = clearinghouse.getRisk(group.perpId); + + if ( + (healthVars.spotAmount > 0) != (healthVars.perpAmount > 0) + ) { + if (healthVars.spotAmount > 0) { + healthVars.basisAmount = MathHelper.min( + healthVars.spotAmount, + -healthVars.perpAmount + ); + } else { + healthVars.basisAmount = MathHelper.max( + healthVars.spotAmount, + -healthVars.perpAmount + ); + } + healthVars.spotAmount -= healthVars.basisAmount; + healthVars.perpAmount += healthVars.basisAmount; + } + + subaccountInfo.perpBalances[ + subaccountInfo.perpCount + ] = PerpBalance({ + productId: group.perpId, + balance: balance, + lpBalance: lpBalance + }); + subaccountInfo.perpProducts[ + subaccountInfo.perpCount++ + ] = PerpProduct({ + productId: group.perpId, + oraclePriceX18: healthVars.pricesX18.perpPriceX18, + risk: healthVars.perpRisk, + state: state, + lpState: lpState, + bookInfo: getBookInfo(group.perpId, perpEngine) + }); + } + + // risk for the basis trade, discounted + if (healthVars.basisAmount != 0) { + int128 posAmount = MathHelper.abs(healthVars.basisAmount); + + for (uint8 healthType = 0; healthType < 3; ++healthType) { + // add the actual value of the basis (PNL) + int128 totalSpreadPenalty = RiskHelper + ._getSpreadPenaltyX18( + healthVars.spotRisk, + healthVars.perpRisk, + posAmount, + IProductEngine.HealthType(healthType) + ) + .mul(posAmount) + .mul( + healthVars.pricesX18.spotPriceX18 + + healthVars.pricesX18.perpPriceX18 + ); + + subaccountInfo.healthContributions[group.spotId][ + healthType + ] += + healthVars.pricesX18.spotPriceX18.mul( + healthVars.basisAmount + ) - + totalSpreadPenalty / + 2; + subaccountInfo.healthContributions[group.perpId][ + healthType + ] += + healthVars.pricesX18.perpPriceX18.mul( + -healthVars.basisAmount + ) - + totalSpreadPenalty / + 2; + } + } + + // apply risk for spot and perp positions + int128 combinedSpot = healthVars.spotAmount + + healthVars.spotInLpAmount; + + for (uint8 healthType = 0; healthType < 3; ++healthType) { + int128 healthContribution = RiskHelper + ._getWeightX18( + healthVars.spotRisk, + combinedSpot, + IProductEngine.HealthType(healthType) + ) + .mul(combinedSpot) + .mul(healthVars.pricesX18.spotPriceX18); + + // Spot LP penalty + healthContribution -= (ONE - + RiskHelper._getWeightX18( + healthVars.spotRisk, + healthVars.spotInLpAmount, + IProductEngine.HealthType(healthType) + )).mul(healthVars.spotInLpAmount).mul( + healthVars.pricesX18.spotPriceX18 + ); + + subaccountInfo.healthContributions[group.spotId][ + healthType + ] += healthContribution; + } + + int128 combinedPerp = healthVars.perpAmount + + healthVars.perpInLpAmount; + + for (uint8 healthType = 0; healthType < 3; ++healthType) { + int128 healthContribution = RiskHelper + ._getWeightX18( + healthVars.perpRisk, + combinedPerp, + IProductEngine.HealthType(healthType) + ) + .mul(combinedPerp) + .mul(healthVars.pricesX18.perpPriceX18); + + // perp LP penalty + healthContribution -= (ONE - + RiskHelper._getWeightX18( + healthVars.perpRisk, + healthVars.perpInLpAmount, + IProductEngine.HealthType(healthType) + )).mul(healthVars.perpInLpAmount).mul( + healthVars.pricesX18.perpPriceX18 + ); + + subaccountInfo.healthContributions[group.perpId][ + healthType + ] += healthContribution; + } + } + + // handle the quote balance since its not present in healthGroups + { + ( + ISpotEngine.State memory state, + ISpotEngine.Balance memory balance + ) = spotEngine.getStateAndBalance(QUOTE_PRODUCT_ID, subaccount); + subaccountInfo + .spotBalances[subaccountInfo.spotCount] + .balance = balance; + subaccountInfo + .spotProducts[subaccountInfo.spotCount] + .oraclePriceX18 = ONE; + subaccountInfo + .spotProducts[subaccountInfo.spotCount] + .risk = clearinghouse.getRisk(QUOTE_PRODUCT_ID); + subaccountInfo + .spotProducts[subaccountInfo.spotCount] + .config = spotEngine.getConfig(QUOTE_PRODUCT_ID); + subaccountInfo + .spotProducts[subaccountInfo.spotCount++] + .state = state; + + for (uint128 i = 0; i < 3; ++i) { + subaccountInfo.healthContributions[QUOTE_PRODUCT_ID][ + i + ] += balance.amount; + } + } + + for (uint128 i = 0; i < 3; ++i) { + for ( + uint128 j = 0; + j < subaccountInfo.healthContributions.length; + ++j + ) { + if (subaccountInfo.healthContributions[j][i] > 0) { + subaccountInfo.healths[i].assets += subaccountInfo + .healthContributions[j][i]; + } else { + subaccountInfo.healths[i].liabilities -= subaccountInfo + .healthContributions[j][i]; + } + } + subaccountInfo.healths[i].health = + subaccountInfo.healths[i].assets - + subaccountInfo.healths[i].liabilities; + } + + return subaccountInfo; + } + + function getSpotBalances(bytes32 subaccount, uint32[] memory productIds) + public + view + returns (SpotBalance[] memory spotBalances) + { + spotBalances = new SpotBalance[](productIds.length); + + for (uint32 i = 0; i < productIds.length; i++) { + uint32 productId = productIds[i]; + spotBalances[i] = getSpotBalance(subaccount, productId); + } + } + + function getPerpBalances(bytes32 subaccount, uint32[] memory productIds) + public + view + returns (PerpBalance[] memory perpBalances) + { + perpBalances = new PerpBalance[](productIds.length); + + for (uint32 i = 0; i < productIds.length; i++) { + uint32 productId = productIds[i]; + perpBalances[i] = getPerpBalance(subaccount, productId); + } + } + + function getSpotBalance(bytes32 subaccount, uint32 productId) + public + view + returns (SpotBalance memory) + { + ( + , + ISpotEngine.LpBalance memory lpBalance, + , + ISpotEngine.Balance memory balance + ) = spotEngine.getStatesAndBalances(productId, subaccount); + return + SpotBalance({ + productId: productId, + lpBalance: lpBalance, + balance: balance + }); + } + + function getPerpBalance(bytes32 subaccount, uint32 productId) + public + view + returns (PerpBalance memory) + { + ( + , + IPerpEngine.LpBalance memory lpBalance, + , + IPerpEngine.Balance memory balance + ) = perpEngine.getStatesAndBalances(productId, subaccount); + return + PerpBalance({ + productId: productId, + lpBalance: lpBalance, + balance: balance + }); + } + + function getBookInfo(uint32 productId, IProductEngine engine) + public + view + returns (BookInfo memory bookInfo) + { + IOffchainBook book = IOffchainBook(engine.getOrderbook(productId)); + IOffchainBook.Market memory market = book.getMarket(); + return + BookInfo({ + sizeIncrement: market.sizeIncrement, + priceIncrementX18: market.priceIncrementX18, + minSize: book.getMinSize(), + collectedFees: market.collectedFees, + lpSpreadX18: market.lpSpreadX18 + }); + } +} diff --git a/contracts/dependencies/vertex/FeeCalculator.sol b/contracts/dependencies/vertex/FeeCalculator.sol new file mode 100644 index 00000000..0fe3dc71 --- /dev/null +++ b/contracts/dependencies/vertex/FeeCalculator.sol @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import "./Version.sol"; +import "./interfaces/IFeeCalculator.sol"; +import "./common/Errors.sol"; + +contract FeeCalculator is Initializable, IFeeCalculator, Version { + address private clearinghouse; + mapping(address => mapping(uint32 => FeeRates)) feeRates; + + function initialize() external initializer {} + + function migrate(address _clearinghouse) external { + require(clearinghouse == address(0), "already migrated"); + clearinghouse = _clearinghouse; + } + + function getClearinghouse() external view returns (address) { + return clearinghouse; + } + + function recordVolume(bytes32 subaccount, uint128 quoteVolume) external {} + + function getFeeFractionX18( + bytes32 subaccount, + uint32 productId, + bool taker + ) external view returns (int128) { + require(productId != 0 && productId <= 42, "invalid productId"); + FeeRates memory userFeeRates = feeRates[ + address(uint160(bytes20(subaccount))) + ][productId]; + if (userFeeRates.isNonDefault == 0) { + // use the default fee rates. + if ( + productId == 1 || + productId == 3 || + productId == 5 || + productId == 6 || + productId == 8 || + productId == 10 || + productId == 12 || + productId == 14 || + productId == 16 || + productId == 18 || + productId == 20 || + productId == 22 || + productId == 24 || + productId == 26 || + productId == 28 || + productId == 30 || + productId == 34 || + productId == 36 || + productId == 38 || + productId == 40 || + productId == 41 + ) { + // btc-spot, eth-spot, arb-spot, arb-perp, bnb-perp, xrp-perp, sol-perp, + // matic-perp, sui-perp, op-perp, apt-perp, ltc-perp, bch-perp, comp-perp, + // mkr-perp, mpepe-perp, doge-perp, link-perp, dydx-perp, crv-perp, vrtx-spot + userFeeRates = FeeRates(0, 300_000_000_000_000, 1); + } else if (productId == 2 || productId == 4 || productId == 31) { + // btc-perp, eth-perp, usdt-spot + userFeeRates = FeeRates(0, 200_000_000_000_000, 1); + } else { + // placeholders + userFeeRates = FeeRates(0, 0, 1); + } + } + return taker ? userFeeRates.takerRateX18 : userFeeRates.makerRateX18; + } + + function getInterestFeeFractionX18( + uint32 /* productId */ + ) external pure returns (int128) { + return 200_000_000_000_000_000; // 20% + } + + function getLiquidationFeeFractionX18( + bytes32, /* subaccount */ + uint32 /* productId */ + ) external pure returns (int128) { + return 500_000_000_000_000_000; // 50% + } + + function updateFeeRates( + address user, + uint32 productId, + int64 makerRateX18, + int64 takerRateX18 + ) external { + require(msg.sender == clearinghouse, ERR_UNAUTHORIZED); + feeRates[user][productId] = FeeRates(makerRateX18, takerRateX18, 1); + } +} diff --git a/contracts/dependencies/vertex/MockSanctionsList.sol b/contracts/dependencies/vertex/MockSanctionsList.sol new file mode 100644 index 00000000..862ae254 --- /dev/null +++ b/contracts/dependencies/vertex/MockSanctionsList.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +contract MockSanctionsList { + function isSanctioned( + address /* addr */ + ) external pure returns (bool) { + return false; + } +} diff --git a/contracts/dependencies/vertex/OffchainBook.sol b/contracts/dependencies/vertex/OffchainBook.sol new file mode 100644 index 00000000..c552345a --- /dev/null +++ b/contracts/dependencies/vertex/OffchainBook.sol @@ -0,0 +1,623 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import "@openzeppelin/contracts-upgradeable/utils/cryptography/draft-EIP712Upgradeable.sol"; +import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import "./interfaces/engine/IProductEngine.sol"; +import "./interfaces/IFeeCalculator.sol"; +import "./libraries/MathSD21x18.sol"; +import "./common/Constants.sol"; +import "./libraries/MathHelper.sol"; +import "./OffchainBook.sol"; +import "./interfaces/IOffchainBook.sol"; +import "./EndpointGated.sol"; +import "./common/Errors.sol"; +import "./Version.sol"; + +// Similar to: https://stackoverflow.com/questions/1023860/exponential-moving-average-sampled-at-varying-times +// Set time constant tau = 600 +// normal calculation for factor looks like: e^(-timedelta/600) +// change this to (e^-1/600)^(timedelta) +// TIME_CONSTANT -> e^(-1/600) +int128 constant EMA_TIME_CONSTANT_X18 = 998334721450938752; + +contract OffchainBook is + IOffchainBook, + EndpointGated, + EIP712Upgradeable, + Version +{ + using MathSD21x18 for int128; + + IClearinghouse public clearinghouse; + IProductEngine public engine; + IFeeCalculator public fees; + Market public market; + + mapping(bytes32 => int128) public filledAmounts; + int128 minSize; + + function initialize( + IClearinghouse _clearinghouse, + IProductEngine _engine, + address _endpoint, + address _admin, + IFeeCalculator _fees, + uint32 _productId, + int128 _sizeIncrement, + int128 _priceIncrementX18, + int128 _minSize, + int128 _lpSpreadX18 + ) external initializer { + __Ownable_init(); + setEndpoint(_endpoint); + transferOwnership(_admin); + + __EIP712_init("Vertex", "0.0.1"); + clearinghouse = _clearinghouse; + engine = _engine; + fees = _fees; + + market = Market({ + productId: _productId, + sizeIncrement: _sizeIncrement, + priceIncrementX18: _priceIncrementX18, + lpSpreadX18: _lpSpreadX18, + collectedFees: 0, + sequencerFees: 0 + }); + minSize = _minSize; + } + + function modifyConfig( + int128 _sizeIncrement, + int128 _priceIncrementX18, + int128 _minSize, + int128 _lpSpreadX18 + ) external { + require(msg.sender == address(engine), "only engine can modify config"); + market.sizeIncrement = _sizeIncrement; + market.priceIncrementX18 = _priceIncrementX18; + market.lpSpreadX18 = _lpSpreadX18; + minSize = _minSize; + } + + function getMinSize() external view returns (int128) { + return minSize; + } + + function getDigest(IEndpoint.Order memory order) + public + view + returns (bytes32) + { + string + memory structType = "Order(bytes32 sender,int128 priceX18,int128 amount,uint64 expiration,uint64 nonce)"; + return + _hashTypedDataV4( + keccak256( + abi.encode( + keccak256(bytes(structType)), + order.sender, + order.priceX18, + order.amount, + order.expiration, + order.nonce + ) + ) + ); + } + + function _checkSignature( + bytes32 subaccount, + bytes32 digest, + address linkedSigner, + bytes memory signature + ) internal view virtual returns (bool) { + address signer = ECDSA.recover(digest, signature); + return + (signer != address(0)) && + (signer == address(uint160(bytes20(subaccount))) || + signer == linkedSigner); + } + + function _expired(uint64 expiration) internal view returns (bool) { + return expiration & ((1 << 58) - 1) <= getOracleTime(); + } + + function _isReduceOnly(uint64 expiration) internal view returns (bool) { + return ((expiration >> 61) & 1) == 1; + } + + function _isTakerFirst(bytes32 orderDigest) internal view returns (bool) { + return filledAmounts[orderDigest] == 0; + } + + function _validateOrder( + Market memory _market, + IEndpoint.SignedOrder memory signedOrder, + bytes32 orderDigest, + address linkedSigner + ) internal view returns (bool) { + IEndpoint.Order memory order = signedOrder.order; + int128 filledAmount = filledAmounts[orderDigest]; + order.amount -= filledAmount; + + if (_isReduceOnly(order.expiration)) { + int128 amount = engine.getBalanceAmount( + _market.productId, + order.sender + ); + if ((order.amount > 0) == (amount > 0)) { + order.amount = 0; + } else if (order.amount > 0) { + order.amount = MathHelper.min(order.amount, -amount); + } else if (order.amount < 0) { + order.amount = MathHelper.max(order.amount, -amount); + } + } + + return + (order.priceX18 > 0) && + (order.priceX18 % _market.priceIncrementX18 == 0) && + _checkSignature( + order.sender, + orderDigest, + linkedSigner, + signedOrder.signature + ) && + // valid amount + (order.amount != 0) && + !_expired(order.expiration); + } + + function _feeAmount( + bytes32 subaccount, + Market memory _market, + int128 amount, + bool taker, + // is this the first instance of this taker order matching + bool takerFirst + ) internal view returns (int128, int128) { + uint32 productId = _market.productId; + int128 keepRateX18 = ONE - + fees.getFeeFractionX18(subaccount, productId, taker); + int128 newAmount = (amount > 0) + ? amount.mul(keepRateX18) + : amount.div(keepRateX18); + int128 feeAmount = amount - newAmount; + _market.collectedFees += feeAmount; + if (takerFirst && taker) { + newAmount -= TAKER_SEQUENCER_FEE; + _market.sequencerFees += TAKER_SEQUENCER_FEE; + } + return (feeAmount, newAmount); + } + + function feeAmount( + bytes32 subaccount, + Market memory _market, + int128 amount, + bool taker, + // is this the first instance of this taker order matching + bool takerFirst + ) internal virtual returns (int128, int128) { + return _feeAmount(subaccount, _market, amount, taker, takerFirst); + } + + struct OrdersInfo { + bytes32 takerDigest; + bytes32 makerDigest; + int128 makerAmount; + } + + function _matchOrderAMM( + Market memory _market, + int128 baseDelta, // change in the LP's base position + int128 quoteDelta, // change in the LP's quote position + IEndpoint.SignedOrder memory taker + ) internal returns (int128, int128) { + // 1. assert that the price is better than the limit price + int128 impliedPriceX18 = quoteDelta.div(baseDelta).abs(); + if (taker.order.amount > 0) { + // if buying, the implied price must be lower than the limit price + require( + impliedPriceX18 <= taker.order.priceX18, + ERR_ORDERS_CANNOT_BE_MATCHED + ); + + // AMM must be selling + // magnitude of what AMM is selling must be less than or equal to what the taker is buying + require( + baseDelta < 0 && taker.order.amount >= -baseDelta, + ERR_ORDERS_CANNOT_BE_MATCHED + ); + } else { + // if selling, the implied price must be higher than the limit price + require( + impliedPriceX18 >= taker.order.priceX18, + ERR_ORDERS_CANNOT_BE_MATCHED + ); + // AMM must be buying + // magnitude of what AMM is buying must be less than or equal to what the taker is selling + require( + baseDelta > 0 && taker.order.amount <= -baseDelta, + ERR_ORDERS_CANNOT_BE_MATCHED + ); + } + + (int128 baseSwapped, int128 quoteSwapped) = engine.swapLp( + _market.productId, + baseDelta, + quoteDelta + ); + + taker.order.amount += baseSwapped; + return (-baseSwapped, -quoteSwapped); + } + + function _matchOrderOrder( + Market memory _market, + IEndpoint.Order memory taker, + IEndpoint.Order memory maker, + OrdersInfo memory ordersInfo + ) internal returns (int128 takerAmountDelta, int128 takerQuoteDelta) { + // execution happens at the maker's price + if (taker.amount < 0) { + takerAmountDelta = MathHelper.max(taker.amount, -maker.amount); + } else if (taker.amount > 0) { + takerAmountDelta = MathHelper.min(taker.amount, -maker.amount); + } else { + return (0, 0); + } + + takerAmountDelta -= takerAmountDelta % _market.sizeIncrement; + + int128 makerQuoteDelta = takerAmountDelta.mul(maker.priceX18); + + takerQuoteDelta = -makerQuoteDelta; + + // apply the maker fee + int128 makerFee; + (makerFee, makerQuoteDelta) = feeAmount( + maker.sender, + _market, + makerQuoteDelta, + false, + false + ); + + taker.amount -= takerAmountDelta; + maker.amount += takerAmountDelta; + + IProductEngine.ProductDelta[] + memory deltas = new IProductEngine.ProductDelta[](2); + + // maker + deltas[0] = IProductEngine.ProductDelta({ + productId: _market.productId, + subaccount: maker.sender, + amountDelta: -takerAmountDelta, + vQuoteDelta: makerQuoteDelta + }); + deltas[1] = IProductEngine.ProductDelta({ + productId: QUOTE_PRODUCT_ID, + subaccount: maker.sender, + amountDelta: makerQuoteDelta, + vQuoteDelta: 0 + }); + + engine.applyDeltas(deltas); + + emit FillOrder( + ordersInfo.makerDigest, + maker.sender, + maker.priceX18, + ordersInfo.makerAmount, + maker.expiration, + maker.nonce, + false, + makerFee, + -takerAmountDelta, + makerQuoteDelta + ); + } + + function matchOrderAMM( + IEndpoint.MatchOrderAMM calldata txn, + address takerLinkedSigner + ) external onlyEndpoint { + Market memory _market = market; + bytes32 takerDigest = getDigest(txn.taker.order); + int128 takerAmount = txn.taker.order.amount; + + // need to convert the taker order from calldata into memory + // otherwise modifications we make to the order's amounts + // don't persist + IEndpoint.SignedOrder memory taker = txn.taker; + + require( + _validateOrder(_market, taker, takerDigest, takerLinkedSigner), + ERR_INVALID_TAKER + ); + + bool isTakerFirst = _isTakerFirst(takerDigest); + + (int128 takerAmountDelta, int128 takerQuoteDelta) = _matchOrderAMM( + _market, + txn.baseDelta, + txn.quoteDelta, + taker + ); + + // apply the taker fee + int128 takerFee; + (takerFee, takerQuoteDelta) = feeAmount( + taker.order.sender, + _market, + takerQuoteDelta, + true, + isTakerFirst + ); + + IProductEngine.ProductDelta[] + memory deltas = new IProductEngine.ProductDelta[](2); + + // taker + deltas[0] = IProductEngine.ProductDelta({ + productId: _market.productId, + subaccount: taker.order.sender, + amountDelta: takerAmountDelta, + vQuoteDelta: takerQuoteDelta + }); + deltas[1] = IProductEngine.ProductDelta({ + productId: QUOTE_PRODUCT_ID, + subaccount: taker.order.sender, + amountDelta: takerQuoteDelta, + vQuoteDelta: 0 + }); + + engine.applyDeltas(deltas); + + require(isHealthy(taker.order.sender), ERR_INVALID_TAKER); + + emit FillOrder( + takerDigest, + taker.order.sender, + taker.order.priceX18, + takerAmount, + taker.order.expiration, + taker.order.nonce, + true, + takerFee, + takerAmountDelta, + takerQuoteDelta + ); + market.collectedFees = _market.collectedFees; + market.sequencerFees = _market.sequencerFees; + filledAmounts[takerDigest] = takerAmount - taker.order.amount; + } + + function isHealthy( + bytes32 /* subaccount */ + ) internal view virtual returns (bool) { + return true; + } + + function matchOrders(IEndpoint.MatchOrdersWithSigner calldata txn) + external + onlyEndpoint + { + Market memory _market = market; + IEndpoint.SignedOrder memory taker = txn.matchOrders.taker; + IEndpoint.SignedOrder memory maker = txn.matchOrders.maker; + + OrdersInfo memory ordersInfo = OrdersInfo({ + takerDigest: getDigest(taker.order), + makerDigest: getDigest(maker.order), + makerAmount: maker.order.amount + }); + + int128 takerAmount = taker.order.amount; + + require( + _validateOrder( + _market, + taker, + ordersInfo.takerDigest, + txn.takerLinkedSigner + ), + ERR_INVALID_TAKER + ); + require( + _validateOrder( + _market, + maker, + ordersInfo.makerDigest, + txn.makerLinkedSigner + ), + ERR_INVALID_MAKER + ); + + // ensure orders are crossing + require( + (maker.order.amount > 0) != (taker.order.amount > 0), + ERR_ORDERS_CANNOT_BE_MATCHED + ); + if (maker.order.amount > 0) { + require( + maker.order.priceX18 >= taker.order.priceX18, + ERR_ORDERS_CANNOT_BE_MATCHED + ); + } else { + require( + maker.order.priceX18 <= taker.order.priceX18, + ERR_ORDERS_CANNOT_BE_MATCHED + ); + } + + bool isTakerFirst = _isTakerFirst(ordersInfo.takerDigest); + + (int128 takerAmountDelta, int128 takerQuoteDelta) = _matchOrderOrder( + _market, + taker.order, + maker.order, + ordersInfo + ); + + // apply the taker fee + int128 takerFee; + (takerFee, takerQuoteDelta) = feeAmount( + taker.order.sender, + _market, + takerQuoteDelta, + true, + isTakerFirst + ); + + { + IProductEngine.ProductDelta[] + memory deltas = new IProductEngine.ProductDelta[](2); + + // taker + deltas[0] = IProductEngine.ProductDelta({ + productId: _market.productId, + subaccount: taker.order.sender, + amountDelta: takerAmountDelta, + vQuoteDelta: takerQuoteDelta + }); + deltas[1] = IProductEngine.ProductDelta({ + productId: QUOTE_PRODUCT_ID, + subaccount: taker.order.sender, + amountDelta: takerQuoteDelta, + vQuoteDelta: 0 + }); + + engine.applyDeltas(deltas); + } + + require(isHealthy(taker.order.sender), ERR_INVALID_TAKER); + require(isHealthy(maker.order.sender), ERR_INVALID_MAKER); + + emit FillOrder( + ordersInfo.takerDigest, + txn.matchOrders.taker.order.sender, + txn.matchOrders.taker.order.priceX18, + takerAmount, + txn.matchOrders.taker.order.expiration, + txn.matchOrders.taker.order.nonce, + true, + takerFee, + takerAmountDelta, + takerQuoteDelta + ); + + market.collectedFees = _market.collectedFees; + market.sequencerFees = _market.sequencerFees; + filledAmounts[ordersInfo.takerDigest] = + takerAmount - + taker.order.amount; + filledAmounts[ordersInfo.makerDigest] = + ordersInfo.makerAmount - + maker.order.amount; + } + + function swapAMM(IEndpoint.SwapAMM calldata txn) external onlyEndpoint { + Market memory _market = market; + if (engine.getEngineType() == IProductEngine.EngineType.PERP) { + require( + txn.amount % _market.sizeIncrement == 0, + ERR_INVALID_SWAP_PARAMS + ); + } + + (int128 takerAmountDelta, int128 takerQuoteDelta) = engine.swapLp( + _market.productId, + txn.amount, + txn.priceX18, + _market.sizeIncrement, + _market.lpSpreadX18 + ); + takerAmountDelta = -takerAmountDelta; + takerQuoteDelta = -takerQuoteDelta; + + int128 takerFee; + (takerFee, takerQuoteDelta) = feeAmount( + txn.sender, + _market, + takerQuoteDelta, + true, + false + ); + + { + IProductEngine.ProductDelta[] + memory deltas = new IProductEngine.ProductDelta[](2); + + // taker + deltas[0] = IProductEngine.ProductDelta({ + productId: _market.productId, + subaccount: txn.sender, + amountDelta: takerAmountDelta, + vQuoteDelta: takerQuoteDelta + }); + deltas[1] = IProductEngine.ProductDelta({ + productId: QUOTE_PRODUCT_ID, + subaccount: txn.sender, + amountDelta: takerQuoteDelta, + vQuoteDelta: 0 + }); + + engine.applyDeltas(deltas); + } + require( + clearinghouse.getHealth( + txn.sender, + IProductEngine.HealthType.INITIAL + ) >= 0, + ERR_INVALID_TAKER + ); + market.collectedFees = _market.collectedFees; + market.sequencerFees = _market.sequencerFees; + } + + function dumpFees() external onlyEndpoint { + IProductEngine.ProductDelta[] + memory feeAccDeltas = new IProductEngine.ProductDelta[](1); + int128 feesAmount = market.collectedFees; + // https://en.wikipedia.org/wiki/Design_Patterns + market.collectedFees = 0; + + if (engine.getEngineType() == IProductEngine.EngineType.SPOT) { + feeAccDeltas[0] = IProductEngine.ProductDelta({ + productId: QUOTE_PRODUCT_ID, + subaccount: FEES_ACCOUNT, + amountDelta: feesAmount, + vQuoteDelta: 0 + }); + } else { + feeAccDeltas[0] = IProductEngine.ProductDelta({ + productId: market.productId, + subaccount: FEES_ACCOUNT, + amountDelta: 0, + vQuoteDelta: feesAmount + }); + } + + engine.applyDeltas(feeAccDeltas); + } + + function claimSequencerFee() external returns (int128 feesAmount) { + require( + msg.sender == address(clearinghouse), + "Only the clearinghouse can claim sequencer fee" + ); + feesAmount = market.sequencerFees; + market.sequencerFees = 0; + } + + function getMarket() external view returns (Market memory) { + return market; + } +} diff --git a/contracts/dependencies/vertex/PerpEngine.sol b/contracts/dependencies/vertex/PerpEngine.sol new file mode 100644 index 00000000..430e13cc --- /dev/null +++ b/contracts/dependencies/vertex/PerpEngine.sol @@ -0,0 +1,295 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +import "hardhat/console.sol"; + +import "./common/Constants.sol"; +import "./common/Errors.sol"; +import "./interfaces/engine/IProductEngine.sol"; +import "./interfaces/engine/IPerpEngine.sol"; +import "./interfaces/clearinghouse/IClearinghouse.sol"; +import "./libraries/MathHelper.sol"; +import "./libraries/MathSD21x18.sol"; +import "./BaseEngine.sol"; +import "./PerpEngineLp.sol"; +import "./Version.sol"; + +contract PerpEngine is PerpEngineLp, Version { + using MathSD21x18 for int128; + + function initialize( + address _clearinghouse, + address _quote, + address _endpoint, + address _admin, + address _fees + ) external { + _initialize(_clearinghouse, _quote, _endpoint, _admin, _fees); + } + + function getEngineType() external pure returns (EngineType) { + return EngineType.PERP; + } + + /** + * Actions + */ + + /// @notice adds a new product with default parameters + function addProduct( + uint32 healthGroup, + address book, + int128 sizeIncrement, + int128 priceIncrementX18, + int128 minSize, + int128 lpSpreadX18, + IClearinghouseState.RiskStore calldata riskStore + ) public onlyOwner { + uint32 productId = _addProductForId( + healthGroup, + riskStore, + book, + sizeIncrement, + priceIncrementX18, + minSize, + lpSpreadX18 + ); + + states[productId] = State({ + cumulativeFundingLongX18: 0, + cumulativeFundingShortX18: 0, + availableSettle: 0, + openInterest: 0 + }); + + lpStates[productId] = LpState({ + supply: 0, + lastCumulativeFundingX18: 0, + cumulativeFundingPerLpX18: 0, + base: 0, + quote: 0 + }); + } + + /// @notice changes the configs of a product, if a new book is provided + /// also clears the book + function updateProduct(bytes calldata tx) external onlyEndpoint { + UpdateProductTx memory tx = abi.decode(tx, (UpdateProductTx)); + IClearinghouseState.RiskStore memory riskStore = tx.riskStore; + + require( + riskStore.longWeightInitial <= riskStore.longWeightMaintenance && + riskStore.shortWeightInitial >= + riskStore.shortWeightMaintenance, + ERR_BAD_PRODUCT_CONFIG + ); + markets[tx.productId].modifyConfig( + tx.sizeIncrement, + tx.priceIncrementX18, + tx.minSize, + tx.lpSpreadX18 + ); + + _clearinghouse.modifyProductConfig(tx.productId, riskStore); + } + + /// @notice updates internal balances; given tuples of (product, subaccount, delta) + /// since tuples aren't a thing in solidity, params specify the transpose + function applyDeltas(IProductEngine.ProductDelta[] calldata deltas) + external + { + // Only a market book can apply deltas + checkCanApplyDeltas(); + + // May load the same product multiple times + for (uint32 i = 0; i < deltas.length; i++) { + uint32 productId = deltas[i].productId; + // For perps, quote deltas are applied in `vQuoteDelta` + if ( + productId == QUOTE_PRODUCT_ID || + (deltas[i].amountDelta == 0 && deltas[i].vQuoteDelta == 0) + ) { + continue; + } + + bytes32 subaccount = deltas[i].subaccount; + int128 amountDelta = deltas[i].amountDelta; + int128 vQuoteDelta = deltas[i].vQuoteDelta; + + State memory state = states[productId]; + Balance memory balance = balances[productId][subaccount]; + + _updateBalance(state, balance, amountDelta, vQuoteDelta); + + states[productId] = state; + balances[productId][subaccount] = balance; + _balanceUpdate(productId, subaccount); + } + } + + function settlePnl(bytes32 subaccount, uint256 productIds) + external + returns (int128) + { + checkCanApplyDeltas(); + int128 totalSettled = 0; + + while (productIds != 0) { + uint32 productId = uint32(productIds & ((1 << 32) - 1)); + // otherwise it means the product is a spot. + if (productId % 2 == 0) { + ( + int128 canSettle, + LpState memory lpState, + LpBalance memory lpBalance, + State memory state, + Balance memory balance + ) = getSettlementState(productId, subaccount); + + state.availableSettle -= canSettle; + balance.vQuoteBalance -= canSettle; + + totalSettled += canSettle; + + lpStates[productId] = lpState; + states[productId] = state; + lpBalances[productId][subaccount] = lpBalance; + balances[productId][subaccount] = balance; + _balanceUpdate(productId, subaccount); + } + productIds >>= 32; + } + return totalSettled; + } + + function calculatePositionPnl( + LpState memory lpState, + LpBalance memory lpBalance, + Balance memory balance, + uint32 productId + ) internal view returns (int128 positionPnl) { + int128 priceX18 = getOraclePriceX18(productId); + + (int128 ammBase, int128 ammQuote) = MathHelper.ammEquilibrium( + lpState.base, + lpState.quote, + priceX18 + ); + + if (lpBalance.amount == 0) { + positionPnl = priceX18.mul(balance.amount) + balance.vQuoteBalance; + } else { + positionPnl = + priceX18.mul( + balance.amount + + ammBase.mul(lpBalance.amount).div(lpState.supply) + ) + + balance.vQuoteBalance + + ammQuote.mul(lpBalance.amount).div(lpState.supply); + } + } + + function getPositionPnl(uint32 productId, bytes32 subaccount) + external + view + returns (int128) + { + ( + LpState memory lpState, + LpBalance memory lpBalance, + , + Balance memory balance + ) = getStatesAndBalances(productId, subaccount); + + return calculatePositionPnl(lpState, lpBalance, balance, productId); + } + + function getSettlementState(uint32 productId, bytes32 subaccount) + public + view + returns ( + int128 availableSettle, + LpState memory lpState, + LpBalance memory lpBalance, + State memory state, + Balance memory balance + ) + { + (lpState, lpBalance, state, balance) = getStatesAndBalances( + productId, + subaccount + ); + + availableSettle = MathHelper.min( + calculatePositionPnl(lpState, lpBalance, balance, productId), + state.availableSettle + ); + } + + function socializeSubaccount(bytes32 subaccount, int128 insurance) + external + returns (int128) + { + require(msg.sender == address(_clearinghouse), ERR_UNAUTHORIZED); + + for (uint128 i = 0; i < productIds.length; ++i) { + uint32 productId = productIds[i]; + (State memory state, Balance memory balance) = getStateAndBalance( + productId, + subaccount + ); + if (balance.vQuoteBalance < 0) { + int128 insuranceCover = MathHelper.min( + insurance, + -balance.vQuoteBalance + ); + insurance -= insuranceCover; + balance.vQuoteBalance += insuranceCover; + state.availableSettle += insuranceCover; + + // actually socialize if still not enough + if (balance.vQuoteBalance < 0) { + // socialize across all other participants + int128 fundingPerShare = -balance.vQuoteBalance.div( + state.openInterest + ) / 2; + state.cumulativeFundingLongX18 += fundingPerShare; + state.cumulativeFundingShortX18 -= fundingPerShare; + + LpState memory lpState = lpStates[productId]; + Balance memory tmp = Balance({ + amount: lpState.base, + vQuoteBalance: 0, + lastCumulativeFundingX18: lpState + .lastCumulativeFundingX18 + }); + _updateBalance(state, tmp, 0, 0); + if (lpState.supply != 0) { + lpState.cumulativeFundingPerLpX18 += tmp + .vQuoteBalance + .div(lpState.supply); + } + lpState.lastCumulativeFundingX18 = state + .cumulativeFundingLongX18; + + lpStates[productId] = lpState; + balance.vQuoteBalance = 0; + } + states[productId] = state; + balances[productId][subaccount] = balance; + _balanceUpdate(productId, subaccount); + } + } + return insurance; + } + + function manualAssert(int128[] calldata openInterests) external view { + for (uint128 i = 0; i < openInterests.length; ++i) { + uint32 productId = productIds[i]; + require( + states[productId].openInterest == openInterests[i], + ERR_DSYNC + ); + } + } +} diff --git a/contracts/dependencies/vertex/PerpEngineLp.sol b/contracts/dependencies/vertex/PerpEngineLp.sol new file mode 100644 index 00000000..23450eb1 --- /dev/null +++ b/contracts/dependencies/vertex/PerpEngineLp.sol @@ -0,0 +1,221 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +import "./PerpEngineState.sol"; + +abstract contract PerpEngineLp is PerpEngineState { + using MathSD21x18 for int128; + + function mintLp( + uint32 productId, + bytes32 subaccount, + int128 amountBase, + int128 quoteAmountLow, + int128 quoteAmountHigh + ) external { + checkCanApplyDeltas(); + + int128 sizeIncrement = IOffchainBook(getOrderbook(productId)) + .getMarket() + .sizeIncrement; + + require( + amountBase > 0 && + quoteAmountLow > 0 && + quoteAmountHigh > 0 && + amountBase % sizeIncrement == 0, + ERR_INVALID_LP_AMOUNT + ); + + ( + LpState memory lpState, + LpBalance memory lpBalance, + State memory state, + Balance memory balance + ) = getStatesAndBalances(productId, subaccount); + + int128 amountQuote = (lpState.base == 0) + ? amountBase.mul(getOraclePriceX18(productId)) + : amountBase.mul(lpState.quote.div(lpState.base)); + require(amountQuote >= quoteAmountLow, ERR_SLIPPAGE_TOO_HIGH); + require(amountQuote <= quoteAmountHigh, ERR_SLIPPAGE_TOO_HIGH); + + int128 toMint; + if (lpState.supply == 0) { + toMint = amountBase + amountQuote; + } else { + toMint = amountBase.div(lpState.base).mul(lpState.supply); + } + + state.openInterest += amountBase; + + lpState.base += amountBase; + lpState.quote += amountQuote; + lpBalance.amount += toMint; + _updateBalance(state, balance, -amountBase, -amountQuote); + lpState.supply += toMint; + + lpBalances[productId][subaccount] = lpBalance; + states[productId] = state; + lpStates[productId] = lpState; + balances[productId][subaccount] = balance; + + _balanceUpdate(productId, subaccount); + } + + function burnLp( + uint32 productId, + bytes32 subaccount, + int128 amountLp + ) public returns (int128 amountBase, int128 amountQuote) { + checkCanApplyDeltas(); + require(amountLp > 0, ERR_INVALID_LP_AMOUNT); + int128 sizeIncrement = IOffchainBook(getOrderbook(productId)) + .getMarket() + .sizeIncrement; + + ( + LpState memory lpState, + LpBalance memory lpBalance, + State memory state, + Balance memory balance + ) = getStatesAndBalances(productId, subaccount); + + if (amountLp == type(int128).max) { + amountLp = lpBalance.amount; + } + if (amountLp == 0) { + return (0, 0); + } + + require(lpBalance.amount >= amountLp, ERR_INSUFFICIENT_LP); + lpBalance.amount -= amountLp; + + amountBase = MathHelper.floor( + int128((int256(amountLp) * lpState.base) / lpState.supply), + sizeIncrement + ); + + amountQuote = int128( + (int256(amountLp) * lpState.quote) / lpState.supply + ); + + state.openInterest -= amountBase; + + _updateBalance(state, balance, amountBase, amountQuote); + lpState.base -= amountBase; + lpState.quote -= amountQuote; + lpState.supply -= amountLp; + + lpStates[productId] = lpState; + lpBalances[productId][subaccount] = lpBalance; + states[productId] = state; + balances[productId][subaccount] = balance; + + _balanceUpdate(productId, subaccount); + } + + function swapLp( + uint32 productId, + // maximum to swap + int128 amount, + int128 priceX18, + int128 sizeIncrement, + int128 lpSpreadX18 + ) external returns (int128 baseSwapped, int128 quoteSwapped) { + checkCanApplyDeltas(); + require(amount % sizeIncrement == 0, ERR_INVALID_LP_AMOUNT); + + LpState memory lpState = lpStates[productId]; + if (lpState.base == 0 || lpState.quote == 0) { + return (0, 0); + } + + (baseSwapped, quoteSwapped) = MathHelper.swap( + amount, + lpState.base, + lpState.quote, + priceX18, + sizeIncrement, + lpSpreadX18 + ); + + states[productId].openInterest += baseSwapped; + + lpState.base += baseSwapped; + lpState.quote += quoteSwapped; + lpStates[productId] = lpState; + _productUpdate(productId); + } + + function swapLp( + uint32 productId, + int128 baseDelta, + int128 quoteDelta + ) external returns (int128, int128) { + checkCanApplyDeltas(); + LpState memory lpState = lpStates[productId]; + require( + MathHelper.isSwapValid( + baseDelta, + quoteDelta, + lpState.base, + lpState.quote + ), + ERR_INVALID_MAKER + ); + + states[productId].openInterest += baseDelta; + + lpState.base += baseDelta; + lpState.quote += quoteDelta; + lpStates[productId] = lpState; + _productUpdate(productId); + return (baseDelta, quoteDelta); + } + + function decomposeLps( + bytes32 liquidatee, + bytes32 liquidator, + address feeCalculator + ) external returns (int128 liquidationFees) { + for (uint128 i = 0; i < productIds.length; ++i) { + uint32 productId = productIds[i]; + (, int128 amountQuote) = burnLp( + productId, + liquidatee, + type(int128).max + ); + int128 rewards = amountQuote.mul( + (ONE - + RiskHelper._getWeightX18( + IClearinghouse(_clearinghouse).getRisk(productId), + amountQuote, + IProductEngine.HealthType.MAINTENANCE + )) / 50 + ); + int128 fees = rewards.mul( + IFeeCalculator(feeCalculator).getLiquidationFeeFractionX18( + liquidator, + productId + ) + ); + rewards -= fees; + liquidationFees += fees; + + // transfer some of the burned proceeds to liquidator + State memory state = states[productId]; + Balance memory liquidateeBalance = balances[productId][liquidatee]; + Balance memory liquidatorBalance = balances[productId][liquidator]; + + _updateBalance(state, liquidateeBalance, 0, -rewards - fees); + _updateBalance(state, liquidatorBalance, 0, rewards); + + states[productId] = state; + balances[productId][liquidatee] = liquidateeBalance; + balances[productId][liquidator] = liquidatorBalance; + _balanceUpdate(productId, liquidator); + _balanceUpdate(productId, liquidatee); + } + } +} diff --git a/contracts/dependencies/vertex/PerpEngineState.sol b/contracts/dependencies/vertex/PerpEngineState.sol new file mode 100644 index 00000000..0ec07a08 --- /dev/null +++ b/contracts/dependencies/vertex/PerpEngineState.sol @@ -0,0 +1,205 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +import "./interfaces/engine/IPerpEngine.sol"; +import "./BaseEngine.sol"; + +int128 constant EMA_TIME_CONSTANT_X18 = 998334721450938752; +int128 constant ONE_DAY_X18 = 86400_000000000000000000; // 24 hours + +// we will want to config this later, but for now this is global and a percentage +int128 constant MAX_DAILY_FUNDING_RATE = 100000000000000000; // 0.1 + +abstract contract PerpEngineState is IPerpEngine, BaseEngine { + using MathSD21x18 for int128; + + mapping(uint32 => State) public states; + mapping(uint32 => mapping(bytes32 => Balance)) public balances; + + mapping(uint32 => LpState) public lpStates; + mapping(uint32 => mapping(bytes32 => LpBalance)) public lpBalances; + + function _updateBalance( + State memory state, + Balance memory balance, + int128 balanceDelta, + int128 vQuoteDelta + ) internal pure { + // pre update + state.openInterest -= (balance.amount > 0) ? balance.amount : int128(0); + int128 cumulativeFundingAmountX18 = (balance.amount > 0) + ? state.cumulativeFundingLongX18 + : state.cumulativeFundingShortX18; + int128 diffX18 = cumulativeFundingAmountX18 - + balance.lastCumulativeFundingX18; + int128 deltaQuote = vQuoteDelta - diffX18.mul(balance.amount); + + // apply delta + balance.amount += balanceDelta; + + // apply vquote + balance.vQuoteBalance += deltaQuote; + + // post update + if (balance.amount > 0) { + state.openInterest += balance.amount; + balance.lastCumulativeFundingX18 = state.cumulativeFundingLongX18; + } else { + balance.lastCumulativeFundingX18 = state.cumulativeFundingShortX18; + } + } + + function _applyLpBalanceFunding( + LpState memory lpState, + LpBalance memory lpBalance, + Balance memory balance + ) internal pure { + int128 vQuoteDelta = (lpState.cumulativeFundingPerLpX18 - + lpBalance.lastCumulativeFundingX18).mul(lpBalance.amount); + balance.vQuoteBalance += vQuoteDelta; + lpBalance.lastCumulativeFundingX18 = lpState.cumulativeFundingPerLpX18; + } + + function getStateAndBalance(uint32 productId, bytes32 subaccount) + public + view + returns (State memory, Balance memory) + { + State memory state = states[productId]; + Balance memory balance = balances[productId][subaccount]; + _updateBalance(state, balance, 0, 0); + return (state, balance); + } + + function getBalance(uint32 productId, bytes32 subaccount) + public + view + returns (Balance memory) + { + State memory state = states[productId]; + Balance memory balance = balances[productId][subaccount]; + _updateBalance(state, balance, 0, 0); + return balance; + } + + function getBalanceAmount(uint32 productId, bytes32 subaccount) + external + view + returns (int128) + { + return getBalance(productId, subaccount).amount; + } + + function hasBalance(uint32 productId, bytes32 subaccount) + external + view + returns (bool) + { + return + balances[productId][subaccount].amount != 0 || + balances[productId][subaccount].vQuoteBalance != 0 || + lpBalances[productId][subaccount].amount != 0; + } + + function getStatesAndBalances(uint32 productId, bytes32 subaccount) + public + view + returns ( + LpState memory, + LpBalance memory, + State memory, + Balance memory + ) + { + LpState memory lpState = lpStates[productId]; + State memory state = states[productId]; + LpBalance memory lpBalance = lpBalances[productId][subaccount]; + Balance memory balance = balances[productId][subaccount]; + + _updateBalance(state, balance, 0, 0); + _applyLpBalanceFunding(lpState, lpBalance, balance); + return (lpState, lpBalance, state, balance); + } + + function getBalances(uint32 productId, bytes32 subaccount) + public + view + returns (LpBalance memory, Balance memory) + { + LpState memory lpState = lpStates[productId]; + State memory state = states[productId]; + LpBalance memory lpBalance = lpBalances[productId][subaccount]; + Balance memory balance = balances[productId][subaccount]; + + _updateBalance(state, balance, 0, 0); + _applyLpBalanceFunding(lpState, lpBalance, balance); + return (lpBalance, balance); + } + + function getLpState(uint32 productId) + external + view + returns (LpState memory) + { + return lpStates[productId]; + } + + function updateStates(uint128 dt, int128[] calldata avgPriceDiffs) + external + onlyEndpoint + { + int128 dtX18 = int128(dt).fromInt(); + for (uint32 i = 0; i < avgPriceDiffs.length; i++) { + uint32 productId = productIds[i]; + State memory state = states[productId]; + if (state.openInterest == 0) { + continue; + } + require(dt < 7 * SECONDS_PER_DAY, ERR_INVALID_TIME); + + LpState memory lpState = lpStates[productId]; + + { + int128 indexPriceX18 = getOraclePriceX18(productId); + + // cap this price diff + int128 priceDiffX18 = avgPriceDiffs[i]; + + int128 maxPriceDiff = dtX18 + .div(ONE_DAY_X18) + .mul(MAX_DAILY_FUNDING_RATE) + .mul(indexPriceX18); + + if (priceDiffX18.abs() > maxPriceDiff) { + // Proper sign + priceDiffX18 = (priceDiffX18 > 0) + ? maxPriceDiff + : -maxPriceDiff; + } + + int128 paymentAmount = priceDiffX18.mul(dtX18).div(ONE_DAY_X18); + state.cumulativeFundingLongX18 += paymentAmount; + state.cumulativeFundingShortX18 += paymentAmount; + } + + { + Balance memory balance = Balance({ + amount: lpState.base, + vQuoteBalance: 0, + lastCumulativeFundingX18: lpState.lastCumulativeFundingX18 + }); + _updateBalance(state, balance, 0, 0); + if (lpState.supply != 0) { + lpState.cumulativeFundingPerLpX18 += balance + .vQuoteBalance + .div(lpState.supply); + } + lpState.lastCumulativeFundingX18 = state + .cumulativeFundingLongX18; + } + lpStates[productId] = lpState; + states[productId] = state; + _productUpdate(productId); + } + } +} diff --git a/contracts/dependencies/vertex/SpotEngine.sol b/contracts/dependencies/vertex/SpotEngine.sol new file mode 100644 index 00000000..ef91ab1a --- /dev/null +++ b/contracts/dependencies/vertex/SpotEngine.sol @@ -0,0 +1,261 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +import "./common/Constants.sol"; +import "./common/Errors.sol"; +import "./interfaces/IOffchainBook.sol"; +import "./interfaces/engine/ISpotEngine.sol"; +import "./interfaces/clearinghouse/IClearinghouse.sol"; +import "./libraries/MathHelper.sol"; +import "./libraries/MathSD21x18.sol"; +import "./BaseEngine.sol"; +import "./SpotEngineState.sol"; +import "./SpotEngineLP.sol"; +import "./Version.sol"; + +contract SpotEngine is SpotEngineLP, Version { + using MathSD21x18 for int128; + + function initialize( + address _clearinghouse, + address _quote, + address _endpoint, + address _admin, + address _fees + ) external { + _initialize(_clearinghouse, _quote, _endpoint, _admin, _fees); + + configs[QUOTE_PRODUCT_ID] = Config({ + token: _quote, + interestInflectionUtilX18: 8e17, // .8 + interestFloorX18: 1e16, // .01 + interestSmallCapX18: 4e16, // .04 + interestLargeCapX18: ONE // 1 + }); + states[QUOTE_PRODUCT_ID] = State({ + cumulativeDepositsMultiplierX18: ONE, + cumulativeBorrowsMultiplierX18: ONE, + totalDepositsNormalized: 0, + totalBorrowsNormalized: 0 + }); + productIds.push(QUOTE_PRODUCT_ID); + emit AddProduct(QUOTE_PRODUCT_ID); + } + + /** + * View + */ + + function getEngineType() external pure returns (EngineType) { + return EngineType.SPOT; + } + + function getConfig(uint32 productId) external view returns (Config memory) { + return configs[productId]; + } + + function getWithdrawFee(uint32 productId) external view returns (int128) { + if (productId == QUOTE_PRODUCT_ID) { + return 1e18; + } else if (productId == 1) { + // BTC + return 4e13; + } else if (productId == 3) { + // ETH + return 6e14; + } else if (productId == 5) { + // ARB + return 1e18; + } else if ( + productId == 7 || + productId == 9 || + productId == 11 || + productId == 13 || + productId == 15 || + productId == 17 || + productId == 19 || + productId == 21 || + productId == 23 || + productId == 25 || + productId == 27 || + productId == 29 || + productId == 33 || + productId == 35 || + productId == 37 || + productId == 39 + ) { + // placeholders + return 0; + } else if (productId == 31) { + // USDT + return 1e18; + } else if (productId == 41) { + // VRTX + return 1e18; + } + revert(ERR_INVALID_PRODUCT); + } + + /** + * Actions + */ + + /// @notice adds a new product with default parameters + function addProduct( + uint32 healthGroup, + address book, + int128 sizeIncrement, + int128 priceIncrementX18, + int128 minSize, + int128 lpSpreadX18, + Config calldata config, + IClearinghouseState.RiskStore calldata riskStore + ) public onlyOwner { + uint32 productId = _addProductForId( + healthGroup, + riskStore, + book, + sizeIncrement, + priceIncrementX18, + minSize, + lpSpreadX18 + ); + + configs[productId] = config; + states[productId] = State({ + cumulativeDepositsMultiplierX18: ONE, + cumulativeBorrowsMultiplierX18: ONE, + totalDepositsNormalized: 0, + totalBorrowsNormalized: 0 + }); + + lpStates[productId] = LpState({ + supply: 0, + quote: Balance({amount: 0, lastCumulativeMultiplierX18: ONE}), + base: Balance({amount: 0, lastCumulativeMultiplierX18: ONE}) + }); + } + + function updateProduct(bytes calldata tx) external onlyEndpoint { + UpdateProductTx memory tx = abi.decode(tx, (UpdateProductTx)); + IClearinghouseState.RiskStore memory riskStore = tx.riskStore; + + require( + riskStore.longWeightInitial <= riskStore.longWeightMaintenance && + riskStore.shortWeightInitial >= + riskStore.shortWeightMaintenance && + (configs[tx.productId].token == + address(uint160(tx.productId)) || + configs[tx.productId].token == tx.config.token), + ERR_BAD_PRODUCT_CONFIG + ); + markets[tx.productId].modifyConfig( + tx.sizeIncrement, + tx.priceIncrementX18, + tx.minSize, + tx.lpSpreadX18 + ); + + configs[tx.productId] = tx.config; + _clearinghouse.modifyProductConfig(tx.productId, riskStore); + } + + /// @notice updates internal balances; given tuples of (product, subaccount, delta) + /// since tuples aren't a thing in solidity, params specify the transpose + function applyDeltas(ProductDelta[] calldata deltas) external { + checkCanApplyDeltas(); + + // May load the same product multiple times + for (uint32 i = 0; i < deltas.length; i++) { + if (deltas[i].amountDelta == 0) { + continue; + } + + uint32 productId = deltas[i].productId; + bytes32 subaccount = deltas[i].subaccount; + int128 amountDelta = deltas[i].amountDelta; + State memory state = states[productId]; + BalanceNormalized memory balance = balances[productId][subaccount] + .balance; + + _updateBalanceNormalized(state, balance, amountDelta); + + states[productId].totalDepositsNormalized = state + .totalDepositsNormalized; + states[productId].totalBorrowsNormalized = state + .totalBorrowsNormalized; + + balances[productId][subaccount].balance = balance; + + _balanceUpdate(productId, subaccount); + } + } + + function socializeSubaccount(bytes32 subaccount) external { + require(msg.sender == address(_clearinghouse), ERR_UNAUTHORIZED); + + for (uint128 i = 0; i < productIds.length; ++i) { + uint32 productId = productIds[i]; + (State memory state, Balance memory balance) = getStateAndBalance( + productId, + subaccount + ); + if (balance.amount < 0) { + int128 totalDeposited = state.totalDepositsNormalized.mul( + state.cumulativeDepositsMultiplierX18 + ); + + state.cumulativeDepositsMultiplierX18 = (totalDeposited + + balance.amount).div(state.totalDepositsNormalized); + + state.totalBorrowsNormalized += balance.amount.div( + state.cumulativeBorrowsMultiplierX18 + ); + + balances[productId][subaccount].balance.amountNormalized = 0; + + if (productId == QUOTE_PRODUCT_ID) { + for (uint128 j = 0; j < productIds.length; ++j) { + uint32 baseProductId = productIds[j]; + if (baseProductId == QUOTE_PRODUCT_ID) { + continue; + } + LpState memory lpState = lpStates[baseProductId]; + _updateBalanceWithoutDelta(state, lpState.quote); + lpStates[baseProductId] = lpState; + _productUpdate(baseProductId); + } + } else { + LpState memory lpState = lpStates[productId]; + _updateBalanceWithoutDelta(state, lpState.base); + lpStates[productId] = lpState; + } + + states[productId] = state; + _balanceUpdate(productId, subaccount); + } + } + } + + function manualAssert( + int128[] calldata totalDeposits, + int128[] calldata totalBorrows + ) external view { + for (uint128 i = 0; i < totalDeposits.length; ++i) { + uint32 productId = productIds[i]; + State memory state = states[productId]; + require( + state.totalDepositsNormalized.mul( + state.cumulativeDepositsMultiplierX18 + ) == totalDeposits[i], + ERR_DSYNC + ); + require( + state.totalBorrowsNormalized.mul( + state.cumulativeBorrowsMultiplierX18 + ) == totalBorrows[i], + ERR_DSYNC + ); + } + } +} diff --git a/contracts/dependencies/vertex/SpotEngineLP.sol b/contracts/dependencies/vertex/SpotEngineLP.sol new file mode 100644 index 00000000..9c0d7184 --- /dev/null +++ b/contracts/dependencies/vertex/SpotEngineLP.sol @@ -0,0 +1,255 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +import "./SpotEngineState.sol"; +import "./OffchainBook.sol"; + +abstract contract SpotEngineLP is SpotEngineState { + using MathSD21x18 for int128; + + function mintLp( + uint32 productId, + bytes32 subaccount, + int128 amountBase, + int128 quoteAmountLow, + int128 quoteAmountHigh + ) external { + checkCanApplyDeltas(); + require( + amountBase > 0 && quoteAmountLow > 0 && quoteAmountHigh > 0, + ERR_INVALID_LP_AMOUNT + ); + + LpState memory lpState = lpStates[productId]; + State memory base = states[productId]; + State memory quote = states[QUOTE_PRODUCT_ID]; + + int128 amountQuote = (lpState.base.amount == 0) + ? amountBase.mul(getOraclePriceX18(productId)) + : amountBase.mul(lpState.quote.amount.div(lpState.base.amount)); + require(amountQuote >= quoteAmountLow, ERR_SLIPPAGE_TOO_HIGH); + require(amountQuote <= quoteAmountHigh, ERR_SLIPPAGE_TOO_HIGH); + + int128 toMint; + if (lpState.supply == 0) { + toMint = amountBase + amountQuote; + } else { + toMint = amountBase.div(lpState.base.amount).mul(lpState.supply); + } + + _updateBalance(base, lpState.base, amountBase); + _updateBalance(quote, lpState.quote, amountQuote); + lpState.supply += toMint; + + balances[productId][subaccount].lpBalance.amount += toMint; + + lpStates[productId] = lpState; + + BalanceNormalized memory baseBalance = balances[productId][subaccount] + .balance; + BalanceNormalized memory quoteBalance = balances[QUOTE_PRODUCT_ID][ + subaccount + ].balance; + + _updateBalanceNormalized(base, baseBalance, -amountBase); + _updateBalanceNormalized(quote, quoteBalance, -amountQuote); + + balances[productId][subaccount].balance = baseBalance; + balances[QUOTE_PRODUCT_ID][subaccount].balance = quoteBalance; + states[productId] = base; + states[QUOTE_PRODUCT_ID] = quote; + + _balanceUpdate(productId, subaccount); + _balanceUpdate(QUOTE_PRODUCT_ID, subaccount); + } + + function burnLp( + uint32 productId, + bytes32 subaccount, + int128 amountLp + ) public returns (int128 amountBase, int128 amountQuote) { + checkCanApplyDeltas(); + require(amountLp > 0, ERR_INVALID_LP_AMOUNT); + + LpState memory lpState = lpStates[productId]; + LpBalance memory lpBalance = balances[productId][subaccount].lpBalance; + State memory base = states[productId]; + State memory quote = states[QUOTE_PRODUCT_ID]; + + if (amountLp == type(int128).max) { + amountLp = lpBalance.amount; + } + if (amountLp == 0) { + return (0, 0); + } + + require(lpBalance.amount >= amountLp, ERR_INSUFFICIENT_LP); + lpBalance.amount -= amountLp; + + amountBase = int128( + (int256(amountLp) * lpState.base.amount) / lpState.supply + ); + amountQuote = int128( + (int256(amountLp) * lpState.quote.amount) / lpState.supply + ); + + _updateBalance(base, lpState.base, -amountBase); + _updateBalance(quote, lpState.quote, -amountQuote); + lpState.supply -= amountLp; + + lpStates[productId] = lpState; + balances[productId][subaccount].lpBalance = lpBalance; + + BalanceNormalized memory baseBalance = balances[productId][subaccount] + .balance; + BalanceNormalized memory quoteBalance = balances[QUOTE_PRODUCT_ID][ + subaccount + ].balance; + + _updateBalanceNormalized(base, baseBalance, amountBase); + _updateBalanceNormalized(quote, quoteBalance, amountQuote); + + balances[productId][subaccount].balance = baseBalance; + balances[QUOTE_PRODUCT_ID][subaccount].balance = quoteBalance; + states[productId] = base; + states[QUOTE_PRODUCT_ID] = quote; + + _balanceUpdate(productId, subaccount); + _balanceUpdate(QUOTE_PRODUCT_ID, subaccount); + } + + function swapLp( + uint32 productId, + // maximum to swap + int128 amount, + int128 priceX18, + int128 sizeIncrement, + int128 lpSpreadX18 + ) external returns (int128 baseSwapped, int128 quoteSwapped) { + checkCanApplyDeltas(); + LpState memory lpState = lpStates[productId]; + + if (lpState.base.amount == 0 || lpState.quote.amount == 0) { + return (0, 0); + } + + int128 baseDepositsMultiplierX18 = states[productId] + .cumulativeDepositsMultiplierX18; + int128 quoteDepositsMultiplierX18 = states[QUOTE_PRODUCT_ID] + .cumulativeDepositsMultiplierX18; + + (baseSwapped, quoteSwapped) = MathHelper.swap( + amount, + lpState.base.amount, + lpState.quote.amount, + priceX18, + sizeIncrement, + lpSpreadX18 + ); + + lpState.base.amount += baseSwapped; + lpState.quote.amount += quoteSwapped; + lpStates[productId] = lpState; + + states[productId].totalDepositsNormalized += baseSwapped.div( + baseDepositsMultiplierX18 + ); + states[QUOTE_PRODUCT_ID].totalDepositsNormalized += quoteSwapped.div( + quoteDepositsMultiplierX18 + ); + + _productUpdate(productId); + // actual balance updates for the subaccount happen in OffchainBook + } + + function swapLp( + uint32 productId, + int128 baseDelta, + int128 quoteDelta + ) external returns (int128, int128) { + checkCanApplyDeltas(); + LpState memory lpState = lpStates[productId]; + require( + MathHelper.isSwapValid( + baseDelta, + quoteDelta, + lpState.base.amount, + lpState.quote.amount + ), + ERR_INVALID_MAKER + ); + + int128 baseDepositsMultiplierX18 = states[productId] + .cumulativeDepositsMultiplierX18; + int128 quoteDepositsMultiplierX18 = states[QUOTE_PRODUCT_ID] + .cumulativeDepositsMultiplierX18; + + lpState.base.amount += baseDelta; + lpState.quote.amount += quoteDelta; + lpStates[productId] = lpState; + + states[productId].totalDepositsNormalized += baseDelta.div( + baseDepositsMultiplierX18 + ); + states[QUOTE_PRODUCT_ID].totalDepositsNormalized += quoteDelta.div( + quoteDepositsMultiplierX18 + ); + _productUpdate(productId); + return (baseDelta, quoteDelta); + } + + function decomposeLps( + bytes32 liquidatee, + bytes32 liquidator, + address feeCalculator + ) external returns (int128 liquidationFees) { + int128 liquidationRewards = 0; + for (uint128 i = 0; i < productIds.length; ++i) { + uint32 productId = productIds[i]; + (, int128 amountQuote) = burnLp( + productId, + liquidatee, + type(int128).max + ); + int128 rewards = amountQuote.mul( + (ONE - + RiskHelper._getWeightX18( + IClearinghouse(_clearinghouse).getRisk(productId), + amountQuote, + IProductEngine.HealthType.MAINTENANCE + )) / 50 + ); + int128 fees = rewards.mul( + IFeeCalculator(feeCalculator).getLiquidationFeeFractionX18( + liquidator, + productId + ) + ); + rewards -= fees; + liquidationRewards += rewards; + liquidationFees += fees; + } + + // transfer some of the burned proceeds to liquidator + State memory quote = states[QUOTE_PRODUCT_ID]; + BalanceNormalized memory liquidateeQuote = balances[QUOTE_PRODUCT_ID][ + liquidatee + ].balance; + BalanceNormalized memory liquidatorQuote = balances[QUOTE_PRODUCT_ID][ + liquidator + ].balance; + + _updateBalanceNormalized( + quote, + liquidateeQuote, + -liquidationRewards - liquidationFees + ); + _updateBalanceNormalized(quote, liquidatorQuote, liquidationRewards); + + balances[QUOTE_PRODUCT_ID][liquidatee].balance = liquidateeQuote; + balances[QUOTE_PRODUCT_ID][liquidator].balance = liquidatorQuote; + states[QUOTE_PRODUCT_ID] = quote; + _balanceUpdate(QUOTE_PRODUCT_ID, liquidator); + _balanceUpdate(QUOTE_PRODUCT_ID, liquidatee); + } +} diff --git a/contracts/dependencies/vertex/SpotEngineState.sol b/contracts/dependencies/vertex/SpotEngineState.sol new file mode 100644 index 00000000..0ed5b378 --- /dev/null +++ b/contracts/dependencies/vertex/SpotEngineState.sol @@ -0,0 +1,367 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +import "./interfaces/engine/ISpotEngine.sol"; +import "./BaseEngine.sol"; + +abstract contract SpotEngineState is ISpotEngine, BaseEngine { + using MathSD21x18 for int128; + + mapping(uint32 => Config) internal configs; + mapping(uint32 => State) public states; + mapping(uint32 => mapping(bytes32 => Balances)) public balances; + + mapping(uint32 => LpState) public lpStates; + + mapping(uint32 => int128) public withdrawFees; + + function _updateBalanceWithoutDelta( + State memory state, + Balance memory balance + ) internal pure { + if (balance.amount == 0) { + balance.lastCumulativeMultiplierX18 = state + .cumulativeDepositsMultiplierX18; + return; + } + + // Current cumulative multiplier associated with product + int128 cumulativeMultiplierX18; + if (balance.amount > 0) { + cumulativeMultiplierX18 = state.cumulativeDepositsMultiplierX18; + } else { + cumulativeMultiplierX18 = state.cumulativeBorrowsMultiplierX18; + } + + if (balance.lastCumulativeMultiplierX18 == cumulativeMultiplierX18) { + return; + } + + balance.amount = balance.amount.mul(cumulativeMultiplierX18).div( + balance.lastCumulativeMultiplierX18 + ); + + balance.lastCumulativeMultiplierX18 = cumulativeMultiplierX18; + } + + function _updateBalance( + State memory state, + Balance memory balance, + int128 balanceDelta + ) internal pure { + if (balance.amount == 0 && balance.lastCumulativeMultiplierX18 == 0) { + balance.lastCumulativeMultiplierX18 = ONE; + } + + if (balance.amount > 0) { + state.totalDepositsNormalized -= balance.amount.div( + balance.lastCumulativeMultiplierX18 + ); + } else { + state.totalBorrowsNormalized += balance.amount.div( + balance.lastCumulativeMultiplierX18 + ); + } + + // Current cumulative multiplier associated with product + int128 cumulativeMultiplierX18; + if (balance.amount > 0) { + cumulativeMultiplierX18 = state.cumulativeDepositsMultiplierX18; + } else { + cumulativeMultiplierX18 = state.cumulativeBorrowsMultiplierX18; + } + + // Apply balance delta and interest rate + balance.amount = + balance.amount.mul( + cumulativeMultiplierX18.div(balance.lastCumulativeMultiplierX18) + ) + + balanceDelta; + + if (balance.amount > 0) { + cumulativeMultiplierX18 = state.cumulativeDepositsMultiplierX18; + } else { + cumulativeMultiplierX18 = state.cumulativeBorrowsMultiplierX18; + } + + balance.lastCumulativeMultiplierX18 = cumulativeMultiplierX18; + + // Update the product given balanceDelta + if (balance.amount > 0) { + state.totalDepositsNormalized += balance.amount.div( + balance.lastCumulativeMultiplierX18 + ); + } else { + state.totalBorrowsNormalized -= balance.amount.div( + balance.lastCumulativeMultiplierX18 + ); + } + } + + function _updateBalanceNormalized( + State memory state, + BalanceNormalized memory balance, + int128 balanceDelta + ) internal pure { + if (balance.amountNormalized > 0) { + state.totalDepositsNormalized -= balance.amountNormalized; + } else { + state.totalBorrowsNormalized += balance.amountNormalized; + } + + // Current cumulative multiplier associated with product + int128 cumulativeMultiplierX18; + if (balance.amountNormalized > 0) { + cumulativeMultiplierX18 = state.cumulativeDepositsMultiplierX18; + } else { + cumulativeMultiplierX18 = state.cumulativeBorrowsMultiplierX18; + } + + int128 newAmount = balance.amountNormalized.mul( + cumulativeMultiplierX18 + ) + balanceDelta; + + if (newAmount > 0) { + cumulativeMultiplierX18 = state.cumulativeDepositsMultiplierX18; + } else { + cumulativeMultiplierX18 = state.cumulativeBorrowsMultiplierX18; + } + + balance.amountNormalized = newAmount.div(cumulativeMultiplierX18); + + // Update the product given balanceDelta + if (balance.amountNormalized > 0) { + state.totalDepositsNormalized += balance.amountNormalized; + } else { + state.totalBorrowsNormalized -= balance.amountNormalized; + } + } + + function _updateState( + uint32 productId, + State memory state, + uint128 dt + ) internal { + int128 utilizationRatioX18; + int128 totalDeposits = state.totalDepositsNormalized.mul( + state.cumulativeDepositsMultiplierX18 + ); + + { + int128 totalBorrows = state.totalBorrowsNormalized.mul( + state.cumulativeBorrowsMultiplierX18 + ); + utilizationRatioX18 = totalDeposits == 0 + ? int128(0) + : totalBorrows.div(totalDeposits); + } + + int128 borrowRateMultiplierX18; + { + Config memory config = configs[productId]; + + // annualized borrower rate + int128 borrowerRateX18 = config.interestFloorX18; + if (utilizationRatioX18 == 0) { + // setting borrowerRateX18 to 0 here has the property that + // adding a product at the beginning of time and not using it until time T + // results in the same state as adding the product at time T + borrowerRateX18 = 0; + } else if (utilizationRatioX18 < config.interestInflectionUtilX18) { + borrowerRateX18 += config + .interestSmallCapX18 + .mul(utilizationRatioX18) + .div(config.interestInflectionUtilX18); + } else { + borrowerRateX18 += + config.interestSmallCapX18 + + config.interestLargeCapX18.mul( + ( + (utilizationRatioX18 - + config.interestInflectionUtilX18).div( + ONE - config.interestInflectionUtilX18 + ) + ) + ); + } + + // convert to per second + borrowerRateX18 = borrowerRateX18.div( + MathSD21x18.fromInt(31536000) + ); + borrowRateMultiplierX18 = (ONE + borrowerRateX18).pow(int128(dt)); + } + + // if we don't take fees into account, the liquidity, which is + // (deposits - borrows) should remain the same after updating state. + + // For simplicity, we use `tb`, `cbm`, `td`, and `cdm` for + // `totalBorrowsNormalized`, `cumulativeBorrowsMultiplier`, + // `totalDepositsNormalized`, and `cumulativeDepositsMultiplier` + + // before the updating, the liquidity is (td * cdm - tb * cbm) + // after the updating, the liquidity is + // (td * cdm * depositRateMultiplier - tb * cbm * borrowRateMultiplier) + // so we can get + // depositRateMultiplier = utilization * (borrowRateMultiplier - 1) + 1 + int128 totalDepositRateX18 = utilizationRatioX18.mul( + borrowRateMultiplierX18 - ONE + ); + + // deduct protocol fees + int128 realizedDepositRateX18 = totalDepositRateX18.mul( + ONE - _fees.getInterestFeeFractionX18(productId) + ); + + // pass fees balance change + int128 feesAmt = totalDeposits.mul( + totalDepositRateX18 - realizedDepositRateX18 + ); + + state.cumulativeBorrowsMultiplierX18 = state + .cumulativeBorrowsMultiplierX18 + .mul(borrowRateMultiplierX18); + + state.cumulativeDepositsMultiplierX18 = state + .cumulativeDepositsMultiplierX18 + .mul(ONE + realizedDepositRateX18); + + if (feesAmt != 0) { + BalanceNormalized memory feesAccBalance = balances[productId][ + FEES_ACCOUNT + ].balance; + _updateBalanceNormalized(state, feesAccBalance, feesAmt); + balances[productId][FEES_ACCOUNT].balance = feesAccBalance; + } + } + + function balanceNormalizedToBalance( + State memory state, + BalanceNormalized memory balance + ) internal pure returns (Balance memory) { + int128 cumulativeMultiplierX18; + if (balance.amountNormalized > 0) { + cumulativeMultiplierX18 = state.cumulativeDepositsMultiplierX18; + } else { + cumulativeMultiplierX18 = state.cumulativeBorrowsMultiplierX18; + } + + return + Balance( + balance.amountNormalized.mul(cumulativeMultiplierX18), + cumulativeMultiplierX18 + ); + } + + function getStateAndBalance(uint32 productId, bytes32 subaccount) + public + view + returns (State memory, Balance memory) + { + State memory state = states[productId]; + BalanceNormalized memory balance = balances[productId][subaccount] + .balance; + return (state, balanceNormalizedToBalance(state, balance)); + } + + function getBalance(uint32 productId, bytes32 subaccount) + public + view + returns (Balance memory) + { + State memory state = states[productId]; + BalanceNormalized memory balance = balances[productId][subaccount] + .balance; + return balanceNormalizedToBalance(state, balance); + } + + function getBalanceAmount(uint32 productId, bytes32 subaccount) + external + view + returns (int128) + { + return getBalance(productId, subaccount).amount; + } + + function hasBalance(uint32 productId, bytes32 subaccount) + external + view + returns (bool) + { + Balances memory allBalances = balances[productId][subaccount]; + return + allBalances.balance.amountNormalized != 0 || + allBalances.lpBalance.amount != 0; + } + + function getStatesAndBalances(uint32 productId, bytes32 subaccount) + external + view + returns ( + LpState memory, + LpBalance memory, + State memory, + Balance memory + ) + { + LpState memory lpState = lpStates[productId]; + State memory state = states[productId]; + LpBalance memory lpBalance = balances[productId][subaccount].lpBalance; + BalanceNormalized memory balance = balances[productId][subaccount] + .balance; + + return ( + lpState, + lpBalance, + state, + balanceNormalizedToBalance(state, balance) + ); + } + + function getBalances(uint32 productId, bytes32 subaccount) + external + view + returns (LpBalance memory, Balance memory) + { + State memory state = states[productId]; + LpBalance memory lpBalance = balances[productId][subaccount].lpBalance; + BalanceNormalized memory balance = balances[productId][subaccount] + .balance; + + return (lpBalance, balanceNormalizedToBalance(state, balance)); + } + + function getLpState(uint32 productId) + external + view + returns (LpState memory) + { + return lpStates[productId]; + } + + function updateStates(uint128 dt) external onlyEndpoint { + State memory quoteState = states[QUOTE_PRODUCT_ID]; + _updateState(QUOTE_PRODUCT_ID, quoteState, dt); + + for (uint32 i = 0; i < productIds.length; i++) { + uint32 productId = productIds[i]; + if (productId == QUOTE_PRODUCT_ID) { + continue; + } + State memory state = states[productId]; + if (state.totalDepositsNormalized == 0) { + continue; + } + require(dt < 7 * SECONDS_PER_DAY, ERR_INVALID_TIME); + LpState memory lpState = lpStates[productId]; + _updateState(productId, state, dt); + _updateBalanceWithoutDelta(state, lpState.base); + _updateBalanceWithoutDelta(quoteState, lpState.quote); + lpStates[productId] = lpState; + states[productId] = state; + _productUpdate(productId); + } + states[QUOTE_PRODUCT_ID] = quoteState; + _productUpdate(QUOTE_PRODUCT_ID); + } +} diff --git a/contracts/dependencies/vertex/Version.sol b/contracts/dependencies/vertex/Version.sol new file mode 100644 index 00000000..0762dffa --- /dev/null +++ b/contracts/dependencies/vertex/Version.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +import "./common/Constants.sol"; +import "./interfaces/IVersion.sol"; + +abstract contract Version is IVersion { + function getVersion() external pure returns (uint64) { + return VERSION; + } +} diff --git a/contracts/dependencies/vertex/common/Constants.sol b/contracts/dependencies/vertex/common/Constants.sol new file mode 100644 index 00000000..52a48539 --- /dev/null +++ b/contracts/dependencies/vertex/common/Constants.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +/// @dev Each clearinghouse has a unique quote product +uint32 constant QUOTE_PRODUCT_ID = 0; + +/// @dev Fees account +bytes32 constant FEES_ACCOUNT = bytes32(0); + +string constant DEFAULT_REFERRAL_CODE = "-1"; + +uint128 constant MINIMUM_LIQUIDITY = 10**3; + +int128 constant ONE = 10**18; + +uint8 constant MAX_DECIMALS = 18; + +int128 constant TAKER_SEQUENCER_FEE = 0; // $0.00 + +int128 constant SLOW_MODE_FEE = 1000000; // $1 + +int128 constant LIQUIDATION_FEE = 1e18; // $1 +int128 constant HEALTHCHECK_FEE = 1e18; // $1 + +uint64 constant VERSION = 25; + +uint128 constant INT128_MAX = uint128(type(int128).max); + +uint64 constant SECONDS_PER_DAY = 3600 * 24; + +uint32 constant VRTX_PRODUCT_ID = 41; diff --git a/contracts/dependencies/vertex/common/Errors.sol b/contracts/dependencies/vertex/common/Errors.sol new file mode 100644 index 00000000..d427ff3d --- /dev/null +++ b/contracts/dependencies/vertex/common/Errors.sol @@ -0,0 +1,120 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +// Trying to take an action on vertex when +string constant ERR_REQUIRES_DEPOSIT = "RS"; + +// ERC20 Transfer failed +string constant ERR_TRANSFER_FAILED = "TF"; + +// Unauthorized +string constant ERR_UNAUTHORIZED = "U"; + +// Invalid product +string constant ERR_INVALID_PRODUCT = "IP"; + +// Subaccount health too low +string constant ERR_SUBACCT_HEALTH = "SH"; + +// Not liquidatable +string constant ERR_NOT_LIQUIDATABLE = "NL"; + +// Liquidator health too low +string constant ERR_NOT_LIQUIDATABLE_INITIAL = "NLI"; + +// Liquidatee has positive initial health +string constant ERR_LIQUIDATED_TOO_MUCH = "LTM"; + +// Trying to liquidate quote, or +string constant ERR_INVALID_LIQUIDATION_PARAMS = "NILP"; + +// Trying to liquidate perp but the amount is not divisible by sizeIncrement +string constant ERR_INVALID_LIQUIDATION_AMOUNT = "NILA"; + +// Tried to liquidate too little, too much or signs are different +string constant ERR_NOT_LIQUIDATABLE_AMT = "NLA"; + +// Tried to liquidate liabilities before perps +string constant ERR_NOT_LIQUIDATABLE_LIABILITIES = "NLL"; + +// Tried to finalize subaccount that cannot be finalized +string constant ERR_NOT_FINALIZABLE_SUBACCOUNT = "NFS"; + +// Not enough quote to settle +string constant ERR_CANNOT_SETTLE = "CS"; + +// Not enough insurance to settle +string constant ERR_NO_INSURANCE = "NI"; + +// Above reserve ratio +string constant ERR_RESERVE_RATIO = "RR"; + +// Invalid socialize amount +string constant ERR_INVALID_SOCIALIZE_AMT = "ISA"; + +// Socializing product with no open interest +string constant ERR_NO_OPEN_INTEREST = "NOI"; + +// FOK not filled, this isn't rly an error so this is jank +string constant ERR_FOK_NOT_FILLED = "ENF"; + +// bad product config via weights +string constant ERR_BAD_PRODUCT_CONFIG = "BPC"; + +// subacct name too long +string constant ERR_LONG_NAME = "LN"; + +// already registered in health group +string constant ERR_ALREADY_REGISTERED = "AR"; + +// invalid health group provided +string constant ERR_INVALID_HEALTH_GROUP = "IHG"; + +string constant ERR_GETTING_ZERO_HEALTH_GROUP = "GZHG"; + +// trying to burn more LP than owned +string constant ERR_INSUFFICIENT_LP = "ILP"; + +// taker order subaccount fails risk or is invalid +string constant ERR_INVALID_TAKER = "IT"; + +// maker order subaccount fails risk or is invalid +string constant ERR_INVALID_MAKER = "IM"; + +string constant ERR_INVALID_SIGNATURE = "IS"; + +string constant ERR_ORDERS_CANNOT_BE_MATCHED = "OCBM"; + +string constant ERR_INVALID_LP_AMOUNT = "ILA"; + +string constant ERR_SLIPPAGE_TOO_HIGH = "STH"; + +string constant ERR_SUBACCOUNT_NOT_FOUND = "SNF"; + +string constant ERR_INVALID_PRICE = "IPR"; + +string constant ERR_INVALID_TIME = "ITI"; + +// states on node and engine are not same +string constant ERR_DSYNC = "DSYNC"; + +string constant ERR_INVALID_SWAP_PARAMS = "ISP"; + +string constant ERR_INVALID_REFERRAL_CODE = "IRC"; + +string constant ERR_CONVERSION_OVERFLOW = "CO"; + +string constant ERR_ONLY_CLEARINGHOUSE_CAN_SET_BOOK = "OCCSB"; + +// we match on containing these strings in sequencer +string constant ERR_INVALID_SUBMISSION_INDEX = "invalid submission index"; +string constant ERR_NO_SLOW_MODE_TXS_REMAINING = "no slow mode transactions remaining"; + +string constant ERR_INVALID_COUNT = "IC"; +string constant ERR_SLOW_TX_TOO_RECENT = "STTR"; +string constant ERR_WALLET_NOT_TRANSFERABLE = "WNT"; + +string constant ERR_WALLET_SANCTIONED = "WS"; + +string constant ERR_SLOW_MODE_WRONG_SENDER = "SMWS"; +string constant ERR_WRONG_NONCE = "WN"; diff --git a/contracts/dependencies/vertex/interfaces/IArbAirdrop.sol b/contracts/dependencies/vertex/interfaces/IArbAirdrop.sol new file mode 100644 index 00000000..3fb67396 --- /dev/null +++ b/contracts/dependencies/vertex/interfaces/IArbAirdrop.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +interface IArbAirdrop { + event ClaimArb(address indexed account, uint32 week, uint256 amount); + + struct ClaimProof { + uint32 week; + uint256 totalAmount; + bytes32[] proof; + } + + function claim(ClaimProof[] calldata claimProofs) external; + + function getClaimed(address account) + external + view + returns (uint256[] memory); +} diff --git a/contracts/dependencies/vertex/interfaces/IERC20Base.sol b/contracts/dependencies/vertex/interfaces/IERC20Base.sol new file mode 100644 index 00000000..44dd1e78 --- /dev/null +++ b/contracts/dependencies/vertex/interfaces/IERC20Base.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +interface IERC20Base { + function decimals() external view returns (uint8); + + /** + * @dev Moves `amount` tokens from the caller's account to `to`. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transfer(address to, uint256 amount) external returns (bool); + + /** + * @dev Moves `amount` tokens from `from` to `to` using the + * allowance mechanism. `amount` is then deducted from the caller's + * allowance. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transferFrom( + address from, + address to, + uint256 amount + ) external returns (bool); + + function increaseAllowance(address spender, uint256 addedValue) + external + returns (bool); + + function decreaseAllowance(address spender, uint256 subtractedValue) + external + returns (bool); +} diff --git a/contracts/dependencies/vertex/interfaces/IEndpoint.sol b/contracts/dependencies/vertex/interfaces/IEndpoint.sol new file mode 100644 index 00000000..6a67b359 --- /dev/null +++ b/contracts/dependencies/vertex/interfaces/IEndpoint.sol @@ -0,0 +1,292 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +import "./clearinghouse/IClearinghouse.sol"; +import "./IVersion.sol"; + +interface IEndpoint is IVersion { + event SubmitTransactions(); + + // events that we parse transactions into + enum TransactionType { + LiquidateSubaccount, + DepositCollateral, + WithdrawCollateral, + SpotTick, + UpdatePrice, + SettlePnl, + MatchOrders, + DepositInsurance, + ExecuteSlowMode, + MintLp, + BurnLp, + SwapAMM, + MatchOrderAMM, + DumpFees, + ClaimSequencerFees, + PerpTick, + ManualAssert, + Rebate, + UpdateProduct, + LinkSigner, + UpdateFeeRates, + BurnLpAndTransfer + } + + struct UpdateProduct { + address engine; + bytes tx; + } + + /// requires signature from sender + enum LiquidationMode { + SPREAD, + SPOT, + PERP + } + + struct LiquidateSubaccount { + bytes32 sender; + bytes32 liquidatee; + uint8 mode; + uint32 healthGroup; + int128 amount; + uint64 nonce; + } + + struct SignedLiquidateSubaccount { + LiquidateSubaccount tx; + bytes signature; + } + + struct DepositCollateral { + bytes32 sender; + uint32 productId; + uint128 amount; + } + + struct SignedDepositCollateral { + DepositCollateral tx; + bytes signature; + } + + struct WithdrawCollateral { + bytes32 sender; + uint32 productId; + uint128 amount; + uint64 nonce; + } + + struct SignedWithdrawCollateral { + WithdrawCollateral tx; + bytes signature; + } + + struct MintLp { + bytes32 sender; + uint32 productId; + uint128 amountBase; + uint128 quoteAmountLow; + uint128 quoteAmountHigh; + uint64 nonce; + } + + struct SignedMintLp { + MintLp tx; + bytes signature; + } + + struct BurnLp { + bytes32 sender; + uint32 productId; + uint128 amount; + uint64 nonce; + } + + struct SignedBurnLp { + BurnLp tx; + bytes signature; + } + + struct LinkSigner { + bytes32 sender; + bytes32 signer; + uint64 nonce; + } + + struct SignedLinkSigner { + LinkSigner tx; + bytes signature; + } + + /// callable by endpoint; no signature verifications needed + struct PerpTick { + uint128 time; + int128[] avgPriceDiffs; + } + + struct SpotTick { + uint128 time; + } + + struct ManualAssert { + int128[] openInterests; + int128[] totalDeposits; + int128[] totalBorrows; + } + + struct Rebate { + bytes32[] subaccounts; + int128[] amounts; + } + + struct UpdateFeeRates { + address user; + uint32 productId; + // the absolute value of fee rates can't be larger than 100%, + // so their X18 values are in the range [-1e18, 1e18], which + // can be stored by using int64. + int64 makerRateX18; + int64 takerRateX18; + } + + struct ClaimSequencerFees { + bytes32 subaccount; + } + + struct UpdatePrice { + uint32 productId; + int128 priceX18; + } + + struct SettlePnl { + bytes32[] subaccounts; + uint256[] productIds; + } + + /// matching + struct Order { + bytes32 sender; + int128 priceX18; + int128 amount; + uint64 expiration; + uint64 nonce; + } + + struct SignedOrder { + Order order; + bytes signature; + } + + struct MatchOrders { + uint32 productId; + bool amm; // whether taker order should hit AMM first (deprecated) + SignedOrder taker; + SignedOrder maker; + } + + struct MatchOrdersWithSigner { + MatchOrders matchOrders; + address takerLinkedSigner; + address makerLinkedSigner; + } + + // just swap against AMM -- theres no maker order + struct MatchOrderAMM { + uint32 productId; + int128 baseDelta; + int128 quoteDelta; + SignedOrder taker; + } + + struct SwapAMM { + bytes32 sender; + uint32 productId; + int128 amount; + int128 priceX18; + } + + struct DepositInsurance { + uint128 amount; + } + + struct SignedDepositInsurance { + DepositInsurance tx; + bytes signature; + } + + struct SlowModeTx { + uint64 executableAt; + address sender; + bytes tx; + } + + struct SlowModeConfig { + uint64 timeout; + uint64 txCount; + uint64 txUpTo; + } + + struct Prices { + int128 spotPriceX18; + int128 perpPriceX18; + } + + struct BurnLpAndTransfer { + bytes32 sender; + uint32 productId; + uint128 amount; + bytes32 recipient; + } + + function depositCollateral( + bytes12 subaccountName, + uint32 productId, + uint128 amount + ) external; + + function depositCollateralWithReferral( + bytes12 subaccountName, + uint32 productId, + uint128 amount, + string calldata referralCode + ) external; + + function depositCollateralWithReferral( + bytes32 subaccount, + uint32 productId, + uint128 amount, + string calldata referralCode + ) external; + + function setBook(uint32 productId, address book) external; + + function getBook(uint32 productId) external view returns (address); + + function submitTransactionsChecked( + uint64 idx, + bytes[] calldata transactions + ) external; + + function submitSlowModeTransaction(bytes calldata transaction) external; + + function getPriceX18(uint32 productId) external view returns (int128); + + function getPricesX18(uint32 healthGroup) + external + view + returns (Prices memory); + + function getTime() external view returns (uint128); + + function getNonce(address sender) external view returns (uint64); + + // function getNumSubaccounts() external view returns (uint64); + // + // function getSubaccountId(bytes32 subaccount) external view returns (uint64); + // + // function getSubaccountById(uint64 subaccountId) + // external + // view + // returns (bytes32); +} diff --git a/contracts/dependencies/vertex/interfaces/IEndpointGated.sol b/contracts/dependencies/vertex/interfaces/IEndpointGated.sol new file mode 100644 index 00000000..4d702105 --- /dev/null +++ b/contracts/dependencies/vertex/interfaces/IEndpointGated.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +import "./IEndpoint.sol"; + +interface IEndpointGated { + // this is all that remains lol, everything else is private or a modifier etc. + function getOraclePriceX18(uint32 productId) external view returns (int128); + + function getOraclePricesX18(uint32 healthGroup) + external + view + returns (IEndpoint.Prices memory); + + function getEndpoint() external view returns (address endpoint); +} diff --git a/contracts/dependencies/vertex/interfaces/IFEndpoint.sol b/contracts/dependencies/vertex/interfaces/IFEndpoint.sol new file mode 100644 index 00000000..036c85ea --- /dev/null +++ b/contracts/dependencies/vertex/interfaces/IFEndpoint.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +import "../Endpoint.sol"; + +interface IFEndpoint { + function setPriceX18(uint32 productId, int128 priceX18) external; +} diff --git a/contracts/dependencies/vertex/interfaces/IFeeCalculator.sol b/contracts/dependencies/vertex/interfaces/IFeeCalculator.sol new file mode 100644 index 00000000..f9871f79 --- /dev/null +++ b/contracts/dependencies/vertex/interfaces/IFeeCalculator.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; +import "./IVersion.sol"; + +interface IFeeCalculator is IVersion { + struct FeeRates { + int64 makerRateX18; + int64 takerRateX18; + uint8 isNonDefault; // 1: non-default, 0: default + } + + function getClearinghouse() external view returns (address); + + function migrate(address _clearinghouse) external; + + function recordVolume(bytes32 subaccount, uint128 quoteVolume) external; + + function getFeeFractionX18( + bytes32 subaccount, + uint32 productId, + bool taker + ) external view returns (int128); + + function getInterestFeeFractionX18(uint32 productId) + external + view + returns (int128); + + function getLiquidationFeeFractionX18(bytes32 subaccount, uint32 productId) + external + view + returns (int128); + + function updateFeeRates( + address user, + uint32 productId, + int64 makerRateX18, + int64 takerRateX18 + ) external; +} diff --git a/contracts/dependencies/vertex/interfaces/IOffchainBook.sol b/contracts/dependencies/vertex/interfaces/IOffchainBook.sol new file mode 100644 index 00000000..ec9025e0 --- /dev/null +++ b/contracts/dependencies/vertex/interfaces/IOffchainBook.sol @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +import "./clearinghouse/IClearinghouse.sol"; +import "./IFeeCalculator.sol"; +import "./IVersion.sol"; + +interface IOffchainBook is IVersion { + event FillOrder( + // original order information + bytes32 indexed digest, + bytes32 indexed subaccount, + int128 priceX18, + int128 amount, + uint64 expiration, + uint64 nonce, + // whether this order is taking or making + bool isTaker, + // amount paid in fees (in quote) + int128 feeAmount, + // change in this subaccount's base balance from this fill + int128 baseDelta, + // change in this subaccount's quote balance from this fill + int128 quoteDelta + ); + + struct Market { + uint32 productId; + int128 sizeIncrement; + int128 priceIncrementX18; + int128 lpSpreadX18; + int128 collectedFees; + int128 sequencerFees; + } + + function initialize( + IClearinghouse _clearinghouse, + IProductEngine _engine, + address _endpoint, + address _admin, + IFeeCalculator _fees, + uint32 _productId, + int128 _sizeIncrement, + int128 _priceIncrementX18, + int128 _minSize, + int128 _lpSpreadX18 + ) external; + + function modifyConfig( + int128 _sizeIncrement, + int128 _priceIncrementX18, + int128 _minSize, + int128 _lpSpreadX18 + ) external; + + function getMinSize() external view returns (int128); + + function getDigest(IEndpoint.Order memory order) + external + view + returns (bytes32); + + function getMarket() external view returns (Market memory); + + function swapAMM(IEndpoint.SwapAMM calldata tx) external; + + function matchOrderAMM( + IEndpoint.MatchOrderAMM calldata tx, + address takerLinkedSigner + ) external; + + function matchOrders(IEndpoint.MatchOrdersWithSigner calldata tx) external; + + function dumpFees() external; + + function claimSequencerFee() external returns (int128); +} diff --git a/contracts/dependencies/vertex/interfaces/IVersion.sol b/contracts/dependencies/vertex/interfaces/IVersion.sol new file mode 100644 index 00000000..43e046e9 --- /dev/null +++ b/contracts/dependencies/vertex/interfaces/IVersion.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +interface IVersion { + function getVersion() external returns (uint64); +} diff --git a/contracts/dependencies/vertex/interfaces/clearinghouse/IClearinghouse.sol b/contracts/dependencies/vertex/interfaces/clearinghouse/IClearinghouse.sol new file mode 100644 index 00000000..b5d544d4 --- /dev/null +++ b/contracts/dependencies/vertex/interfaces/clearinghouse/IClearinghouse.sol @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +import "./IClearinghouseState.sol"; +import "./IClearinghouseEventEmitter.sol"; +import "../engine/IProductEngine.sol"; +import "../IEndpoint.sol"; +import "../IEndpointGated.sol"; +import "../IVersion.sol"; + +interface IClearinghouse is + IClearinghouseState, + IClearinghouseEventEmitter, + IEndpointGated, + IVersion +{ + function addEngine(address engine, IProductEngine.EngineType engineType) + external; + + function registerProductForId( + address book, + RiskStore memory riskStore, + uint32 healthGroup + ) external returns (uint32); + + function modifyProductConfig(uint32 productId, RiskStore memory riskStore) + external; + + function depositCollateral(IEndpoint.DepositCollateral calldata tx) + external; + + function withdrawCollateral(IEndpoint.WithdrawCollateral calldata tx) + external; + + function mintLp(IEndpoint.MintLp calldata tx) external; + + function mintLpSlowMode(IEndpoint.MintLp calldata tx) external; + + function burnLp(IEndpoint.BurnLp calldata tx) external; + + function burnLpAndTransfer(IEndpoint.BurnLpAndTransfer calldata tx) + external; + + function liquidateSubaccount(IEndpoint.LiquidateSubaccount calldata tx) + external; + + function depositInsurance(IEndpoint.DepositInsurance calldata tx) external; + + function settlePnl(IEndpoint.SettlePnl calldata tx) external; + + function updateFeeRates(IEndpoint.UpdateFeeRates calldata tx) external; + + function claimSequencerFees( + IEndpoint.ClaimSequencerFees calldata tx, + int128[] calldata fees + ) external; + + /// @notice Retrieve quote ERC20 address + function getQuote() external view returns (address); + + /// @notice Returns all supported engine types for the clearinghouse + function getSupportedEngines() + external + view + returns (IProductEngine.EngineType[] memory); + + /// @notice Returns the registered engine address by type + function getEngineByType(IProductEngine.EngineType engineType) + external + view + returns (address); + + /// @notice Returns the engine associated with a product ID + function getEngineByProduct(uint32 productId) + external + view + returns (address); + + /// @notice Returns the orderbook associated with a product ID + function getOrderbook(uint32 productId) external view returns (address); + + /// @notice Returns number of registered products + function getNumProducts() external view returns (uint32); + + /// @notice Returns health for the subaccount across all engines + function getHealth(bytes32 subaccount, IProductEngine.HealthType healthType) + external + view + returns (int128); + + /// @notice Returns the amount of insurance remaining in this clearinghouse + function getInsurance() external view returns (int128); + + function getAllBooks() external view returns (address[] memory); +} diff --git a/contracts/dependencies/vertex/interfaces/clearinghouse/IClearinghouseEventEmitter.sol b/contracts/dependencies/vertex/interfaces/clearinghouse/IClearinghouseEventEmitter.sol new file mode 100644 index 00000000..ca72b268 --- /dev/null +++ b/contracts/dependencies/vertex/interfaces/clearinghouse/IClearinghouseEventEmitter.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +interface IClearinghouseEventEmitter { + /// @notice Emitted during initialization + event ClearinghouseInitialized( + address endpoint, + address quote, + address fees + ); + + /// @notice Emitted when collateral is modified for a subaccount + event ModifyCollateral( + int128 amount, + bytes32 indexed subaccount, + uint32 productId + ); + + event Liquidation( + bytes32 indexed liquidatorSubaccount, + bytes32 indexed liquidateeSubaccount, + uint8 indexed mode, + uint32 healthGroup, + int128 amount, + int128 amountQuote, + int128 insuranceCover + ); +} diff --git a/contracts/dependencies/vertex/interfaces/clearinghouse/IClearinghouseLiq.sol b/contracts/dependencies/vertex/interfaces/clearinghouse/IClearinghouseLiq.sol new file mode 100644 index 00000000..4b5247ae --- /dev/null +++ b/contracts/dependencies/vertex/interfaces/clearinghouse/IClearinghouseLiq.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +import "./IClearinghouseState.sol"; +import "./IClearinghouseEventEmitter.sol"; +import "../engine/IProductEngine.sol"; +import "../IEndpoint.sol"; +import "../IEndpointGated.sol"; + +interface IClearinghouseLiq is + IClearinghouseState, + IClearinghouseEventEmitter, + IEndpointGated +{ + function liquidateSubaccountImpl(IEndpoint.LiquidateSubaccount calldata tx) + external; +} diff --git a/contracts/dependencies/vertex/interfaces/clearinghouse/IClearinghouseState.sol b/contracts/dependencies/vertex/interfaces/clearinghouse/IClearinghouseState.sol new file mode 100644 index 00000000..29115725 --- /dev/null +++ b/contracts/dependencies/vertex/interfaces/clearinghouse/IClearinghouseState.sol @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +import "../engine/IProductEngine.sol"; +import "../IEndpoint.sol"; +import "../../libraries/RiskHelper.sol"; + +interface IClearinghouseState { + struct RiskStore { + // these weights are all + // between 0 and 2 + // these integers are the real + // weights times 1e9 + int32 longWeightInitial; + int32 shortWeightInitial; + int32 longWeightMaintenance; + int32 shortWeightMaintenance; + int32 largePositionPenalty; + } + + struct HealthGroup { + uint32 spotId; + uint32 perpId; + } + + struct HealthVars { + int128 spotAmount; + int128 perpAmount; + // 1 unit of basis amount is 1 unit long spot and 1 unit short perp + int128 basisAmount; + int128 spotInLpAmount; + int128 perpInLpAmount; + IEndpoint.Prices pricesX18; + RiskHelper.Risk spotRisk; + RiskHelper.Risk perpRisk; + } + + function getMaxHealthGroup() external view returns (uint32); + + function getRisk(uint32 productId) + external + view + returns (RiskHelper.Risk memory); +} diff --git a/contracts/dependencies/vertex/interfaces/engine/IPerpEngine.sol b/contracts/dependencies/vertex/interfaces/engine/IPerpEngine.sol new file mode 100644 index 00000000..3e77668d --- /dev/null +++ b/contracts/dependencies/vertex/interfaces/engine/IPerpEngine.sol @@ -0,0 +1,109 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +import "./IProductEngine.sol"; +import "../clearinghouse/IClearinghouseState.sol"; + +interface IPerpEngine is IProductEngine { + struct State { + int128 cumulativeFundingLongX18; + int128 cumulativeFundingShortX18; + int128 availableSettle; + int128 openInterest; + } + + struct Balance { + int128 amount; + int128 vQuoteBalance; + int128 lastCumulativeFundingX18; + } + + struct LpState { + int128 supply; + // TODO: this should be removed; we can just get it from State.cumulativeFundingLongX18 + int128 lastCumulativeFundingX18; + int128 cumulativeFundingPerLpX18; + int128 base; + int128 quote; + } + + struct LpBalance { + int128 amount; + // NOTE: funding payments should be rolled + // into Balance.vQuoteBalance; + int128 lastCumulativeFundingX18; + } + + struct UpdateProductTx { + uint32 productId; + int128 sizeIncrement; + int128 priceIncrementX18; + int128 minSize; + int128 lpSpreadX18; + IClearinghouseState.RiskStore riskStore; + } + + function getStateAndBalance(uint32 productId, bytes32 subaccount) + external + view + returns (State memory, Balance memory); + + function getBalance(uint32 productId, bytes32 subaccount) + external + view + returns (Balance memory); + + function hasBalance(uint32 productId, bytes32 subaccount) + external + view + returns (bool); + + function getStatesAndBalances(uint32 productId, bytes32 subaccount) + external + view + returns ( + LpState memory, + LpBalance memory, + State memory, + Balance memory + ); + + function getBalances(uint32 productId, bytes32 subaccount) + external + view + returns (LpBalance memory, Balance memory); + + function getLpState(uint32 productId) + external + view + returns (LpState memory); + + /// @dev Returns amount settled and emits SettlePnl events for each product + function settlePnl(bytes32 subaccount, uint256 productIds) + external + returns (int128); + + function getSettlementState(uint32 productId, bytes32 subaccount) + external + view + returns ( + int128 availableSettle, + LpState memory lpState, + LpBalance memory lpBalance, + State memory state, + Balance memory balance + ); + + function updateStates(uint128 dt, int128[] calldata avgPriceDiffs) external; + + function manualAssert(int128[] calldata openInterests) external view; + + function getPositionPnl(uint32 productId, bytes32 subaccount) + external + view + returns (int128); + + function socializeSubaccount(bytes32 subaccount, int128 insurance) + external + returns (int128); +} diff --git a/contracts/dependencies/vertex/interfaces/engine/IProductEngine.sol b/contracts/dependencies/vertex/interfaces/engine/IProductEngine.sol new file mode 100644 index 00000000..3201fdca --- /dev/null +++ b/contracts/dependencies/vertex/interfaces/engine/IProductEngine.sol @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "../clearinghouse/IClearinghouse.sol"; +import "./IProductEngineState.sol"; + +interface IProductEngine is IProductEngineState { + event AddProduct(uint32 productId); + + enum EngineType { + SPOT, + PERP + } + + enum HealthType { + INITIAL, + MAINTENANCE, + PNL + } + + struct ProductDelta { + uint32 productId; + bytes32 subaccount; + int128 amountDelta; + int128 vQuoteDelta; + } + + /// @notice Initializes the engine + function initialize( + address _clearinghouse, + address _quote, + address _endpoint, + address _admin, + address _fees + ) external; + + /// @notice updates internal balances; given tuples of (product, subaccount, delta) + /// since tuples aren't a thing in solidity, params specify the transpose + function applyDeltas(ProductDelta[] calldata deltas) external; + + function updateProduct(bytes calldata txn) external; + + function swapLp( + uint32 productId, + int128 amount, + int128 priceX18, + int128 sizeIncrement, + int128 lpSpreadX18 + ) external returns (int128, int128); + + function swapLp( + uint32 productId, + int128 baseDelta, + int128 quoteDelta + ) external returns (int128, int128); + + function mintLp( + uint32 productId, + bytes32 subaccount, + int128 amountBase, + int128 quoteAmountLow, + int128 quoteAmountHigh + ) external; + + function burnLp( + uint32 productId, + bytes32 subaccount, + // passing 0 here means to burn all + int128 amountLp + ) external returns (int128, int128); + + function decomposeLps( + bytes32 liquidatee, + bytes32 liquidator, + address feeCalculator + ) external returns (int128); +} diff --git a/contracts/dependencies/vertex/interfaces/engine/IProductEngineState.sol b/contracts/dependencies/vertex/interfaces/engine/IProductEngineState.sol new file mode 100644 index 00000000..2cf17870 --- /dev/null +++ b/contracts/dependencies/vertex/interfaces/engine/IProductEngineState.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +import "./IProductEngine.sol"; + +interface IProductEngineState { + /// @notice return clearinghouse addr + function getClearinghouse() external view returns (address); + + /// @notice return productIds associated with engine + function getProductIds() external view returns (uint32[] memory); + + /// @notice return the type of engine + function getEngineType() external pure returns (IProductEngine.EngineType); + + /// @notice Returns orderbook for a product ID + function getOrderbook(uint32 productId) external view returns (address); + + /// @notice Returns balance amount for some subaccount / productId + function getBalanceAmount(uint32 productId, bytes32 subaccount) + external + view + returns (int128); +} diff --git a/contracts/dependencies/vertex/interfaces/engine/ISpotEngine.sol b/contracts/dependencies/vertex/interfaces/engine/ISpotEngine.sol new file mode 100644 index 00000000..aa33c836 --- /dev/null +++ b/contracts/dependencies/vertex/interfaces/engine/ISpotEngine.sol @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +import "./IProductEngine.sol"; +import "../clearinghouse/IClearinghouseState.sol"; + +interface ISpotEngine is IProductEngine { + struct Config { + address token; + int128 interestInflectionUtilX18; + int128 interestFloorX18; + int128 interestSmallCapX18; + int128 interestLargeCapX18; + } + + struct State { + int128 cumulativeDepositsMultiplierX18; + int128 cumulativeBorrowsMultiplierX18; + int128 totalDepositsNormalized; + int128 totalBorrowsNormalized; + } + + struct Balance { + int128 amount; + int128 lastCumulativeMultiplierX18; + } + + struct BalanceNormalized { + int128 amountNormalized; + } + + struct LpState { + int128 supply; + Balance quote; + Balance base; + } + + struct LpBalance { + int128 amount; + } + + struct Balances { + BalanceNormalized balance; + LpBalance lpBalance; + } + + struct UpdateProductTx { + uint32 productId; + int128 sizeIncrement; + int128 priceIncrementX18; + int128 minSize; + int128 lpSpreadX18; + Config config; + IClearinghouseState.RiskStore riskStore; + } + + function getStateAndBalance(uint32 productId, bytes32 subaccount) + external + view + returns (State memory, Balance memory); + + function getBalance(uint32 productId, bytes32 subaccount) + external + view + returns (Balance memory); + + function hasBalance(uint32 productId, bytes32 subaccount) + external + view + returns (bool); + + function getStatesAndBalances(uint32 productId, bytes32 subaccount) + external + view + returns ( + LpState memory, + LpBalance memory, + State memory, + Balance memory + ); + + function getBalances(uint32 productId, bytes32 subaccount) + external + view + returns (LpBalance memory, Balance memory); + + function getLpState(uint32 productId) + external + view + returns (LpState memory); + + function getConfig(uint32 productId) external view returns (Config memory); + + function getWithdrawFee(uint32 productId) external view returns (int128); + + function updateStates(uint128 dt) external; + + function manualAssert( + int128[] calldata totalDeposits, + int128[] calldata totalBorrows + ) external view; + + function socializeSubaccount(bytes32 subaccount) external; +} diff --git a/contracts/dependencies/vertex/libraries/ERC20Helper.sol b/contracts/dependencies/vertex/libraries/ERC20Helper.sol new file mode 100644 index 00000000..53af98ad --- /dev/null +++ b/contracts/dependencies/vertex/libraries/ERC20Helper.sol @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +import "../interfaces/IERC20Base.sol"; +import "../common/Errors.sol"; +import "hardhat/console.sol"; + +// @dev Adapted from https://github.com/Uniswap/v3-core/blob/main/contracts/libraries/TransferHelper.sol +library ERC20Helper { + function safeTransfer( + IERC20Base self, + address to, + uint256 amount + ) internal { + (bool success, bytes memory data) = address(self).call( + abi.encodeWithSelector(IERC20Base.transfer.selector, to, amount) + ); + require( + success && (data.length == 0 || abi.decode(data, (bool))), + ERR_TRANSFER_FAILED + ); + } + + function safeTransferFrom( + IERC20Base self, + address from, + address to, + uint256 amount + ) internal { + (bool success, bytes memory data) = address(self).call( + abi.encodeWithSelector( + IERC20Base.transferFrom.selector, + from, + to, + amount + ) + ); + + require( + success && (data.length == 0 || abi.decode(data, (bool))), + ERR_TRANSFER_FAILED + ); + } +} diff --git a/contracts/dependencies/vertex/libraries/Logger.sol b/contracts/dependencies/vertex/libraries/Logger.sol new file mode 100644 index 00000000..050d89ed --- /dev/null +++ b/contracts/dependencies/vertex/libraries/Logger.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; +import "@openzeppelin/contracts/utils/Strings.sol"; +import "./MathHelper.sol"; + +library Logger { + event VertexEVMLog(string message); + + function log(string memory message) internal { + emit VertexEVMLog(message); + } + + function log(string memory message, int128 value) internal { + log(string.concat(message, " ", MathHelper.int2str(value))); + } + + function log(string memory message, uint128 value) internal { + log(string.concat(message, " ", MathHelper.uint2str(value))); + } + + function log(string memory message, address value) internal { + log( + string.concat(message, " ", Strings.toHexString(uint160(value), 20)) + ); + } + + function log(string memory messages, bytes32 value) internal { + log(string.concat(messages, " ", string(abi.encodePacked(value)))); + } +} diff --git a/contracts/dependencies/vertex/libraries/MathHelper.sol b/contracts/dependencies/vertex/libraries/MathHelper.sol new file mode 100644 index 00000000..b11cfaa3 --- /dev/null +++ b/contracts/dependencies/vertex/libraries/MathHelper.sol @@ -0,0 +1,231 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; +import "./MathSD21x18.sol"; + +/// @title MathHelper +/// @dev Provides basic math functions +library MathHelper { + using MathSD21x18 for int128; + + /// @notice Returns market id for two given product ids + function max(int128 a, int128 b) internal pure returns (int128) { + return a > b ? a : b; + } + + function min(int128 a, int128 b) internal pure returns (int128) { + return a < b ? a : b; + } + + function abs(int128 val) internal pure returns (int128) { + return val < 0 ? -val : val; + } + + // babylonian method (https://en.wikipedia.org/wiki/Methods_of_computing_square_roots#Babylonian_method) + function sqrt(int128 y) internal pure returns (int128 z) { + require(y >= 0, "ds-math-sqrt-non-positive"); + if (y > 3) { + z = y; + int128 x = y / 2 + 1; + while (x < z) { + z = x; + x = (y / x + x) / 2; + } + } else if (y != 0) { + z = 1; + } + } + + function sqrt256(int256 y) internal pure returns (int256 z) { + require(y >= 0, "ds-math-sqrt-non-positive"); + if (y > 3) { + z = y; + int256 x = y / 2 + 1; + while (x < z) { + z = x; + x = (y / x + x) / 2; + } + } else if (y != 0) { + z = 1; + } + } + + function int2str(int128 value) internal pure returns (string memory) { + if (value == 0) { + return "0"; + } + + bool negative = value < 0; + uint128 absval = uint128(negative ? -value : value); + string memory out = uint2str(absval); + if (negative) { + out = string.concat("-", out); + } + return out; + } + + function uint2str(uint128 value) internal pure returns (string memory) { + if (value == 0) { + return "0"; + } + uint128 temp = value; + uint128 digits; + while (temp != 0) { + digits++; + temp /= 10; + } + bytes memory buffer = new bytes(digits); + while (value != 0) { + digits -= 1; + buffer[digits] = bytes1(uint8(48 + uint128(value % 10))); + value /= 10; + } + return string(buffer); + } + + // https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v3.1.0/contracts/math/SignedSafeMath.sol#L86 + function add(int128 x, int128 y) internal pure returns (int128) { + int128 z = x + y; + require((y >= 0 && z >= x) || (y < 0 && z < x), "ds-math-add-overflow"); + return z; + } + + // https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v3.1.0/contracts/math/SignedSafeMath.sol#L69 + function sub(int128 x, int128 y) internal pure returns (int128) { + int128 z = x - y; + require( + (y >= 0 && z <= x) || (y < 0 && z > x), + "ds-math-sub-underflow" + ); + return z; + } + + function mul(int128 x, int128 y) internal pure returns (int128 z) { + require(y == 0 || (z = x * y) / y == x, "ds-math-mul-overflow"); + } + + function floor(int128 x, int128 y) internal pure returns (int128 z) { + require(y > 0, "ds-math-floor-neg-mod"); + int128 r = x % y; + if (r == 0) { + z = x; + } else { + z = (x >= 0 ? x - r : x - r - y); + } + } + + function ceil(int128 x, int128 y) internal pure returns (int128 z) { + require(y > 0, "ds-math-ceil-neg-mod"); + int128 r = x % y; + if (r == 0) { + z = x; + } else { + z = (x >= 0 ? x + y - r : x - r); + } + } + + // we don't need to floor base with sizeIncrement in this function + // because this function is only used by `view` functions, which means + // the returned values will not be written into storage. + function ammEquilibrium( + int128 base, + int128 quote, + int128 priceX18 + ) internal pure returns (int128, int128) { + if (base == 0 || quote == 0) { + return (0, 0); + } + int256 k = int256(base) * quote; + // base * price * base == k + // base = sqrt(k / price); + base = int128(MathHelper.sqrt256((k * 1e18) / priceX18)); + quote = (base == 0) ? int128(0) : int128(k / base); + return (base, quote); + } + + function isSwapValid( + int128 baseDelta, + int128 quoteDelta, + int128 base, + int128 quote + ) internal pure returns (bool) { + if ( + base == 0 || + quote == 0 || + base + baseDelta <= 0 || + quote + quoteDelta <= 0 + ) { + return false; + } + int256 kPrev = int256(base) * quote; + int256 kNew = int256(base + baseDelta) * (quote + quoteDelta); + return kNew > kPrev; + } + + function swap( + int128 amountSwap, + int128 base, + int128 quote, + int128 priceX18, + int128 sizeIncrement, + int128 lpSpreadX18 + ) internal pure returns (int128, int128) { + // (amountSwap % sizeIncrement) is guaranteed to be 0 + if (base == 0 || quote == 0) { + return (0, 0); + } + int128 currentPriceX18 = quote.div(base); + + int128 keepRateX18 = 1e18 - lpSpreadX18; + + // selling + if (amountSwap > 0) { + priceX18 = priceX18.div(keepRateX18); + if (priceX18 >= currentPriceX18) { + return (0, 0); + } + } else { + priceX18 = priceX18.mul(keepRateX18); + if (priceX18 <= currentPriceX18) { + return (0, 0); + } + } + + int256 k = int256(base) * quote; + int128 baseAtPrice = int128( + (MathHelper.sqrt256(k) * 1e9) / MathHelper.sqrt(priceX18) + ); + // base -> base + amountSwap + + int128 baseSwapped; + + if ( + (amountSwap > 0 && base + amountSwap > baseAtPrice) || + (amountSwap < 0 && base + amountSwap < baseAtPrice) + ) { + // we hit price limits before we exhaust amountSwap + if (baseAtPrice >= base) { + baseSwapped = MathHelper.floor( + baseAtPrice - base, + sizeIncrement + ); + } else { + baseSwapped = MathHelper.ceil( + baseAtPrice - base, + sizeIncrement + ); + } + } else { + // just swap it all + // amountSwap is already guaranteed to adhere to sizeIncrement + baseSwapped = amountSwap; + } + + int128 quoteSwapped = int128(k / (base + baseSwapped) - quote); + if (amountSwap > 0) { + quoteSwapped = quoteSwapped.mul(keepRateX18); + } else { + quoteSwapped = quoteSwapped.div(keepRateX18); + } + return (baseSwapped, quoteSwapped); + } +} diff --git a/contracts/dependencies/vertex/libraries/MathSD21x18.sol b/contracts/dependencies/vertex/libraries/MathSD21x18.sol new file mode 100644 index 00000000..2440625d --- /dev/null +++ b/contracts/dependencies/vertex/libraries/MathSD21x18.sol @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +import "../../math/PRBMathSD59x18.sol"; + +library MathSD21x18 { + using PRBMathSD59x18 for int256; + + int128 private constant ONE_X18 = 1000000000000000000; + int128 private constant MIN_X18 = -0x80000000000000000000000000000000; + int128 private constant MAX_X18 = 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF; + string private constant ERR_OVERFLOW = "OF"; + string private constant ERR_DIV_BY_ZERO = "DBZ"; + + function fromInt(int128 x) internal pure returns (int128) { + unchecked { + int256 result = int256(x) * ONE_X18; + require(result >= MIN_X18 && result <= MAX_X18, ERR_OVERFLOW); + return int128(result); + } + } + + function toInt(int128 x) internal pure returns (int128) { + unchecked { + return int128(x / ONE_X18); + } + } + + function add(int128 x, int128 y) internal pure returns (int128) { + unchecked { + int256 result = int256(x) + y; + require(result >= MIN_X18 && result <= MAX_X18, ERR_OVERFLOW); + return int128(result); + } + } + + function sub(int128 x, int128 y) internal pure returns (int128) { + unchecked { + int256 result = int256(x) - y; + require(result >= MIN_X18 && result <= MAX_X18, ERR_OVERFLOW); + return int128(result); + } + } + + function mul(int128 x, int128 y) internal pure returns (int128) { + unchecked { + int256 result = (int256(x) * y) / ONE_X18; + require(result >= MIN_X18 && result <= MAX_X18, ERR_OVERFLOW); + return int128(result); + } + } + + function div(int128 x, int128 y) internal pure returns (int128) { + unchecked { + require(y != 0, ERR_DIV_BY_ZERO); + int256 result = (int256(x) * ONE_X18) / y; + require(result >= MIN_X18 && result <= MAX_X18, ERR_OVERFLOW); + return int128(result); + } + } + + function abs(int128 x) internal pure returns (int128) { + unchecked { + require(x != MIN_X18, ERR_OVERFLOW); + return x < 0 ? -x : x; + } + } + + function sqrt(int128 x) internal pure returns (int128) { + unchecked { + int256 result = int256(x).sqrt(); + require(result >= MIN_X18 && result <= MAX_X18, ERR_OVERFLOW); + return int128(result); + } + } + + // note that y is not X18 + function pow(int128 x, int128 y) internal pure returns (int128) { + unchecked { + require(y >= 0, ERR_OVERFLOW); + int128 result = ONE_X18; + for (int128 i = 1; i <= y; i *= 2) { + if (i & y != 0) { + result = mul(result, x); + } + x = mul(x, x); + } + return result; + } + } +} diff --git a/contracts/dependencies/vertex/libraries/RiskHelper.sol b/contracts/dependencies/vertex/libraries/RiskHelper.sol new file mode 100644 index 00000000..14b1fb76 --- /dev/null +++ b/contracts/dependencies/vertex/libraries/RiskHelper.sol @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; +import "./MathSD21x18.sol"; +import "../interfaces/engine/IProductEngine.sol"; +import "../common/Constants.sol"; +import "./MathHelper.sol"; + +/// @title RiskHelper +/// @dev Provides basic math functions +library RiskHelper { + using MathSD21x18 for int128; + + struct Risk { + int128 longWeightInitialX18; + int128 shortWeightInitialX18; + int128 longWeightMaintenanceX18; + int128 shortWeightMaintenanceX18; + int128 largePositionPenaltyX18; + } + + function _getSpreadPenaltyX18( + Risk memory spotRisk, + Risk memory perpRisk, + int128 amount, + IProductEngine.HealthType healthType + ) internal pure returns (int128 spreadPenaltyX18) { + if (amount >= 0) { + spreadPenaltyX18 = + (ONE - _getWeightX18(perpRisk, amount, healthType)) / + 5; + } else { + spreadPenaltyX18 = + (_getWeightX18(spotRisk, amount, healthType) - ONE) / + 5; + } + } + + function _getWeightX18( + Risk memory risk, + int128 amount, + IProductEngine.HealthType healthType + ) internal pure returns (int128) { + // (1 + imf * sqrt(amount)) + if (healthType == IProductEngine.HealthType.PNL) { + return ONE; + } + + int128 weight; + if (amount >= 0) { + weight = healthType == IProductEngine.HealthType.INITIAL + ? risk.longWeightInitialX18 + : risk.longWeightMaintenanceX18; + } else { + weight = healthType == IProductEngine.HealthType.INITIAL + ? risk.shortWeightInitialX18 + : risk.shortWeightMaintenanceX18; + } + + if (risk.largePositionPenaltyX18 > 0) { + if (amount > 0) { + // 1.1 / (1 + imf * sqrt(amount)) + int128 threshold_sqrt = (int128(11e17).div(weight) - ONE).div( + risk.largePositionPenaltyX18 + ); + if (amount.abs() > threshold_sqrt.mul(threshold_sqrt)) { + weight = int128(11e17).div( + ONE + + risk.largePositionPenaltyX18.mul( + amount.abs().sqrt() + ) + ); + } + } else { + // 0.9 * (1 + imf * sqrt(amount)) + int128 threshold_sqrt = (weight.div(int128(9e17)) - ONE).div( + risk.largePositionPenaltyX18 + ); + if (amount.abs() > threshold_sqrt.mul(threshold_sqrt)) { + weight = int128(9e17).mul( + ONE + + risk.largePositionPenaltyX18.mul( + amount.abs().sqrt() + ) + ); + } + } + } + + return weight; + } +} diff --git a/contracts/dependencies/vertex/util/ArbGasInfo.sol b/contracts/dependencies/vertex/util/ArbGasInfo.sol new file mode 100644 index 00000000..1916ce09 --- /dev/null +++ b/contracts/dependencies/vertex/util/ArbGasInfo.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +contract ArbGasInfo { + function getL1BaseFeeEstimate() public pure returns (uint256) { + return 0; + } + + function getPricesInWei() + public + pure + returns ( + uint256, + uint256, + uint256, + uint256, + uint256, + uint256 + ) + { + return (0, 0, 0, 0, 0, 0); + } +} diff --git a/contracts/dependencies/vertex/util/MockERC20.sol b/contracts/dependencies/vertex/util/MockERC20.sol new file mode 100644 index 00000000..6e7189b5 --- /dev/null +++ b/contracts/dependencies/vertex/util/MockERC20.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +contract MockERC20 is ERC20 { + uint256 private constant INITIAL_SUPPLY = 100 ether; + uint8 private _decimals; + + constructor( + string memory name_, + string memory symbol_, + uint8 decimals_ + ) ERC20(name_, symbol_) { + _mint(msg.sender, INITIAL_SUPPLY); + _decimals = decimals_; + } + + /// @dev Unpermissioned minting for testing + function mint(address account, uint256 amount) external { + require(amount < 100 ether, "MockERC20: amount too large"); + _mint(account, amount); + } + + function decimals() public view virtual override returns (uint8) { + return _decimals; + } +} diff --git a/helpers/contracts-deployments.ts b/helpers/contracts-deployments.ts index b825dbb2..491ff4de 100644 --- a/helpers/contracts-deployments.ts +++ b/helpers/contracts-deployments.ts @@ -156,6 +156,15 @@ import { PoolAAPositionMover__factory, PoolBorrowAndStake__factory, PoolBorrowAndStake, + ClearinghouseLiq, + FeeCalculator, + FQuerier, + OffchainBook, + Clearinghouse, + SpotEngine, + PerpEngine, + Endpoint, + MockSanctionsList, } from "../types"; import { getACLManager, @@ -173,6 +182,11 @@ import { getPunks, getTimeLockProxy, getUniswapV3SwapRouter, + getVertexClearinghouse, + getVertexEndpoint, + getVertexOffchainBook, + getVertexPerpEngine, + getVertexSpotEngine, getWETH, } from "./contracts-getters"; import { @@ -3265,6 +3279,393 @@ export const deployAccountRegistry = async ( verify ) as Promise; +export const deployVertexOffchainBookWithoutInitializing = async ( + admin: tEthereumAddress, + verify?: boolean +) => { + const proxyInstance = await getInitializableAdminUpgradeabilityProxy( + ( + await deployVertexOffchainBookProxy(verify) + ).address + ); + + const impl = (await withSaveAndVerify( + await getContractFactory("OffchainBook"), + eContractid.VertexOffchainBookImpl, + [], + verify + )) as OffchainBook; + + await waitForTx( + await proxyInstance["initialize(address,address,bytes)"]( + impl.address, + admin, + "0x", + GLOBAL_OVERRIDES + ) + ); + + return getVertexOffchainBook(proxyInstance.address); +}; + +export const deployVertexOffchainBookProxy = async (verify?: boolean) => { + const proxyInstance = await withSaveAndVerify( + await getContractFactory("InitializableAdminUpgradeabilityProxy"), + eContractid.VertexOffchainBookProxy, + [], + verify + ); + + return getVertexClearinghouse(proxyInstance.address); +}; + +export const deployVertexOffchainBookImplAndAssignItToProxy = async ( + args: [ + tEthereumAddress, + tEthereumAddress, + tEthereumAddress, + tEthereumAddress, + tEthereumAddress, + string, + string, + string, + string, + string + ], + admin: tEthereumAddress, + verify?: boolean +) => { + const impl = (await withSaveAndVerify( + await getContractFactory("OffchainBook"), + eContractid.VertexOffchainBookImpl, + [], + verify + )) as OffchainBook; + + const initData = impl.interface.encodeFunctionData("initialize", [...args]); + + const proxyInstance = await getInitializableAdminUpgradeabilityProxy( + ( + await getVertexOffchainBook() + ).address + ); + + await waitForTx( + await proxyInstance["initialize(address,address,bytes)"]( + impl.address, + admin, + initData, + GLOBAL_OVERRIDES + ) + ); + + return getVertexOffchainBook(proxyInstance.address); +}; + +export const deployVertexClearinghouseLiq = async (verify?: boolean) => + withSaveAndVerify( + await getContractFactory("ClearinghouseLiq"), + eContractid.VertexClearinghouseLiq, + [], + verify + ) as Promise; + +export const deployVertexClearinghouseProxy = async (verify?: boolean) => { + const proxyInstance = await withSaveAndVerify( + await getContractFactory("InitializableAdminUpgradeabilityProxy"), + eContractid.VertexClearinghouseProxy, + [], + verify + ); + + return getVertexClearinghouse(proxyInstance.address); +}; + +export const deployVertexClearinghouseImplAndAssignItToProxy = async ( + args: [ + tEthereumAddress, + tEthereumAddress, + tEthereumAddress, + tEthereumAddress + ], + admin: tEthereumAddress, + verify?: boolean +) => { + const impl = (await withSaveAndVerify( + await getContractFactory("Clearinghouse"), + eContractid.VertexClearinghouseImpl, + [], + verify + )) as Clearinghouse; + + const initData = impl.interface.encodeFunctionData("initialize", [...args]); + + const proxyInstance = await getInitializableAdminUpgradeabilityProxy( + ( + await getVertexClearinghouse() + ).address + ); + + await waitForTx( + await proxyInstance["initialize(address,address,bytes)"]( + impl.address, + admin, + initData, + GLOBAL_OVERRIDES + ) + ); + + return getVertexClearinghouse(proxyInstance.address); +}; + +export const deployVertexClearinghouse = async ( + args: [ + tEthereumAddress, + tEthereumAddress, + tEthereumAddress, + tEthereumAddress + ], + verify?: boolean +) => + withSaveAndVerify( + await getContractFactory("Clearinghouse"), + eContractid.VertexClearinghouseImpl, + [...args], + verify + ) as Promise; + +export const deployVertexSpotEngineWithoutInitializing = async ( + admin: tEthereumAddress, + verify?: boolean +) => { + const proxyInstance = await withSaveAndVerify( + await getContractFactory("InitializableAdminUpgradeabilityProxy"), + eContractid.VertexSpotEngineProxy, + [], + verify + ); + + const impl = (await withSaveAndVerify( + await getContractFactory("SpotEngine"), + eContractid.VertexSpotEngineImpl, + [], + verify + )) as SpotEngine; + + await waitForTx( + await proxyInstance["initialize(address,address,bytes)"]( + impl.address, + admin, + "0x", + GLOBAL_OVERRIDES + ) + ); + + return getVertexSpotEngine(proxyInstance.address); +}; + +export const deployVertexSpotEngineProxy = async (verify?: boolean) => { + const proxyInstance = await withSaveAndVerify( + await getContractFactory("InitializableAdminUpgradeabilityProxy"), + eContractid.VertexSpotEngineProxy, + [], + verify + ); + + return getVertexSpotEngine(proxyInstance.address); +}; + +export const deployVertexSpotEngineImplAndAssignItToProxy = async ( + args: [ + tEthereumAddress, + tEthereumAddress, + tEthereumAddress, + tEthereumAddress, + tEthereumAddress + ], + admin: tEthereumAddress, + verify?: boolean +) => { + const impl = (await withSaveAndVerify( + await getContractFactory("SpotEngine"), + eContractid.VertexSpotEngineImpl, + [], + verify + )) as SpotEngine; + + const initData = impl.interface.encodeFunctionData("initialize", [...args]); + + const proxyInstance = await getInitializableAdminUpgradeabilityProxy( + ( + await getVertexSpotEngine() + ).address + ); + + await waitForTx( + await proxyInstance["initialize(address,address,bytes)"]( + impl.address, + admin, + initData, + GLOBAL_OVERRIDES + ) + ); + + return getVertexSpotEngine(proxyInstance.address); +}; + +export const deployVertexPerpEngineWithoutInitializing = async ( + admin: tEthereumAddress, + verify?: boolean +) => { + const proxyInstance = await withSaveAndVerify( + await getContractFactory("InitializableAdminUpgradeabilityProxy"), + eContractid.VertexPerpEngineProxy, + [], + verify + ); + + const impl = (await withSaveAndVerify( + await getContractFactory("PerpEngine"), + eContractid.VertexPerpEngineImpl, + [], + verify + )) as PerpEngine; + + await waitForTx( + await proxyInstance["initialize(address,address,bytes)"]( + impl.address, + admin, + "0x", + GLOBAL_OVERRIDES + ) + ); + + return getVertexPerpEngine(proxyInstance.address); +}; + +export const deployVertexPerpEngineProxy = async (verify?: boolean) => { + const proxyInstance = await withSaveAndVerify( + await getContractFactory("InitializableAdminUpgradeabilityProxy"), + eContractid.VertexPerpEngineProxy, + [], + verify + ); + + return getVertexPerpEngine(proxyInstance.address); +}; + +export const deployVertexPerpEngineImplAndAssignItToProxy = async ( + args: [ + tEthereumAddress, + tEthereumAddress, + tEthereumAddress, + tEthereumAddress, + tEthereumAddress + ], + admin: tEthereumAddress, + verify?: boolean +) => { + const impl = (await withSaveAndVerify( + await getContractFactory("PerpEngine"), + eContractid.VertexPerpEngineImpl, + [], + verify + )) as PerpEngine; + + const initData = impl.interface.encodeFunctionData("initialize", [...args]); + + const proxyInstance = await getInitializableAdminUpgradeabilityProxy( + ( + await getVertexPerpEngine() + ).address + ); + + await waitForTx( + await proxyInstance["initialize(address,address,bytes)"]( + impl.address, + admin, + initData, + GLOBAL_OVERRIDES + ) + ); + + return getVertexPerpEngine(proxyInstance.address); +}; + +export const deployVertexFQuerier = async (verify?: boolean) => + withSaveAndVerify( + await getContractFactory("FQuerier"), + eContractid.VertexFQuerier, + [], + verify + ) as Promise; + +export const deployVertexFeeCalculator = async (verify?: boolean) => + withSaveAndVerify( + await getContractFactory("FeeCalculator"), + eContractid.VertexFeeCalculator, + [], + verify + ) as Promise; + +export const deployVertexEndpointProxy = async (verify?: boolean) => { + const proxyInstance = await withSaveAndVerify( + await getContractFactory("InitializableAdminUpgradeabilityProxy"), + eContractid.VertexEndpointProxy, + [], + verify + ); + + return getVertexEndpoint(proxyInstance.address); +}; + +export const deployVertexEndpointImplAndAssignItToProxy = async ( + args: [ + tEthereumAddress, + tEthereumAddress, + tEthereumAddress, + string, + string, + string[] + ], + admin: tEthereumAddress, + verify?: boolean +) => { + const impl = (await withSaveAndVerify( + await getContractFactory("Endpoint"), + eContractid.VertexEndpointImpl, + [], + verify + )) as Endpoint; + + const initData = impl.interface.encodeFunctionData("initialize", [...args]); + + const proxyInstance = await getInitializableAdminUpgradeabilityProxy( + ( + await getVertexEndpoint() + ).address + ); + + await waitForTx( + await proxyInstance["initialize(address,address,bytes)"]( + impl.address, + admin, + initData, + GLOBAL_OVERRIDES + ) + ); + + return getVertexEndpoint(proxyInstance.address); +}; + +export const deployVertexMockSanctionsList = async (verify?: boolean) => + withSaveAndVerify( + await getContractFactory("MockSanctionsList"), + eContractid.VertexMockSanctionsList, + [], + verify + ) as Promise; + //////////////////////////////////////////////////////////////////////////////// // MOCK //////////////////////////////////////////////////////////////////////////////// diff --git a/helpers/contracts-getters.ts b/helpers/contracts-getters.ts index 693b9916..2fa580d8 100644 --- a/helpers/contracts-getters.ts +++ b/helpers/contracts-getters.ts @@ -100,6 +100,11 @@ import { Account__factory, AccountFactory__factory, AccountRegistry__factory, + Clearinghouse__factory, + Endpoint__factory, + SpotEngine__factory, + PerpEngine__factory, + OffchainBook__factory, } from "../types"; import { getEthersSigners, @@ -1348,6 +1353,61 @@ export const getAccountFactory = async (address?: tEthereumAddress) => await getFirstSigner() ); +export const getVertexClearinghouse = async (address?: tEthereumAddress) => + await Clearinghouse__factory.connect( + address || + ( + await getDb() + .get(`${eContractid.VertexClearinghouseProxy}.${DRE.network.name}`) + .value() + ).address, + await getFirstSigner() + ); + +export const getVertexEndpoint = async (address?: tEthereumAddress) => + await Endpoint__factory.connect( + address || + ( + await getDb() + .get(`${eContractid.VertexEndpointProxy}.${DRE.network.name}`) + .value() + ).address, + await getFirstSigner() + ); + +export const getVertexSpotEngine = async (address?: tEthereumAddress) => + await SpotEngine__factory.connect( + address || + ( + await getDb() + .get(`${eContractid.VertexSpotEngineProxy}.${DRE.network.name}`) + .value() + ).address, + await getFirstSigner() + ); + +export const getVertexPerpEngine = async (address?: tEthereumAddress) => + await PerpEngine__factory.connect( + address || + ( + await getDb() + .get(`${eContractid.VertexPerpEngineProxy}.${DRE.network.name}`) + .value() + ).address, + await getFirstSigner() + ); + +export const getVertexOffchainBook = async (address?: tEthereumAddress) => + await OffchainBook__factory.connect( + address || + ( + await getDb() + .get(`${eContractid.VertexOffchainBookProxy}.${DRE.network.name}`) + .value() + ).address, + await getFirstSigner() + ); + //////////////////////////////////////////////////////////////////////////////// // MOCK //////////////////////////////////////////////////////////////////////////////// diff --git a/helpers/hardhat-constants.ts b/helpers/hardhat-constants.ts index ef66a0b0..898ac42f 100644 --- a/helpers/hardhat-constants.ts +++ b/helpers/hardhat-constants.ts @@ -155,7 +155,7 @@ export const BROWSER_URLS = { }; export const DEPLOY_START = parseInt(process.env.DEPLOY_START || "0"); -export const DEPLOY_END = parseInt(process.env.DEPLOY_END || "25"); +export const DEPLOY_END = parseInt(process.env.DEPLOY_END || "26"); export const DEPLOY_INCREMENTAL = process.env.DEPLOY_INCREMENTAL == "true" ? true : false; @@ -498,5 +498,4 @@ export const XTOKEN_TYPE_UPGRADE_WHITELIST = .split(/\s?,\s?/) .map((x) => +x); export const XTOKEN_SYMBOL_UPGRADE_WHITELIST = - process.env.XTOKEN_SYMBOL_UPGRADE_WHITELIST?.trim() - .split(/\s?,\s?/); + process.env.XTOKEN_SYMBOL_UPGRADE_WHITELIST?.trim().split(/\s?,\s?/); diff --git a/helpers/types.ts b/helpers/types.ts index 5e6c1842..bca36a71 100644 --- a/helpers/types.ts +++ b/helpers/types.ts @@ -296,6 +296,20 @@ export enum eContractid { AccountFactory = "AccountFactory", AccountProxy = "AccountProxy", AccountRegistry = "AccountRegistry", + VertexClearinghouseLiq = "VertexClearinghouseLiq", + VertexClearinghouseProxy = "VertexClearinghouseProxy", + VertexClearinghouseImpl = "VertexClearinghouseImpl", + VertexEndpointImpl = "VertexEndpointImpl", + VertexEndpointProxy = "VertexEndpointProxy", + VertexSpotEngineImpl = "VertexSpotEngineImpl", + VertexSpotEngineProxy = "VertexSpotEngineProxy", + VertexPerpEngineImpl = "VertexPerpEngineImpl", + VertexPerpEngineProxy = "VertexPerpEngineProxy", + VertexOffchainBookImpl = "VertexOffchainBookImpl", + VertexOffchainBookProxy = "VertexOffchainBookProxy", + VertexFQuerier = "VertexFQuerier", + VertexFeeCalculator = "VertexFeeCalculator", + VertexMockSanctionsList = "VertexMockSanctionsList", } /* @@ -862,6 +876,37 @@ export interface IUniswapConfig { V3NFTPositionManager?: tEthereumAddress; } +export interface IVertexRiskStoreConfig { + longWeightInitial: string; + shortWeightInitial: string; + longWeightMaintenance: string; + shortWeightMaintenance: string; + largePositionPenalty: string; +} + +export interface IVertexInterestRateConfig { + token: tEthereumAddress; + interestInflectionUtilX18: string; + interestFloorX18: string; + interestSmallCapX18: string; + interestLargeCapX18: string; +} + +export interface IVertexMarketConfig { + healthGroup: string; + riskStore: IVertexRiskStoreConfig; + book: tEthereumAddress; + sizeIncrement: string; + priceIncrementX18: string; + minSize: string; + interestRateConfig: IVertexInterestRateConfig | undefined; + lpSpreadX18: string; +} + +export interface IVertexConfig { + markets; +} + export interface IBendDAOConfig { LendingPool?: tEthereumAddress; LendingPoolLoan?: tEthereumAddress; @@ -933,6 +978,7 @@ export interface ICommonConfiguration { Tokens: iMultiPoolsAssets; YogaLabs: IYogaLabs; Uniswap: IUniswapConfig; + Vertex: IVertexConfig | undefined; BendDAO: IBendDAOConfig; ParaSpaceV1: IParaSpaceV1Config | undefined; Stakefish: IStakefish; @@ -964,3 +1010,14 @@ export type Action = [ BigNumberish, // executeTime boolean // withDelegatecall ]; + +export enum EngineType { + SPOT = 0, + PERP = 1, +} + +export enum HealthType { + INITIAL = 0, + MAINTENANCE = 1, + PNL = 2, +} diff --git a/market-config/index.ts b/market-config/index.ts index 5eecaa4c..bfe905bf 100644 --- a/market-config/index.ts +++ b/market-config/index.ts @@ -105,6 +105,7 @@ export const CommonConfig: Pick< | "Governance" | "ParaSpaceV1" | "AccountAbstraction" + | "Vertex" > = { WrappedNativeTokenId: ERC20TokenContractId.WETH, MarketId: "ParaSpaceMM", @@ -141,6 +142,7 @@ export const CommonConfig: Pick< rpcUrl: `https://api.stackup.sh/v1/node/${process.env.STACKUP_KEY}`, paymasterUrl: `https://api.stackup.sh/v1/paymaster/${process.env.STACKUP_KEY}`, }, + Vertex: undefined, }; export const HardhatConfig: IParaSpaceConfiguration = { diff --git a/scripts/deployments/steps/25_vertex.ts b/scripts/deployments/steps/25_vertex.ts new file mode 100644 index 00000000..f83cad19 --- /dev/null +++ b/scripts/deployments/steps/25_vertex.ts @@ -0,0 +1,164 @@ +import {ZERO_ADDRESS} from "../../../helpers/constants"; +import { + deployVertexClearinghouseImplAndAssignItToProxy, + deployVertexClearinghouseLiq, + deployVertexClearinghouseProxy, + deployVertexEndpointImplAndAssignItToProxy, + deployVertexEndpointProxy, + deployVertexFQuerier, + deployVertexFeeCalculator, + deployVertexMockSanctionsList, + deployVertexOffchainBookWithoutInitializing, + deployVertexPerpEngineWithoutInitializing, + deployVertexSpotEngineWithoutInitializing, +} from "../../../helpers/contracts-deployments"; +import {getAllTokens, getFirstSigner} from "../../../helpers/contracts-getters"; +import {waitForTx} from "../../../helpers/misc-utils"; +import { + ERC20TokenContractId, + EngineType, + IVertexMarketConfig, +} from "../../../helpers/types"; + +export const step_25 = async (verify = false) => { + try { + const feeCalculator = await deployVertexFeeCalculator(verify); + await waitForTx(await feeCalculator.initialize()); + + const sanctions = await deployVertexMockSanctionsList(verify); + const clearinghouseLiq = await deployVertexClearinghouseLiq(verify); + const clearinghouse = await deployVertexClearinghouseProxy(verify); + const endpoint = await deployVertexEndpointProxy(verify); + const spotEngine = await deployVertexSpotEngineWithoutInitializing( + ZERO_ADDRESS, + verify + ); + const perpEngine = await deployVertexPerpEngineWithoutInitializing( + ZERO_ADDRESS, + verify + ); + + const sequencer = await getFirstSigner(); + const allTokens = await getAllTokens(); + + await deployVertexClearinghouseImplAndAssignItToProxy( + [ + endpoint.address, + allTokens[ERC20TokenContractId.USDC].address, + feeCalculator.address, + clearinghouseLiq.address, + ], + ZERO_ADDRESS, + verify + ); + await waitForTx( + await clearinghouse.addEngine(spotEngine.address, EngineType.SPOT) + ); + await waitForTx( + await clearinghouse.addEngine(perpEngine.address, EngineType.PERP) + ); + await waitForTx(await feeCalculator.migrate(clearinghouse.address)); + + await deployVertexEndpointImplAndAssignItToProxy( + [ + sanctions.address, + await sequencer.getAddress(), + clearinghouse.address, + "72000", + Math.floor(new Date().valueOf() / 1000).toString(), + [], + ], + ZERO_ADDRESS, + verify + ); + + const fquerier = await deployVertexFQuerier(verify); + await waitForTx(await fquerier.initialize(clearinghouse.address)); + + const maxHealthGroup = await clearinghouse.getMaxHealthGroup(); + const vertexConfigs: IVertexMarketConfig[] = [ + // BTC-spot + { + healthGroup: maxHealthGroup.toString(), + riskStore: { + longWeightInitial: "900000000", + shortWeightInitial: "1100000000", + longWeightMaintenance: "950000000", + shortWeightMaintenance: "1050000000", + largePositionPenalty: "0", + }, + interestRateConfig: { + token: allTokens[ERC20TokenContractId.WBTC].address, + interestInflectionUtilX18: "800000000000000000", + interestFloorX18: "10000000000000000", + interestSmallCapX18: "40000000000000000", + interestLargeCapX18: "1000000000000000000", + }, + book: ( + await deployVertexOffchainBookWithoutInitializing( + ZERO_ADDRESS, + verify + ) + ).address, + sizeIncrement: "1000000000000000", + priceIncrementX18: "1000000000000000000", + minSize: "0", + lpSpreadX18: "3000000000000000", + }, + // BTC-perp + { + healthGroup: maxHealthGroup.toString(), + riskStore: { + longWeightInitial: "900000000", + shortWeightInitial: "1100000000", + longWeightMaintenance: "950000000", + shortWeightMaintenance: "1050000000", + largePositionPenalty: "0", + }, + interestRateConfig: undefined, + book: ( + await deployVertexOffchainBookWithoutInitializing( + await sequencer.getAddress(), + verify + ) + ).address, + sizeIncrement: "1000000000000000", + priceIncrementX18: "1000000000000000000", + minSize: "0", + lpSpreadX18: "3000000000000000", + }, + ]; + + for (const config of vertexConfigs) { + if (config.interestRateConfig) { + await waitForTx( + await spotEngine.addProduct( + config.healthGroup, + config.book, + config.sizeIncrement, + config.priceIncrementX18, + config.minSize, + config.lpSpreadX18, + config.interestRateConfig, + config.riskStore + ) + ); + } else { + await waitForTx( + await perpEngine.addProduct( + config.healthGroup, + config.book, + config.sizeIncrement, + config.priceIncrementX18, + config.minSize, + config.lpSpreadX18, + config.riskStore + ) + ); + } + } + } catch (error) { + console.error(error); + process.exit(1); + } +}; diff --git a/scripts/deployments/steps/index.ts b/scripts/deployments/steps/index.ts index 15fdde3c..0459952e 100644 --- a/scripts/deployments/steps/index.ts +++ b/scripts/deployments/steps/index.ts @@ -24,6 +24,7 @@ export const getAllSteps = async () => { const {step_22} = await import("./22_timelock"); const {step_23} = await import("./23_renounceOwnership"); const {step_24} = await import("./24_accountAbstraction"); + const {step_25} = await import("./25_vertex"); return [ step_00, @@ -51,5 +52,6 @@ export const getAllSteps = async () => { step_22, step_23, step_24, + step_25, ]; }; diff --git a/scripts/upgrade/ntoken.ts b/scripts/upgrade/ntoken.ts index ba3ca36f..21713a50 100644 --- a/scripts/upgrade/ntoken.ts +++ b/scripts/upgrade/ntoken.ts @@ -76,12 +76,14 @@ export const upgradeNToken = async (verify = false) => { continue; } - if (XTOKEN_SYMBOL_UPGRADE_WHITELIST && !XTOKEN_SYMBOL_UPGRADE_WHITELIST.includes(symbol)) { + if ( + XTOKEN_SYMBOL_UPGRADE_WHITELIST && + !XTOKEN_SYMBOL_UPGRADE_WHITELIST.includes(symbol) + ) { console.log(symbol + "not in XTOKEN_SYMBOL_UPGRADE_WHITELIST, skip..."); continue; } - if (xTokenType == XTokenType.NTokenBAYC) { if (!nTokenBAYCImplementationAddress) { console.log("deploy NTokenBAYC implementation");