Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

V2.1.0 #170

Merged
merged 16 commits into from
Jul 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
// }
// }
// }
50 changes: 39 additions & 11 deletions src/Account.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;
}

/*//////////////////////////////////////////////////////////////
Expand Down 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 @@ -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
//////////////////////////////////////////////////////////////*/
Expand Down Expand Up @@ -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;
}
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
event TokenWhitelistStatusUpdated(address token);
Expand All @@ -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
Expand All @@ -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
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
Loading