From 58d7041386bfb4e3084e3a8af9221ac40407eea2 Mon Sep 17 00:00:00 2001 From: JChiaramonte7 Date: Tue, 1 Oct 2024 13:42:49 -0400 Subject: [PATCH] add new upgrade script / test --- script/upgrades/v2.1.5/Upgrade.s.sol | 89 ++++++ src/Account.sol | 2 +- test/upgrades/v2.1.5/Upgrade.t.sol | 304 +++++++++++++++++++ test/upgrades/v2.1.5/interfaces/IAccount.sol | 244 +++++++++++++++ 4 files changed, 638 insertions(+), 1 deletion(-) create mode 100644 script/upgrades/v2.1.5/Upgrade.s.sol create mode 100644 test/upgrades/v2.1.5/Upgrade.t.sol create mode 100644 test/upgrades/v2.1.5/interfaces/IAccount.sol diff --git a/script/upgrades/v2.1.5/Upgrade.s.sol b/script/upgrades/v2.1.5/Upgrade.s.sol new file mode 100644 index 00000000..0b4d49ca --- /dev/null +++ b/script/upgrades/v2.1.5/Upgrade.s.sol @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.18; + +import {Script} from "lib/forge-std/src/Script.sol"; + +import {IAddressResolver} from "script/utils/interfaces/IAddressResolver.sol"; + +import {Account} from "src/Account.sol"; +import {Events} from "src/Events.sol"; +import {Settings} from "src/Settings.sol"; +import {IAccount} from "src/interfaces/IAccount.sol"; + +import { + OPTIMISM_GELATO, + OPTIMISM_OPS, + FUTURES_MARKET_MANAGER, + OPTIMISM_FACTORY, + OPTIMISM_SYNTHETIX_ADDRESS_RESOLVER, + OPTIMISM_UNISWAP_PERMIT2, + OPTIMISM_UNISWAP_UNIVERSAL_ROUTER, + OPTIMISM_SETTINGS, + OPTIMISM_EVENTS, + PERPS_V2_EXCHANGE_RATE, + PROXY_SUSD, + SYSTEM_STATUS, + OPTIMISM_PDAO, + OPTIMISM_DEPLOYER, + OPTIMISM_USDC, + OPTIMISM_DAI, + OPTIMISM_USDT, + OPTIMISM_LUSD +} from "script/utils/parameters/OptimismParameters.sol"; + +import { + OPTIMISM_SEPOLIA_DEPLOYER, + OPTIMISM_SEPOLIA_SYNTHETIX_ADDRESS_RESOLVER, + OPTIMISM_SEPOLIA_GELATO, + OPTIMISM_SEPOLIA_OPS, + OPTIMISM_SEPOLIA_FACTORY, + OPTIMISM_SEPOLIA_UNISWAP_UNIVERSAL_ROUTER, + OPTIMISM_SEPOLIA_UNISWAP_PERMIT2 +} from "script/utils/parameters/OptimismSepoliaParameters.sol"; + +/// @title Script to upgrade the Account implementation v2.1.4 -> v2.1.5 +/// @author JaredBorders (jaredborders@pm.me) + +/// @dev steps to deploy and verify on Optimism: +/// (1) load the variables in the .env file via `source .env` +/// (2) run `forge script script/upgrades/v2.1.5/Upgrade.s.sol:UpgradeAccountOptimism --rpc-url $ARCHIVE_NODE_URL_L2 --broadcast --verify -vvvv` +/// (3) Smart Margin Account Factory owner (i.e. Kwenta pDAO) will need to call `upgradeAccountImplementation` on the Factory with the address of the new Account implementation +contract UpgradeAccountOptimism is Script { + function run() public { + uint256 deployerPrivateKey = vm.envUint("DEPLOYER_PRIVATE_KEY"); + vm.startBroadcast(deployerPrivateKey); + + upgrade(); + + vm.stopBroadcast(); + } + + function upgrade() public returns (address implementation) { + IAddressResolver addressResolver = + IAddressResolver(OPTIMISM_SYNTHETIX_ADDRESS_RESOLVER); + + address marginAsset = addressResolver.getAddress({name: PROXY_SUSD}); + address perpsV2ExchangeRate = + addressResolver.getAddress({name: PERPS_V2_EXCHANGE_RATE}); + address futuresMarketManager = + addressResolver.getAddress({name: FUTURES_MARKET_MANAGER}); + address systemStatus = addressResolver.getAddress({name: SYSTEM_STATUS}); + + IAccount.AccountConstructorParams memory params = IAccount + .AccountConstructorParams({ + factory: OPTIMISM_FACTORY, + events: OPTIMISM_EVENTS, + marginAsset: marginAsset, + perpsV2ExchangeRate: perpsV2ExchangeRate, + futuresMarketManager: futuresMarketManager, + systemStatus: systemStatus, + gelato: OPTIMISM_GELATO, + ops: OPTIMISM_OPS, + settings: OPTIMISM_SETTINGS, + universalRouter: OPTIMISM_UNISWAP_UNIVERSAL_ROUTER, + permit2: OPTIMISM_UNISWAP_PERMIT2 + }); + + implementation = address(new Account(params)); + } +} diff --git a/src/Account.sol b/src/Account.sol index 07641598..36fe5efc 100644 --- a/src/Account.sol +++ b/src/Account.sol @@ -39,7 +39,7 @@ contract Account is IAccount, Auth, OpsReady { //////////////////////////////////////////////////////////////*/ /// @inheritdoc IAccount - bytes32 public constant VERSION = "2.1.4"; + bytes32 public constant VERSION = "2.1.5"; /// @notice tracking code used when modifying positions bytes32 internal constant TRACKING_CODE = "KWENTA"; diff --git a/test/upgrades/v2.1.5/Upgrade.t.sol b/test/upgrades/v2.1.5/Upgrade.t.sol new file mode 100644 index 00000000..f129dccd --- /dev/null +++ b/test/upgrades/v2.1.5/Upgrade.t.sol @@ -0,0 +1,304 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.18; + +import {Test} from "lib/forge-std/src/Test.sol"; + +import {UpgradeAccountOptimism} from "script/upgrades/v2.1.5/Upgrade.s.sol"; +import { + OPTIMISM_FACTORY, + OPTIMISM_PDAO +} from "script/utils/parameters/OptimismParameters.sol"; + +import {Factory} from "src/Factory.sol"; +import {IAccount as OldAccount} from + "test/upgrades/v2.1.4/interfaces/IAccount.sol"; +import {IAccount as NewAccount} from + "test/upgrades/v2.1.5/interfaces/IAccount.sol"; +import {IERC20} from "src/interfaces/token/IERC20.sol"; +import {ISynth} from "test/utils/interfaces/ISynth.sol"; + +import {IAddressResolver} from "test/utils/interfaces/IAddressResolver.sol"; +import {ADDRESS_RESOLVER, PROXY_SUSD} from "test/utils/Constants.sol"; + +import {AccountExposed} from "test/utils/AccountExposed.sol"; +import {IAccount} from "src/interfaces/IAccount.sol"; + +contract UpgradeTest is Test { + AccountExposed private accountExposed; + // BLOCK_NUMBER_UPGRADE corresponds to Optimism network state @ (Oct-01-2024 05:32:21 PM +UTC) + // hard coded addresses are only guaranteed for this block + uint256 private constant BLOCK_NUMBER_UPGRADE = 126_102_582; + + address private constant DELEGATE = address(0xDE1A6A7E); + + /*////////////////////////////////////////////////////////////// + SLOTS + //////////////////////////////////////////////////////////////*/ + + uint256 internal constant OWNER_SLOT = 0; + uint256 internal constant DELEGATES_SLOT = 1; + uint256 internal constant COMMITTED_MARGIN_SLOT = 21; + uint256 internal constant CONDITIONAL_ORDER_ID_SLOT = 22; + uint256 internal constant CONDITIONAL_ORDERS_SLOT = 23; + uint256 internal constant LOCKED_SLOT = 24; + + /*////////////////////////////////////////////////////////////// + V2.1.5 IMPLEMENTATION + //////////////////////////////////////////////////////////////*/ + + address private NEW_IMPLEMENTATION; + + /*////////////////////////////////////////////////////////////// + V2.1.4 ACTIVE ACCOUNT + //////////////////////////////////////////////////////////////*/ + + address private activeAccount; + + /*////////////////////////////////////////////////////////////// + SETUP + //////////////////////////////////////////////////////////////*/ + + function setUp() public { + vm.rollFork(BLOCK_NUMBER_UPGRADE); + + // create active v2.1.3 account + activeAccount = initAccountForStateTesting(); + + // define Setup contract used for upgrades + UpgradeAccountOptimism upgradeAccountOptimism = + new UpgradeAccountOptimism(); + + // deploy v2.1.5 implementation + address implementationAddr = upgradeAccountOptimism.upgrade(); + NEW_IMPLEMENTATION = payable(implementationAddr); + + // Test mutable storage slots setup + IAccount.AccountConstructorParams memory params = IAccount + .AccountConstructorParams( + address(0), + address(0), + address(0), + address(0), + address(0), + address(0), + address(0), + address(0), + address(0), + address(0), + address(0) + ); + + accountExposed = new AccountExposed(params); + } + + /*////////////////////////////////////////////////////////////// + TESTS + //////////////////////////////////////////////////////////////*/ + + function test_Deployed_Account_Version() public { + (, bytes memory response) = + activeAccount.call(abi.encodeWithSignature("VERSION()")); + (bytes32 version) = abi.decode(response, (bytes32)); + assertEq(version, "2.1.4", "wrong version"); + } + + function test_Upgrade_v2_1_4() public { + /** + * RECORD ALL STATE PRIOR TO UPGRADE + */ + + // fetch commited margin from Active Account + (, bytes memory response) = + activeAccount.call(abi.encodeWithSignature("committedMargin()")); + (uint256 commitedMargin) = abi.decode(response, (uint256)); + assertGt(commitedMargin, 0, "commitedMargin is zero"); + + // fetch current conditional order id from Active Account + (, response) = + activeAccount.call(abi.encodeWithSignature("conditionalOrderId()")); + (uint256 conditionalOrderId) = abi.decode(response, (uint256)); + assertGt(conditionalOrderId, 0, "conditionalOrderId is zero"); + + // fetch current conditional orders from Active Account + OldAccount.ConditionalOrder[] memory orders = + new OldAccount.ConditionalOrder[](conditionalOrderId); + for (uint256 index = 0; index < conditionalOrderId; index++) { + (, response) = activeAccount.call( + abi.encodeWithSignature("getConditionalOrder(uint256)", index) + ); + (OldAccount.ConditionalOrder memory order) = + abi.decode(response, (OldAccount.ConditionalOrder)); + orders[index] = order; + } + + // fetch owner from Active Account + (, response) = activeAccount.call(abi.encodeWithSignature("owner()")); + (address owner) = abi.decode(response, (address)); + assert(owner != address(0)); + + // fetch delegate from Active Account + (, response) = activeAccount.call( + abi.encodeWithSignature("delegates(address)", DELEGATE) + ); + assertEq(true, abi.decode(response, (bool)), "delegate missmatch"); + + /** + * EXECUTE UPGRADE + */ + + // upgrade Active Account to v2.1.5 + vm.prank(OPTIMISM_PDAO); + Factory(OPTIMISM_FACTORY).upgradeAccountImplementation( + address(NEW_IMPLEMENTATION) + ); + + /** + * VERIFY VERSION DID CHANGE + */ + (, response) = activeAccount.call(abi.encodeWithSignature("VERSION()")); + (bytes32 version) = abi.decode(response, (bytes32)); + assert(version != "2.1.3"); + + /** + * CHECK STATE DID NOT CHANGE + */ + (, response) = + activeAccount.call(abi.encodeWithSignature("committedMargin()")); + assertEq( + commitedMargin, + abi.decode(response, (uint256)), + "commitedMargin missmatch" + ); + + // fetch current conditional order id from Active Account + (, response) = + activeAccount.call(abi.encodeWithSignature("conditionalOrderId()")); + assertEq( + conditionalOrderId, + abi.decode(response, (uint256)), + "conditionalOrderId missmatch" + ); + + // fetch current conditional orders from Active Account + for (uint256 index = 0; index < conditionalOrderId; index++) { + (, response) = activeAccount.call( + abi.encodeWithSignature("getConditionalOrder(uint256)", index) + ); + assertEq( + orders[index].marketKey, + abi.decode(response, (NewAccount.ConditionalOrder)).marketKey, + "conditionalOrder missmatch" + ); + } + + // fetch owner from Active Account + (, response) = activeAccount.call(abi.encodeWithSignature("owner()")); + assertEq(owner, abi.decode(response, (address)), "owner missmatch"); + + // fetch delegate from Active Account + (, response) = activeAccount.call( + abi.encodeWithSignature("delegates(address)", DELEGATE) + ); + assertEq(true, abi.decode(response, (bool)), "delegate missmatch"); + } + + function test_owner_slot() public { + /// @dev slot should NEVER change + assertEq( + accountExposed.expose_owner_slot(), OWNER_SLOT, "slot missmatch" + ); + } + + function test_delegates_slot() public { + /// @dev slot should NEVER change + assertEq( + accountExposed.expose_delegates_slot(), + DELEGATES_SLOT, + "slot missmatch" + ); + } + + function test_committedMargin_slot() public { + /// @dev slot should NEVER change + assertEq( + accountExposed.expose_committedMargin_slot(), + COMMITTED_MARGIN_SLOT, + "slot missmatch" + ); + } + + function test_conditionalOrderId_slot() public { + /// @dev slot should NEVER change + assertEq( + accountExposed.expose_conditionalOrderId_slot(), + CONDITIONAL_ORDER_ID_SLOT, + "slot missmatch" + ); + } + + function test_conditionalOrders_slot() public { + /// @dev slot should NEVER change + assertEq( + accountExposed.expose_conditionalOrders_slot(), + CONDITIONAL_ORDERS_SLOT, + "slot missmatch" + ); + } + + function test_locked_slot() public { + /// @dev slot should NEVER change + assertEq( + accountExposed.expose_locked_slot(), LOCKED_SLOT, "slot missmatch" + ); + } + + /*////////////////////////////////////////////////////////////// + UTILITIES + //////////////////////////////////////////////////////////////*/ + + function initAccountForStateTesting() internal returns (address) { + uint256 amount = 10_000 ether; + + /// @notice create account + address payable accountAddress = Factory(OPTIMISM_FACTORY).newAccount(); + + /// @notice mint sUSD to this contract + address issuer = IAddressResolver(ADDRESS_RESOLVER).getAddress("Issuer"); + ISynth synthsUSD = + ISynth(IAddressResolver(ADDRESS_RESOLVER).getAddress("SynthsUSD")); + vm.prank(issuer); + synthsUSD.issue(address(this), amount); + + /// @notice fund SM account with eth and sUSD (i.e. margin) + vm.deal(accountAddress, 1 ether); + IERC20(IAddressResolver(ADDRESS_RESOLVER).getAddress(PROXY_SUSD)) + .approve(address(accountAddress), type(uint256).max); + OldAccount.Command[] memory commands = new OldAccount.Command[](1); + commands[0] = OldAccount.Command.ACCOUNT_MODIFY_MARGIN; + bytes[] memory inputs = new bytes[](1); + inputs[0] = abi.encode(amount); + OldAccount(accountAddress).execute(commands, inputs); + + /// @notice create/submit conditional order which lock up margin + commands[0] = OldAccount.Command.GELATO_PLACE_CONDITIONAL_ORDER; + bytes32 marketKey = bytes32("sETHPERP"); + inputs[0] = abi.encode( + marketKey, + int256(amount / 2), + int256(1 ether), + 10_000 ether, + OldAccount.ConditionalOrderTypes.LIMIT, + 1000 ether, + true + ); + OldAccount(accountAddress).execute(commands, inputs); + + /// @notice add delegate + (bool s,) = accountAddress.call( + abi.encodeWithSignature("addDelegate(address)", DELEGATE) + ); + assertEq(s, true, "addDelegate failed"); + + return accountAddress; + } +} diff --git a/test/upgrades/v2.1.5/interfaces/IAccount.sol b/test/upgrades/v2.1.5/interfaces/IAccount.sol new file mode 100644 index 00000000..eaa43f81 --- /dev/null +++ b/test/upgrades/v2.1.5/interfaces/IAccount.sol @@ -0,0 +1,244 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.18; + +import {IPerpsV2MarketConsolidated} from + "src/interfaces/synthetix/IPerpsV2MarketConsolidated.sol"; + +/// @title Kwenta Smart Margin Account v2.1.5 Implementation Interface +/// @author JaredBorders (jaredborders@pm.me), JChiaramonte7 (jeremy@bytecode.llc) +interface IAccount { + /*/////////////////////////////////////////////////////////////// + Types + ///////////////////////////////////////////////////////////////*/ + + /// @notice Command Flags used to decode commands to execute + /// @dev under the hood ACCOUNT_MODIFY_MARGIN = 0, ACCOUNT_WITHDRAW_ETH = 1 + enum Command { + ACCOUNT_MODIFY_MARGIN, // 0 + ACCOUNT_WITHDRAW_ETH, + PERPS_V2_MODIFY_MARGIN, + PERPS_V2_WITHDRAW_ALL_MARGIN, + PERPS_V2_SUBMIT_ATOMIC_ORDER, + PERPS_V2_SUBMIT_DELAYED_ORDER, // 5 + PERPS_V2_SUBMIT_OFFCHAIN_DELAYED_ORDER, + PERPS_V2_CLOSE_POSITION, + PERPS_V2_SUBMIT_CLOSE_DELAYED_ORDER, + PERPS_V2_SUBMIT_CLOSE_OFFCHAIN_DELAYED_ORDER, + PERPS_V2_CANCEL_DELAYED_ORDER, // 10 + PERPS_V2_CANCEL_OFFCHAIN_DELAYED_ORDER, + GELATO_PLACE_CONDITIONAL_ORDER, + GELATO_CANCEL_CONDITIONAL_ORDER, + UNISWAP_V3_SWAP, + PERMIT2_PERMIT, // 15 + PERPS_V2_SET_MIN_KEEPER_FEE + } + + /// @notice denotes conditional order types for code clarity + /// @dev under the hood LIMIT = 0, STOP = 1 + enum ConditionalOrderTypes { + LIMIT, + STOP + } + + /// @notice denotes conditional order cancelled reasons for code clarity + /// @dev under the hood CONDITIONAL_ORDER_CANCELLED_BY_USER = 0, CONDITIONAL_ORDER_CANCELLED_NOT_REDUCE_ONLY = 1 + enum ConditionalOrderCancelledReason { + CONDITIONAL_ORDER_CANCELLED_BY_USER, + CONDITIONAL_ORDER_CANCELLED_NOT_REDUCE_ONLY + } + + /// @notice denotes what oracle is used for price when executing conditional orders + /// @dev under the hood PYTH = 0, CHAINLINK = 1 + enum PriceOracleUsed { + PYTH, + CHAINLINK + } + + /// @param factory: address of the Smart Margin Account Factory + /// @param events: address of the contract used by all accounts for emitting events + /// @param marginAsset: address of the Synthetix ProxyERC20sUSD contract used as the margin asset + /// @param perpsV2ExchangeRate: address of the Synthetix PerpsV2ExchangeRate + /// @param futuresMarketManager: address of the Synthetix FuturesMarketManager + /// @param systemStatus: address of the Synthetix SystemStatus + /// @param gelato: address of Gelato + /// @param ops: address of Ops + /// @param settings: address of contract used to store global settings + /// @param universalRouter: address of Uniswap's Universal Router + /// @param permit2: address of Uniswap's Permit2 + struct AccountConstructorParams { + address factory; + address events; + address marginAsset; + address perpsV2ExchangeRate; + address futuresMarketManager; + address systemStatus; + address gelato; + address ops; + address settings; + address universalRouter; + address permit2; + } + + /// marketKey: Synthetix PerpsV2 Market id/key + /// marginDelta: amount of margin to deposit or withdraw; positive indicates deposit, negative withdraw + /// sizeDelta: denoted in market currency (i.e. ETH, BTC, etc), size of Synthetix PerpsV2 position + /// targetPrice: limit or stop price target needing to be met to submit Synthetix PerpsV2 order + /// gelatoTaskId: unqiue taskId from gelato necessary for cancelling conditional orders + /// conditionalOrderType: conditional order type to determine conditional order fill logic + /// desiredFillPrice: desired price to fill Synthetix PerpsV2 order at execution time + /// reduceOnly: if true, only allows position's absolute size to decrease + struct ConditionalOrder { + bytes32 marketKey; + int256 marginDelta; + int256 sizeDelta; + uint256 targetPrice; + bytes32 gelatoTaskId; + ConditionalOrderTypes conditionalOrderType; + uint256 desiredFillPrice; + bool reduceOnly; + } + /// @dev see example below elucidating targtPrice vs desiredFillPrice: + /// 1. targetPrice met (ex: targetPrice = X) + /// 2. account submits delayed order to Synthetix PerpsV2 with desiredFillPrice = Y + /// 3. keeper executes Synthetix PerpsV2 order after delay period + /// 4. if current market price defined by Synthetix PerpsV2 + /// after delay period satisfies desiredFillPrice order is filled + + /*////////////////////////////////////////////////////////////// + ERRORS + //////////////////////////////////////////////////////////////*/ + + /// @notice thrown when commands length does not equal inputs length + error LengthMismatch(); + + /// @notice thrown when Command given is not valid + error InvalidCommandType(uint256 commandType); + + /// @notice thrown when conditional order type given is not valid due to zero sizeDelta + error ZeroSizeDelta(); + + /// @notice exceeds useable margin + /// @param available: amount of useable margin asset + /// @param required: amount of margin asset required + error InsufficientFreeMargin(uint256 available, uint256 required); + + /// @notice call to transfer ETH on withdrawal fails + error EthWithdrawalFailed(); + + /// @notice base price from the oracle was invalid + /// @dev Rate can be invalid either due to: + /// 1. Returned as invalid from ExchangeRates - due to being stale or flagged by oracle + /// 2. Out of deviation bounds w.r.t. to previously stored rate + /// 3. if there is no valid stored rate, w.r.t. to previous 3 oracle rates + /// 4. Price is zero + error InvalidPrice(); + + /// @notice thrown when account execution has been disabled in the settings contract + error AccountExecutionDisabled(); + + /// @notice thrown when a call attempts to reenter the protected function + error Reentrancy(); + + /// @notice thrown when token swap attempted with invalid token (i.e. token that is not whitelisted) + /// @param tokenIn: token attempting to swap from + /// @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 call to set/updates the min keeper fee fails + error SetMinKeeperFeeFailed(); + + /*////////////////////////////////////////////////////////////// + VIEWS + //////////////////////////////////////////////////////////////*/ + + /// @notice returns the version of the Account + function VERSION() external view returns (bytes32); + + /// @return returns the amount of margin locked for future events (i.e. conditional orders) + function committedMargin() external view returns (uint256); + + /// @return returns current conditional order id + function conditionalOrderId() external view returns (uint256); + + /// @notice get delayed order data from Synthetix PerpsV2 + /// @dev call reverts if _marketKey is invalid + /// @param _marketKey: key for Synthetix PerpsV2 Market + /// @return delayed order struct defining delayed order (will return empty struct if no delayed order exists) + function getDelayedOrder(bytes32 _marketKey) + external + returns (IPerpsV2MarketConsolidated.DelayedOrder memory); + + /// @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); + + /// @notice the current withdrawable or usable balance + /// @return free margin amount + function freeMargin() external view returns (uint256); + + /// @notice get up-to-date position data from Synthetix PerpsV2 + /// @param _marketKey: key for Synthetix PerpsV2 Market + /// @return position struct defining current position + function getPosition(bytes32 _marketKey) + external + returns (IPerpsV2MarketConsolidated.Position memory); + + /// @notice conditional order id mapped to conditional order + /// @param _conditionalOrderId: id of conditional order + /// @return conditional order + function getConditionalOrder(uint256 _conditionalOrderId) + external + view + returns (ConditionalOrder memory); + + /// @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 orderFlowFee order flow fee expected for the given market and size delta + function getExpectedOrderFlowFee( + address _market, + int256 _sizeDelta, + uint256 _desiredFillPrice + ) external view returns (uint256 orderFlowFee); + + /*////////////////////////////////////////////////////////////// + MUTATIVE + //////////////////////////////////////////////////////////////*/ + + /// @notice sets the initial owner of the account + /// @dev only called once by the factory on account creation + /// @param _owner: address of the owner + function setInitialOwnership(address _owner) external; + + /// @notice executes commands along with provided inputs + /// @param _commands: array of commands, each represented as an enum + /// @param _inputs: array of byte strings containing abi encoded inputs for each command + function execute(Command[] calldata _commands, bytes[] calldata _inputs) + external + payable; + + /// @notice execute queued conditional order + /// @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; +}