diff --git a/test/upgrades/v2.1.0/interfaces/IAccount.sol b/test/upgrades/v2.1.0/interfaces/IAccount.sol index fd8e1a8a..58cd6502 100644 --- a/test/upgrades/v2.1.0/interfaces/IAccount.sol +++ b/test/upgrades/v2.1.0/interfaces/IAccount.sol @@ -16,20 +16,20 @@ interface IAccount { enum Command { ACCOUNT_MODIFY_MARGIN, // 0 ACCOUNT_WITHDRAW_ETH, - UNISWAP_V3_SWAP, - PERMIT2_PERMIT, PERPS_V2_MODIFY_MARGIN, - PERPS_V2_WITHDRAW_ALL_MARGIN, // 5 + PERPS_V2_WITHDRAW_ALL_MARGIN, PERPS_V2_SUBMIT_ATOMIC_ORDER, - PERPS_V2_SUBMIT_DELAYED_ORDER, + PERPS_V2_SUBMIT_DELAYED_ORDER, // 5 PERPS_V2_SUBMIT_OFFCHAIN_DELAYED_ORDER, PERPS_V2_CLOSE_POSITION, - PERPS_V2_SUBMIT_CLOSE_DELAYED_ORDER, // 10 + PERPS_V2_SUBMIT_CLOSE_DELAYED_ORDER, PERPS_V2_SUBMIT_CLOSE_OFFCHAIN_DELAYED_ORDER, - PERPS_V2_CANCEL_DELAYED_ORDER, + PERPS_V2_CANCEL_DELAYED_ORDER, // 10 PERPS_V2_CANCEL_OFFCHAIN_DELAYED_ORDER, GELATO_PLACE_CONDITIONAL_ORDER, - GELATO_CANCEL_CONDITIONAL_ORDER // 15 + GELATO_CANCEL_CONDITIONAL_ORDER, + UNISWAP_V3_SWAP, + PERMIT2_PERMIT // 15 } /// @notice denotes conditional order types for code clarity diff --git a/test/upgrades/v2.1.1/Upgrade.t.sol b/test/upgrades/v2.1.1/Upgrade.t.sol new file mode 100644 index 00000000..21e1da73 --- /dev/null +++ b/test/upgrades/v2.1.1/Upgrade.t.sol @@ -0,0 +1,232 @@ +// 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.1/Upgrade.s.sol"; +import { + OPTIMISM_FACTORY, + OPTIMISM_PDAO, + OPTIMISM_UNISWAP_PERMIT2 +} from "script/utils/parameters/OptimismParameters.sol"; + +import {Factory} from "src/Factory.sol"; +import {IAccount as OldAccount} from + "test/upgrades/v2.1.0/interfaces/IAccount.sol"; +import {IAccount as NewAccount} from + "test/upgrades/v2.1.1/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"; + +contract UpgradeTest is Test { + // BLOCK_NUMBER_UPGRADE corresponds to Optimism network state @ Sep-26-2023 08:39:37 PM +UTC + // hard coded addresses are only guaranteed for this block + uint256 private constant BLOCK_NUMBER_UPGRADE = 110_081_000; + + address private constant DELEGATE = address(0xDE1A6A7E); + + /*////////////////////////////////////////////////////////////// + V2.1.0 IMPLEMENTATION + //////////////////////////////////////////////////////////////*/ + + address private constant OLD_IMPLEMENTATION = + 0x83E13069aA457778ca349E0128927B417A2c2B3f; + + /*////////////////////////////////////////////////////////////// + V2.1.1 IMPLEMENTATION + //////////////////////////////////////////////////////////////*/ + + address private NEW_IMPLEMENTATION; + + /*////////////////////////////////////////////////////////////// + V2.1.1 ACTIVE ACCOUNT + //////////////////////////////////////////////////////////////*/ + + address private activeAccount; + + /*////////////////////////////////////////////////////////////// + SETUP + //////////////////////////////////////////////////////////////*/ + + function setUp() public { + vm.rollFork(BLOCK_NUMBER_UPGRADE); + + // create active v2.1.0 account + activeAccount = initAccountForStateTesting(); + + // define Setup contract used for upgrades + UpgradeAccountOptimism upgradeAccountOptimism = + new UpgradeAccountOptimism(); + + // deploy v2.1.1 implementation + address implementationAddr = upgradeAccountOptimism.upgrade(); + NEW_IMPLEMENTATION = payable(implementationAddr); + } + + /*////////////////////////////////////////////////////////////// + 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.0", "wrong version"); + } + + function test_Upgrade() 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.0 + 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)); + assertEq(version, "2.1.1", "wrong version"); + + /** + * 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"); + } + + /*////////////////////////////////////////////////////////////// + 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.1/interfaces/IAccount.sol b/test/upgrades/v2.1.1/interfaces/IAccount.sol new file mode 100644 index 00000000..31f15c77 --- /dev/null +++ b/test/upgrades/v2.1.1/interfaces/IAccount.sol @@ -0,0 +1,234 @@ +// 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.1 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); + + /*////////////////////////////////////////////////////////////// + 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; +}