Skip to content

Commit

Permalink
Merge branch 'v2.1.0' into v2.0.3
Browse files Browse the repository at this point in the history
  • Loading branch information
JaredBorders authored Jul 17, 2023
2 parents 8a8027b + 86efc1f commit b743857
Show file tree
Hide file tree
Showing 16 changed files with 519 additions and 40 deletions.
2 changes: 1 addition & 1 deletion script/upgrades/v2.0.2/Upgrade.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -97,4 +97,4 @@ pragma solidity 0.8.18;

// console2.log("Account Implementation v2.0.2 Deployed:", implementation);
// }
// }
// }
42 changes: 35 additions & 7 deletions src/Account.sol
Original file line number Diff line number Diff line change
Expand Up @@ -678,7 +678,7 @@ contract Account is IAccount, Auth, OpsReady {
}

/*//////////////////////////////////////////////////////////////
CONDITIONAL ORDERS
CREATE CONDITIONAL ORDER
//////////////////////////////////////////////////////////////*/

/// @notice register a conditional order internally and with gelato
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 =
Expand Down Expand Up @@ -868,6 +881,7 @@ contract Account is IAccount, Auth, OpsReady {
_market: address(market),
_amount: conditionalOrder.marginDelta
});

_perpsV2SubmitOffchainDelayedOrder({
_market: address(market),
_sizeDelta: conditionalOrder.sizeDelta,
Expand All @@ -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
Expand Down
9 changes: 9 additions & 0 deletions src/Events.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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});
}
}
10 changes: 10 additions & 0 deletions src/Settings.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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
Expand Down
16 changes: 12 additions & 4 deletions src/interfaces/IAccount.sol
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,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
//////////////////////////////////////////////////////////////*/
Expand Down Expand Up @@ -212,11 +223,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;
}
6 changes: 6 additions & 0 deletions src/interfaces/IEvents.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
12 changes: 12 additions & 0 deletions src/interfaces/ISettings.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
/// @param isWhitelisted: true if token is whitelisted, false if not
Expand All @@ -25,6 +29,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
Expand All @@ -38,6 +46,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
Expand Down
116 changes: 116 additions & 0 deletions src/utils/executors/OrderExecution.sol
Original file line number Diff line number Diff line change
@@ -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 ([email protected])
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);
}
}
13 changes: 1 addition & 12 deletions src/utils/gelato/OpsReady.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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
Expand All @@ -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");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {}

/*//////////////////////////////////////////////////////////////
Expand Down Expand Up @@ -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({
Expand Down
Loading

0 comments on commit b743857

Please sign in to comment.