From fb567590ac13ad8139c19aed5440c2530ba9df74 Mon Sep 17 00:00:00 2001 From: JChiaramonte7 Date: Tue, 1 Oct 2024 11:14:07 -0400 Subject: [PATCH 1/6] add try/catch return to margin pulling call --- src/Account.sol | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/Account.sol b/src/Account.sol index ca860d80..d1ca6ef0 100644 --- a/src/Account.sol +++ b/src/Account.sol @@ -617,6 +617,20 @@ contract Account is IAccount, Auth, OpsReady { IPerpsV2MarketConsolidated(_market).transferMargin(_amount); } + /// @notice deposit/withdraw margin to/from a Synthetix PerpsV2 Market + /// @param _market: address of market + /// @param _amount: amount of margin to deposit/withdraw + function _perpsV2ModifyMarginNoExternalRevert(address _market, int256 _amount) internal returns (bool) { + if (_amount > 0) { + _sufficientMargin(_amount); + } + try IPerpsV2MarketConsolidated(_market).transferMargin(_amount) { + return true; + } catch { + return false; + } + } + /// @notice withdraw margin from market back to this account /// @dev this will *not* fail if market has zero margin function _perpsV2WithdrawAllMargin(address _market) internal { @@ -657,7 +671,9 @@ contract Account is IAccount, Auth, OpsReady { /// @dev this will revert if market does not /// have sufficient available margin - _perpsV2ModifyMargin(_market, -int256(difference)); + bool success = _perpsV2ModifyMarginNoExternalRevert(_market, -int256(difference)); + /// @dev breakout of fee impose if market reverts (max leverage scenario) + if (!success) return; } // impose fee on account from account margin From d9d156c9ec36ba20ee0ada554c38f66217e0255f Mon Sep 17 00:00:00 2001 From: JChiaramonte7 Date: Tue, 1 Oct 2024 13:12:52 -0400 Subject: [PATCH 2/6] add tests to verify hotfix --- test/integration/orderFlowFee.behavior.t.sol | 107 +++++++++++++++++++ 1 file changed, 107 insertions(+) diff --git a/test/integration/orderFlowFee.behavior.t.sol b/test/integration/orderFlowFee.behavior.t.sol index 23810b12..e9fe5495 100644 --- a/test/integration/orderFlowFee.behavior.t.sol +++ b/test/integration/orderFlowFee.behavior.t.sol @@ -410,6 +410,113 @@ contract OrderFlowFeeTest is Test, ConsolidatedEvents { assertEq(treasuryPreBalance, treasuryPostBalance); } + /// Verifies that transaction does not revert and charges a fee if there is sufficient margin to cover for orderFlowFee + /// @dev Synthetix makes transaction reverts if the resulting position is too large, outside the max leverage, or is liquidating. + function test_imposeOrderFlowFee_on_valid_close() public { + uint256 treasuryPreBalance = sUSD.balanceOf(settings.TREASURY()); + + /// @dev Ensures first trade doesn't charge a fee + uint256 testOrderFlowFee = 0; // 0% + settings.setOrderFlowFee(testOrderFlowFee); + + // create a long position in the ETH market + address market = getMarketAddressFromKey(sETHPERP); + + /// Deposit all margin so that account has no margin to cover orderFlowFee + int256 marginDelta = int256(AMOUNT); // 10_000 dollars + int256 sizeDelta = 5 ether; // ~ Estimated notional $9,396.3052993 + // Leverage is .94x + + (uint256 desiredFillPrice,) = + IPerpsV2MarketConsolidated(market).assetPrice(); + + submitAtomicOrder(sETHPERP, marginDelta, sizeDelta, desiredFillPrice); + + // Margin after fees charged: ~ $7182.31 + + // Set orderflow fee high to easily test that transaction fails if neither account or market has sufficient margin to cover for fees + testOrderFlowFee = 10_000; // 10% + settings.setOrderFlowFee(testOrderFlowFee); + + // Fee is 90% of notional = $936 fee + // Margin would be 7182.31 - 936 = 6246.31 + // Remaining leverage is 1.5x + // Resulting in valid fee charge + + // define close position order + IAccount.Command[] memory commands = new IAccount.Command[](1); + commands[0] = IAccount.Command.PERPS_V2_CLOSE_POSITION; + bytes[] memory inputs = new bytes[](1); + desiredFillPrice -= 1 ether; + inputs[0] = abi.encode(market, desiredFillPrice); + + // close position + account.execute(commands, inputs); + + // Assert position is closed + IPerpsV2MarketConsolidated.Position memory position = + account.getPosition(sETHPERP); + assertEq(0, position.size); + + uint256 treasuryPostBalance = sUSD.balanceOf(settings.TREASURY()); + + // Assert treasury balance is now greater than before + assertLt(treasuryPreBalance, treasuryPostBalance); + } + + /// Verifies that transaction does not revert and does not charge a fee if Synthetix trade reverts (due to max leverage etc..) + /// @dev Synthetix makes transaction reverts if the resulting position is too large, outside the max leverage, or is liquidating. + function test_imposeOrderFlowFee_market_revert_bypass() public { + uint256 treasuryPreBalance = sUSD.balanceOf(settings.TREASURY()); + + /// @dev Ensures first trade doesn't charge a fee + uint256 testOrderFlowFee = 0; // 0% + settings.setOrderFlowFee(testOrderFlowFee); + + // create a long position in the ETH market + address market = getMarketAddressFromKey(sETHPERP); + + /// Deposit all margin so that account has no margin to cover orderFlowFee + int256 marginDelta = int256(AMOUNT); // 10_000 dollars + int256 sizeDelta = 5 ether; // ~ Estimated notional $9,396.3052993 + // Leverage is .94x + + (uint256 desiredFillPrice,) = + IPerpsV2MarketConsolidated(market).assetPrice(); + + submitAtomicOrder(sETHPERP, marginDelta, sizeDelta, desiredFillPrice); + + // Margin after fees charged: ~ $7182.31 + + // Set orderflow fee high to easily test that transaction fails if neither account or market has sufficient margin to cover for fees + testOrderFlowFee = 75_000; // 75% + settings.setOrderFlowFee(testOrderFlowFee); + + // Fee is 75% of notional = $7,047 fee + // Margin would be $7182.31 - $7,047 = $135.31 + // Resulting in 69x leverage + + // define close position order + IAccount.Command[] memory commands = new IAccount.Command[](1); + commands[0] = IAccount.Command.PERPS_V2_CLOSE_POSITION; + bytes[] memory inputs = new bytes[](1); + desiredFillPrice -= 1 ether; + inputs[0] = abi.encode(market, desiredFillPrice); + + // close position + account.execute(commands, inputs); + + // Assert position is closed + IPerpsV2MarketConsolidated.Position memory position = + account.getPosition(sETHPERP); + assertEq(0, position.size); + + uint256 treasuryPostBalance = sUSD.balanceOf(settings.TREASURY()); + + // Assert that no overflowfee was distributed + assertEq(treasuryPreBalance, treasuryPostBalance); + } + /// Verifies that the correct Event is emitted with correct fee value function test_imposeOrderFlowFee_event() public { // create a long position in the ETH market From 8b3c60c2ca6ffe0f3bf6911f9ec29a89c949cf0e Mon Sep 17 00:00:00 2001 From: JChiaramonte7 Date: Tue, 1 Oct 2024 13:15:07 -0400 Subject: [PATCH 3/6] forge fmt --- src/Account.sol | 11 ++++++++--- test/integration/orderFlowFee.behavior.t.sol | 4 ++-- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/Account.sol b/src/Account.sol index d1ca6ef0..07641598 100644 --- a/src/Account.sol +++ b/src/Account.sol @@ -620,7 +620,10 @@ contract Account is IAccount, Auth, OpsReady { /// @notice deposit/withdraw margin to/from a Synthetix PerpsV2 Market /// @param _market: address of market /// @param _amount: amount of margin to deposit/withdraw - function _perpsV2ModifyMarginNoExternalRevert(address _market, int256 _amount) internal returns (bool) { + function _perpsV2ModifyMarginNoExternalRevert( + address _market, + int256 _amount + ) internal returns (bool) { if (_amount > 0) { _sufficientMargin(_amount); } @@ -671,7 +674,9 @@ contract Account is IAccount, Auth, OpsReady { /// @dev this will revert if market does not /// have sufficient available margin - bool success = _perpsV2ModifyMarginNoExternalRevert(_market, -int256(difference)); + bool success = _perpsV2ModifyMarginNoExternalRevert( + _market, -int256(difference) + ); /// @dev breakout of fee impose if market reverts (max leverage scenario) if (!success) return; } @@ -958,7 +963,7 @@ contract Account is IAccount, Auth, OpsReady { execAddress: address(this), execData: abi.encodeCall( this.executeConditionalOrder, conditionalOrderId - ), + ), moduleData: moduleData, feeToken: ETH }); diff --git a/test/integration/orderFlowFee.behavior.t.sol b/test/integration/orderFlowFee.behavior.t.sol index e9fe5495..1c20d5d2 100644 --- a/test/integration/orderFlowFee.behavior.t.sol +++ b/test/integration/orderFlowFee.behavior.t.sol @@ -454,7 +454,7 @@ contract OrderFlowFeeTest is Test, ConsolidatedEvents { account.execute(commands, inputs); // Assert position is closed - IPerpsV2MarketConsolidated.Position memory position = + IPerpsV2MarketConsolidated.Position memory position = account.getPosition(sETHPERP); assertEq(0, position.size); @@ -507,7 +507,7 @@ contract OrderFlowFeeTest is Test, ConsolidatedEvents { account.execute(commands, inputs); // Assert position is closed - IPerpsV2MarketConsolidated.Position memory position = + IPerpsV2MarketConsolidated.Position memory position = account.getPosition(sETHPERP); assertEq(0, position.size); From 58d7041386bfb4e3084e3a8af9221ac40407eea2 Mon Sep 17 00:00:00 2001 From: JChiaramonte7 Date: Tue, 1 Oct 2024 13:42:49 -0400 Subject: [PATCH 4/6] 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; +} From c54be77eef38ab2770b34f03e2144fceccfcd1a9 Mon Sep 17 00:00:00 2001 From: JChiaramonte7 Date: Tue, 1 Oct 2024 13:47:01 -0400 Subject: [PATCH 5/6] fix version test --- test/unit/Account.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/Account.t.sol b/test/unit/Account.t.sol index d89b4751..fd8eebf5 100644 --- a/test/unit/Account.t.sol +++ b/test/unit/Account.t.sol @@ -119,7 +119,7 @@ contract AccountTest is Test, ConsolidatedEvents { //////////////////////////////////////////////////////////////*/ function test_GetVersion() public view { - assert(account.VERSION() == "2.1.4"); + assert(account.VERSION() == "2.1.5"); } function test_GetTrackingCode() public view { From b13bb68677bb6555bd37d73df16d55ccb74e490b Mon Sep 17 00:00:00 2001 From: JChiaramonte7 Date: Tue, 1 Oct 2024 14:10:14 -0400 Subject: [PATCH 6/6] deploy upgrade 2.1.5 --- README.md | 13 ++++++++----- deploy-addresses/optimism.json | 2 +- script/utils/parameters/OptimismParameters.sol | 8 ++++---- test/upgrades/v2.1.5/Upgrade.t.sol | 6 +++--- 4 files changed, 16 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index e9f7f605..65d0e5ed 100644 --- a/README.md +++ b/README.md @@ -178,12 +178,15 @@ forge test --fork-url $(grep ARCHIVE_NODE_URL_L2 .env | cut -d '=' -f2) --match- 8. Call `Factory.upgradeAccountImplementation` with new `Account` address (can be done on etherscan) > Only factory owner can do this 9. Update `./deploy-addresses/optimism-goerli.json` with new `Account` address -10. Ensure testnet accounts are updated and functional (ensure state is correct) -11. Run script and deploy to Mainnet -12. Call `Factory.upgradeAccountImplementation` with new `Account` address (can be done on etherscan) +10. Update `utils/parameters/OptimismGoerliParameters.sol` with new `Account` address +11. Ensure testnet accounts are updated and functional (ensure state is correct) +12. Run script and deploy to Mainnet +13. Call `Factory.upgradeAccountImplementation` with new `Account` address (can be done on etherscan) > Only factory owner can do this (pDAO) -13. Update `./deploy-addresses/optimism.json` with new `Account` address -14. Ensure mainnet accounts are updated and functional (ensure state is correct) +14. Update `./deploy-addresses/optimism.json` with new `Account` address +15. Update `utils/parameters/OptimismParameters.sol` with new `Account` address +16. Double-check and update any other fields necessary on Parameters constant file. (for example if there is a new deployer address) +17. Ensure mainnet accounts are updated and functional (ensure state is correct) ## External Conditional Order Executors > As of SM v2.1.0, public actors can execute conditional orders and receive a fee for doing so diff --git a/deploy-addresses/optimism.json b/deploy-addresses/optimism.json index 065d97c3..935dff89 100644 --- a/deploy-addresses/optimism.json +++ b/deploy-addresses/optimism.json @@ -1,6 +1,6 @@ { "prod": { - "Account": "0x0f716Fc517955863824CD9317603E4795EDfffb4", + "Account": "0xbada5ec9fa0568e0cd9D252a0744E6b6b52E438C", "Events": "0x6B32d15a6Cb77ea227A6Fb19532b2de542c45AC6", "Factory": "0x8234F990b149Ae59416dc260305E565e5DAfEb54", "Settings": "0xf36003a5dd0B17D51ca1525857dEf220E579447D" diff --git a/script/utils/parameters/OptimismParameters.sol b/script/utils/parameters/OptimismParameters.sol index 3ec9fba8..c7f01969 100644 --- a/script/utils/parameters/OptimismParameters.sol +++ b/script/utils/parameters/OptimismParameters.sol @@ -4,8 +4,8 @@ pragma solidity 0.8.18; /// @dev for Synthetix addresses see: /// https://github.com/Synthetixio/synthetix-docs/blob/master/content/addresses.md#mainnet-optimism-l2 -// v2.1.3 deployer -address constant OPTIMISM_DEPLOYER = 0x12d970154Ac171293323f20757130d5731850deB; +// v2.1.5 deployer +address constant OPTIMISM_DEPLOYER = 0x264bF33f2442001dC6bE0c3FC777df5495b8A5e7; address constant OPTIMISM_PDAO = 0xe826d43961a87fBE71C91d9B73F7ef9b16721C07; @@ -16,9 +16,9 @@ address constant OPTIMISM_GELATO = 0x01051113D81D7d6DA508462F2ad6d7fD96cF42Ef; address constant OPTIMISM_OPS = 0x340759c8346A1E6Ed92035FB8B6ec57cE1D82c2c; -// v2.1.4 +// v2.1.5 address constant OPTIMISM_IMPLEMENTATION = - 0x0f716Fc517955863824CD9317603E4795EDfffb4; + 0xbada5ec9fa0568e0cd9D252a0744E6b6b52E438C; // released with v2.1.4 implementation (used by v2.1.*) address constant OPTIMISM_EVENTS = 0x6B32d15a6Cb77ea227A6Fb19532b2de542c45AC6; diff --git a/test/upgrades/v2.1.5/Upgrade.t.sol b/test/upgrades/v2.1.5/Upgrade.t.sol index f129dccd..4be61f24 100644 --- a/test/upgrades/v2.1.5/Upgrade.t.sol +++ b/test/upgrades/v2.1.5/Upgrade.t.sol @@ -61,7 +61,7 @@ contract UpgradeTest is Test { function setUp() public { vm.rollFork(BLOCK_NUMBER_UPGRADE); - // create active v2.1.3 account + // create active v2.1.4 account activeAccount = initAccountForStateTesting(); // define Setup contract used for upgrades @@ -102,7 +102,7 @@ contract UpgradeTest is Test { assertEq(version, "2.1.4", "wrong version"); } - function test_Upgrade_v2_1_4() public { + function test_Upgrade_v2_1_5() public { /** * RECORD ALL STATE PRIOR TO UPGRADE */ @@ -157,7 +157,7 @@ contract UpgradeTest is Test { */ (, response) = activeAccount.call(abi.encodeWithSignature("VERSION()")); (bytes32 version) = abi.decode(response, (bytes32)); - assert(version != "2.1.3"); + assert(version == "2.1.5"); /** * CHECK STATE DID NOT CHANGE