From bed56c2dcd689caade5e5dfe049b0195500a85a4 Mon Sep 17 00:00:00 2001 From: Flocqst Date: Thu, 2 May 2024 16:20:39 +0200 Subject: [PATCH 01/16] =?UTF-8?q?=F0=9F=91=B7=20Add=20orderFlowFeeImposed?= =?UTF-8?q?=20event?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Account.sol | 4 ++-- src/Events.sol | 9 ++++++++- src/interfaces/IEvents.sol | 7 +++++-- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/Account.sol b/src/Account.sol index b557ef01..3678576d 100644 --- a/src/Account.sol +++ b/src/Account.sol @@ -635,7 +635,7 @@ contract Account is IAccount, Auth, OpsReady { MARGIN_ASSET.transfer(SETTINGS.TREASURY(), fee); } - /// @custom:todo add event emission for order flow fee imposed + EVENTS.emitOrderFlowFeeImposed({amount: fee}); } /// @notice calculate order flow fee for a given market and size delta @@ -886,7 +886,7 @@ contract Account is IAccount, Auth, OpsReady { execAddress: address(this), execData: abi.encodeCall( this.executeConditionalOrder, conditionalOrderId - ), + ), moduleData: moduleData, feeToken: ETH }); diff --git a/src/Events.sol b/src/Events.sol index 9a05f3c2..fa3b39c3 100644 --- a/src/Events.sol +++ b/src/Events.sol @@ -171,5 +171,12 @@ contract Events is IEvents { emit DelegatedAccountRemoved({caller: caller, delegate: delegate}); } - /// @custom:todo add event for order flow fee imposed + /// @inheritdoc IEvents + function emitOrderFlowFeeImposed(uint256 amount) + external + override + onlyAccounts + { + emit OrderFlowFeeImposed({account: msg.sender, amount: amount}); + } } diff --git a/src/interfaces/IEvents.sol b/src/interfaces/IEvents.sol index 1a91f6eb..3a0c87b0 100644 --- a/src/interfaces/IEvents.sol +++ b/src/interfaces/IEvents.sol @@ -178,6 +178,9 @@ interface IEvents { address indexed caller, address indexed delegate ); - /// @custom:todo add event function for order flow fee imposed - /// @custom:todo add event for order flow fee imposed + /// @notice emitted after order flow fee is imposed + /// @param amount: amount of the imposed order flow fee + function emitOrderFlowFeeImposed(uint256 amount) external; + + event OrderFlowFeeImposed(address indexed account, uint256 amount); } From 62d429e449830e5d2b9e78fac92fdb2f8096842c Mon Sep 17 00:00:00 2001 From: Flocqst Date: Thu, 2 May 2024 16:21:59 +0200 Subject: [PATCH 02/16] =?UTF-8?q?=E2=9C=85=20Add=20emitOrderFlowFeeImposed?= =?UTF-8?q?=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/unit/Events.t.sol | 12 +++++++++++- test/utils/ConsolidatedEvents.sol | 2 ++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/test/unit/Events.t.sol b/test/unit/Events.t.sol index 4e8f0d2c..ee4a2ee1 100644 --- a/test/unit/Events.t.sol +++ b/test/unit/Events.t.sol @@ -305,5 +305,15 @@ contract EventsTest is Test, ConsolidatedEvents { }); } - /// @custom:todo test event for order flow fee imposed as done above + function test_emitOrderFlowFeeImposed_Event() public { + vm.expectEmit(true, true, true, true); + emit OrderFlowFeeImposed(address(account), AMOUNT); + vm.prank(account); + events.emitOrderFlowFeeImposed({amount: AMOUNT}); + } + + function test_emitOrderFlowFeeImposed_OnlyAccounts() public { + vm.expectRevert(abi.encodeWithSelector(IEvents.OnlyAccounts.selector)); + events.emitOrderFlowFeeImposed({amount: AMOUNT}); + } } diff --git a/test/utils/ConsolidatedEvents.sol b/test/utils/ConsolidatedEvents.sol index 8846cb75..e3a4a488 100644 --- a/test/utils/ConsolidatedEvents.sol +++ b/test/utils/ConsolidatedEvents.sol @@ -85,6 +85,8 @@ contract ConsolidatedEvents { IAccount.PriceOracleUsed priceOracle ); + event OrderFlowFeeImposed(address indexed account, uint256 amount); + /*////////////////////////////////////////////////////////////// ISETTINGS //////////////////////////////////////////////////////////////*/ From 2193fc82d9f0ecc6e72e2f10e3cb2f1b2b7bb70f Mon Sep 17 00:00:00 2001 From: Flocqst Date: Thu, 2 May 2024 16:22:37 +0200 Subject: [PATCH 03/16] =?UTF-8?q?=E2=9C=85=20Add=20setOrderFlowFee=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/unit/Settings.t.sol | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/test/unit/Settings.t.sol b/test/unit/Settings.t.sol index c4711f0e..896112c9 100644 --- a/test/unit/Settings.t.sol +++ b/test/unit/Settings.t.sol @@ -5,6 +5,8 @@ import {Test} from "lib/forge-std/src/Test.sol"; import {Settings} from "src/Settings.sol"; +import {ISettings} from "src/interfaces/ISettings.sol"; + import {ConsolidatedEvents} from "test/utils/ConsolidatedEvents.sol"; import {MARGIN_ASSET, USER} from "test/utils/Constants.sol"; @@ -116,18 +118,26 @@ contract SettingsTest is Test, ConsolidatedEvents { //////////////////////////////////////////////////////////////*/ function test_setOrderFlowFee(uint256 fee) public { - /// @custom:todo + if (fee > 100_000) { + vm.expectRevert( + abi.encodeWithSelector(ISettings.InvalidOrderFlowFee.selector) + ); + settings.setOrderFlowFee(fee); + } else { + settings.setOrderFlowFee(fee); + assertEq(settings.orderFlowFee(), fee); + } } function test_setOrderFlowFee_OnlyOwner() public { - /// @custom:todo + vm.expectRevert("UNAUTHORIZED"); + vm.prank(USER); + settings.setExecutorFee(5); } function test_setOrderFlowFee_Event() public { - /// @custom:todo - } - - function test_setOrderFlowFee_InvalidOrderFlowFee() public { - /// @custom:todo + vm.expectEmit(true, true, true, true); + emit ExecutorFeeSet(5); + settings.setExecutorFee(5); } } From e57841a890047ea523c91aca303568f8f476a5c2 Mon Sep 17 00:00:00 2001 From: Flocqst Date: Fri, 3 May 2024 01:57:49 +0200 Subject: [PATCH 04/16] =?UTF-8?q?=F0=9F=91=B7=20Add=20delayed=20order=20or?= =?UTF-8?q?derFlowFee=20calculation=20logic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Account.sol | 52 ++++++++++++++++++++++++++++++------- src/interfaces/IAccount.sol | 5 ++-- 2 files changed, 45 insertions(+), 12 deletions(-) diff --git a/src/Account.sol b/src/Account.sol index 3678576d..0014e593 100644 --- a/src/Account.sol +++ b/src/Account.sol @@ -215,9 +215,12 @@ contract Account is IAccount, Auth, OpsReady { public view override - returns (uint256) + returns (uint256 sUSDmarketRate, uint256 orderFlowFee) { - return _calculateOrderFlowFee(_market, _sizeDelta); + // fetch current sUSD exchange rate for market + (sUSDmarketRate,) = _sUSDRate(IPerpsV2MarketConsolidated(_market)); + + orderFlowFee = _calculateOrderFlowFee(_market, _sizeDelta); } /*////////////////////////////////////////////////////////////// @@ -608,14 +611,16 @@ contract Account is IAccount, Auth, OpsReady { /// @notice impose an order flow fee on the account /// @param _market: address of market /// @param _sizeDelta: size delta of order + /// @param _desiredFillPrice: desiredFillPrice if this is a delayed order, 0 otherwise /// @dev will attempt to deduct fee from account's idle margin first /// @dev if fee exceeds idle margin, fee will be deducted from market's margin /// if possible. If not possible, the transaction will revert. /// @dev if fee is deducted from market's margin, then the following order /// may be rejected if the account has insufficient available market margin - function _imposeOrderFlowFee(address _market, int256 _sizeDelta) internal { + /// @dev _desiredFillPrice is used to calculate orderFlowFee with correct price for Delayed orders + function _imposeOrderFlowFee(address _market, int256 _sizeDelta, uint256 _desiredFillPrice) internal { // calculate order flow fee - uint256 fee = _calculateOrderFlowFee(_market, _sizeDelta); + uint256 fee = _calculateOrderFlowFee(_market, _sizeDelta, _desiredFillPrice); if (fee != 0) { uint256 idleMargin = freeMargin(); @@ -638,11 +643,25 @@ contract Account is IAccount, Auth, OpsReady { EVENTS.emitOrderFlowFeeImposed({amount: fee}); } + /// @notice impose an order flow fee on the account + /// @param _market: address of market + /// @param _sizeDelta: size delta of order + /// @dev will attempt to deduct fee from account's idle margin first + /// @dev if fee exceeds idle margin, fee will be deducted from market's margin + /// if possible. If not possible, the transaction will revert. + /// @dev if fee is deducted from market's margin, then the following order + /// may be rejected if the account has insufficient available market margin + function _imposeOrderFlowFee(address _market, int256 _sizeDelta) internal { + _imposeOrderFlowFee(_market, _sizeDelta, 0); + } + /// @notice calculate order flow fee for a given market and size delta /// @param _market: address of market /// @param _sizeDelta: size delta of order + /// @param _desiredFillPrice desiredFillPrice in case of a delayed Order /// @return fee: order flow fee to impose - function _calculateOrderFlowFee(address _market, int256 _sizeDelta) + /// @dev _desiredFillPrice is used to calculate orderFlowFee for Delayed Orders + function _calculateOrderFlowFee(address _market, int256 _sizeDelta, uint256 _desiredFillPrice) internal view returns (uint256) @@ -654,13 +673,27 @@ contract Account is IAccount, Auth, OpsReady { IPerpsV2MarketConsolidated market = IPerpsV2MarketConsolidated(_market); (uint256 price,) = _sUSDRate(market); + uint256 usedPrice = (_desiredFillPrice != 0) ? _desiredFillPrice : price; + // calculate notional value of order - uint256 notionalValue = _abs(_sizeDelta) * price; + uint256 notionalValue = _abs(_sizeDelta) * usedPrice; // calculate fee to impose return notionalValue * orderFlowFee / SETTINGS.MAX_ORDER_FLOW_FEE(); } + /// @notice calculate order flow fee for a given market and size delta + /// @param _market: address of market + /// @param _sizeDelta: size delta of order + /// @return fee: order flow fee to impose + function _calculateOrderFlowFee(address _market, int256 _sizeDelta) + internal + view + returns (uint256) + { + _calculateOrderFlowFee(_market, _sizeDelta, 0); + } + /*////////////////////////////////////////////////////////////// ATOMIC ORDERS //////////////////////////////////////////////////////////////*/ @@ -774,8 +807,7 @@ contract Account is IAccount, Auth, OpsReady { int256 _sizeDelta, uint256 _desiredFillPrice ) internal { - /// @custom:todo use _desiredFillPrice - _imposeOrderFlowFee(_market, _sizeDelta); + _imposeOrderFlowFee(_market, _sizeDelta, _desiredFillPrice); IPerpsV2MarketConsolidated(_market) .submitOffchainDelayedOrderWithTracking({ @@ -800,12 +832,12 @@ contract Account is IAccount, Auth, OpsReady { address _market, uint256 _desiredFillPrice ) internal { - /// @custom:todo use _desiredFillPrice _imposeOrderFlowFee( _market, IPerpsV2MarketConsolidated(_market).positions({ account: address(this) - }).size + }).size, + _desiredFillPrice ); // close position (i.e. reduce size to zero) diff --git a/src/interfaces/IAccount.sol b/src/interfaces/IAccount.sol index fef1beb8..19754bfa 100644 --- a/src/interfaces/IAccount.sol +++ b/src/interfaces/IAccount.sol @@ -214,11 +214,12 @@ interface IAccount { /// @notice get the expected order flow fee for a given market and size delta /// @param _market: address of market /// @param _sizeDelta: size delta of order - /// @return order flow fee expected for the given market and size delta + /// @return sUSDmarketRate sUSD exchange rate for market + /// @return orderFlowFee order flow fee expected for the given market and size delta function getExpectedOrderFlowFee(address _market, int256 _sizeDelta) external view - returns (uint256); + returns (uint256 sUSDmarketRate, uint256 orderFlowFee); /*////////////////////////////////////////////////////////////// MUTATIVE From 400c1fd9f8c6687cfa1490ec1f28fcc09701a07b Mon Sep 17 00:00:00 2001 From: Flocqst Date: Fri, 3 May 2024 02:08:33 +0200 Subject: [PATCH 05/16] =?UTF-8?q?=F0=9F=91=B7=20Add=20OrderFlowFee=20calcu?= =?UTF-8?q?lation=20logic=20for=20Delayed=20orders?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Account.sol | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/Account.sol b/src/Account.sol index 0014e593..5dec36c1 100644 --- a/src/Account.sol +++ b/src/Account.sol @@ -682,16 +682,12 @@ contract Account is IAccount, Auth, OpsReady { return notionalValue * orderFlowFee / SETTINGS.MAX_ORDER_FLOW_FEE(); } - /// @notice calculate order flow fee for a given market and size delta - /// @param _market: address of market - /// @param _sizeDelta: size delta of order - /// @return fee: order flow fee to impose function _calculateOrderFlowFee(address _market, int256 _sizeDelta) internal view - returns (uint256) + returns (uint256 orderflowFee) { - _calculateOrderFlowFee(_market, _sizeDelta, 0); + orderflowFee = _calculateOrderFlowFee(_market, _sizeDelta, 0); } /*////////////////////////////////////////////////////////////// From 1b5284f474fd4417d69f6a4b82f591c175c3143d Mon Sep 17 00:00:00 2001 From: Flocqst Date: Fri, 3 May 2024 02:09:45 +0200 Subject: [PATCH 06/16] =?UTF-8?q?=E2=9C=A8=20forge=20format?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Account.sol | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/Account.sol b/src/Account.sol index 5dec36c1..737e7f66 100644 --- a/src/Account.sol +++ b/src/Account.sol @@ -219,7 +219,7 @@ contract Account is IAccount, Auth, OpsReady { { // fetch current sUSD exchange rate for market (sUSDmarketRate,) = _sUSDRate(IPerpsV2MarketConsolidated(_market)); - + orderFlowFee = _calculateOrderFlowFee(_market, _sizeDelta); } @@ -618,9 +618,14 @@ contract Account is IAccount, Auth, OpsReady { /// @dev if fee is deducted from market's margin, then the following order /// may be rejected if the account has insufficient available market margin /// @dev _desiredFillPrice is used to calculate orderFlowFee with correct price for Delayed orders - function _imposeOrderFlowFee(address _market, int256 _sizeDelta, uint256 _desiredFillPrice) internal { + function _imposeOrderFlowFee( + address _market, + int256 _sizeDelta, + uint256 _desiredFillPrice + ) internal { // calculate order flow fee - uint256 fee = _calculateOrderFlowFee(_market, _sizeDelta, _desiredFillPrice); + uint256 fee = + _calculateOrderFlowFee(_market, _sizeDelta, _desiredFillPrice); if (fee != 0) { uint256 idleMargin = freeMargin(); @@ -661,11 +666,11 @@ contract Account is IAccount, Auth, OpsReady { /// @param _desiredFillPrice desiredFillPrice in case of a delayed Order /// @return fee: order flow fee to impose /// @dev _desiredFillPrice is used to calculate orderFlowFee for Delayed Orders - function _calculateOrderFlowFee(address _market, int256 _sizeDelta, uint256 _desiredFillPrice) - internal - view - returns (uint256) - { + function _calculateOrderFlowFee( + address _market, + int256 _sizeDelta, + uint256 _desiredFillPrice + ) internal view returns (uint256) { // fetch order flow fee from settings uint256 orderFlowFee = SETTINGS.orderFlowFee(); From 7e8812b5bfaa48bd5de5b520ff54751fe953d7ba Mon Sep 17 00:00:00 2001 From: Flocqst Date: Fri, 3 May 2024 02:10:33 +0200 Subject: [PATCH 07/16] =?UTF-8?q?=E2=9C=85=20Add=20tests=20for=20imposeOrd?= =?UTF-8?q?erFlowFee?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/integration/orderFlowFee.behavior.t.sol | 230 ++++++++++++++++--- 1 file changed, 200 insertions(+), 30 deletions(-) diff --git a/test/integration/orderFlowFee.behavior.t.sol b/test/integration/orderFlowFee.behavior.t.sol index 278c11f2..b1b3f5da 100644 --- a/test/integration/orderFlowFee.behavior.t.sol +++ b/test/integration/orderFlowFee.behavior.t.sol @@ -12,15 +12,23 @@ import {IERC20} from "src/interfaces/token/IERC20.sol"; import {AccountExposed} from "test/utils/AccountExposed.sol"; import {ConsolidatedEvents} from "test/utils/ConsolidatedEvents.sol"; import {IAddressResolver} from "test/utils/interfaces/IAddressResolver.sol"; +import {ISynth} from "test/utils/interfaces/ISynth.sol"; +import {IFuturesMarketManager} from + "src/interfaces/synthetix/IFuturesMarketManager.sol"; +import {IPerpsV2MarketConsolidated} from "src/interfaces/IAccount.sol"; +import {IPerpsV2ExchangeRate} from + "src/interfaces/synthetix/IPerpsV2ExchangeRate.sol"; import { ADDRESS_RESOLVER, + AMOUNT, BLOCK_NUMBER, FUTURES_MARKET_MANAGER, GELATO, OPS, PERPS_V2_EXCHANGE_RATE, PROXY_SUSD, + sETHPERP, SYSTEM_STATUS, UNISWAP_PERMIT2, UNISWAP_UNIVERSAL_ROUTER @@ -90,7 +98,7 @@ contract OrderFlowFeeTest is Test, ConsolidatedEvents { systemStatus, GELATO, OPS, - address(settings), + address(0), UNISWAP_UNIVERSAL_ROUTER, UNISWAP_PERMIT2 ); @@ -107,57 +115,219 @@ contract OrderFlowFeeTest is Test, ConsolidatedEvents { TESTS //////////////////////////////////////////////////////////////*/ - /// @custom:todo use atomic order flow to simplify testing + /// Verifies that Order Flow Fee is correctly calculated + /// For this test, it is assumed that there is enough account margin to cover fee function test_calculateOrderFlowFee(uint256 fee) public { vm.assume(fee < settings.MAX_ORDER_FLOW_FEE()); settings.setOrderFlowFee(fee); - /// @custom:todo test the following: - /// 1. what happens if {orderFlowFee} is zero - /// 2. what happens if {orderFlowFee} is non-zero but fee is zero - /// 3. what happens if {orderFlowFee} is non-zero and fee is non-zero - /// 4. can division by zero occur - /// 5. is division completely safe - /// - /// use public Account.getExpectedOrderFlowFee() to calculate the expected fee - /// - /// use fuzzing + fundAccount(AMOUNT); + + // create a long position in the ETH market + address market = getMarketAddressFromKey(sETHPERP); + + /// Keep account margin to cover for orderFlowFee + int256 marginDelta = int256(AMOUNT) / 5; + int256 sizeDelta = 1; + + (uint256 desiredFillPrice,) = + IPerpsV2MarketConsolidated(market).assetPrice(); + + submitAtomicOrder(sETHPERP, marginDelta, sizeDelta, desiredFillPrice); + + (uint256 sUSDmarketRate, uint256 imposedOrderFlowFee) = + account.getExpectedOrderFlowFee(market, sizeDelta); + + if (fee == 0) { + assertEq(imposedOrderFlowFee, 0); + } else { + uint256 orderFlowFeeMath = abs(sizeDelta) * sUSDmarketRate + * settings.orderFlowFee() / settings.MAX_ORDER_FLOW_FEE(); + assertEq(orderFlowFeeMath, imposedOrderFlowFee); + } } - /// @custom:todo use atomic order flow to simplify testing + /// Verifies that OrderFlowFee is correctly sent from account margin when there is enough funds to cover orderFlowFee function test_imposeOrderFlowFee_account_margin() public { - /// @custom:todo test the following assuming the account - /// has sufficient margin: - /// 1. is the fee sent to the correct address - /// 2. can this be gameable at all + fundAccount(AMOUNT); + // create a long position in the ETH market + address market = getMarketAddressFromKey(sETHPERP); + + /// Keep account margin to cover for orderFlowFee + int256 marginDelta = int256(AMOUNT) / 5; + int256 sizeDelta = 1; + + (uint256 desiredFillPrice,) = + IPerpsV2MarketConsolidated(market).assetPrice(); + + submitAtomicOrder(sETHPERP, marginDelta, sizeDelta, desiredFillPrice); + + /// inital funding - sETHPERP margin = 8000 ether + uint256 accountMarginBeforeFee = AMOUNT - uint256(marginDelta); + + (, uint256 imposedFee) = + account.getExpectedOrderFlowFee(market, sizeDelta); + + assertEq(accountMarginBeforeFee - imposedFee, account.freeMargin()); } - /// @custom:todo use atomic order flow to simplify testing + /// Verifies that OrderFlowFee is correctly sent from market margin when there is no funds to cover orderFlowFee in account margin function test_imposeOrderFlowFee_market_margin() public { - /// @custom:todo test the following assuming the account - /// has insufficient margin but the market has sufficient margin: - /// 1. is the fee sent to the correct address - /// 2. can this be gameable at all - /// 3. is only what is necessary taken from the market + fundAccount(AMOUNT); + + // create a long position in the ETH market + address market = getMarketAddressFromKey(sETHPERP); + + /// Deposit all margin so that account has no margin to cover orderFlowFee + int256 marginDelta = int256(AMOUNT); + int256 sizeDelta = 1; + + (uint256 desiredFillPrice,) = + IPerpsV2MarketConsolidated(market).assetPrice(); + + submitAtomicOrder(sETHPERP, marginDelta, sizeDelta, desiredFillPrice); + + IPerpsV2MarketConsolidated.Position memory position = + account.getPosition(sETHPERP); + + (, uint256 imposedFee) = + account.getExpectedOrderFlowFee(market, position.size); + + assertEq(uint256(position.margin), AMOUNT - imposedFee - 563); + } + + /// Verifies that OrderFlowFee is correctly sent from both market margin and account margin when there is not enough funds to cover orderFlowFee in account margin + function test_imposeOrderFlowFee_both_margin() public { + fundAccount(AMOUNT); + + // create a long position in the ETH market + address market = getMarketAddressFromKey(sETHPERP); + + /// Leave 10_000 in account (not enough to cover fees) + int256 marginDelta = int256(AMOUNT) - 10_000; + int256 sizeDelta = 1; + + (uint256 desiredFillPrice,) = + IPerpsV2MarketConsolidated(market).assetPrice(); + + submitAtomicOrder(sETHPERP, marginDelta, sizeDelta, desiredFillPrice); + + IPerpsV2MarketConsolidated.Position memory position = + account.getPosition(sETHPERP); + + (, uint256 imposedFee) = + account.getExpectedOrderFlowFee(market, position.size); + + // Account margin is emptied + assertEq(account.freeMargin(), 0); + + // Market margin is reduced by (imposedFee - 10 000) + assertEq( + uint256(position.margin), + uint256(marginDelta) - (imposedFee - 10_000) - 563 + ); } /// @custom:todo use atomic order flow to simplify testing + /// Synthetix makes modify position (which is called if there is not enough account margin + /// Reverts if the resulting position is too large, outside the max leverage, or is liquidating. function test_imposeOrderFlowFee_market_margin_failed() public { /// @custom:todo test the following assuming the account /// has insufficient margin and the market has insufficient margin /// (i.e., withdrawing from the market fails due to outstanding position or /// order exceeding allowed leverage if margin is taken): /// 1. error is caught for each scenario where the market margin is insufficient + // } - /// @custom:todo desired fill price behaviour? floor/ceiling price behaviour? reverts? etc - /// @custom:todo what happens if withdrawing margin from market results in leverage exceeding allowed leverage? revert? - /// @custom:todo think deeply about the edge cases - - /// @custom:todo use atomic order flow to simplify testing + /// Verifies that the correct Event is emitted with correct value function test_imposeOrderFlowFee_event() public { /// @custom:todo test the following: - /// 1. is the correct event emitted - /// 2. is the correct fee emitted with it + + fundAccount(AMOUNT); + // create a long position in the ETH market + address market = getMarketAddressFromKey(sETHPERP); + + /// Keep account margin to cover for orderFlowFee + int256 marginDelta = int256(AMOUNT) / 5; + int256 sizeDelta = 1; + + (uint256 desiredFillPrice,) = + IPerpsV2MarketConsolidated(market).assetPrice(); + + vm.expectEmit(true, true, true, true); + // orderFlowFee is 94025250000000000 in this configuration + emit OrderFlowFeeImposed(address(account), 94_025_250_000_000_000); + + submitAtomicOrder(sETHPERP, marginDelta, sizeDelta, desiredFillPrice); + } + + /*////////////////////////////////////////////////////////////// + HELPERS + //////////////////////////////////////////////////////////////*/ + + function mintSUSD(address to, uint256 amount) private { + address issuer = IAddressResolver(ADDRESS_RESOLVER).getAddress("Issuer"); + ISynth synthsUSD = + ISynth(IAddressResolver(ADDRESS_RESOLVER).getAddress("SynthsUSD")); + vm.prank(issuer); + synthsUSD.issue(to, amount); + } + + function fundAccount(uint256 amount) private { + vm.deal(address(account), 1 ether); + mintSUSD(address(this), amount); + modifyAccountMargin({amount: int256(amount)}); + } + + function getMarketAddressFromKey(bytes32 key) + private + view + returns (address market) + { + market = address( + IPerpsV2MarketConsolidated( + IFuturesMarketManager( + IAddressResolver(ADDRESS_RESOLVER).getAddress( + "FuturesMarketManager" + ) + ).marketForKey(key) + ) + ); + } + + function abs(int256 x) private pure returns (uint256 z) { + assembly { + let mask := sub(0, shr(255, x)) + z := xor(mask, add(mask, x)) + } + } + + /*////////////////////////////////////////////////////////////// + COMMAND SHORTCUTS + //////////////////////////////////////////////////////////////*/ + + function modifyAccountMargin(int256 amount) private { + IAccount.Command[] memory commands = new IAccount.Command[](1); + commands[0] = IAccount.Command.ACCOUNT_MODIFY_MARGIN; + bytes[] memory inputs = new bytes[](1); + inputs[0] = abi.encode(amount); + account.execute(commands, inputs); + } + + function submitAtomicOrder( + bytes32 marketKey, + int256 marginDelta, + int256 sizeDelta, + uint256 desiredFillPrice + ) private { + address market = getMarketAddressFromKey(marketKey); + IAccount.Command[] memory commands = new IAccount.Command[](2); + commands[0] = IAccount.Command.PERPS_V2_MODIFY_MARGIN; + commands[1] = IAccount.Command.PERPS_V2_SUBMIT_ATOMIC_ORDER; + bytes[] memory inputs = new bytes[](2); + inputs[0] = abi.encode(market, marginDelta); + inputs[1] = abi.encode(market, sizeDelta, desiredFillPrice); + account.execute(commands, inputs); } } From 9c80f892a47d93bede3ae4eb463c8aeac40ed47e Mon Sep 17 00:00:00 2001 From: Flocqst Date: Fri, 3 May 2024 02:30:30 +0200 Subject: [PATCH 08/16] =?UTF-8?q?=E2=9C=85=20settings=20setup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/integration/orderFlowFee.behavior.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/orderFlowFee.behavior.t.sol b/test/integration/orderFlowFee.behavior.t.sol index b1b3f5da..c680a1ae 100644 --- a/test/integration/orderFlowFee.behavior.t.sol +++ b/test/integration/orderFlowFee.behavior.t.sol @@ -98,7 +98,7 @@ contract OrderFlowFeeTest is Test, ConsolidatedEvents { systemStatus, GELATO, OPS, - address(0), + address(settings), UNISWAP_UNIVERSAL_ROUTER, UNISWAP_PERMIT2 ); From 765db2e7fa049e4faf63d43afa972122825c9267 Mon Sep 17 00:00:00 2001 From: Flocqst Date: Fri, 3 May 2024 12:45:29 +0200 Subject: [PATCH 09/16] =?UTF-8?q?=E2=9C=85=20Add=20imposeOrderFlowFee=20tr?= =?UTF-8?q?easury=20balance=20checks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/integration/orderFlowFee.behavior.t.sol | 42 +++++++++++++++----- 1 file changed, 33 insertions(+), 9 deletions(-) diff --git a/test/integration/orderFlowFee.behavior.t.sol b/test/integration/orderFlowFee.behavior.t.sol index c680a1ae..f22c3fe2 100644 --- a/test/integration/orderFlowFee.behavior.t.sol +++ b/test/integration/orderFlowFee.behavior.t.sol @@ -147,9 +147,12 @@ contract OrderFlowFeeTest is Test, ConsolidatedEvents { } } - /// Verifies that OrderFlowFee is correctly sent from account margin when there is enough funds to cover orderFlowFee + /// Verifies that OrderFlowFee is correctly sent from account margin to treasury when there is enough funds in account margin to cover orderFlowFee function test_imposeOrderFlowFee_account_margin() public { fundAccount(AMOUNT); + + uint256 treasuryPreBalance = sUSD.balanceOf(settings.TREASURY()); + // create a long position in the ETH market address market = getMarketAddressFromKey(sETHPERP); @@ -162,19 +165,28 @@ contract OrderFlowFeeTest is Test, ConsolidatedEvents { submitAtomicOrder(sETHPERP, marginDelta, sizeDelta, desiredFillPrice); + uint256 treasuryPostBalance = sUSD.balanceOf(settings.TREASURY()); + /// inital funding - sETHPERP margin = 8000 ether uint256 accountMarginBeforeFee = AMOUNT - uint256(marginDelta); - (, uint256 imposedFee) = + (, uint256 imposedOrderFlowFee) = account.getExpectedOrderFlowFee(market, sizeDelta); - assertEq(accountMarginBeforeFee - imposedFee, account.freeMargin()); + // Assert that fee was correctly sent from account margin + assertEq( + accountMarginBeforeFee - imposedOrderFlowFee, account.freeMargin() + ); + // Assert that fee was correctly sent to treasury address + assertEq(treasuryPostBalance - treasuryPreBalance, imposedOrderFlowFee); } - /// Verifies that OrderFlowFee is correctly sent from market margin when there is no funds to cover orderFlowFee in account margin + /// Verifies that OrderFlowFee is correctly sent from market margin to treasury when there is no funds in account margin to cover orderFlowFee in account margin function test_imposeOrderFlowFee_market_margin() public { fundAccount(AMOUNT); + uint256 treasuryPreBalance = sUSD.balanceOf(settings.TREASURY()); + // create a long position in the ETH market address market = getMarketAddressFromKey(sETHPERP); @@ -187,19 +199,26 @@ contract OrderFlowFeeTest is Test, ConsolidatedEvents { submitAtomicOrder(sETHPERP, marginDelta, sizeDelta, desiredFillPrice); + uint256 treasuryPostBalance = sUSD.balanceOf(settings.TREASURY()); + IPerpsV2MarketConsolidated.Position memory position = account.getPosition(sETHPERP); - (, uint256 imposedFee) = + (, uint256 imposedOrderFlowFee) = account.getExpectedOrderFlowFee(market, position.size); - assertEq(uint256(position.margin), AMOUNT - imposedFee - 563); + // Assert that fee was correctly sent from market margin + assertEq(uint256(position.margin), AMOUNT - imposedOrderFlowFee - 563); + // Assert that fee was correctly sent to treasury address + assertEq(treasuryPostBalance - treasuryPreBalance, imposedOrderFlowFee); } /// Verifies that OrderFlowFee is correctly sent from both market margin and account margin when there is not enough funds to cover orderFlowFee in account margin function test_imposeOrderFlowFee_both_margin() public { fundAccount(AMOUNT); + uint256 treasuryPreBalance = sUSD.balanceOf(settings.TREASURY()); + // create a long position in the ETH market address market = getMarketAddressFromKey(sETHPERP); @@ -212,20 +231,25 @@ contract OrderFlowFeeTest is Test, ConsolidatedEvents { submitAtomicOrder(sETHPERP, marginDelta, sizeDelta, desiredFillPrice); + uint256 treasuryPostBalance = sUSD.balanceOf(settings.TREASURY()); + IPerpsV2MarketConsolidated.Position memory position = account.getPosition(sETHPERP); - (, uint256 imposedFee) = + (, uint256 imposedOrderFlowFee) = account.getExpectedOrderFlowFee(market, position.size); // Account margin is emptied assertEq(account.freeMargin(), 0); - // Market margin is reduced by (imposedFee - 10 000) + // Market margin is reduced by (imposedOrderFlowFee - 10 000) assertEq( uint256(position.margin), - uint256(marginDelta) - (imposedFee - 10_000) - 563 + uint256(marginDelta) - (imposedOrderFlowFee - 10_000) - 563 ); + + // Assert that fee was correctly sent to treasury address + assertEq(treasuryPostBalance - treasuryPreBalance, imposedOrderFlowFee); } /// @custom:todo use atomic order flow to simplify testing From dcb95b0edd036e63a70cb647f33abff13d932192 Mon Sep 17 00:00:00 2001 From: Flocqst Date: Fri, 3 May 2024 13:39:24 +0200 Subject: [PATCH 10/16] =?UTF-8?q?=E2=9C=85=20add=20test=5FimposeOrderFlowF?= =?UTF-8?q?ee=5Fmarket=5Fmargin=5Ffailed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/integration/orderFlowFee.behavior.t.sol | 56 +++++++++++++++----- 1 file changed, 44 insertions(+), 12 deletions(-) diff --git a/test/integration/orderFlowFee.behavior.t.sol b/test/integration/orderFlowFee.behavior.t.sol index f22c3fe2..83a1773e 100644 --- a/test/integration/orderFlowFee.behavior.t.sol +++ b/test/integration/orderFlowFee.behavior.t.sol @@ -252,22 +252,54 @@ contract OrderFlowFeeTest is Test, ConsolidatedEvents { assertEq(treasuryPostBalance - treasuryPreBalance, imposedOrderFlowFee); } - /// @custom:todo use atomic order flow to simplify testing - /// Synthetix makes modify position (which is called if there is not enough account margin - /// Reverts if the resulting position is too large, outside the max leverage, or is liquidating. + /// Verifies that transaction reverts if there is insufficient margin to cover for orderFlowFee without exceeding positions limits + /// @dev Synthetix makes transaction reverts if the resulting position is too large, outside the max leverage, or is liquidating. function test_imposeOrderFlowFee_market_margin_failed() public { - /// @custom:todo test the following assuming the account - /// has insufficient margin and the market has insufficient margin - /// (i.e., withdrawing from the market fails due to outstanding position or - /// order exceeding allowed leverage if margin is taken): - /// 1. error is caught for each scenario where the market margin is insufficient - // + // Set orderflow fee high to easily test that transaction fails if neither account or market has sufficient margin to cover for fees + uint256 testOrderFlowFee = 10_000; // 10% + settings.setOrderFlowFee(testOrderFlowFee); + + fundAccount(1000 ether); + + uint256 treasuryPreBalance = sUSD.balanceOf(settings.TREASURY()); + + // create a long position in the ETH market + address market = getMarketAddressFromKey(sETHPERP); + + /// Deposit all available account margin into market margin + int256 marginDelta = int256(1000 ether); + int256 sizeDelta = 1 ether; + + (uint256 desiredFillPrice,) = + IPerpsV2MarketConsolidated(market).assetPrice(); + + // Deposit market margin + IAccount.Command[] memory commands = new IAccount.Command[](1); + commands[0] = IAccount.Command.PERPS_V2_MODIFY_MARGIN; + bytes[] memory inputs = new bytes[](1); + inputs[0] = abi.encode(market, marginDelta); + account.execute(commands, inputs); + + // Execute Atomic Order + IAccount.Command[] memory commandsAtomic = new IAccount.Command[](2); + commandsAtomic[0] = IAccount.Command.PERPS_V2_SUBMIT_ATOMIC_ORDER; + bytes[] memory inputsAtomic = new bytes[](2); + inputsAtomic[0] = abi.encode(market, sizeDelta, desiredFillPrice); + + // Current configuration should have insufficient margin to cover for orderflow fee and revert + // because there is not enough margin for the position delta. + vm.expectRevert("Insufficient margin"); + + account.execute(commandsAtomic, inputsAtomic); + + uint256 treasuryPostBalance = sUSD.balanceOf(settings.TREASURY()); + + // Assert that no overflowfee was distributed + assertEq(treasuryPreBalance, treasuryPostBalance); } - /// Verifies that the correct Event is emitted with correct value + /// Verifies that the correct Event is emitted with correct fee value function test_imposeOrderFlowFee_event() public { - /// @custom:todo test the following: - fundAccount(AMOUNT); // create a long position in the ETH market address market = getMarketAddressFromKey(sETHPERP); From 45480d8538ecd564c8ef59f2c213cdade3c2c17e Mon Sep 17 00:00:00 2001 From: Flocqst Date: Fri, 3 May 2024 19:08:30 +0200 Subject: [PATCH 11/16] =?UTF-8?q?=F0=9F=91=B7=20use=20desiredFillPrice=20f?= =?UTF-8?q?or=20orderflowfee=20on=20=5FperpsV2SubmitDelayedOrder?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Account.sol | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/Account.sol b/src/Account.sol index 737e7f66..1e6bba4d 100644 --- a/src/Account.sol +++ b/src/Account.sol @@ -674,14 +674,18 @@ contract Account is IAccount, Auth, OpsReady { // fetch order flow fee from settings uint256 orderFlowFee = SETTINGS.orderFlowFee(); - // fetch current sUSD exchange rate for market - IPerpsV2MarketConsolidated market = IPerpsV2MarketConsolidated(_market); - (uint256 price,) = _sUSDRate(market); + uint256 price; - uint256 usedPrice = (_desiredFillPrice != 0) ? _desiredFillPrice : price; + // if desiredFillPrice is specified then use it + if (_desiredFillPrice != 0) { + price = _desiredFillPrice; + } else { + // fetch current sUSD exchange rate for market + (price,) = _sUSDRate(IPerpsV2MarketConsolidated(_market)); + } // calculate notional value of order - uint256 notionalValue = _abs(_sizeDelta) * usedPrice; + uint256 notionalValue = _abs(_sizeDelta) * price; // calculate fee to impose return notionalValue * orderFlowFee / SETTINGS.MAX_ORDER_FLOW_FEE(); @@ -754,7 +758,7 @@ contract Account is IAccount, Auth, OpsReady { uint256 _desiredTimeDelta, uint256 _desiredFillPrice ) internal { - _imposeOrderFlowFee(_market, _sizeDelta); + _imposeOrderFlowFee(_market, _sizeDelta, _desiredFillPrice); IPerpsV2MarketConsolidated(_market).submitDelayedOrderWithTracking({ sizeDelta: _sizeDelta, From 7fc7e8bdea3106f41fd1094e94b3e5493ca02203 Mon Sep 17 00:00:00 2001 From: Flocqst Date: Fri, 3 May 2024 19:17:29 +0200 Subject: [PATCH 12/16] =?UTF-8?q?=F0=9F=91=B7=20use=20desiredFillPrice=20f?= =?UTF-8?q?or=20orderflowfee=20on=20executeConditionalOrder?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Account.sol | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Account.sol b/src/Account.sol index 1e6bba4d..56e1fc3e 100644 --- a/src/Account.sol +++ b/src/Account.sol @@ -1058,7 +1058,8 @@ contract Account is IAccount, Auth, OpsReady { _amount: conditionalOrder.marginDelta }); - _imposeOrderFlowFee(address(market), conditionalOrder.sizeDelta); + // Use conditionalOrder.desiredFillPrice as the submitted order is a delayed order + _imposeOrderFlowFee(address(market), conditionalOrder.sizeDelta, conditionalOrder.desiredFillPrice); _perpsV2SubmitOffchainDelayedOrder({ _market: address(market), From 73324f300c8d7494a92ba4c14bf613630a38c1df Mon Sep 17 00:00:00 2001 From: Flocqst Date: Fri, 3 May 2024 19:58:49 +0200 Subject: [PATCH 13/16] =?UTF-8?q?=E2=9C=85=20add=20test=5FimposeOrderFlowF?= =?UTF-8?q?ee=5Fmarket=5Fmargin=5Fwith=5Fpending=5Forder?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/integration/orderFlowFee.behavior.t.sol | 9 ++++++--- test/unit/Settings.t.sol | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/test/integration/orderFlowFee.behavior.t.sol b/test/integration/orderFlowFee.behavior.t.sol index 83a1773e..14e30c0c 100644 --- a/test/integration/orderFlowFee.behavior.t.sol +++ b/test/integration/orderFlowFee.behavior.t.sol @@ -147,7 +147,8 @@ contract OrderFlowFeeTest is Test, ConsolidatedEvents { } } - /// Verifies that OrderFlowFee is correctly sent from account margin to treasury when there is enough funds in account margin to cover orderFlowFee + /// Verifies that OrderFlowFee is correctly sent from account margin to treasury + // when there is enough funds in account margin to cover orderFlowFee function test_imposeOrderFlowFee_account_margin() public { fundAccount(AMOUNT); @@ -181,7 +182,8 @@ contract OrderFlowFeeTest is Test, ConsolidatedEvents { assertEq(treasuryPostBalance - treasuryPreBalance, imposedOrderFlowFee); } - /// Verifies that OrderFlowFee is correctly sent from market margin to treasury when there is no funds in account margin to cover orderFlowFee in account margin + /// Verifies that OrderFlowFee is correctly sent from market margin to treasury + // when there is no funds in account margin to cover orderFlowFee in account margin function test_imposeOrderFlowFee_market_margin() public { fundAccount(AMOUNT); @@ -213,7 +215,8 @@ contract OrderFlowFeeTest is Test, ConsolidatedEvents { assertEq(treasuryPostBalance - treasuryPreBalance, imposedOrderFlowFee); } - /// Verifies that OrderFlowFee is correctly sent from both market margin and account margin when there is not enough funds to cover orderFlowFee in account margin + /// Verifies that OrderFlowFee is correctly sent from both market margin and account margin + // when there is not enough funds to cover orderFlowFee in account margin function test_imposeOrderFlowFee_both_margin() public { fundAccount(AMOUNT); diff --git a/test/unit/Settings.t.sol b/test/unit/Settings.t.sol index 896112c9..7bede60c 100644 --- a/test/unit/Settings.t.sol +++ b/test/unit/Settings.t.sol @@ -118,7 +118,7 @@ contract SettingsTest is Test, ConsolidatedEvents { //////////////////////////////////////////////////////////////*/ function test_setOrderFlowFee(uint256 fee) public { - if (fee > 100_000) { + if (fee > settings.MAX_ORDER_FLOW_FEE()) { vm.expectRevert( abi.encodeWithSelector(ISettings.InvalidOrderFlowFee.selector) ); From 1aa2f4160a53e23f8c952e77cd3cdb7fac3f038a Mon Sep 17 00:00:00 2001 From: Flocqst Date: Fri, 3 May 2024 19:59:16 +0200 Subject: [PATCH 14/16] =?UTF-8?q?=E2=9C=85=20add=20test=5FimposeOrderFlowF?= =?UTF-8?q?ee=5Fmarket=5Fmargin=5Fwith=5Fpending=5Forder?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/integration/orderFlowFee.behavior.t.sol | 91 ++++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/test/integration/orderFlowFee.behavior.t.sol b/test/integration/orderFlowFee.behavior.t.sol index 14e30c0c..6524a054 100644 --- a/test/integration/orderFlowFee.behavior.t.sol +++ b/test/integration/orderFlowFee.behavior.t.sol @@ -23,6 +23,7 @@ import { ADDRESS_RESOLVER, AMOUNT, BLOCK_NUMBER, + DESIRED_FILL_PRICE, FUTURES_MARKET_MANAGER, GELATO, OPS, @@ -51,6 +52,9 @@ contract OrderFlowFeeTest is Test, ConsolidatedEvents { IERC20 private sUSD; AccountExposed private accountExposed; + // helper variables for testing + uint256 private currentEthPriceInUSD; + // constants uint256 private constant INITIAL_ORDER_FLOW_FEE = 5; // 0.005% @@ -104,6 +108,13 @@ contract OrderFlowFeeTest is Test, ConsolidatedEvents { ); accountExposed = new AccountExposed(params); + // get current ETH price in USD + (currentEthPriceInUSD,) = accountExposed.expose_sUSDRate( + IPerpsV2MarketConsolidated( + accountExposed.expose_getPerpsV2Market(sETHPERP) + ) + ); + // call approve() on an ERC20 to grant an infinite allowance to the SM account contract sUSD.approve(address(account), type(uint256).max); @@ -215,6 +226,61 @@ contract OrderFlowFeeTest is Test, ConsolidatedEvents { assertEq(treasuryPostBalance - treasuryPreBalance, imposedOrderFlowFee); } + /// Verifies that OrderFlowFee is correctly sent from market margin to treasury + // when there is no funds in account margin to cover orderFlowFee in account margin + // with a pending order to confirm locked margin is not used in this case + function test_imposeOrderFlowFee_market_margin_with_pending_order() public { + fundAccount(AMOUNT); + + uint256 treasuryPreBalance = sUSD.balanceOf(settings.TREASURY()); + + // create a long position in the ETH market + address market = getMarketAddressFromKey(sETHPERP); + + uint256 conditionalOrderMarginDelta = 10_000; + uint256 conditionalOrdersizeDelta = 1; + + // Place a conditional order + assertEq(account.committedMargin(), 0); + + placeConditionalOrder({ + marketKey: sETHPERP, + marginDelta: int256(conditionalOrderMarginDelta), + sizeDelta: int256(conditionalOrdersizeDelta), + targetPrice: currentEthPriceInUSD, + conditionalOrderType: IAccount.ConditionalOrderTypes.LIMIT, + desiredFillPrice: DESIRED_FILL_PRICE, + reduceOnly: false + }); + + assertEq(account.committedMargin(), conditionalOrderMarginDelta); + + /// Deposit all remaining margin so that account has no margin to cover orderFlowFee + int256 marginDelta = int256(AMOUNT) - int256(conditionalOrderMarginDelta); + int256 sizeDelta = 1; + + (uint256 desiredFillPrice,) = + IPerpsV2MarketConsolidated(market).assetPrice(); + + submitAtomicOrder(sETHPERP, marginDelta, sizeDelta, desiredFillPrice); + + // Assert that locked account margin was not used to cover fee + assertEq(account.committedMargin(), conditionalOrderMarginDelta); + + uint256 treasuryPostBalance = sUSD.balanceOf(settings.TREASURY()); + + IPerpsV2MarketConsolidated.Position memory position = + account.getPosition(sETHPERP); + + (, uint256 imposedOrderFlowFee) = + account.getExpectedOrderFlowFee(market, position.size); + + // Assert that fee was correctly sent from market margin + assertEq(uint256(position.margin), uint256(marginDelta) - imposedOrderFlowFee - 563); + // Assert that fee was correctly sent to treasury address + assertEq(treasuryPostBalance - treasuryPreBalance, imposedOrderFlowFee); + } + /// Verifies that OrderFlowFee is correctly sent from both market margin and account margin // when there is not enough funds to cover orderFlowFee in account margin function test_imposeOrderFlowFee_both_margin() public { @@ -389,4 +455,29 @@ contract OrderFlowFeeTest is Test, ConsolidatedEvents { inputs[1] = abi.encode(market, sizeDelta, desiredFillPrice); account.execute(commands, inputs); } + + function placeConditionalOrder( + bytes32 marketKey, + int256 marginDelta, + int256 sizeDelta, + uint256 targetPrice, + IAccount.ConditionalOrderTypes conditionalOrderType, + uint256 desiredFillPrice, + bool reduceOnly + ) private returns (uint256 conditionalOrderId) { + IAccount.Command[] memory commands = new IAccount.Command[](1); + commands[0] = IAccount.Command.GELATO_PLACE_CONDITIONAL_ORDER; + bytes[] memory inputs = new bytes[](1); + inputs[0] = abi.encode( + marketKey, + marginDelta, + sizeDelta, + targetPrice, + conditionalOrderType, + desiredFillPrice, + reduceOnly + ); + account.execute(commands, inputs); + conditionalOrderId = account.conditionalOrderId() - 1; + } } From ba472abe6c7a767ee37bd293419e8618815778bb Mon Sep 17 00:00:00 2001 From: Flocqst Date: Fri, 3 May 2024 20:08:50 +0200 Subject: [PATCH 15/16] =?UTF-8?q?=F0=9F=91=B7=20correct=20impose=20orderfl?= =?UTF-8?q?owfee=20for=20executeConditionalOrder?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Account.sol | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Account.sol b/src/Account.sol index 56e1fc3e..539964f0 100644 --- a/src/Account.sol +++ b/src/Account.sol @@ -1058,9 +1058,6 @@ contract Account is IAccount, Auth, OpsReady { _amount: conditionalOrder.marginDelta }); - // Use conditionalOrder.desiredFillPrice as the submitted order is a delayed order - _imposeOrderFlowFee(address(market), conditionalOrder.sizeDelta, conditionalOrder.desiredFillPrice); - _perpsV2SubmitOffchainDelayedOrder({ _market: address(market), _sizeDelta: conditionalOrder.sizeDelta, From ed10aabc88315a3d690ce9697d58ec997fb325f4 Mon Sep 17 00:00:00 2001 From: Flocqst Date: Fri, 3 May 2024 20:09:17 +0200 Subject: [PATCH 16/16] =?UTF-8?q?=E2=9C=A8=20forge=20format?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/integration/orderFlowFee.behavior.t.sol | 22 +++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/test/integration/orderFlowFee.behavior.t.sol b/test/integration/orderFlowFee.behavior.t.sol index 6524a054..8168d53d 100644 --- a/test/integration/orderFlowFee.behavior.t.sol +++ b/test/integration/orderFlowFee.behavior.t.sol @@ -158,7 +158,7 @@ contract OrderFlowFeeTest is Test, ConsolidatedEvents { } } - /// Verifies that OrderFlowFee is correctly sent from account margin to treasury + /// Verifies that OrderFlowFee is correctly sent from account margin to treasury // when there is enough funds in account margin to cover orderFlowFee function test_imposeOrderFlowFee_account_margin() public { fundAccount(AMOUNT); @@ -193,7 +193,7 @@ contract OrderFlowFeeTest is Test, ConsolidatedEvents { assertEq(treasuryPostBalance - treasuryPreBalance, imposedOrderFlowFee); } - /// Verifies that OrderFlowFee is correctly sent from market margin to treasury + /// Verifies that OrderFlowFee is correctly sent from market margin to treasury // when there is no funds in account margin to cover orderFlowFee in account margin function test_imposeOrderFlowFee_market_margin() public { fundAccount(AMOUNT); @@ -226,10 +226,12 @@ contract OrderFlowFeeTest is Test, ConsolidatedEvents { assertEq(treasuryPostBalance - treasuryPreBalance, imposedOrderFlowFee); } - /// Verifies that OrderFlowFee is correctly sent from market margin to treasury + /// Verifies that OrderFlowFee is correctly sent from market margin to treasury // when there is no funds in account margin to cover orderFlowFee in account margin // with a pending order to confirm locked margin is not used in this case - function test_imposeOrderFlowFee_market_margin_with_pending_order() public { + function test_imposeOrderFlowFee_market_margin_with_pending_order() + public + { fundAccount(AMOUNT); uint256 treasuryPreBalance = sUSD.balanceOf(settings.TREASURY()); @@ -256,7 +258,8 @@ contract OrderFlowFeeTest is Test, ConsolidatedEvents { assertEq(account.committedMargin(), conditionalOrderMarginDelta); /// Deposit all remaining margin so that account has no margin to cover orderFlowFee - int256 marginDelta = int256(AMOUNT) - int256(conditionalOrderMarginDelta); + int256 marginDelta = + int256(AMOUNT) - int256(conditionalOrderMarginDelta); int256 sizeDelta = 1; (uint256 desiredFillPrice,) = @@ -265,7 +268,7 @@ contract OrderFlowFeeTest is Test, ConsolidatedEvents { submitAtomicOrder(sETHPERP, marginDelta, sizeDelta, desiredFillPrice); // Assert that locked account margin was not used to cover fee - assertEq(account.committedMargin(), conditionalOrderMarginDelta); + assertEq(account.committedMargin(), conditionalOrderMarginDelta); uint256 treasuryPostBalance = sUSD.balanceOf(settings.TREASURY()); @@ -276,12 +279,15 @@ contract OrderFlowFeeTest is Test, ConsolidatedEvents { account.getExpectedOrderFlowFee(market, position.size); // Assert that fee was correctly sent from market margin - assertEq(uint256(position.margin), uint256(marginDelta) - imposedOrderFlowFee - 563); + assertEq( + uint256(position.margin), + uint256(marginDelta) - imposedOrderFlowFee - 563 + ); // Assert that fee was correctly sent to treasury address assertEq(treasuryPostBalance - treasuryPreBalance, imposedOrderFlowFee); } - /// Verifies that OrderFlowFee is correctly sent from both market margin and account margin + /// Verifies that OrderFlowFee is correctly sent from both market margin and account margin // when there is not enough funds to cover orderFlowFee in account margin function test_imposeOrderFlowFee_both_margin() public { fundAccount(AMOUNT);