From 1121bb5f01c99a6bbd8421785baca87f0c8a970e Mon Sep 17 00:00:00 2001 From: JaredBorders Date: Mon, 10 Jul 2023 12:49:50 -0400 Subject: [PATCH 01/16] =?UTF-8?q?=F0=9F=97=91=EF=B8=8F=20Remove=20onlyOps?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Account.sol | 1 - src/utils/gelato/OpsReady.sol | 8 -------- test/integration/order.behavior.t.sol | 6 ------ 3 files changed, 15 deletions(-) diff --git a/src/Account.sol b/src/Account.sol index 036266d3..1cf77e83 100644 --- a/src/Account.sol +++ b/src/Account.sol @@ -805,7 +805,6 @@ contract Account is IAccount, Auth, OpsReady { external override isAccountExecutionEnabled - onlyOps { // store conditional order in memory ConditionalOrder memory conditionalOrder = diff --git a/src/utils/gelato/OpsReady.sol b/src/utils/gelato/OpsReady.sol index 993b3688..e86a07bc 100644 --- a/src/utils/gelato/OpsReady.sol +++ b/src/utils/gelato/OpsReady.sol @@ -5,8 +5,6 @@ pragma solidity 0.8.18; /// contract to make synchronous fee payments and have /// call restrictions for functions to be automated. abstract contract OpsReady { - error OnlyOps(); - /// @notice address of Gelato Network contract address public immutable GELATO; @@ -16,12 +14,6 @@ abstract contract OpsReady { /// @notice internal address representation of ETH (used by Gelato) address public constant ETH = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; - /// @notice modifier to restrict access to the `Automate` contract - modifier onlyOps() { - if (msg.sender != OPS) revert OnlyOps(); - _; - } - /// @notice sets the addresses of the Gelato Network contracts /// @param _gelato: address of the Gelato Network contract /// @param _ops: address of the Gelato `Automate` contract diff --git a/test/integration/order.behavior.t.sol b/test/integration/order.behavior.t.sol index 872aa772..bf3e3d2f 100644 --- a/test/integration/order.behavior.t.sol +++ b/test/integration/order.behavior.t.sol @@ -649,12 +649,6 @@ contract OrderBehaviorTest is Test, ConsolidatedEvents { EXECUTING CONDITIONAL ORDERS: GENERAL //////////////////////////////////////////////////////////////*/ - function test_ExecuteConditionalOrder_Invalid_NotOps() public { - vm.prank(USER); - vm.expectRevert(abi.encodeWithSelector(OpsReady.OnlyOps.selector)); - account.executeConditionalOrder({_conditionalOrderId: 0}); - } - function test_ExecuteConditionalOrder_MarketIsPaused() public { // place conditional order for sAUDPERP market uint256 conditionalOrderId = placeConditionalOrder({ From 319f19bb3593dbffd07d8202f07fabeab04e1610 Mon Sep 17 00:00:00 2001 From: JaredBorders Date: Mon, 10 Jul 2023 17:08:20 -0400 Subject: [PATCH 02/16] =?UTF-8?q?=F0=9F=91=B7=F0=9F=8F=BB=E2=80=8D?= =?UTF-8?q?=E2=99=82=EF=B8=8F=20Introduce=20executeConditionalOrderWithPri?= =?UTF-8?q?ceUpdate?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Account.sol | 89 +++++++++++++++++-- src/interfaces/IAccount.sol | 34 ++++++- src/interfaces/pyth/IPyth.sol | 24 +++++ .../synthetix/IPerpsV2ExchangeRate.sol | 4 + src/utils/gelato/OpsReady.sol | 5 +- 5 files changed, 145 insertions(+), 11 deletions(-) create mode 100644 src/interfaces/pyth/IPyth.sol diff --git a/src/Account.sol b/src/Account.sol index 1cf77e83..78761233 100644 --- a/src/Account.sol +++ b/src/Account.sol @@ -10,6 +10,7 @@ import {IFactory} from "src/interfaces/IFactory.sol"; import {IFuturesMarketManager} from "src/interfaces/synthetix/IFuturesMarketManager.sol"; import {IPermit2} from "src/interfaces/uniswap/IPermit2.sol"; +import {IPyth} from "src/interfaces/pyth/IPyth.sol"; import {ISettings} from "src/interfaces/ISettings.sol"; import {ISystemStatus} from "src/interfaces/synthetix/ISystemStatus.sol"; import {IOps} from "src/interfaces/gelato/IOps.sol"; @@ -103,6 +104,10 @@ contract Account is IAccount, Auth, OpsReady { /// @notice value used for reentrancy protection uint256 internal locked = 1; + /// @notice fee the SM account is willing to pay a conditional order executor + /// @notice this fee can be calibrated by the owner + uint256 public executorFee = 1 ether / 1000; + /*////////////////////////////////////////////////////////////// MODIFIERS //////////////////////////////////////////////////////////////*/ @@ -678,7 +683,7 @@ contract Account is IAccount, Auth, OpsReady { } /*////////////////////////////////////////////////////////////// - CONDITIONAL ORDERS + CREATE CONDITIONAL ORDER //////////////////////////////////////////////////////////////*/ /// @notice register a conditional order internally and with gelato @@ -770,6 +775,10 @@ contract Account is IAccount, Auth, OpsReady { ); } + /*////////////////////////////////////////////////////////////// + CANCEL CONDITIONAL ORDER + //////////////////////////////////////////////////////////////*/ + /// @notice cancel a gelato queued conditional order /// @param _conditionalOrderId: key for an active conditional order function _cancelConditionalOrder(uint256 _conditionalOrderId) internal { @@ -797,16 +806,32 @@ contract Account is IAccount, Auth, OpsReady { } /*////////////////////////////////////////////////////////////// - GELATO CONDITIONAL ORDER HANDLING + EXECUTE CONDITIONAL ORDER //////////////////////////////////////////////////////////////*/ /// @inheritdoc IAccount function executeConditionalOrder(uint256 _conditionalOrderId) external override + nonReentrant isAccountExecutionEnabled { - // store conditional order in memory + _executeConditionalOrder(_conditionalOrderId); + } + + /// @inheritdoc IAccount + function executeConditionalOrderWithPriceUpdate( + uint256 _conditionalOrderId, + bytes[] calldata _priceUpdateData + ) external override nonReentrant isAccountExecutionEnabled { + // update pyth price feed prior to executing conditional order + _updatePythPrice(_priceUpdateData); + + _executeConditionalOrder(_conditionalOrderId); + } + + function _executeConditionalOrder(uint256 _conditionalOrderId) internal { + // store conditional order object in memory ConditionalOrder memory conditionalOrder = getConditionalOrder(_conditionalOrderId); @@ -815,11 +840,20 @@ contract Account is IAccount, Auth, OpsReady { // remove gelato task from their accounting /// @dev will revert if task id does not exist {Automate.cancelTask: Task not found} + /// @dev if executor is not Gelato, the task will still be cancelled IOps(OPS).cancelTask({taskId: conditionalOrder.gelatoTaskId}); - // pay Gelato imposed fee for conditional order execution - (uint256 fee, address feeToken) = IOps(OPS).getFeeDetails(); - _transfer({_amount: fee, _paymentToken: feeToken}); + // verify conditional order is ready for execution + /// @dev it is understood this is a duplicate check if the executor is Gelato + if (!_validConditionalOrder(_conditionalOrderId)) { + revert CannotExecuteConditionalOrder({ + conditionalOrderId: _conditionalOrderId, + executor: msg.sender + }); + } + + // impose and record fee paid to executor + uint256 fee = _payExecutorFee(); // define Synthetix PerpsV2 market IPerpsV2MarketConsolidated market = @@ -867,6 +901,7 @@ contract Account is IAccount, Auth, OpsReady { _market: address(market), _amount: conditionalOrder.marginDelta }); + _perpsV2SubmitOffchainDelayedOrder({ _market: address(market), _sizeDelta: conditionalOrder.sizeDelta, @@ -882,6 +917,38 @@ contract Account is IAccount, Auth, OpsReady { }); } + /// @notice attempt to update the Pyth price feed + /// @dev this will revert if the price update fails due to insufficient eth + /// @param priceUpdateData: array of bytes containing price update data + function _updatePythPrice(bytes[] calldata priceUpdateData) internal { + IPyth oracle = PERPS_V2_EXCHANGE_RATE.offchainOracle(); + + // determine fee amount to pay to Pyth for price update + uint256 fee = oracle.getUpdateFee(priceUpdateData); + + // update the price data (and pay the fee) + /// @dev the SM account pays the fee, not the caller (i.e. not the executor) + try oracle.updatePriceFeeds{value: fee}(priceUpdateData) {} + catch { + revert PythPriceUpdateFailed(); + } + } + + /// @notice pay fee for conditional order execution + /// @dev fee will be different depending on executor + /// @return fee amount paid + function _payExecutorFee() internal returns (uint256 fee) { + if (msg.sender == OPS) { + // pay Gelato imposed fee for conditional order execution + address feeToken; + (fee, feeToken) = IOps(OPS).getFeeDetails(); + _transfer({_amount: fee, _paymentToken: feeToken}); + } else { + (bool success,) = payable(msg.sender).call{value: executorFee}(""); + if (!success) revert CannotPayExecutorFee(executorFee, msg.sender); + } + } + /// @notice order logic condition checker /// @dev this is where order type logic checks are handled /// @param _conditionalOrderId: key for an active order @@ -1092,6 +1159,16 @@ contract Account is IAccount, Auth, OpsReady { } } + /*////////////////////////////////////////////////////////////// + SETTER UTILITIES + //////////////////////////////////////////////////////////////*/ + + /// @inheritdoc IAccount + function setExecutorFee(uint256 _executorFee) external override { + if (!isOwner()) revert Unauthorized(); + executorFee = _executorFee; + } + /*////////////////////////////////////////////////////////////// GETTER UTILITIES //////////////////////////////////////////////////////////////*/ diff --git a/src/interfaces/IAccount.sol b/src/interfaces/IAccount.sol index 96c4fc93..21a8cb4a 100644 --- a/src/interfaces/IAccount.sol +++ b/src/interfaces/IAccount.sol @@ -142,6 +142,21 @@ interface IAccount { /// @param tokenOut: token attempting to swap to error TokenSwapNotAllowed(address tokenIn, address tokenOut); + /// @notice thrown when a conditional order is attempted to be executed during invalid market conditions + /// @param conditionalOrderId: conditional order id + /// @param executor: address of executor + error CannotExecuteConditionalOrder( + uint256 conditionalOrderId, address executor + ); + + /// @notice thrown when a conditional order is attempted to be executed but SM account cannot pay fee + /// @param executorFee: fee required to execute conditional order + error CannotPayExecutorFee(uint256 executorFee, address executor); + + /// @notice thrown when a price update is attempted to be executed but SM account cannot pay fee + /// OR it fails for some other reason (ex: bad price feed data) + error PythPriceUpdateFailed(); + /*////////////////////////////////////////////////////////////// VIEWS //////////////////////////////////////////////////////////////*/ @@ -211,11 +226,22 @@ interface IAccount { external payable; - /// @notice execute a gelato queued conditional order - /// @notice only keepers can trigger this function + /// @notice execute queued conditional order /// @dev currently only supports conditional order submission via PERPS_V2_SUBMIT_OFFCHAIN_DELAYED_ORDER COMMAND - /// @custom:audit a compromised Gelato Ops cannot drain accounts due to several interactions with Synthetix PerpsV2 - /// requiring a valid market which could not be initialized with an invalid conditional order id /// @param _conditionalOrderId: key for an active conditional order function executeConditionalOrder(uint256 _conditionalOrderId) external; + + /// @notice execute a gelato queued conditional order with a pyth oracle price feed update + /// @param _conditionalOrderId: key for an active conditional order + /// @param priceUpdateData: array of bytes containing price update data for pyth oracle + function executeConditionalOrderWithPriceUpdate( + uint256 _conditionalOrderId, + bytes[] calldata priceUpdateData + ) external; + + /// @notice set the executor fee for conditional order execution + /// @dev only owner can set executor fee + /// @dev there are no checks against new executor fee + /// @param _executorFee: new executor fee + function setExecutorFee(uint256 _executorFee) external; } diff --git a/src/interfaces/pyth/IPyth.sol b/src/interfaces/pyth/IPyth.sol new file mode 100644 index 00000000..2144828c --- /dev/null +++ b/src/interfaces/pyth/IPyth.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.18; + +/// @title Consume prices from the Pyth Network (https://pyth.network/). +/// @dev Please refer to the guidance at https://docs.pyth.network/consumers/best-practices for how to consume prices safely. +/// @author Pyth Data Association +interface IPyth { + /// @notice Update price feeds with given update messages. + /// This method requires the caller to pay a fee in wei; the required fee can be computed by calling + /// `getUpdateFee` with the length of the `updateData` array. + /// Prices will be updated if they are more recent than the current stored prices. + /// The call will succeed even if the update is not the most recent. + /// @dev Reverts if the transferred fee is not sufficient or the updateData is invalid. + /// @param updateData Array of price update data. + function updatePriceFeeds(bytes[] calldata updateData) external payable; + + /// @notice Returns the required fee to update an array of price updates. + /// @param updateData Array of price update data. + /// @return feeAmount The required fee in Wei. + function getUpdateFee(bytes[] calldata updateData) + external + view + returns (uint256 feeAmount); +} diff --git a/src/interfaces/synthetix/IPerpsV2ExchangeRate.sol b/src/interfaces/synthetix/IPerpsV2ExchangeRate.sol index 9354a739..7660df86 100644 --- a/src/interfaces/synthetix/IPerpsV2ExchangeRate.sol +++ b/src/interfaces/synthetix/IPerpsV2ExchangeRate.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity 0.8.18; +import {IPyth} from "src/interfaces/pyth/IPyth.sol"; + /// Used to fetch a price with a degree of uncertainty, represented as a price +- a confidence interval. /// /// The confidence interval roughly corresponds to the standard error of a normal distribution. @@ -23,4 +25,6 @@ interface IPerpsV2ExchangeRate { external view returns (uint256 price, uint256 publishTime); + + function offchainOracle() external view returns (IPyth); } diff --git a/src/utils/gelato/OpsReady.sol b/src/utils/gelato/OpsReady.sol index e86a07bc..af0749f6 100644 --- a/src/utils/gelato/OpsReady.sol +++ b/src/utils/gelato/OpsReady.sol @@ -14,6 +14,9 @@ abstract contract OpsReady { /// @notice internal address representation of ETH (used by Gelato) address public constant ETH = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + /// @notice thrown when SM account cannot pay the fee + error CannotPayGelatoFee(uint256 amount); + /// @notice sets the addresses of the Gelato Network contracts /// @param _gelato: address of the Gelato Network contract /// @param _ops: address of the Gelato `Automate` contract @@ -30,6 +33,6 @@ abstract contract OpsReady { /// @dev Smart Margin Accounts will only pay fees in ETH assert(_paymentToken == ETH); (bool success,) = GELATO.call{value: _amount}(""); - require(success, "OpsReady: ETH transfer failed"); + if (!success) revert CannotPayGelatoFee(_amount); } } From 7a64f41a02b2d15e9983a921afbed5558a4abd65 Mon Sep 17 00:00:00 2001 From: JaredBorders Date: Tue, 11 Jul 2023 14:59:01 -0400 Subject: [PATCH 03/16] =?UTF-8?q?=E2=8F=AA=20Roll=20back=20changes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/utils/gelato/OpsReady.sol | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/utils/gelato/OpsReady.sol b/src/utils/gelato/OpsReady.sol index af0749f6..e86a07bc 100644 --- a/src/utils/gelato/OpsReady.sol +++ b/src/utils/gelato/OpsReady.sol @@ -14,9 +14,6 @@ abstract contract OpsReady { /// @notice internal address representation of ETH (used by Gelato) address public constant ETH = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; - /// @notice thrown when SM account cannot pay the fee - error CannotPayGelatoFee(uint256 amount); - /// @notice sets the addresses of the Gelato Network contracts /// @param _gelato: address of the Gelato Network contract /// @param _ops: address of the Gelato `Automate` contract @@ -33,6 +30,6 @@ abstract contract OpsReady { /// @dev Smart Margin Accounts will only pay fees in ETH assert(_paymentToken == ETH); (bool success,) = GELATO.call{value: _amount}(""); - if (!success) revert CannotPayGelatoFee(_amount); + require(success, "OpsReady: ETH transfer failed"); } } From 72cd9e50b4ee52d28cc99f82243b6691d54706f0 Mon Sep 17 00:00:00 2001 From: JaredBorders Date: Tue, 11 Jul 2023 14:59:25 -0400 Subject: [PATCH 04/16] =?UTF-8?q?=F0=9F=91=B7=F0=9F=8F=BB=E2=80=8D?= =?UTF-8?q?=E2=99=82=EF=B8=8F=20Adjust=20logic=20flow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Account.sol | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Account.sol b/src/Account.sol index 78761233..ecf8b27e 100644 --- a/src/Account.sol +++ b/src/Account.sol @@ -835,14 +835,6 @@ contract Account is IAccount, Auth, OpsReady { ConditionalOrder memory conditionalOrder = getConditionalOrder(_conditionalOrderId); - // remove conditional order from internal accounting - delete conditionalOrders[_conditionalOrderId]; - - // remove gelato task from their accounting - /// @dev will revert if task id does not exist {Automate.cancelTask: Task not found} - /// @dev if executor is not Gelato, the task will still be cancelled - IOps(OPS).cancelTask({taskId: conditionalOrder.gelatoTaskId}); - // verify conditional order is ready for execution /// @dev it is understood this is a duplicate check if the executor is Gelato if (!_validConditionalOrder(_conditionalOrderId)) { @@ -852,6 +844,14 @@ contract Account is IAccount, Auth, OpsReady { }); } + // remove conditional order from internal accounting + delete conditionalOrders[_conditionalOrderId]; + + // remove gelato task from their accounting + /// @dev will revert if task id does not exist {Automate.cancelTask: Task not found} + /// @dev if executor is not Gelato, the task will still be cancelled + IOps(OPS).cancelTask({taskId: conditionalOrder.gelatoTaskId}); + // impose and record fee paid to executor uint256 fee = _payExecutorFee(); From b07e1d03e1f3d7c12c374b1d75becbc0072c9970 Mon Sep 17 00:00:00 2001 From: JaredBorders Date: Tue, 11 Jul 2023 17:18:01 -0400 Subject: [PATCH 05/16] =?UTF-8?q?=F0=9F=91=B7=F0=9F=8F=BB=E2=80=8D?= =?UTF-8?q?=E2=99=82=EF=B8=8F=20Add=20new=20event?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Account.sol | 2 ++ src/Events.sol | 9 +++++++++ src/interfaces/IEvents.sol | 6 ++++++ 3 files changed, 17 insertions(+) diff --git a/src/Account.sol b/src/Account.sol index ecf8b27e..2ab2e30e 100644 --- a/src/Account.sol +++ b/src/Account.sol @@ -1167,6 +1167,8 @@ contract Account is IAccount, Auth, OpsReady { function setExecutorFee(uint256 _executorFee) external override { if (!isOwner()) revert Unauthorized(); executorFee = _executorFee; + + EVENTS.emitExecutorFeeSet({executorFee: _executorFee}); } /*////////////////////////////////////////////////////////////// diff --git a/src/Events.sol b/src/Events.sol index 78d79a1e..43593ca5 100644 --- a/src/Events.sol +++ b/src/Events.sol @@ -143,4 +143,13 @@ contract Events is IEvents { priceOracle: priceOracle }); } + + /// @inheritdoc IEvents + function emitExecutorFeeSet(uint256 executorFee) + external + override + onlyAccounts + { + emit ExecutorFeeSet({account: msg.sender, executorFee: executorFee}); + } } diff --git a/src/interfaces/IEvents.sol b/src/interfaces/IEvents.sol index ded3db16..a2c3dbff 100644 --- a/src/interfaces/IEvents.sol +++ b/src/interfaces/IEvents.sol @@ -147,4 +147,10 @@ interface IEvents { uint256 keeperFee, IAccount.PriceOracleUsed priceOracle ); + + /// @notice emitter when executor fee is set by the account owner + /// @param executorFee: executor fee + function emitExecutorFeeSet(uint256 executorFee) external; + + event ExecutorFeeSet(address indexed account, uint256 indexed executorFee); } From aac639cae67b482522d20cd9b44088bd5b21d35b Mon Sep 17 00:00:00 2001 From: JaredBorders Date: Tue, 11 Jul 2023 17:18:16 -0400 Subject: [PATCH 06/16] =?UTF-8?q?=E2=9C=85=20Test=20event?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/unit/Account.t.sol | 23 +++++++++++++++++++++++ test/utils/ConsolidatedEvents.sol | 2 ++ 2 files changed, 25 insertions(+) diff --git a/test/unit/Account.t.sol b/test/unit/Account.t.sol index 1c7b609d..189432a2 100644 --- a/test/unit/Account.t.sol +++ b/test/unit/Account.t.sol @@ -981,6 +981,29 @@ contract AccountTest is Test, ConsolidatedEvents { } } + /*////////////////////////////////////////////////////////////// + SETTER UTILITIES + //////////////////////////////////////////////////////////////*/ + + function test_SetExecutorFee_OnlyOwner() public { + uint fee = 1; + vm.prank(DELEGATE); + vm.expectRevert(abi.encodeWithSelector(Auth.Unauthorized.selector)); + account.setExecutorFee(fee); + } + + function test_SetExecutorFee(uint256 fee) public { + account.setExecutorFee(fee); + assertEq(account.executorFee(), fee); + } + + function test_SetExecutorFee_Event() public { + uint256 fee = 1 ether; + vm.expectEmit(true, true, true, true); + emit ExecutorFeeSet(address(account), fee); + account.setExecutorFee(fee); + } + /*////////////////////////////////////////////////////////////// HELPERS //////////////////////////////////////////////////////////////*/ diff --git a/test/utils/ConsolidatedEvents.sol b/test/utils/ConsolidatedEvents.sol index a1905c20..2954acdf 100644 --- a/test/utils/ConsolidatedEvents.sol +++ b/test/utils/ConsolidatedEvents.sol @@ -85,6 +85,8 @@ contract ConsolidatedEvents { IAccount.PriceOracleUsed priceOracle ); + event ExecutorFeeSet(address indexed account, uint256 indexed executorFee); + /*////////////////////////////////////////////////////////////// ISETTINGS //////////////////////////////////////////////////////////////*/ From f0caeb5ca1b935df672e829b0a0cc6cd5e51d75f Mon Sep 17 00:00:00 2001 From: JaredBorders Date: Tue, 11 Jul 2023 17:34:43 -0400 Subject: [PATCH 07/16] =?UTF-8?q?=E2=9C=A8=20Prettify?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- script/upgrades/v2.0.2/Upgrade.s.sol | 2 +- test/upgrades/v2.0.2/upgrade.t.sol | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/script/upgrades/v2.0.2/Upgrade.s.sol b/script/upgrades/v2.0.2/Upgrade.s.sol index 9b62c9b1..966fd613 100644 --- a/script/upgrades/v2.0.2/Upgrade.s.sol +++ b/script/upgrades/v2.0.2/Upgrade.s.sol @@ -97,4 +97,4 @@ pragma solidity 0.8.18; // console2.log("Account Implementation v2.0.2 Deployed:", implementation); // } -// } \ No newline at end of file +// } diff --git a/test/upgrades/v2.0.2/upgrade.t.sol b/test/upgrades/v2.0.2/upgrade.t.sol index 9811404b..8ad43628 100644 --- a/test/upgrades/v2.0.2/upgrade.t.sol +++ b/test/upgrades/v2.0.2/upgrade.t.sol @@ -160,4 +160,4 @@ pragma solidity 0.8.18; // ); // assertEq(true, abi.decode(response, (bool)), "delegate missmatch"); // } -// } \ No newline at end of file +// } From d901d277dd944308f71fd91fb314fc7dc9280a37 Mon Sep 17 00:00:00 2001 From: JaredBorders Date: Tue, 11 Jul 2023 17:35:04 -0400 Subject: [PATCH 08/16] =?UTF-8?q?=E2=9C=85=20Add=20test=20stubs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...vior.t.sol => order.gelato.behavior.t.sol} | 2 +- test/integration/order.public.behavior.t.sol | 294 ++++++++++++++++++ test/unit/Account.t.sol | 4 +- 3 files changed, 297 insertions(+), 3 deletions(-) rename test/integration/{order.behavior.t.sol => order.gelato.behavior.t.sol} (99%) create mode 100644 test/integration/order.public.behavior.t.sol diff --git a/test/integration/order.behavior.t.sol b/test/integration/order.gelato.behavior.t.sol similarity index 99% rename from test/integration/order.behavior.t.sol rename to test/integration/order.gelato.behavior.t.sol index bf3e3d2f..465fee5e 100644 --- a/test/integration/order.behavior.t.sol +++ b/test/integration/order.gelato.behavior.t.sol @@ -49,7 +49,7 @@ import { // functions tagged with @HELPER are helper functions and not tests // tests tagged with @AUDITOR are flags for desired increased scrutiny by the auditors -contract OrderBehaviorTest is Test, ConsolidatedEvents { +contract OrderGelatoBehaviorTest is Test, ConsolidatedEvents { receive() external payable {} /*////////////////////////////////////////////////////////////// diff --git a/test/integration/order.public.behavior.t.sol b/test/integration/order.public.behavior.t.sol new file mode 100644 index 00000000..0ca99de3 --- /dev/null +++ b/test/integration/order.public.behavior.t.sol @@ -0,0 +1,294 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.18; + +import {Test} from "lib/forge-std/src/Test.sol"; + +import {Setup} from "script/Deploy.s.sol"; + +import {Account} from "src/Account.sol"; +import {Events} from "src/Events.sol"; +import {Factory} from "src/Factory.sol"; +import {IAccount} from "src/interfaces/IAccount.sol"; +import {IFuturesMarketManager} from + "src/interfaces/synthetix/IFuturesMarketManager.sol"; +import {IOps} from "src/interfaces/gelato/IOps.sol"; +import {IPermit2} from "src/interfaces/uniswap/IPermit2.sol"; +import {IPerpsV2MarketConsolidated} from + "src/interfaces/synthetix/IPerpsV2MarketConsolidated.sol"; +import {IERC20} from "src/interfaces/token/IERC20.sol"; +import {Settings} from "src/Settings.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 {ISystemStatus} from "test/utils/interfaces/ISystemStatus.sol"; + +import { + ADDRESS_RESOLVER, + AMOUNT, + BLOCK_NUMBER, + DESIRED_FILL_PRICE, + ETH, + FUTURES_MARKET_MANAGER, + GELATO, + GELATO_FEE, + OPS, + PERPS_V2_EXCHANGE_RATE, + PROXY_SUSD, + sAUDPERP, + sETHPERP, + SYSTEM_STATUS, + TRACKING_CODE, + UNISWAP_PERMIT2, + UNISWAP_UNIVERSAL_ROUTER, + USER +} from "test/utils/Constants.sol"; + +contract OrderPublicBehaviorTest is Test, ConsolidatedEvents { + receive() external payable {} + + /*////////////////////////////////////////////////////////////// + STATE + //////////////////////////////////////////////////////////////*/ + + // main contracts + Factory private factory; + Events private events; + Settings private settings; + Account private account; + + // helper contracts for testing + IERC20 private sUSD; + AccountExposed private accountExposed; + ISystemStatus private systemStatus; + + // helper variables for testing + uint256 private currentEthPriceInUSD; + + IPermit2 private PERMIT2; + + // conditional order variables + uint256 conditionalOrderId; + + /*////////////////////////////////////////////////////////////// + SETUP + //////////////////////////////////////////////////////////////*/ + + function setUp() public { + vm.rollFork(BLOCK_NUMBER); + + Setup setup = new Setup(); + + (factory, events, settings,) = setup.deploySystem({ + _deployer: address(0), + _owner: address(this), + _addressResolver: ADDRESS_RESOLVER, + _gelato: GELATO, + _ops: OPS, + _universalRouter: UNISWAP_UNIVERSAL_ROUTER, + _permit2: UNISWAP_PERMIT2 + }); + + // define helper contracts + IAddressResolver addressResolver = IAddressResolver(ADDRESS_RESOLVER); + sUSD = IERC20(addressResolver.getAddress(PROXY_SUSD)); + address futuresMarketManager = + addressResolver.getAddress(FUTURES_MARKET_MANAGER); + systemStatus = ISystemStatus(addressResolver.getAddress(SYSTEM_STATUS)); + address perpsV2ExchangeRate = + addressResolver.getAddress(PERPS_V2_EXCHANGE_RATE); + + IAccount.AccountConstructorParams memory params = IAccount + .AccountConstructorParams( + address(factory), + address(events), + address(sUSD), + perpsV2ExchangeRate, + futuresMarketManager, + address(systemStatus), + GELATO, + OPS, + address(settings), + UNISWAP_UNIVERSAL_ROUTER, + UNISWAP_PERMIT2 + ); + accountExposed = new AccountExposed(params); + + account = Account(payable(factory.newAccount())); + + (currentEthPriceInUSD,) = accountExposed.expose_sUSDRate( + IPerpsV2MarketConsolidated( + accountExposed.expose_getPerpsV2Market(sETHPERP) + ) + ); + + PERMIT2 = IPermit2(UNISWAP_PERMIT2); + sUSD.approve(UNISWAP_PERMIT2, type(uint256).max); + PERMIT2.approve( + address(sUSD), address(account), type(uint160).max, type(uint48).max + ); + + fundAccount(AMOUNT); + + conditionalOrderId = placeConditionalOrder({ + marketKey: sETHPERP, + marginDelta: int256(currentEthPriceInUSD), + sizeDelta: 1 ether, + targetPrice: currentEthPriceInUSD, + conditionalOrderType: IAccount.ConditionalOrderTypes.LIMIT, + desiredFillPrice: DESIRED_FILL_PRICE, + reduceOnly: false + }); + } + + /*////////////////////////////////////////////////////////////// + TESTS + //////////////////////////////////////////////////////////////*/ + function test_ExecuteConditionalOrder() public { + account.executeConditionalOrder(conditionalOrderId); + IAccount.ConditionalOrder memory conditionalOrder = + account.getConditionalOrder(conditionalOrderId); + assertEq(conditionalOrder.sizeDelta, 0); + } + + function test_ExecuteConditionalOrderWithPriceUpdate() public { + /// @custom:todo + } + + function test_ExecuteConditionalOrderWithPriceUpdate_Executor_Fee() + public + { + /// @custom:todo + } + + function test_ExecuteConditionalOrderWithPriceUpdate_Pyth_Updated() + public + { + /// @custom:todo + } + + function test_ExecuteConditionalOrderWithPriceUpdate_Pyth_Fee() public { + /// @custom:todo + } + + function test_ExecuteConditionalOrderWithPriceUpdate_Invalid_PriceFeed() + public + { + /// @custom:todo + } + + function test_ExecuteConditionalOrder_Invalid_ConditionalOrder() public { + /// @custom:todo + // expect -> CannotExecuteConditionalOrder Custom Error + } + + /*////////////////////////////////////////////////////////////// + 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 generateGelatoModuleData(uint256 conditionalOrderId) + internal + view + returns (bytes memory executionData, IOps.ModuleData memory moduleData) + { + executionData = + abi.encodeCall(account.executeConditionalOrder, conditionalOrderId); + + moduleData = IOps.ModuleData({ + modules: new IOps.Module[](1), + args: new bytes[](1) + }); + + moduleData.modules[0] = IOps.Module.RESOLVER; + + moduleData.args[0] = abi.encode( + address(account), + abi.encodeCall(account.checker, conditionalOrderId) + ); + } + + function suspendPerpsV2Market(bytes32 market) internal { + // fetch owner address of SystemStatus contract + (bool success, bytes memory response) = + address(systemStatus).call(abi.encodeWithSignature("owner()")); + address systemStatusOwner = + success ? abi.decode(response, (address)) : address(0); + + // add owner to access control list so they can suspend perpsv2 market + vm.startPrank(systemStatusOwner); + systemStatus.updateAccessControl({ + section: bytes32("Futures"), + account: systemStatusOwner, + canSuspend: true, + canResume: true + }); + + // suspend market + systemStatus.suspendFuturesMarket({marketKey: market, reason: 69}); + vm.stopPrank(); + } + + 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 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; + } +} diff --git a/test/unit/Account.t.sol b/test/unit/Account.t.sol index 189432a2..5b736774 100644 --- a/test/unit/Account.t.sol +++ b/test/unit/Account.t.sol @@ -986,7 +986,7 @@ contract AccountTest is Test, ConsolidatedEvents { //////////////////////////////////////////////////////////////*/ function test_SetExecutorFee_OnlyOwner() public { - uint fee = 1; + uint256 fee = 1; vm.prank(DELEGATE); vm.expectRevert(abi.encodeWithSelector(Auth.Unauthorized.selector)); account.setExecutorFee(fee); @@ -1003,7 +1003,7 @@ contract AccountTest is Test, ConsolidatedEvents { emit ExecutorFeeSet(address(account), fee); account.setExecutorFee(fee); } - + /*////////////////////////////////////////////////////////////// HELPERS //////////////////////////////////////////////////////////////*/ From 2fdc9fd3a883d046be5dd7c73774f4d885accda2 Mon Sep 17 00:00:00 2001 From: JaredBorders Date: Tue, 11 Jul 2023 17:39:56 -0400 Subject: [PATCH 09/16] =?UTF-8?q?=E2=9C=85=20Update=20AccountExposed=20for?= =?UTF-8?q?=20testing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/integration/order.public.behavior.t.sol | 8 ++++++++ test/unit/Account.t.sol | 12 ++++++------ test/utils/AccountExposed.sol | 10 +++++++++- 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/test/integration/order.public.behavior.t.sol b/test/integration/order.public.behavior.t.sol index 0ca99de3..b5131dd2 100644 --- a/test/integration/order.public.behavior.t.sol +++ b/test/integration/order.public.behavior.t.sol @@ -152,6 +152,14 @@ contract OrderPublicBehaviorTest is Test, ConsolidatedEvents { assertEq(conditionalOrder.sizeDelta, 0); } + function test_UpdatePythPrice() public { + /// @custom:todo will likely need to mock + } + + function test_PayExecutorFee() public { + /// @custom:todo + } + function test_ExecuteConditionalOrderWithPriceUpdate() public { /// @custom:todo } diff --git a/test/unit/Account.t.sol b/test/unit/Account.t.sol index 5b736774..6731a9d2 100644 --- a/test/unit/Account.t.sol +++ b/test/unit/Account.t.sol @@ -879,7 +879,7 @@ contract AccountTest is Test, ConsolidatedEvents { ); (address tokenIn, address tokenOut) = - accountExposed.expose__getTokenInTokenOut(path); + accountExposed.expose_getTokenInTokenOut(path); assertEq(tokenIn, address(0xA)); assertEq(tokenOut, address(0xB)); } @@ -902,7 +902,7 @@ contract AccountTest is Test, ConsolidatedEvents { ); (address tokenIn, address tokenOut) = - accountExposed.expose__getTokenInTokenOut(path); + accountExposed.expose_getTokenInTokenOut(path); assertEq(tokenIn, address(0xA)); assertEq(tokenOut, address(0x10)); } @@ -913,7 +913,7 @@ contract AccountTest is Test, ConsolidatedEvents { vm.expectRevert( abi.encodeWithSelector(BytesLib.SliceOutOfBounds.selector) ); - accountExposed.expose__getTokenInTokenOut(path); + accountExposed.expose_getTokenInTokenOut(path); } function test_GetTokenInTokenOut_Invalid_TokenIn() public { @@ -922,7 +922,7 @@ contract AccountTest is Test, ConsolidatedEvents { vm.expectRevert( abi.encodeWithSelector(BytesLib.SliceOutOfBounds.selector) ); - accountExposed.expose__getTokenInTokenOut(path); + accountExposed.expose_getTokenInTokenOut(path); } function test_GetTokenInTokenOut_Invalid_Fee() public { @@ -932,7 +932,7 @@ contract AccountTest is Test, ConsolidatedEvents { vm.expectRevert( abi.encodeWithSelector(BytesLib.SliceOutOfBounds.selector) ); - accountExposed.expose__getTokenInTokenOut(path); + accountExposed.expose_getTokenInTokenOut(path); } function test_GetTokenInTokenOut_Invalid_Pools_No_Revert( @@ -941,7 +941,7 @@ contract AccountTest is Test, ConsolidatedEvents { vm.assume(path.length >= MULTIPLE_V3_POOLS_MIN_LENGTH); (address tokenIn, address tokenOut) = - accountExposed.expose__getTokenInTokenOut(path); + accountExposed.expose_getTokenInTokenOut(path); // _getTokenInTokenOut makes no assurances about the validity of the tokenIn/tokenOut; // it only checks that the path length is valid. diff --git a/test/utils/AccountExposed.sol b/test/utils/AccountExposed.sol index 878c8589..b8d76919 100644 --- a/test/utils/AccountExposed.sol +++ b/test/utils/AccountExposed.sol @@ -104,11 +104,19 @@ contract AccountExposed is Account { return locked; } - function expose__getTokenInTokenOut(bytes calldata _path) + function expose_getTokenInTokenOut(bytes calldata _path) public pure returns (address, address) { return _getTokenInTokenOut(_path); } + + function expose_updatePythPrice(bytes[] calldata priceUpdateData) public { + _updatePythPrice(priceUpdateData); + } + + function expose_payExecutorFee() public returns (uint256) { + return _payExecutorFee(); + } } From 74e5efb7a20743110e02a58d569877d2d5146657 Mon Sep 17 00:00:00 2001 From: JaredBorders Date: Thu, 13 Jul 2023 13:01:53 -0400 Subject: [PATCH 10/16] =?UTF-8?q?=F0=9F=91=B7=F0=9F=8F=BB=E2=80=8D?= =?UTF-8?q?=E2=99=82=EF=B8=8F=20Implement=20executor=20fee=20logic/state?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Account.sol | 68 +++++------------------------------- src/Settings.sol | 10 ++++++ src/interfaces/IAccount.sol | 18 ---------- src/interfaces/ISettings.sol | 12 +++++++ 4 files changed, 31 insertions(+), 77 deletions(-) diff --git a/src/Account.sol b/src/Account.sol index 2ab2e30e..535002da 100644 --- a/src/Account.sol +++ b/src/Account.sol @@ -10,7 +10,6 @@ import {IFactory} from "src/interfaces/IFactory.sol"; import {IFuturesMarketManager} from "src/interfaces/synthetix/IFuturesMarketManager.sol"; import {IPermit2} from "src/interfaces/uniswap/IPermit2.sol"; -import {IPyth} from "src/interfaces/pyth/IPyth.sol"; import {ISettings} from "src/interfaces/ISettings.sol"; import {ISystemStatus} from "src/interfaces/synthetix/ISystemStatus.sol"; import {IOps} from "src/interfaces/gelato/IOps.sol"; @@ -102,11 +101,7 @@ contract Account is IAccount, Auth, OpsReady { mapping(uint256 id => ConditionalOrder order) internal conditionalOrders; /// @notice value used for reentrancy protection - uint256 internal locked = 1; - - /// @notice fee the SM account is willing to pay a conditional order executor - /// @notice this fee can be calibrated by the owner - uint256 public executorFee = 1 ether / 1000; + uint256 internal locked; /*////////////////////////////////////////////////////////////// MODIFIERS @@ -121,12 +116,12 @@ contract Account is IAccount, Auth, OpsReady { } modifier nonReentrant() { - if (locked == 2) revert Reentrancy(); - locked = 2; + if (locked == 1) revert Reentrancy(); + locked = 1; _; - locked = 1; + locked = 0; } /*////////////////////////////////////////////////////////////// @@ -816,21 +811,6 @@ contract Account is IAccount, Auth, OpsReady { nonReentrant isAccountExecutionEnabled { - _executeConditionalOrder(_conditionalOrderId); - } - - /// @inheritdoc IAccount - function executeConditionalOrderWithPriceUpdate( - uint256 _conditionalOrderId, - bytes[] calldata _priceUpdateData - ) external override nonReentrant isAccountExecutionEnabled { - // update pyth price feed prior to executing conditional order - _updatePythPrice(_priceUpdateData); - - _executeConditionalOrder(_conditionalOrderId); - } - - function _executeConditionalOrder(uint256 _conditionalOrderId) internal { // store conditional order object in memory ConditionalOrder memory conditionalOrder = getConditionalOrder(_conditionalOrderId); @@ -917,35 +897,17 @@ contract Account is IAccount, Auth, OpsReady { }); } - /// @notice attempt to update the Pyth price feed - /// @dev this will revert if the price update fails due to insufficient eth - /// @param priceUpdateData: array of bytes containing price update data - function _updatePythPrice(bytes[] calldata priceUpdateData) internal { - IPyth oracle = PERPS_V2_EXCHANGE_RATE.offchainOracle(); - - // determine fee amount to pay to Pyth for price update - uint256 fee = oracle.getUpdateFee(priceUpdateData); - - // update the price data (and pay the fee) - /// @dev the SM account pays the fee, not the caller (i.e. not the executor) - try oracle.updatePriceFeeds{value: fee}(priceUpdateData) {} - catch { - revert PythPriceUpdateFailed(); - } - } - /// @notice pay fee for conditional order execution /// @dev fee will be different depending on executor /// @return fee amount paid function _payExecutorFee() internal returns (uint256 fee) { if (msg.sender == OPS) { - // pay Gelato imposed fee for conditional order execution - address feeToken; - (fee, feeToken) = IOps(OPS).getFeeDetails(); - _transfer({_amount: fee, _paymentToken: feeToken}); + (fee,) = IOps(OPS).getFeeDetails(); + _transfer({_amount: fee}); } else { - (bool success,) = payable(msg.sender).call{value: executorFee}(""); - if (!success) revert CannotPayExecutorFee(executorFee, msg.sender); + fee = SETTINGS.executorFee(); + (bool success,) = payable(msg.sender).call{value: fee}(""); + if (!success) revert CannotPayExecutorFee(fee, msg.sender); } } @@ -1159,18 +1121,6 @@ contract Account is IAccount, Auth, OpsReady { } } - /*////////////////////////////////////////////////////////////// - SETTER UTILITIES - //////////////////////////////////////////////////////////////*/ - - /// @inheritdoc IAccount - function setExecutorFee(uint256 _executorFee) external override { - if (!isOwner()) revert Unauthorized(); - executorFee = _executorFee; - - EVENTS.emitExecutorFeeSet({executorFee: _executorFee}); - } - /*////////////////////////////////////////////////////////////// GETTER UTILITIES //////////////////////////////////////////////////////////////*/ diff --git a/src/Settings.sol b/src/Settings.sol index 3e7e1065..8aa244fd 100644 --- a/src/Settings.sol +++ b/src/Settings.sol @@ -18,6 +18,9 @@ contract Settings is ISettings, Owned { /// @inheritdoc ISettings bool public accountExecutionEnabled = true; + /// @inheritdoc ISettings + uint256 public executorFee = 1 ether / 1000; + /// @notice mapping of whitelisted tokens available for swapping via uniswap commands mapping(address => bool) internal _whitelistedTokens; @@ -58,6 +61,13 @@ contract Settings is ISettings, Owned { emit AccountExecutionEnabledSet(_enabled); } + /// @inheritdoc ISettings + function setExecutorFee(uint256 _executorFee) external override onlyOwner { + executorFee = _executorFee; + + emit ExecutorFeeSet(_executorFee); + } + /// @inheritdoc ISettings function setTokenWhitelistStatus(address _token, bool _isWhitelisted) external diff --git a/src/interfaces/IAccount.sol b/src/interfaces/IAccount.sol index 21a8cb4a..d944e910 100644 --- a/src/interfaces/IAccount.sol +++ b/src/interfaces/IAccount.sol @@ -153,10 +153,6 @@ interface IAccount { /// @param executorFee: fee required to execute conditional order error CannotPayExecutorFee(uint256 executorFee, address executor); - /// @notice thrown when a price update is attempted to be executed but SM account cannot pay fee - /// OR it fails for some other reason (ex: bad price feed data) - error PythPriceUpdateFailed(); - /*////////////////////////////////////////////////////////////// VIEWS //////////////////////////////////////////////////////////////*/ @@ -230,18 +226,4 @@ interface IAccount { /// @dev currently only supports conditional order submission via PERPS_V2_SUBMIT_OFFCHAIN_DELAYED_ORDER COMMAND /// @param _conditionalOrderId: key for an active conditional order function executeConditionalOrder(uint256 _conditionalOrderId) external; - - /// @notice execute a gelato queued conditional order with a pyth oracle price feed update - /// @param _conditionalOrderId: key for an active conditional order - /// @param priceUpdateData: array of bytes containing price update data for pyth oracle - function executeConditionalOrderWithPriceUpdate( - uint256 _conditionalOrderId, - bytes[] calldata priceUpdateData - ) external; - - /// @notice set the executor fee for conditional order execution - /// @dev only owner can set executor fee - /// @dev there are no checks against new executor fee - /// @param _executorFee: new executor fee - function setExecutorFee(uint256 _executorFee) external; } diff --git a/src/interfaces/ISettings.sol b/src/interfaces/ISettings.sol index 9aff64eb..ef51632b 100644 --- a/src/interfaces/ISettings.sol +++ b/src/interfaces/ISettings.sol @@ -12,6 +12,10 @@ interface ISettings { /// @param enabled: true if account execution is enabled, false if disabled event AccountExecutionEnabledSet(bool enabled); + /// @notice emitted when the executor fee is updated + /// @param executorFee: the executor fee + event ExecutorFeeSet(uint256 executorFee); + /// @notice emitted when a token is added to or removed from the whitelist /// @param token: address of the token event TokenWhitelistStatusUpdated(address token); @@ -24,6 +28,10 @@ interface ISettings { /// @return enabled: true if account execution is enabled, false if disabled function accountExecutionEnabled() external view returns (bool); + /// @notice gets the conditional order executor fee + /// @return executorFee: the executor fee + function executorFee() external view returns (uint256); + /// @notice checks if token is whitelisted /// @param _token: address of the token to check /// @return true if token is whitelisted, false if not @@ -37,6 +45,10 @@ interface ISettings { /// @param _enabled: true if account execution is enabled, false if disabled function setAccountExecutionEnabled(bool _enabled) external; + /// @notice sets the conditional order executor fee + /// @param _executorFee: the executor fee + function setExecutorFee(uint256 _executorFee) external; + /// @notice adds/removes token to/from whitelist /// @dev does not check if token was previously whitelisted /// @param _token: address of the token to add From 21557547058e6d23b56472a2ca4702fe6557daaf Mon Sep 17 00:00:00 2001 From: JaredBorders Date: Thu, 13 Jul 2023 13:02:11 -0400 Subject: [PATCH 11/16] =?UTF-8?q?=F0=9F=97=91=EF=B8=8F=20Remove=20IPyth?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/interfaces/pyth/IPyth.sol | 24 ------------------- .../synthetix/IPerpsV2ExchangeRate.sol | 4 ---- 2 files changed, 28 deletions(-) delete mode 100644 src/interfaces/pyth/IPyth.sol diff --git a/src/interfaces/pyth/IPyth.sol b/src/interfaces/pyth/IPyth.sol deleted file mode 100644 index 2144828c..00000000 --- a/src/interfaces/pyth/IPyth.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity 0.8.18; - -/// @title Consume prices from the Pyth Network (https://pyth.network/). -/// @dev Please refer to the guidance at https://docs.pyth.network/consumers/best-practices for how to consume prices safely. -/// @author Pyth Data Association -interface IPyth { - /// @notice Update price feeds with given update messages. - /// This method requires the caller to pay a fee in wei; the required fee can be computed by calling - /// `getUpdateFee` with the length of the `updateData` array. - /// Prices will be updated if they are more recent than the current stored prices. - /// The call will succeed even if the update is not the most recent. - /// @dev Reverts if the transferred fee is not sufficient or the updateData is invalid. - /// @param updateData Array of price update data. - function updatePriceFeeds(bytes[] calldata updateData) external payable; - - /// @notice Returns the required fee to update an array of price updates. - /// @param updateData Array of price update data. - /// @return feeAmount The required fee in Wei. - function getUpdateFee(bytes[] calldata updateData) - external - view - returns (uint256 feeAmount); -} diff --git a/src/interfaces/synthetix/IPerpsV2ExchangeRate.sol b/src/interfaces/synthetix/IPerpsV2ExchangeRate.sol index 7660df86..9354a739 100644 --- a/src/interfaces/synthetix/IPerpsV2ExchangeRate.sol +++ b/src/interfaces/synthetix/IPerpsV2ExchangeRate.sol @@ -1,8 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity 0.8.18; -import {IPyth} from "src/interfaces/pyth/IPyth.sol"; - /// Used to fetch a price with a degree of uncertainty, represented as a price +- a confidence interval. /// /// The confidence interval roughly corresponds to the standard error of a normal distribution. @@ -25,6 +23,4 @@ interface IPerpsV2ExchangeRate { external view returns (uint256 price, uint256 publishTime); - - function offchainOracle() external view returns (IPyth); } From d42a8ff9880924e5db02bea2dd30940066ab3ac1 Mon Sep 17 00:00:00 2001 From: JaredBorders Date: Thu, 13 Jul 2023 13:02:25 -0400 Subject: [PATCH 12/16] =?UTF-8?q?=F0=9F=97=91=EF=B8=8F=20Remove=20payment?= =?UTF-8?q?=20token?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/utils/gelato/OpsReady.sol | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/utils/gelato/OpsReady.sol b/src/utils/gelato/OpsReady.sol index e86a07bc..f3337108 100644 --- a/src/utils/gelato/OpsReady.sol +++ b/src/utils/gelato/OpsReady.sol @@ -25,10 +25,7 @@ abstract contract OpsReady { /// @notice transfers fee (in ETH) to gelato for synchronous fee payments /// @dev happens at task execution time /// @param _amount: amount of asset to transfer - /// @param _paymentToken: address of the token to transfer - function _transfer(uint256 _amount, address _paymentToken) internal { - /// @dev Smart Margin Accounts will only pay fees in ETH - assert(_paymentToken == ETH); + function _transfer(uint256 _amount) internal { (bool success,) = GELATO.call{value: _amount}(""); require(success, "OpsReady: ETH transfer failed"); } From e1bb9b52283ad4264200a1655c7b5e735d56c8ac Mon Sep 17 00:00:00 2001 From: JaredBorders Date: Thu, 13 Jul 2023 13:02:46 -0400 Subject: [PATCH 13/16] =?UTF-8?q?=E2=9C=85=20Test=20executor=20fee=20chang?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/integration/order.public.behavior.t.sol | 115 ++++++++----------- test/unit/Account.t.sol | 27 +---- test/unit/Settings.t.sol | 23 ++++ test/utils/AccountExposed.sol | 4 - test/utils/ConsolidatedEvents.sol | 2 + 5 files changed, 72 insertions(+), 99 deletions(-) diff --git a/test/integration/order.public.behavior.t.sol b/test/integration/order.public.behavior.t.sol index b5131dd2..ae0ea7fd 100644 --- a/test/integration/order.public.behavior.t.sol +++ b/test/integration/order.public.behavior.t.sol @@ -69,7 +69,7 @@ contract OrderPublicBehaviorTest is Test, ConsolidatedEvents { IPermit2 private PERMIT2; // conditional order variables - uint256 conditionalOrderId; + uint256 private conditionalOrderId; /*////////////////////////////////////////////////////////////// SETUP @@ -145,50 +145,54 @@ contract OrderPublicBehaviorTest is Test, ConsolidatedEvents { /*////////////////////////////////////////////////////////////// TESTS //////////////////////////////////////////////////////////////*/ - function test_ExecuteConditionalOrder() public { + + function test_ExecuteConditionalOrder_Public() public { + vm.deal(USER, 1 ether); + vm.prank(USER); account.executeConditionalOrder(conditionalOrderId); IAccount.ConditionalOrder memory conditionalOrder = account.getConditionalOrder(conditionalOrderId); assertEq(conditionalOrder.sizeDelta, 0); } - function test_UpdatePythPrice() public { - /// @custom:todo will likely need to mock - } - - function test_PayExecutorFee() public { - /// @custom:todo - } - - function test_ExecuteConditionalOrderWithPriceUpdate() public { - /// @custom:todo - } - - function test_ExecuteConditionalOrderWithPriceUpdate_Executor_Fee() - public - { - /// @custom:todo + function test_ExecuteConditionalOrder_Public_PayExecutorFee() public { + vm.deal(USER, 1 ether); + uint256 balanceBefore = USER.balance; + vm.prank(USER); + account.executeConditionalOrder(conditionalOrderId); + uint256 balanceAfter = USER.balance; + assertGt(balanceAfter, balanceBefore); } - function test_ExecuteConditionalOrderWithPriceUpdate_Pyth_Updated() + function test_ExecuteConditionalOrder_Public_Cannot_PayExecutorFee() public { - /// @custom:todo - } - - function test_ExecuteConditionalOrderWithPriceUpdate_Pyth_Fee() public { - /// @custom:todo + withdrawEth(address(account).balance); + vm.startPrank(USER); + vm.expectRevert( + abi.encodeWithSelector( + IAccount.CannotPayExecutorFee.selector, + settings.executorFee(), + USER + ) + ); + account.executeConditionalOrder(conditionalOrderId); + vm.stopPrank(); } - function test_ExecuteConditionalOrderWithPriceUpdate_Invalid_PriceFeed() + function test_ExecuteConditionalOrder_Public_Invalid_ConditionalOrder() public { - /// @custom:todo - } - - function test_ExecuteConditionalOrder_Invalid_ConditionalOrder() public { - /// @custom:todo - // expect -> CannotExecuteConditionalOrder Custom Error + suspendPerpsV2Market(sETHPERP); + vm.prank(USER); + vm.expectRevert( + abi.encodeWithSelector( + IAccount.CannotExecuteConditionalOrder.selector, + conditionalOrderId, + USER + ) + ); + account.executeConditionalOrder(conditionalOrderId); } /*////////////////////////////////////////////////////////////// @@ -209,43 +213,6 @@ contract OrderPublicBehaviorTest is Test, ConsolidatedEvents { 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 generateGelatoModuleData(uint256 conditionalOrderId) - internal - view - returns (bytes memory executionData, IOps.ModuleData memory moduleData) - { - executionData = - abi.encodeCall(account.executeConditionalOrder, conditionalOrderId); - - moduleData = IOps.ModuleData({ - modules: new IOps.Module[](1), - args: new bytes[](1) - }); - - moduleData.modules[0] = IOps.Module.RESOLVER; - - moduleData.args[0] = abi.encode( - address(account), - abi.encodeCall(account.checker, conditionalOrderId) - ); - } - function suspendPerpsV2Market(bytes32 market) internal { // fetch owner address of SystemStatus contract (bool success, bytes memory response) = @@ -283,7 +250,7 @@ contract OrderPublicBehaviorTest is Test, ConsolidatedEvents { IAccount.ConditionalOrderTypes conditionalOrderType, uint256 desiredFillPrice, bool reduceOnly - ) private returns (uint256 conditionalOrderId) { + ) private returns (uint256) { IAccount.Command[] memory commands = new IAccount.Command[](1); commands[0] = IAccount.Command.GELATO_PLACE_CONDITIONAL_ORDER; bytes[] memory inputs = new bytes[](1); @@ -297,6 +264,14 @@ contract OrderPublicBehaviorTest is Test, ConsolidatedEvents { reduceOnly ); account.execute(commands, inputs); - conditionalOrderId = account.conditionalOrderId() - 1; + return account.conditionalOrderId() - 1; + } + + function withdrawEth(uint256 amount) private { + IAccount.Command[] memory commands = new IAccount.Command[](1); + commands[0] = IAccount.Command.ACCOUNT_WITHDRAW_ETH; + bytes[] memory inputs = new bytes[](1); + inputs[0] = abi.encode(amount); + account.execute(commands, inputs); } } diff --git a/test/unit/Account.t.sol b/test/unit/Account.t.sol index 6731a9d2..ed355323 100644 --- a/test/unit/Account.t.sol +++ b/test/unit/Account.t.sol @@ -413,9 +413,9 @@ contract AccountTest is Test, ConsolidatedEvents { IAccount.Command[] memory commands = new IAccount.Command[](1); commands[0] = IAccount.Command.ACCOUNT_MODIFY_MARGIN; bytes[] memory inputs = new bytes[](1); - assertEq(1, accountExposed.expose_locked()); + assertEq(0, accountExposed.expose_locked()); account.execute(commands, inputs); - assertEq(1, accountExposed.expose_locked()); + assertEq(0, accountExposed.expose_locked()); } /*////////////////////////////////////////////////////////////// @@ -981,29 +981,6 @@ contract AccountTest is Test, ConsolidatedEvents { } } - /*////////////////////////////////////////////////////////////// - SETTER UTILITIES - //////////////////////////////////////////////////////////////*/ - - function test_SetExecutorFee_OnlyOwner() public { - uint256 fee = 1; - vm.prank(DELEGATE); - vm.expectRevert(abi.encodeWithSelector(Auth.Unauthorized.selector)); - account.setExecutorFee(fee); - } - - function test_SetExecutorFee(uint256 fee) public { - account.setExecutorFee(fee); - assertEq(account.executorFee(), fee); - } - - function test_SetExecutorFee_Event() public { - uint256 fee = 1 ether; - vm.expectEmit(true, true, true, true); - emit ExecutorFeeSet(address(account), fee); - account.setExecutorFee(fee); - } - /*////////////////////////////////////////////////////////////// HELPERS //////////////////////////////////////////////////////////////*/ diff --git a/test/unit/Settings.t.sol b/test/unit/Settings.t.sol index c7b94053..9a8a149e 100644 --- a/test/unit/Settings.t.sol +++ b/test/unit/Settings.t.sol @@ -55,6 +55,29 @@ contract SettingsTest is Test, ConsolidatedEvents { settings.setAccountExecutionEnabled(false); } + /*////////////////////////////////////////////////////////////// + EXECUTOR FEE + //////////////////////////////////////////////////////////////*/ + + function test_setExecutorFee() public { + assertEq(settings.executorFee(), 1 ether / 1000); + settings.setExecutorFee(1 ether / 100); + assertEq(settings.executorFee(), 1 ether / 100); + } + + function test_setExecutorFee_OnlyOwner() public { + vm.expectRevert("UNAUTHORIZED"); + vm.prank(USER); + settings.setExecutorFee(1 ether / 100); + } + + function test_setExecutorFee_Event() public { + vm.expectEmit(true, true, true, true); + emit ExecutorFeeSet(1 ether / 100); + settings.setExecutorFee(1 ether / 100); + } + + /*////////////////////////////////////////////////////////////// WHITELISTING TOKENS //////////////////////////////////////////////////////////////*/ diff --git a/test/utils/AccountExposed.sol b/test/utils/AccountExposed.sol index b8d76919..6780e847 100644 --- a/test/utils/AccountExposed.sol +++ b/test/utils/AccountExposed.sol @@ -112,10 +112,6 @@ contract AccountExposed is Account { return _getTokenInTokenOut(_path); } - function expose_updatePythPrice(bytes[] calldata priceUpdateData) public { - _updatePythPrice(priceUpdateData); - } - function expose_payExecutorFee() public returns (uint256) { return _payExecutorFee(); } diff --git a/test/utils/ConsolidatedEvents.sol b/test/utils/ConsolidatedEvents.sol index 2954acdf..2f7cbafb 100644 --- a/test/utils/ConsolidatedEvents.sol +++ b/test/utils/ConsolidatedEvents.sol @@ -93,5 +93,7 @@ contract ConsolidatedEvents { event AccountExecutionEnabledSet(bool enabled); + event ExecutorFeeSet(uint256 executorFee); + event TokenWhitelistStatusUpdated(address token); } From 066e40f6e5f0f02c07a6875ea9df81d5d33b42b2 Mon Sep 17 00:00:00 2001 From: JaredBorders Date: Thu, 13 Jul 2023 13:03:08 -0400 Subject: [PATCH 14/16] =?UTF-8?q?=E2=9C=A8=20Prettify?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/unit/Settings.t.sol | 1 - 1 file changed, 1 deletion(-) diff --git a/test/unit/Settings.t.sol b/test/unit/Settings.t.sol index 9a8a149e..8e2cf85d 100644 --- a/test/unit/Settings.t.sol +++ b/test/unit/Settings.t.sol @@ -77,7 +77,6 @@ contract SettingsTest is Test, ConsolidatedEvents { settings.setExecutorFee(1 ether / 100); } - /*////////////////////////////////////////////////////////////// WHITELISTING TOKENS //////////////////////////////////////////////////////////////*/ From aaf19634f5c62e0eaf295b544d1db2289a815917 Mon Sep 17 00:00:00 2001 From: JaredBorders Date: Sun, 16 Jul 2023 21:30:33 -0400 Subject: [PATCH 15/16] =?UTF-8?q?=F0=9F=92=A5=20Add=20periphery=20contract?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/utils/executors/OrderExecution.sol | 113 +++++++++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 src/utils/executors/OrderExecution.sol diff --git a/src/utils/executors/OrderExecution.sol b/src/utils/executors/OrderExecution.sol new file mode 100644 index 00000000..7ad43b45 --- /dev/null +++ b/src/utils/executors/OrderExecution.sol @@ -0,0 +1,113 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.18; + +/// @title Utility contract for executing conditional orders +/// @notice This contract is untested and should be used with caution +/// @custom:auditor ignore +/// @author JaredBorders (jaredborders@pm.me) +interface IAccount { + /// @param _conditionalOrderId: key for an active conditional order + function executeConditionalOrder(uint256 _conditionalOrderId) external; + + /// @notice checker() is the Resolver for Gelato + /// (see https://docs.gelato.network/developer-services/automate/guides/custom-logic-triggers/smart-contract-resolvers) + /// @notice signal to a keeper that a conditional order is valid/invalid for execution + /// @dev call reverts if conditional order Id does not map to a valid conditional order; + /// ConditionalOrder.marketKey would be invalid + /// @param _conditionalOrderId: key for an active conditional order + /// @return canExec boolean that signals to keeper a conditional order can be executed by Gelato + /// @return execPayload calldata for executing a conditional order + function checker(uint256 _conditionalOrderId) + external + view + returns (bool canExec, bytes memory execPayload); +} + +interface IPerpsV2ExchangeRate { + /// @notice fetches the Pyth oracle contract address from Synthetix + /// @return Pyth contract + function offchainOracle() external view returns (IPyth); +} + +interface IPyth { + /// @notice Update price feeds with given update messages. + /// This method requires the caller to pay a fee in wei; the required fee can be computed by calling + /// `getUpdateFee` with the length of the `updateData` array. + /// Prices will be updated if they are more recent than the current stored prices. + /// The call will succeed even if the update is not the most recent. + /// @dev Reverts if the transferred fee is not sufficient or the updateData is invalid. + /// @param updateData Array of price update data. + function updatePriceFeeds(bytes[] calldata updateData) external payable; + + /// @notice Returns the required fee to update an array of price updates. + /// @param updateData Array of price update data. + /// @return feeAmount The required fee in Wei. + function getUpdateFee(bytes[] calldata updateData) + external + view + returns (uint256 feeAmount); +} + +contract OrderExecution { + + IPerpsV2ExchangeRate public immutable PERPS_V2_EXCHANGE_RATE; + + error PythPriceUpdateFailed(); + + constructor(address _perpsV2ExchangeRate) { + PERPS_V2_EXCHANGE_RATE = IPerpsV2ExchangeRate(_perpsV2ExchangeRate); + } + + /// @dev updates the Pyth oracle price feed and refunds the caller any unused value + /// not used to update feed + function updatePythPrice(bytes[] calldata priceUpdateData) public payable { + /// @custom:optimization oracle could be immutable if we can guarantee it will never change + IPyth oracle = PERPS_V2_EXCHANGE_RATE.offchainOracle(); + + // determine fee amount to pay to Pyth for price update + uint256 fee = oracle.getUpdateFee(priceUpdateData); + + // try to update the price data (and pay the fee) + try oracle.updatePriceFeeds{value: fee}(priceUpdateData) {} + catch { + revert PythPriceUpdateFailed(); + } + + uint refund = msg.value - fee; + if (refund > 0) { + // refund caller the unused value + (bool success, ) = msg.sender.call{value: refund}(""); + assert(success); + } + } + + /// @dev executes a batch of conditional orders in reverse order (i.e. LIFO) + function executeOrders(address[] calldata accounts, uint[] calldata ids) public { + assert(accounts.length > 0); + assert(accounts.length == ids.length); + + uint i = accounts.length; + do { + unchecked { --i; } + + /** + * @custom:logic could ensure onchain order can be executed via call to `checker` + * + * (bool canExec,) = IAccount(accounts[i]).checker(ids[i]); + * assert(canExec); + * + */ + + IAccount(accounts[i]).executeConditionalOrder(ids[i]); + } while ( i != 0); + } + + function updatePriceThenExecuteOrders( + bytes[] calldata priceUpdateData, + address[] calldata accounts, + uint[] calldata ids + ) external payable { + updatePythPrice(priceUpdateData); + executeOrders(accounts, ids); + } +} \ No newline at end of file From 86efc1ff2dba15c27d9c55c7ed7720da9613a2c8 Mon Sep 17 00:00:00 2001 From: JaredBorders Date: Sun, 16 Jul 2023 21:35:47 -0400 Subject: [PATCH 16/16] =?UTF-8?q?=F0=9F=92=A5=20Update=20OrderExecution.so?= =?UTF-8?q?l?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/utils/executors/OrderExecution.sol | 35 ++++++++++++++------------ 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/src/utils/executors/OrderExecution.sol b/src/utils/executors/OrderExecution.sol index 7ad43b45..70746aa5 100644 --- a/src/utils/executors/OrderExecution.sol +++ b/src/utils/executors/OrderExecution.sol @@ -1,12 +1,8 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity 0.8.18; -/// @title Utility contract for executing conditional orders -/// @notice This contract is untested and should be used with caution -/// @custom:auditor ignore -/// @author JaredBorders (jaredborders@pm.me) interface IAccount { - /// @param _conditionalOrderId: key for an active conditional order + /// @param _conditionalOrderId: key for an active conditional order function executeConditionalOrder(uint256 _conditionalOrderId) external; /// @notice checker() is the Resolver for Gelato @@ -24,7 +20,7 @@ interface IAccount { } interface IPerpsV2ExchangeRate { - /// @notice fetches the Pyth oracle contract address from Synthetix + /// @notice fetches the Pyth oracle contract address from Synthetix /// @return Pyth contract function offchainOracle() external view returns (IPyth); } @@ -48,8 +44,11 @@ interface IPyth { returns (uint256 feeAmount); } +/// @title Utility contract for executing conditional orders +/// @notice This contract is untested and should be used with caution +/// @custom:auditor ignore +/// @author JaredBorders (jaredborders@pm.me) contract OrderExecution { - IPerpsV2ExchangeRate public immutable PERPS_V2_EXCHANGE_RATE; error PythPriceUpdateFailed(); @@ -57,7 +56,7 @@ contract OrderExecution { constructor(address _perpsV2ExchangeRate) { PERPS_V2_EXCHANGE_RATE = IPerpsV2ExchangeRate(_perpsV2ExchangeRate); } - + /// @dev updates the Pyth oracle price feed and refunds the caller any unused value /// not used to update feed function updatePythPrice(bytes[] calldata priceUpdateData) public payable { @@ -73,22 +72,26 @@ contract OrderExecution { revert PythPriceUpdateFailed(); } - uint refund = msg.value - fee; + uint256 refund = msg.value - fee; if (refund > 0) { // refund caller the unused value - (bool success, ) = msg.sender.call{value: refund}(""); + (bool success,) = msg.sender.call{value: refund}(""); assert(success); } } /// @dev executes a batch of conditional orders in reverse order (i.e. LIFO) - function executeOrders(address[] calldata accounts, uint[] calldata ids) public { + function executeOrders(address[] calldata accounts, uint256[] calldata ids) + public + { assert(accounts.length > 0); assert(accounts.length == ids.length); - uint i = accounts.length; + uint256 i = accounts.length; do { - unchecked { --i; } + unchecked { + --i; + } /** * @custom:logic could ensure onchain order can be executed via call to `checker` @@ -99,15 +102,15 @@ contract OrderExecution { */ IAccount(accounts[i]).executeConditionalOrder(ids[i]); - } while ( i != 0); + } while (i != 0); } function updatePriceThenExecuteOrders( bytes[] calldata priceUpdateData, address[] calldata accounts, - uint[] calldata ids + uint256[] calldata ids ) external payable { updatePythPrice(priceUpdateData); executeOrders(accounts, ids); } -} \ No newline at end of file +}