Skip to content

Commit

Permalink
👷🏻‍♂️ Update co fee mechanism
Browse files Browse the repository at this point in the history
  • Loading branch information
JaredBorders committed Oct 10, 2023
1 parent 8c1f471 commit 270a53d
Show file tree
Hide file tree
Showing 2 changed files with 117 additions and 67 deletions.
120 changes: 71 additions & 49 deletions src/Engine.sol
Original file line number Diff line number Diff line change
Expand Up @@ -36,21 +36,6 @@ contract Engine is IEngine, Multicallable, EIP712, EIP7412, ERC2771Context {
/// @notice "0" synthMarketId represents sUSD in Synthetix v3
uint128 internal constant USD_SYNTH_ID = 0;

/// @notice max fee that can be charged for a conditional order execution
/// @dev 50 USD
uint256 internal constant UPPER_FEE_CAP = 50 ether;

/// @notice min fee that can be charged for a conditional order execution
/// @dev 2 USD
uint256 internal constant LOWER_FEE_CAP = 2 ether;

/// @notice percentage of the simulated order fee that is charged for a conditional order execution
/// @dev denoted in BPS (basis points) where 1% = 100 BPS and 100% = 10000 BPS
uint256 internal constant FEE_SCALING_FACTOR = 1000;

/// @notice max BPS
uint256 internal constant MAX_BPS = 10_000;

/// @notice max number of conditions that can be defined for a conditional order
uint256 internal constant MAX_CONDITIONS = 8;

Expand Down Expand Up @@ -97,6 +82,11 @@ contract Engine is IEngine, Multicallable, EIP712, EIP7412, ERC2771Context {
mapping(uint128 accountId => mapping(uint256 index => uint256 bitmap))
public nonceBitmap;

/// @notice mapping of account id to ETH balance
/// @dev ETH can be deposited/withdrawn from the
/// Engine contract to pay for fee(s) (conditional order execution, etc.)
mapping(uint128 accountId => uint256 ethBalance) public ethBalances;

/*//////////////////////////////////////////////////////////////
CONSTRUCTOR
//////////////////////////////////////////////////////////////*/
Expand Down Expand Up @@ -162,6 +152,49 @@ contract Engine is IEngine, Multicallable, EIP712, EIP7412, ERC2771Context {
);
}

/*//////////////////////////////////////////////////////////////
ETH MANAGEMENT
//////////////////////////////////////////////////////////////*/

/// @custom:todo discuss whether reentrancy guard is needed
/// @custom:todo discuss whether msg.value is safe to use given ERC 2771 context
/// @custom:todo discuss whether multicall is needed?
/// @custom:todo should smv3 deploy its own trusted forwarder contract *instead*?
/// @custom:todo with the trusted forwarder that can call multiple functions in a single transaction,
/// is there concern for double spend attacks?

/// @inheritdoc IEngine
function depositEth(uint128 _accountId) external payable override {
ethBalances[_accountId] += msg.value;

emit EthDeposit(_accountId, msg.value);
}

/// @inheritdoc IEngine
function withdrawEth(uint128 _accountId, uint256 _amount) external override {
address payable caller = payable(_msgSender());

if (!isAccountOwner(_accountId, caller)) revert Unauthorized();

_withdrawEth(caller, _accountId, _amount);

emit EthWithdraw(_accountId, _amount);
}

/// @notice debit ETH from the account and transfer it to the caller
/// @dev UNSAFE to call directly; use `withdrawEth` instead
/// @param _caller the caller of the function
/// @param _accountId the account id to debit ETH from
function _withdrawEth(address _caller, uint128 _accountId, uint256 _amount) internal {
if (_amount > ethBalances[_accountId]) revert InsufficientEthBalance();

ethBalances[_accountId] -= _amount;

(bool sent,) = _caller.call{value: _amount}("");

if (!sent) revert EthTransferFailed();
}

/*//////////////////////////////////////////////////////////////
NONCE MANAGEMENT
//////////////////////////////////////////////////////////////*/
Expand Down Expand Up @@ -401,24 +434,32 @@ contract Engine is IEngine, Multicallable, EIP712, EIP7412, ERC2771Context {
//////////////////////////////////////////////////////////////*/

/// @inheritdoc IEngine
function execute(ConditionalOrder calldata _co, bytes calldata _signature)
function execute(ConditionalOrder calldata _co, bytes calldata _signature, uint256 _fee)
external
override
returns (
IPerpsMarketProxy.Data memory retOrder,
uint256 fees,
uint256 conditionalOrderFee
)
{
/// @dev check: (1) nonce has not been executed before
/// @dev check: (2) signer is authorized to interact with the account
/// @dev check: (3) signature for the order was signed by the signer
/// @dev check: (4) conditions are met || trusted executor is msg sender
if (!canExecute(_co, _signature)) revert CannotExecuteOrder();
{
/// @dev check: (1) fee does not exceed the max fee set by the conditional order
/// @dev check: (2) fee does not exceed balance credited to the account
/// @dev check: (3) nonce has not been executed before
/// @dev check: (4) signer is authorized to interact with the account
/// @dev check: (5) signature for the order was signed by the signer
/// @dev check: (6) conditions are met || trusted executor is msg sender
if (!canExecute(_co, _signature, _fee)) revert CannotExecuteOrder();

/// @dev spend the nonce associated with the order; this prevents replay
_useUnorderedNonce(_co.orderDetails.accountId, _co.nonce);

/// @dev impose a fee for executing the conditional order
/// @dev the fee is denoted in ETH and is paid to the caller (conditional order executor)
/// @dev the fee does not exceed the max fee set by the conditional order and
/// this is enforced by the `canExecute` function
_withdrawEth(_msgSender(), _co.orderDetails.accountId, _co.orderDetails.accountId);

/// @notice get size delta from order details
/// @dev up to the caller to not waste gas by passing in a size delta of zero
int128 sizeDelta = _co.orderDetails.sizeDelta;
Expand Down Expand Up @@ -456,32 +497,6 @@ contract Engine is IEngine, Multicallable, EIP712, EIP7412, ERC2771Context {
}
}

/// @dev fetch estimated order fees to be used to
/// calculate conditional order fee
(uint256 orderFees,) = PERPS_MARKET_PROXY.computeOrderFees({
marketId: _co.orderDetails.marketId,
sizeDelta: sizeDelta
});

/// @dev calculate conditional order fee based on scaled order fees
conditionalOrderFee = (orderFees * FEE_SCALING_FACTOR) / MAX_BPS;

/// @dev ensure conditional order fee is within bounds
if (conditionalOrderFee < LOWER_FEE_CAP) {
conditionalOrderFee = LOWER_FEE_CAP;
} else if (conditionalOrderFee > UPPER_FEE_CAP) {
conditionalOrderFee = UPPER_FEE_CAP;
}

/// @dev withdraw conditional order fee from account prior to executing order
_withdrawCollateral({
_to: _msgSender(),
_synth: SUSD,
_accountId: _co.orderDetails.accountId,
_synthMarketId: USD_SYNTH_ID,
_amount: -int256(conditionalOrderFee)
});

/// @dev execute the order
(retOrder, fees) = _commitOrder({
_perpsMarketId: _co.orderDetails.marketId,
Expand All @@ -497,8 +512,15 @@ contract Engine is IEngine, Multicallable, EIP712, EIP7412, ERC2771Context {
/// @inheritdoc IEngine
function canExecute(
ConditionalOrder calldata _co,
bytes calldata _signature
bytes calldata _signature,
uint256 _fee
) public view override returns (bool) {
// verify fee does not exceed the max fee set by the conditional order
if (_fee > _co.maxExecutorFee) return false;

// verify account has enough credit (ETH) to pay the fee
if (_fee > ethBalances[_co.orderDetails.accountId]) return false;

// verify nonce has not been executed before
if (hasUnorderedNonceBeenUsed(_co.orderDetails.accountId, _co.nonce)) {
return false;
Expand Down
64 changes: 46 additions & 18 deletions src/interfaces/IEngine.sol
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ interface IEngine {
bool requireVerified;
// address that can execute the order if requireVerified is false
address trustedExecutor;
// max fee denominated in ETH that can be paid to the executor
uint256 maxExecutorFee;
// array of extra conditions to be met
bytes[] conditions;
}
Expand All @@ -68,15 +70,34 @@ interface IEngine {

/// @notice thrown when attempting to verify a condition identified by an invalid selector
error InvalidConditionSelector(bytes4 selector);

/// @notice thrown when attempting to debit an account with insufficient balance
error InsufficientEthBalance();

/// @notice thrown when attempting to transfer eth fails
error EthTransferFailed();

/*//////////////////////////////////////////////////////////////
EVENTS
//////////////////////////////////////////////////////////////*/

/// @notice emitted when the account owner or delegate successfully invalidates an unordered nonce
/// @param accountId the id of the account that was invalidated
/// @param word the word position of the bitmap that was invalidated
/// @param mask the mask used to invalidate the bitmap
event UnorderedNonceInvalidation(
uint128 indexed accountId, uint256 word, uint256 mask
);

/// @notice emitted when eth is deposited into the engine and credited to an account
/// @param accountId the id of the account that was credited
/// @param amount the amount of eth deposited
event EthDeposit(uint128 indexed accountId, uint256 amount);

/// @notice emitted when eth is withdrawn from the engine and debited from an account
/// @param accountId the id of the account that was debited
/// @param amount the amount of eth withdrawn
event EthWithdraw(uint128 indexed accountId, uint256 amount);

/*//////////////////////////////////////////////////////////////
AUTHENTICATION
Expand All @@ -103,6 +124,19 @@ interface IEngine {
external
view
returns (bool);

/*//////////////////////////////////////////////////////////////
ETH MANAGEMENT
//////////////////////////////////////////////////////////////*/

/// @notice deposit eth into the engine and credit the account identified by the accountId
/// @param _accountId the id of the account to credit
function depositEth(uint128 _accountId) external payable;

/// @notice withdraw eth from the engine and debit the account identified by the accountId
/// @param _accountId the id of the account to debit
/// @param _amount the amount of eth to withdraw
function withdrawEth(uint128 _accountId, uint256 _amount) external;

/*//////////////////////////////////////////////////////////////
NONCE MANAGEMENT
Expand Down Expand Up @@ -171,41 +205,35 @@ interface IEngine {
CONDITIONAL ORDER MANAGEMENT
//////////////////////////////////////////////////////////////*/

/// In order for a conditional order to be committed and then executed there are a number of requirements that need to be met:
///
/// (1) The account must have sufficient snxUSD collateral to handle the order
/// (2) The account must not have another order committed
/// (3) The order’s set `acceptablePrice` needs to be met both on committing the order and when it gets executed
/// (users should choose a value for this that is likely to execute based on the conditions set)
/// (4) The order can only be executed within Synthetix’s set settlement window
/// (5) There must be a keeper that executes a conditional order
///
/// @notice There is no guarantee a conditional order will be executed
/// @custom:docs for in-depth documentation of conditional order mechanism,
/// please refer to https://github.com/Kwenta/smart-margin-v3/wiki/Conditional-Orders

/// @notice execute a conditional order
/// @param _co the conditional order
/// @param _signature the signature of the conditional order
/// @param _fee the fee paid to executor for the conditional order
/// @return retOrder the order committed
/// @return fees the fees paid for the order to Synthetix
/// @return conditionalOrderFee the fee paid to executor for the conditional order
function execute(ConditionalOrder calldata _co, bytes calldata _signature)
function execute(ConditionalOrder calldata _co, bytes calldata _signature, uint256 _fee)
external
returns (
IPerpsMarketProxy.Data memory retOrder,
uint256 fees,
uint256 conditionalOrderFee
);

/// @notice checks if the order can be executed based on defined conditions
/// @dev this function does NOT check if the order can be executed based on the account's balance
/// (i.e. does not check if enough USD is available to pay for the order fee nor does it check
/// if enough collateral is available to cover the order)
/// @param _co the conditional order
/// @notice checks if the conditional order can be executed
/// @param _co the conditional order which details the order to be executed and the conditions to be met
/// @param _signature the signature of the conditional order
/// @return true if the order can be executed based on defined conditions, false otherwise
/// @param _fee the executor specified fee for the executing the conditional order
/// @dev if the fee is greater than the maxExecutorFee defined in the conditional order,
/// or if the account lacks sufficient ETH credit to pay the fee, canExecute will return false
/// @return true if the order can be executed, false otherwise
function canExecute(
ConditionalOrder calldata _co,
bytes calldata _signature
bytes calldata _signature,
uint256 _fee
) external view returns (bool);

/// @notice verify the conditional order signer is the owner or delegate of the account
Expand Down

0 comments on commit 270a53d

Please sign in to comment.