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/src/Account.sol b/src/Account.sol index 036266d3..535002da 100644 --- a/src/Account.sol +++ b/src/Account.sol @@ -101,7 +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; + uint256 internal locked; /*////////////////////////////////////////////////////////////// MODIFIERS @@ -116,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; } /*////////////////////////////////////////////////////////////// @@ -678,7 +678,7 @@ contract Account is IAccount, Auth, OpsReady { } /*////////////////////////////////////////////////////////////// - CONDITIONAL ORDERS + CREATE CONDITIONAL ORDER //////////////////////////////////////////////////////////////*/ /// @notice register a conditional order internally and with gelato @@ -770,6 +770,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,30 +801,39 @@ contract Account is IAccount, Auth, OpsReady { } /*////////////////////////////////////////////////////////////// - GELATO CONDITIONAL ORDER HANDLING + EXECUTE CONDITIONAL ORDER //////////////////////////////////////////////////////////////*/ /// @inheritdoc IAccount function executeConditionalOrder(uint256 _conditionalOrderId) external override + nonReentrant isAccountExecutionEnabled - onlyOps { - // store conditional order in memory + // store conditional order object in memory ConditionalOrder memory conditionalOrder = getConditionalOrder(_conditionalOrderId); + // 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 + }); + } + // 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}); - // pay Gelato imposed fee for conditional order execution - (uint256 fee, address feeToken) = IOps(OPS).getFeeDetails(); - _transfer({_amount: fee, _paymentToken: feeToken}); + // impose and record fee paid to executor + uint256 fee = _payExecutorFee(); // define Synthetix PerpsV2 market IPerpsV2MarketConsolidated market = @@ -868,6 +881,7 @@ contract Account is IAccount, Auth, OpsReady { _market: address(market), _amount: conditionalOrder.marginDelta }); + _perpsV2SubmitOffchainDelayedOrder({ _market: address(market), _sizeDelta: conditionalOrder.sizeDelta, @@ -883,6 +897,20 @@ contract Account is IAccount, Auth, OpsReady { }); } + /// @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) { + (fee,) = IOps(OPS).getFeeDetails(); + _transfer({_amount: fee}); + } else { + fee = SETTINGS.executorFee(); + (bool success,) = payable(msg.sender).call{value: fee}(""); + if (!success) revert CannotPayExecutorFee(fee, msg.sender); + } + } + /// @notice order logic condition checker /// @dev this is where order type logic checks are handled /// @param _conditionalOrderId: key for an active order 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/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 96c4fc93..d944e910 100644 --- a/src/interfaces/IAccount.sol +++ b/src/interfaces/IAccount.sol @@ -142,6 +142,17 @@ 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); + /*////////////////////////////////////////////////////////////// VIEWS //////////////////////////////////////////////////////////////*/ @@ -211,11 +222,8 @@ 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; } 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); } 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 diff --git a/src/utils/executors/OrderExecution.sol b/src/utils/executors/OrderExecution.sol new file mode 100644 index 00000000..70746aa5 --- /dev/null +++ b/src/utils/executors/OrderExecution.sol @@ -0,0 +1,116 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.18; + +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); +} + +/// @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(); + + 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(); + } + + uint256 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, uint256[] calldata ids) + public + { + assert(accounts.length > 0); + assert(accounts.length == ids.length); + + uint256 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, + uint256[] calldata ids + ) external payable { + updatePythPrice(priceUpdateData); + executeOrders(accounts, ids); + } +} diff --git a/src/utils/gelato/OpsReady.sol b/src/utils/gelato/OpsReady.sol index 993b3688..f3337108 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 @@ -33,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"); } 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 872aa772..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 {} /*////////////////////////////////////////////////////////////// @@ -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({ diff --git a/test/integration/order.public.behavior.t.sol b/test/integration/order.public.behavior.t.sol new file mode 100644 index 00000000..ae0ea7fd --- /dev/null +++ b/test/integration/order.public.behavior.t.sol @@ -0,0 +1,277 @@ +// 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 private 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() 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_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_ExecuteConditionalOrder_Public_Cannot_PayExecutorFee() + public + { + withdrawEth(address(account).balance); + vm.startPrank(USER); + vm.expectRevert( + abi.encodeWithSelector( + IAccount.CannotPayExecutorFee.selector, + settings.executorFee(), + USER + ) + ); + account.executeConditionalOrder(conditionalOrderId); + vm.stopPrank(); + } + + function test_ExecuteConditionalOrder_Public_Invalid_ConditionalOrder() + public + { + suspendPerpsV2Market(sETHPERP); + vm.prank(USER); + vm.expectRevert( + abi.encodeWithSelector( + IAccount.CannotExecuteConditionalOrder.selector, + conditionalOrderId, + USER + ) + ); + account.executeConditionalOrder(conditionalOrderId); + } + + /*////////////////////////////////////////////////////////////// + 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 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) { + 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); + 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 1c7b609d..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()); } /*////////////////////////////////////////////////////////////// @@ -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/unit/Settings.t.sol b/test/unit/Settings.t.sol index c7b94053..8e2cf85d 100644 --- a/test/unit/Settings.t.sol +++ b/test/unit/Settings.t.sol @@ -55,6 +55,28 @@ 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/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 +// } diff --git a/test/utils/AccountExposed.sol b/test/utils/AccountExposed.sol index 878c8589..6780e847 100644 --- a/test/utils/AccountExposed.sol +++ b/test/utils/AccountExposed.sol @@ -104,11 +104,15 @@ 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_payExecutorFee() public returns (uint256) { + return _payExecutorFee(); + } } diff --git a/test/utils/ConsolidatedEvents.sol b/test/utils/ConsolidatedEvents.sol index a1905c20..2f7cbafb 100644 --- a/test/utils/ConsolidatedEvents.sol +++ b/test/utils/ConsolidatedEvents.sol @@ -85,11 +85,15 @@ contract ConsolidatedEvents { IAccount.PriceOracleUsed priceOracle ); + event ExecutorFeeSet(address indexed account, uint256 indexed executorFee); + /*////////////////////////////////////////////////////////////// ISETTINGS //////////////////////////////////////////////////////////////*/ event AccountExecutionEnabledSet(bool enabled); + event ExecutorFeeSet(uint256 executorFee); + event TokenWhitelistStatusUpdated(address token); }