- Architecture
- Contracts
- Contract Interactions
- Orders
- Transactions
- Signatures
- Events
- Types
- Standard relayer API
- Miscellaneous
0x protocol uses an approach we refer to as off-chain order relay with on-chain settlement. In this approach, cryptographically signed orders are broadcast off of the blockchain through any arbitrary communication channel; an interested counterparty may inject one or more of these orders into 0x protocol's Exchange
contract to execute and settle trades directly to the blockchain.
0x uses a modular system of Ethereum smart contracts which allows each component of the system to be upgraded via governance without affecting other components of the system and without causing active markets to be disrupted. Version 2 of 0x protocol further modularizes this contract pipeline through the introduction of AssetProxy
contracts, which allow new token standards, interfaces and payloads to be supported over time.
The Exchange contract contains the bulk of the business logic within 0x protocol. It is the entry point for:
- Filling orders
- Canceling orders
- Executing transactions
- Validating signatures
- Registering new
AssetProxy
contracts into the system
The AssetProxy
contracts are responsible for:
- Decoding asset specific metadata contained within an order
- Performing the actual asset transfer
- Authorizing/unauthorizing Exchange contract addresses from calling the transfer methods on this
AssetProxy
In order to opt-in to using 0x protocol, users must approve an asset's associated AssetProxy
to transfer the asset on their behalf.
All AssetProxy
contracts have the following minimum interface:
contract IAuthorizable {
/// @dev Gets all authorized addresses.
/// @return Array of authorized addresses.
function getAuthorizedAddresses()
external
view
returns (address[]);
/// @dev Authorizes an address.
/// @param target Address to authorize.
function addAuthorizedAddress(address target)
external;
/// @dev Removes authorizion of an address.
/// @param target Address to remove authorization from.
function removeAuthorizedAddress(address target)
external;
/// @dev Removes authorizion of an address.
/// @param target Address to remove authorization from.
/// @param index Index of target in authorities array.
function removeAuthorizedAddressAtIndex(
address target,
uint256 index
)
external;
}
contract IAssetProxy is
IAuthorizable
{
/// @dev Transfers assets. Either succeeds or throws.
/// @param assetData Byte array encoded for the respective asset proxy.
/// @param from Address to transfer asset from.
/// @param to Address to transfer asset to.
/// @param amount Amount of asset to transfer.
function transferFrom(
bytes assetData,
address from,
address to,
uint256 amount
)
external;
/// @dev Gets the proxy id associated with the proxy address.
/// @return Proxy id.
function getProxyId()
external
view
returns (uint8);
}
Currently, the protocol includes AssetProxy
contracts for ERC20 and ERC721 tokens.
The ERC20Proxy
is responsible for transferring ERC20 tokens. Users must first approve this contract by calling the approve
method on the token that will be exchanged. It is recommended that users approve a value of 2^256 -1. This minimizes the amount of times approve
must be called, and also increases efficiency for many ERC20 tokens.
This contract expects ERC20 assetData
to be encoded using ABIv2 with the following 4 byte id:
// 0xf47261b0
bytes4 ERC20_SELECTOR = bytes4(keccak256("ERC20Token(address)"));
The data is then encoded as:
Offset | Length | Contents |
---|---|---|
0x00 | 4 | ERC20Proxy id (always 0xf47261b0) |
0x04 | 32 | Address of ERC20 token, left padded with zeroes |
NOTE: The ERC20Proxy
does not enforce strict length checks for assetData
, which means that extra data may be appended to this field with any arbitrary encoding. Any extra data will be ignored by the ERC20Proxy
but may be used in external contracts interacting with the Exchange
contract. Relayers that do not desire this behavior should validate the length of all assetData
fields contained in orders before acceptance.
The ERC20Proxy
performs the transfer by calling the token's transferFrom
method. The transaction will be reverted if the owner has insufficient balance or if the ERC20Proxy
does not have sufficient allowance to perform the transfer.
The ERC721Proxy
is responsible for transferring ERC721 tokens. Users must first approve this contract by calling the approve
or setApprovalForAll
methods on the token that will be exchanged. setApprovalForAll
is highly recommended, because it allows the user to approve multiple tokenIds
with a single transaction.
This contract expects ERC721 assetData
to be encoded using ABIv2 with the following 4 byte id:
// 0x02571792
bytes4 ERC721_SELECTOR = bytes4(keccak256("ERC721Token(address,uint256)"));
The data is then encoded as:
Offset | Length | Contents |
---|---|---|
0x00 | 4 | ERC721 proxy id (always 0x02571792) |
0x04 | 32 | Address of ERC721 token, left padded with zeroes |
0x24 | 32 | tokenId of ERC721 token |
NOTE: The ERC721Proxy
does not enforce strict length checks for assetData
, which means that extra data may be appended to this field with any arbitrary encoding. Any extra data will be ignored by the ERC721Proxy
but may be used in external contracts interacting with the Exchange
contract. Relayers that do not desire this behavior should validate the length of all assetData
fields contained in orders before acceptance.
The ERC721Proxy
performs the transfer by calling the token's transferFrom
method. The transaction will be reverted if the owner has insufficient balance or if the ERC721Proxy
is not approved to perform the transfer.
The MultiAssetProxy
expects an amounts
(uint256
array) and a nestedAssetData
(array of asseData
byte arrays) to be encoded within its own assetData
. Each element of amounts
corresponds to an element at the same index of nestedAssetData
. The MultiAssetProxy
will multiply each amounts
element by the amount
passed into MultiAssetProxy.transferFrom
and then dispatch the corresponding element of nestedAssetProxy
to the relevant AssetProxy
contract with the resulting totalAmount
. This contract does not perform any transferFrom
calls to assets directly and therefore does not require any additional user approvals.
This contract expects its assetData
to be encoded using ABIv2 with the following 4 byte id:
// 0x94cfcdd7
MultiAsset(uint256[],bytes[])
The data is then encoded as:
Offset | Length | Contents |
---|---|---|
0x00 | 4 | MultiAsset proxy id (always 0x94cfcdd7) |
0x04 | 32 | Offset to amounts |
0x24 | 32 | Offset to nestedAssetData |
0x44 | 32 | amounts length |
0x64 | a | amounts contents |
0x84 + a | 32 | nestedAssetData length |
0xA4 + a | b | nestedAssetData contents |
Each element of nestedAssetData
must be encoded according to the specification of the corresponding AssetProxy
contract. Note that initially, the MultiAssetProxy
will not support dispatching a transfer to itself.
For more information on how the MultiAssetProxy
works at a higher level, please refer to ZEIP23.
The AssetProxyOwner
contract is indirectly responsible for updating the Exchange
contracts that are allowed to call the transfer methods on each AssetProxy
contract. It is the only address that is allowed to call addAuthorizedAddress
and removeAuthorizedAddressAtIndex
on each AssetProxy
. Any transaction created by the AssetProxyOwner
must be proposed, confirmed, and then may be executed after a 2 week timelock. The only exception to this is that removeAuthorizedAddressAtIndex
may be executed immediately, in case of security related bugs. The AssetProxyOwner
may also call transferOwnership
, allowing it to swap itself out with an upgraded contract.
The diagrams provided below demonstrate interactions between various 0x smart contracts that make up the system. The arrow represents execution context within the EVM as a transaction is processed. Execution context is passed from the originating Ethereum account (circle) and between 0x's Ethereum smart contracts (rectangles) as they make external function calls into each other. Arrows are directed from the caller to the callee. Pseudocode is provided alongside each diagram to demonstrate what is happening at each step in the sequence of external function calls that occur during a given transaction.
A trade is initiated when an order is passed into the Exchange
contract. If the order is valid, the Exchange
contract will attempt to settle each leg of the trade by calling into the appropriate AssetProxy
contract for each asset being exchanged. Each AssetProxy
accepts and processes a payload of asset metadata and initiates a transfer. To simplify the trade settlement diagrams below, we assume that the orders being settled have zero fees.
Transaction #1
Exchange.fillOrder(order, value)
ERC20Proxy.transferFrom(assetData, from, to, value)
ERC20Token(assetData.address).transferFrom(from, to, value)
- ERC20Token: (revert on failure)
- ERC20Proxy: (revert on failure)
ERC20Proxy.transferFrom(assetData, from, to, value)
ERC20Token(assetData.address).transferFrom(from, to, value)
- ERC20Token: (revert on failure)
- ERC20Proxy: (revert on failure)
- Exchange: (return
FillResults
)
Transaction #1
Exchange.fillOrder(order, value)
ERC721Proxy.transferFrom(assetData, from, to, value)
ERC721Token(assetData.address).transferFrom(from, to, assetData.tokenId)
- ERC721Token: (revert on failure)
- ERC721Proxy: (revert on failure)
ERC20Proxy.transferFrom(assetData, from, to, value)
ERC20Token(assetData.address).transferFrom(from, to, value)
- ERC20Token: (revert on failure)
- ERC20Proxy: (revert on failure)
- Exchange: (return
FillResults
)
New Exchange
contracts can be added by calling addAuthorizedAddress
on each AssetProxy
contract. Multiple Exchange
contracts may exists at the same time, but typically older versions will be deprecated and removed by calling removeAuthorizedAddressAtIndex
on each AssetProxy
contract. Only the AssetProxyOwner
contract can call these methods.
The AssetProxyOwner
contract can be upgraded by calling transferOwnership
on each AssetProxy
contract, transferring ownership of the AssetProxy
contracts to an upgraded contract. Future versions of the AssetProxyOwner
will be able to execute transactions in batches, allowing upgrades to occur accross all AssetProxy
contracts atomically.
New AssetProxy
contracts may be added into the system by calling registerAssetProxy
on the Exchange
contract. Only the AssetProxyOwner
contract may call this method.
An order message consists of the following parameters:
Parameter | Type | Description |
---|---|---|
makerAddress | address | Address that created the order. |
takerAddress | address | Address that is allowed to fill the order. If set to 0, any address is allowed to fill the order. |
feeRecipientAddress | address | Address that will receive fees when order is filled. |
senderAddress | address | Address that is allowed to call Exchange contract methods that affect this order. If set to 0, any address is allowed to call these methods. |
makerAssetAmount | uint256 | Amount of makerAsset being offered by maker. Must be greater than 0. |
takerAssetAmount | uint256 | Amount of takerAsset being bid on by maker. Must be greater than 0. |
makerFee | uint256 | Amount of ZRX paid to feeRecipient by maker when order is filled. If set to 0, no transfer of ZRX from maker to feeRecipient will be attempted. |
takerFee | uint256 | Amount of ZRX paid to feeRecipient by taker when order is filled. If set to 0, no transfer of ZRX from taker to feeRecipient will be attempted. |
expirationTimeSeconds | uint256 | Timestamp in seconds at which order expires. |
salt | uint256 | Arbitrary number to facilitate uniqueness of the order's hash. |
makerAssetData | bytes | ABIv2 encoded data that can be decoded by a specified proxy contract when transferring makerAsset. |
takerAssetData | bytes | ABIv2 encoded data that can be decoded by a specified proxy contract when transferring takerAsset. |
If the senderAddress
of an order is not set to 0, only that address may call Exchange
contract methods that affect that order. See the filter contracts examples for more information.
An order's salt
parameter has two main usecases:
- To ensure uniqueness within an order's hash.
- To be used in combination with
cancelOrdersUpTo
. When creating an order, thesalt
value should be equal to the value of the current timestamp in milliseconds. This allows maker to create 1000 orders with the same parameters per second. Note that although this is part of the protocol specification, there is currently no way to enforce this usage andsalt
values should not be relied upon as a source of truth.
The makerAssetData
and takerAssetData
fields of an order contain information specific to that asset. These fields are encoded using ABIv2 with a 4 byte id that references the proxy that is intended to decode the data. See the ERC20Proxy
and ERC721Proxy
sections for the layouts of the assetData
fields for each AssetProxy
contract.
The hash of an order is used as a unique identifier of that order. An order is hashed according to the EIP712 specification. See the EIP712 Usage section for information on how to calculate the required domain separator for hashing an order.
bytes32 constant EIP712_ORDER_SCHEMA_HASH = keccak256(abi.encodePacked(
"Order(",
"address makerAddress,",
"address takerAddress,",
"address feeRecipientAddress,",
"address senderAddress,",
"uint256 makerAssetAmount,",
"uint256 takerAssetAmount,",
"uint256 makerFee,",
"uint256 takerFee,",
"uint256 expirationTimeSeconds,",
"uint256 salt,",
"bytes makerAssetData,",
"bytes takerAssetData",
")"
));
bytes32 orderHash = keccak256(abi.encodePacked(
EIP191_HEADER,
EIP712_DOMAIN_HASH,
keccak256(abi.encodePacked(
EIP712_ORDER_SCHEMA_HASH,
bytes32(order.makerAddress),
bytes32(order.takerAddress),
bytes32(order.feeRecipientAddress),
bytes32(order.senderAddress),
order.makerAssetAmount,
order.takerAssetAmount,
order.makerFee,
order.takerFee,
order.expirationTimeSeconds,
order.salt,
keccak256(order.makerAssetData),
keccak256(order.takerAssetData)
))
));
An order may only be filled if it can be paired with an associated valid signature. Signatures are only validated the first time an order is filled. For later fills, no signature must be submitted. An order's hash must be signed with a supported signature type.
Orders can be filled by calling the following methods on the Exchange contract.
This is the most basic way to fill an order. All of the other methods call fillOrder
under the hood with additional logic. This function will attempt to fill the amount specified by the caller. However, if the remaining fillable amount is less than the amount specified, the remaining amount will be filled. Partial fills are allowed when filling orders.
fillOrder
will revert under the following conditions:
- The caller of
fillOrder
is different from thesender
specified in the order (unlesssender == address(0)
). - The taker of
fillOrder
is different from thetaker
specified in the order (unlesstaker == address(0)
). - An invalid signature is submitted (this is only checked the first time an order is filled).
- The
makerAssetAmount
ortakerAssetAmount
specified in the order are equal to 0. - The amount that the taker is attempting to fill is 0.
- The order has expired.
- The order has been cancelled.
- The order has already been fully filled.
- Filling the order results in a rounding error > 0.1% of the
takerAssetAmount
that would otherwise be filled. - Any transfers associated with the fill fails.
- The amount the taker is attempting to fill multiplied by the
makerAssetAmount
is greater than 256 bits. - The amount the taker is attempting to fill multiplied by the
makerFee
is greater than 256 bits. - The amount the taker is attempting to fill multiplied by the
takerFee
is greater than 256 bits. - Reentrancy is attempted to any function within the
Exchange
contract that contains a mutex.
If successful, fillOrder
will emit a Fill
event. If the transaction does not revert, a FillResults
instance will be returned.
/// @dev Fills the input order.
/// @param order Order struct containing order specifications.
/// @param takerAssetFillAmount Desired amount of takerAsset to sell.
/// @param signature Proof that order has been created by maker.
/// @return Amounts filled and fees paid by maker and taker.
function fillOrder(
Order memory order,
uint256 takerAssetFillAmount,
bytes memory signature
)
public
returns (FillResults memory fillResults);
fillOrKillOrder
behaves almost exactly the same as fillOrder
. However, the transaction will revert if the amount specified is not filled exactly.
/// @dev Fills the input order. Reverts if exact takerAssetFillAmount not filled.
/// @param order Order struct containing order specifications.
/// @param takerAssetFillAmount Desired amount of takerAsset to sell.
/// @param signature Proof that order has been created by maker.
function fillOrKillOrder(
Order memory order,
uint256 takerAssetFillAmount,
bytes memory signature
)
public
returns (FillResults memory fillResults);
fillOrderNoThrow
also behaves very similary to fillOrder
. However, the transaction will never revert and will instead return a FillResults
instance that contains all 0 values. This is useful when calling the batch methods listed below, where a user may not want an entire transaction to fail when a single fill is reverted.
/// @dev Fills an order with specified parameters and ECDSA signature.
/// Returns false if the transaction would otherwise revert.
/// @param order Order struct containing order specifications.
/// @param takerAssetFillAmount Desired amount of takerAsset to sell.
/// @param signature Proof that order has been created by maker.
/// @return Amounts filled and fees paid by maker and taker.
function fillOrderNoThrow(
Order memory order,
uint256 takerAssetFillAmount,
bytes memory signature
)
public
returns (FillResults memory fillResults);
batchFillOrders
calls fillOrder
sequentially for each provided order, amount, and signature.
/// @dev Synchronously executes multiple calls of fillOrder.
/// @param orders Array of order specifications.
/// @param takerAssetFillAmounts Array of desired amounts of takerAsset to sell in orders.
/// @param signatures Proofs that orders have been created by makers.
/// @return Amounts filled and fees paid by makers and taker.
/// NOTE: makerAssetFilledAmount and takerAssetFilledAmount may include amounts filled of different assets.
function batchFillOrders(
Order[] memory orders,
uint256[] memory takerAssetFillAmounts,
bytes[] memory signatures
)
public;
returns (FillResults memory totalFillResults)
batchFillOrKillOrders
calls fillOrKillOrder
sequentially for each provided order, amount, and signature.
/// @dev Synchronously executes multiple calls of fillOrKill.
/// @param orders Array of order specifications.
/// @param takerAssetFillAmounts Array of desired amounts of takerAsset to sell in orders.
/// @param signatures Proofs that orders have been created by makers.
/// @return Amounts filled and fees paid by makers and taker.
/// NOTE: makerAssetFilledAmount and takerAssetFilledAmount may include amounts filled of different assets.
function batchFillOrKillOrders(
Order[] memory orders,
uint256[] memory takerAssetFillAmounts,
bytes[] memory signatures
)
public;
returns (FillResults memory totalFillResults)
batchFillOrdersNoThrow
calls fillOrderNoThrow
sequentially for each provided order, amount, and signature.
/// @dev Synchronously executes multiple calles of fillOrderNoThrow.
/// @param orders Array of order specifications.
/// @param takerAssetFillAmounts Array of desired amounts of takerAsset to sell in orders.
/// @param signatures Proofs that orders have been created by makers.
/// @return Amounts filled and fees paid by makers and taker.
/// NOTE: makerAssetFilledAmount and takerAssetFilledAmount may include amounts filled of different assets.
function batchFillOrdersNoThrow(
Order[] memory orders,
uint256[] memory takerAssetFillAmounts,
bytes[] memory signatures
)
public;
returns (FillResults memory totalFillResults)
marketSellOrders
calls fillOrder
sequentially for each provided order and signature until the total takerAssetAmount
has been sold by the taker. If successful, marketSellOrders
returns a FillResults
instance containing the cumulative amounts filled and fees paid.
Note that marketSellOrders
assumes that the takerAssetData
is equal for each order. For any order passed in after the first, the takerAssetData
byte array will be ignored (allowing null byte arrays to be passed in). If an order was intended to use a different takerAssetData
field, the fill will fail at signature validation.
/// @dev Synchronously executes multiple calls of fillOrder until total amount of takerAsset is sold by taker.
/// @param orders Array of order specifications.
/// @param takerAssetFillAmount Desired amount of takerAsset to sell.
/// @param signatures Proofs that orders have been created by makers.
/// @return Amounts filled and fees paid by makers and taker.
function marketSellOrders(
Order[] memory orders,
uint256 takerAssetFillAmount,
bytes[] memory signatures
)
public
returns (FillResults memory totalFillResults);
marketSellOrdersNoThrow
calls fillOrderNoThrow
sequentially for each provided order and signature until the total takerAssetAmount
has been sold by the taker. If successful, marketSellOrdersNoThrow
returns a FillResults
instance containing the cumulative amounts filled and fees paid.
Note that marketSellOrdersNoThrow
assumes that the takerAssetData
is equal for each order. For any order passed in after the first, the takerAssetData
byte array will be ignored (allowing null byte arrays to be passed in). If an order was intended to use a different takerAssetData
field, the fill will fail at signature validation.
/// @dev Synchronously executes multiple calls of fillOrder until total amount of takerAsset is sold by taker.
/// Returns false if the transaction would otherwise revert.
/// @param orders Array of order specifications.
/// @param takerAssetFillAmount Desired amount of takerAsset to sell.
/// @param signatures Proofs that orders have been signed by makers.
/// @return Amounts filled and fees paid by makers and taker.
function marketSellOrdersNoThrow(
Order[] memory orders,
uint256 takerAssetFillAmount,
bytes[] memory signatures
)
public
returns (FillResults memory totalFillResults);
marketBuyOrders
calls fillOrder
sequentially for each provided order and signature until the total makerAssetAmount
has been bought by the taker. If successful, marketBuyOrders
returns a FillResults
instance containing the cumulative amounts filled and fees paid.
Note that marketBuyOrders
assumes that the makerAssetData
is equal for each order. For any order passed in after the first, the makerAssetData
byte array will be ignored (allowing null byte arrays to be passed in). If an order was intended to use a different makerAssetData
field, the fill will fail at signature validation.
/// @dev Synchronously executes multiple calls of fillOrder until total amount of makerAsset is bought by taker.
/// @param orders Array of order specifications.
/// @param makerAssetFillAmount Desired amount of makerAsset to buy.
/// @param signatures Proofs that orders have been signed by makers.
/// @return Amounts filled and fees paid by makers and taker.
function marketBuyOrders(
Order[] memory orders,
uint256 makerAssetFillAmount,
bytes[] memory signatures
)
public
returns (FillResults memory totalFillResults);
marketBuyOrdersNoThrow
calls fillOrderNoThrow
sequentially for each provided order and signature until the total makerAssetAmount
has been bought by taker
. If successful, marketBuyOrdersNoThrow
returns a FillResults
instance containing the cumulative amounts filled and fees paid.
/// @dev Synchronously executes multiple fill orders in a single transaction until total amount is bought by taker.
/// Returns false if the transaction would otherwise revert.
/// @param orders Array of order specifications.
/// @param makerAssetFillAmount Desired amount of makerAsset to buy.
/// @param signatures Proofs that orders have been signed by makers.
/// @return Amounts filled and fees paid by makers and taker.
function marketBuyOrdersNoThrow(
Order[] memory orders,
uint256 makerAssetFillAmount,
bytes[] memory signatures
)
public
returns (FillResults memory totalFillResults);
Two orders that represent a bid and an ask for the same token pair may be matched together if the spread between their respective prices is negative. This is satified by the following equation:
(leftOrder.makerAssetAmount * rightOrder.makerAssetAmount) >= (leftOrder.takerAssetAmount * rightOrder.takerAssetAmount)
The caller of matchOrders
is considered the taker for each order. The taker will pay the takerFee
for each order, but will also receive the spread between both orders. The spread is always denominated in terms of the left order's makerAsset. No balance is required in order to call matchOrders
, and the taker never holds intermediate balances of either asset.
matchOrders
will revert if either order fails the validation checks for fillOrder. Note that matchOrders
assumes that rightOrder.makerAssetData == leftOrder.takerAssetData
and rightOrder.takerAssetData == leftOrder.makerAssetData
, allowing null byte arrays to be passed in for both assetData fields of rightOrder
. If other assetData fields were part of the original rightOrder
, this function will fail when validating the signature of the rightOrder
.
If successful, matchOrders
will emit a Fill event for each matched order.
/// @dev Match two complementary orders that have a profitable spread.
/// Each order is filled at their respective price point. However, the calculations are
/// carried out as though the orders are both being filled at the right order's price point.
/// The profit made by the left order goes to the taker (who matched the two orders).
/// @param leftOrder First order to match.
/// @param rightOrder Second order to match.
/// @param leftSignature Proof that order was created by the left maker.
/// @param rightSignature Proof that order was created by the right maker.
/// @return matchedFillResults Amounts filled and fees paid by maker and taker of matched orders.
function matchOrders(
Order memory leftOrder,
Order memory rightOrder,
bytes memory leftSignature,
bytes memory rightSignature
)
public
returns (MatchedFillResults memory matchedFillResults);
cancelOrder
cancels the specified order. Partial cancels are not allowed.
cancelOrder
will revert under the following conditions:
- The
makerAssetAmount
ortakerAssetAmount
specified in the order are equal to 0. - The caller of
cancelOrder
is different from thesenderAddress
specified in the order (unlesssenderAddress == address(0)
). - The maker of the order has not authorized the cancel, either by calling
cancelOrder
through an Ethereum transaction or a 0x transaction. - The order has expired.
- The order has already been cancelled.
If successful, cancelOrder
will emit a Cancel
event.
/// @dev After calling, the order can not be filled anymore.
/// @param order Order struct containing order specifications.
/// @return True if the order state changed to cancelled.
/// False if the transaction was already cancelled or expired.
function cancelOrder(Order memory order)
public;
cancelOrdersUpTo
invalidates all orders created by the maker that have 1) a salt
value that is less than or equal to the specified targetOrderEpoch
and 2) that have a senderAddress
value equal to the caller (or null address if the caller is the maker). cancelOrdersUpTo
also updates the current orderEpoch
. This function will revert if targetOrderEpoch
is less than or equal to the current orderEpoch
. If successful, cancelOrdersUpTo
will emit a CancelUpTo
event.
/// @dev Cancels all orders created by makerAddress with a salt less than or equal to the targetOrderEpoch
/// and senderAddress equal to msg.sender (or null address if msg.sender == makerAddress).
/// @param targetOrderEpoch Orders created with a salt less or equal to this value will be cancelled.
function cancelOrdersUpTo(uint256 targetOrderEpoch)
external;
batchCancelOrders
calls cancelOrder
sequentially for each provided order.
/// @dev Synchronously executed musltiple calls of cancelOrder.
/// @param orders Array of order specifications.
function batchCancelOrders(Order[] memory orders)
public;
The Exchange contract contains a mapping that records the nominal amount of an order's takerAssetAmount
that has already been filled. This mapping is updated each time an order is successfully filled, allowing for partial fills.
// Mapping of orderHash => amount of takerAsset already bought by maker
mapping (bytes32 => uint256) public filled;
The Exchange contract contains a mapping that records if an order has been cancelled.
// Mapping of orderHash => cancelled
mapping (bytes32 => bool) public cancelled;
The Exchange contract contains a mapping that specifies the orderEpoch
for a given makerAddress
/senderAddress
pair, which invalidates all orders containing that pair that contain a salt value less than or equal to the current orderEpoch
.
// Mapping of makerAddress => senderAddress => lowest salt an order can have in order to be fillable
// Orders with specified senderAddress and with a salt less than their epoch to are considered cancelled
mapping (address => mapping (address => uint256)) public orderEpoch;
getOrderInfo
is a public method that returns the state, hash, and amount of an order that has already been filled as an OrderInfo instance:
/// @dev Gets information about an order: status, hash, and amount filled.
/// @param order Order to gather information on.
/// @return OrderInfo Information about the order and its state.
/// See LibOrder.OrderInfo for a complete description.
function getOrderInfo(Order memory order)
public
view
returns (OrderInfo memory orderInfo);
getOrdersInfo
calls getOrderInfo
sequentially for each provided order.
/// @dev Fetches information for all passed in orders.
/// @param orders Array of order specifications.
/// @return Array of OrderInfo instances that correspond to each order.
function getOrdersInfo(LibOrder.Order[] memory orders)
public
view
returns (LibOrder.OrderInfo[] memory);
Transaction messages exist for the purpose of calling methods on the Exchange
contract in the context of another address (see ZEIP18). This is especially useful for implementing filter contracts.
Parameter | Type | Description |
---|---|---|
signerAddress | address | Address of transaction signer |
salt | uint256 | Arbitrary number to facilitate uniqueness of the transactions's hash. |
data | bytes | The calldata that is to be executed. This must call an Exchange contract method. |
The hash of a transaction is used as a unique identifier for that transaction. A transaction is hashed according to the EIP712 specification. See the EIP712 Usage section for information on how to calculate the required domain separator for hashing an order.
// Hash for the EIP712 ZeroEx Transaction Schema
bytes32 constant internal EIP712_ZEROEX_TRANSACTION_SCHEMA_HASH = keccak256(abi.encodePacked(
"ZeroExTransaction(",
"uint256 salt,",
"address signerAddress,",
"bytes data",
")"
));
bytes32 transactionHash = keccak256(abi.encodePacked(
EIP191_HEADER,
EIP712_DOMAIN_HASH,
keccak256(abi.encodePacked(
EIP712_ZEROEX_TRANSACTION_SCHEMA_HASH,
salt,
bytes32(signerAddress),
keccak256(data)
))
));
A transaction may only be executed if it can be paired with an associated valid signature. A transaction's hash must be signed with a supported signature type.
A transaction may only be executed by calling the executeTransaction
method of the Exchange contract. executeTransaction
attempts to execute any function on the Exchange contract in the context of the transaction signer (rather than msg.sender
).
executeTransaction
will revert under the following conditions:
- Reentrancy is attempted (e.g
executeTransaction
callsexecuteTransaction
again). - A transaction with an equivalent hash has already been executed.
- An invalid signature is submitted.
- The execution of the provided data reverts.
/// @dev Executes an exchange method call in the context of signer.
/// @param salt Arbitrary number to ensure uniqueness of transaction hash.
/// @param signerAddress Address of transaction signer.
/// @param data AbiV2 encoded calldata.
/// @param signature Proof that transaction has been signed by signer.
function executeTransaction(
uint256 salt,
address signerAddress,
bytes data,
bytes signature
)
external;
A filter contract is intended to add or remove logic to how orders are executed. An order may be tied to a specific filter contract by setting its senderAddress
to the address of the desired filter contract.
Here are some simple examples that demonstrate how filter contracts may be used:
This contract does not add any additional logic to how orders are filled or cancelled. It is primarily intended to show the flow of data from a filter contract to the Exchange
contract. It is important to note that orders that specify this contract as the senderAddress
would only be able to use the methods defined in this filter contract. Those orders could not be filled or cancelled with any other Exchange
methods.
contract ExchangeWrapper {
// Exchange contract.
// solhint-disable-next-line var-name-mixedcase
IExchange internal EXCHANGE;
constructor (address _exchange)
public
{
EXCHANGE = IExchange(_exchange);
}
/// @dev Cancels all orders created by sender with a salt less than or equal to the targetOrderEpoch
/// and senderAddress equal to this contract.
/// @param targetOrderEpoch Orders created with a salt less or equal to this value will be cancelled.
/// @param salt Arbitrary value to gaurantee uniqueness of 0x transaction hash.
/// @param makerSignature Proof that maker wishes to call this function with given params.
function cancelOrdersUpTo(
uint256 targetOrderEpoch,
uint256 salt,
bytes makerSignature
)
external
{
address makerAddress = msg.sender;
// Encode arguments into byte array.
bytes memory data = abi.encodeWithSelector(
EXCHANGE.cancelOrdersUpTo.selector,
targetOrderEpoch
);
// Call `cancelOrdersUpTo` via `executeTransaction`.
EXCHANGE.executeTransaction(
salt,
makerAddress,
data,
makerSignature
);
}
/// @dev Fills an order using `msg.sender` as the taker.
/// @param order Order struct containing order specifications.
/// @param takerAssetFillAmount Desired amount of takerAsset to sell.
/// @param salt Arbitrary value to gaurantee uniqueness of 0x transaction hash.
/// @param orderSignature Proof that order has been created by maker.
/// @param takerSignature Proof that taker wishes to call this function with given params.
function fillOrder(
LibOrder.Order memory order,
uint256 takerAssetFillAmount,
uint256 salt,
bytes memory orderSignature,
bytes memory takerSignature
)
public
{
address takerAddress = msg.sender;
// Encode arguments into byte array.
bytes memory data = abi.encodeWithSelector(
EXCHANGE.fillOrder.selector,
order,
takerAssetFillAmount,
orderSignature
);
// Call `fillOrder` via `executeTransaction`.
EXCHANGE.executeTransaction(
salt,
takerAddress,
data,
takerSignature
);
}
}
This contract is a bit more complex than the last. Orders that specify this contract as the senderAddress
could only be filled if both the maker and taker of the order are on a whitelist created by the filter contract owner.
This contract also makes use of the Validator
signature type. Rather than requiring the taker to sign a 0x transaction and an Ethereum transaction to call this contract, this contract makes use of tx.origin
to only require a signed Ethereum transaction (this may have dangerous consequences without extra measures and is only intended to be an example).
contract Whitelist is
Ownable
{
// Mapping of address => whitelist status.
mapping (address => bool) public isWhitelisted;
// Exchange contract.
// solhint-disable var-name-mixedcase
IExchange internal EXCHANGE;
bytes internal TX_ORIGIN_SIGNATURE;
// solhint-enable var-name-mixedcase
byte constant internal VALIDATOR_SIGNATURE_BYTE = "\x06";
constructor (address _exchange)
public
{
EXCHANGE = IExchange(_exchange);
TX_ORIGIN_SIGNATURE = abi.encodePacked(address(this), VALIDATOR_SIGNATURE_BYTE);
}
/// @dev Adds or removes an address from the whitelist.
/// @param target Address to add or remove from whitelist.
/// @param isApproved Whitelist status to assign to address.
function updateWhitelistStatus(
address target,
bool isApproved
)
external
onlyOwner
{
isWhitelisted[target] = isApproved;
}
/// @dev Verifies signer is same as signer of current Ethereum transaction.
/// NOTE: This function can currently be used to validate signatures coming from outside of this contract.
/// Extra safety checks can be added for a production contract.
/// @param signerAddress Address that should have signed the given hash.
/// @param signature Proof of signing.
/// @return Validity of order signature.
// solhint-disable no-unused-vars
function isValidSignature(
bytes32 hash,
address signerAddress,
bytes signature
)
external
view
returns (bool isValid)
{
// solhint-disable-next-line avoid-tx-origin
return signerAddress == tx.origin;
}
// solhint-enable no-unused-vars
/// @dev Fills an order using `msg.sender` as the taker.
/// The transaction will revert if both the maker and taker are not whitelisted.
/// Orders should specify this contract as the `senderAddress` in order to gaurantee
/// that both maker and taker have been whitelisted.
/// @param order Order struct containing order specifications.
/// @param takerAssetFillAmount Desired amount of takerAsset to sell.
/// @param salt Arbitrary value to gaurantee uniqueness of 0x transaction hash.
/// @param orderSignature Proof that order has been created by maker.
function fillOrderIfWhitelisted(
LibOrder.Order memory order,
uint256 takerAssetFillAmount,
uint256 salt,
bytes memory orderSignature
)
public
{
address takerAddress = msg.sender;
// This contract must be the entry point for the transaction.
require(
// solhint-disable-next-line avoid-tx-origin
takerAddress == tx.origin,
"INVALID_SENDER"
);
// Check if maker is on the whitelist.
require(
isWhitelisted[order.makerAddress],
"MAKER_NOT_WHITELISTED"
);
// Check if taker is on the whitelist.
require(
isWhitelisted[takerAddress],
"TAKER_NOT_WHITELISTED"
);
// Encode arguments into byte array.
bytes memory data = abi.encodeWithSelector(
EXCHANGE.fillOrder.selector,
order,
takerAssetFillAmount,
orderSignature
);
// Call `fillOrder` via `executeTransaction`.
EXCHANGE.executeTransaction(
salt,
takerAddress,
data,
TX_ORIGIN_SIGNATURE
);
}
}
The Exchange
contract includes a public method isValidSignature
for validating signatures. This method has the following interface:
/// @dev Verifies that a signature is valid.
/// @param hash Message hash that is signed.
/// @param signerAddress Address of signer.
/// @param signature Proof of signing.
/// @return Validity of order signature.
function isValidSignature(
bytes32 hash,
address signerAddress,
bytes memory signature
)
public
view
returns (bool isValid);
All signatures submitted to the Exchange contract are represented as a byte array of arbitrary length, where the last byte (the "signature byte") specifies the signatures type. The signature type is popped from the signature byte array before validation. The following signature types are supported within the protocol:
Signature byte | Signature type |
---|---|
0x00 | Illegal |
0x01 | Invalid |
0x02 | EIP712 |
0x03 | EthSign |
0x04 | Wallet |
0x05 | Validator |
0x06 | PreSigned |
This is the default value of the signature byte. A transaction that includes an Illegal signature will be reverted. Therefore, users must explicitly specify a valid signature type.
An Invalid
signature always returns false. An invalid signature can always be recreated and is therefore offered explicitly. This signature type is largely used for testing purposes.
An EIP712
signature is considered valid if the address recovered from calling ecrecover
with the given hash and decoded v
, r
, s
values is the same as the specified signer. In this case, the signature is encoded in the following way:
Offset | Length | Contents |
---|---|---|
0x00 | 1 | v (always 27 or 28) |
0x01 | 32 | r |
0x21 | 32 | s |
An EthSign
signature is considered valid if the address recovered from calling ecrecover
with the an EthSign-prefixed hash and decoded v
, r
, s
values is the same as the specified signer.
The prefixed msgHash
is calculated with:
string constant ETH_PERSONAL_MESSAGE = "\x19Ethereum Signed Message:\n32";
bytes32 msgHash = keccak256(abi.encodePacked(ETH_PERSONAL_MESSAGE, hash));
v
, r
, and s
are encoded in the signature byte array using the same scheme as EIP712 signatures.
The Wallet
signature type allows a contract to trade on behalf of any other address(es) by defining its own signature validation function. When used with order signing, the Wallet
contract is the maker
of the order and should hold any assets that will be traded. When using this signature type, the Exchange
contract makes a STATICCALL
to the Wallet
contract's isValidSignature
method, which means that signature verifcation will fail and revert if the Wallet
attempts to update state. This contract should have the following interface:
contract IWallet {
/// @dev Verifies that a signature is valid.
/// @param hash Message hash that is signed.
/// @param signature Proof of signing.
/// @return Validity of order signature.
function isValidSignature(
bytes32 hash,
bytes signature
)
external
view
returns (bytes4 magicValue);
}
A Wallet
contract's isValidSignature
method must return the following magic value if successful:
// 0xb0671381
bytes4 WALLET_MAGIC_VALUE = bytes4(keccak256("isValidWalletSignature(bytes32,address,bytes)"));
Note when using this method to sign orders: although it can be useful to allow the validity of signatures to be determined by some state stored on the blockchain, it should be noted that the signature will only be checked the first time an order is filled. Therefore, the signature cannot be later invalidated by updating the associates state.
The Validator
signature type allows an address to delegate signature verification to any other address. The Validator
contract must first be approved by calling the setSignatureValidatorApproval
method:
// Mapping of signer => validator => approved
mapping (address => mapping (address => bool)) public allowedValidators;
/// @dev Approves/unnapproves a Validator contract to verify signatures on signer's behalf.
/// @param validatorAddress Address of Validator contract.
/// @param approval Approval or disapproval of Validator contract.
function setSignatureValidatorApproval(
address validatorAddress,
bool approval
)
external;
The setSignatureValidatorApproval
method emits a SignatureValidatorApproval
event when executed.
A Validator signature is then encoded as:
Offset | Length | Contents |
---|---|---|
0x00 | x | signature |
0x00 + x | 20 | Validator contract address |
A Validator contract must have the following interface:
contract IValidator {
/// @dev Verifies that a signature is valid.
/// @param hash Message hash that is signed.
/// @param signerAddress Address that should have signed the given hash.
/// @param signature Proof of signing.
/// @return Validity of order signature.
function isValidSignature(
bytes32 hash,
address signerAddress,
bytes signature
)
external
view
returns (bytes4 magicValue);
}
A Validator
contract's isValidSignature
method must return the following magic value if successful:
// 0x42b38674
bytes4 VALIDATOR_MAGIC_VALUE = bytes4(keccak256("isValidValidatorSignature(address,bytes32,address,bytes)"));
The signature is validated by calling the Validator
contract's isValidSignature
method. When using this signature type, the Exchange
contract makes a STATICCALL
to the Validator
contract's isValidSignature
method, which means that signature verifcation will fail and revert if the Validator
attempts to update state.
// Pop last 20 bytes off of signature byte array.
address validatorAddress = popAddress(signature);
// Ensure signer has approved validator.
if (!allowedValidators[signerAddress][validatorAddress]) {
return false;
}
magicValue = isValidValidatorSignature(
validatorAddress,
hash,
signerAddress,
signature
);
Allows any address to sign a hash on-chain by calling the preSign
method on the Exchange contract.
// Mapping of hash => signer => signed
mapping (bytes32 => mapping(address => bool)) public preSigned;
/// @dev Approves a hash on-chain using any valid signature type or `msg.sender`.
/// After presigning a hash, the preSign signature type will become valid for that hash and signer.
/// @param signerAddress Address that should have signed the given hash.
/// @param signature Proof that the hash has been signed by signer.
function preSign(
bytes32 hash,
address signerAddress,
bytes signature
)
external;
The hash can then be validated with only a PreSigned
signature byte by checking the state of the preSigned
mapping when a transaction is submitted.
isValid = preSigned[hash][signerAddress];
return isValid;
A Fill
event is emitted when an order is filled.
event Fill(
address indexed makerAddress, // Address that created the order.
address indexed feeRecipientAddress, // Address that received fees.
address takerAddress, // Address that filled the order.
address senderAddress, // Address that called the Exchange contract (msg.sender).
uint256 makerAssetFilledAmount, // Amount of makerAsset sold by maker and bought by taker.
uint256 takerAssetFilledAmount, // Amount of takerAsset sold by taker and bought by maker.
uint256 makerFeePaid, // Amount of ZRX paid to feeRecipient by maker.
uint256 takerFeePaid, // Amount of ZRX paid to feeRecipient by taker.
bytes32 indexed orderHash, // EIP712 hash of order (see LibOrder.getOrderHash).
bytes makerAssetData, // Encoded data specific to makerAsset.
bytes takerAssetData // Encoded data specific to takerAsset.
);
A Cancel
event is emitted whenever an individual order is cancelled.
event Cancel(
address indexed makerAddress, // Address that created the order.
address indexed feeRecipientAddress, // Address that would have received fees if order was filled.
address senderAddress, // Address that called the Exchange contract (msg.sender).
bytes32 indexed orderHash, // EIP712 hash of order (see LibOrder.getOrderHash).
bytes makerAssetData, // Encoded data specific to makerAsset.
bytes takerAssetData // Encoded data specific to takerAsset.
);
A CancelUpTo
event is emitted whenever a cancelOrdersUpTo
call is successful.
event CancelUpTo(
address indexed makerAddress, // Orders cancelled must have been created by this address.
address indexed senderAddress, // Orders cancelled must have a `senderAddress` equal to this address.
uint256 orderEpoch // Orders with specified makerAddress and senderAddress with a salt less than this value are considered cancelled.
);
A SignatureValidatorApproval
event is emitted whenever a Validator
contract is approved or disapproved to verify signatures created by a signer via setSignatureValidatorApproval
.
event SignatureValidatorApproval(
address indexed signerAddress, // Address that approves or disapproves a contract to verify signatures.
address indexed validatorAddress, // Address of signature validator contract.
bool approved // Approval or disapproval of validator contract.
);
Whenever an AssetProxy
is registered the Exchange
contract, an AssetProxyRegistered
is emitted.
event AssetProxyRegistered(
uint8 id, // Id of new registered AssetProxy.
address assetProxy, // Address of new registered AssetProxy.
);
An AuthorizedAddressAdded
event is emitted when a new address becomes authorized to call an AssetProxy
contract's transfer functions.
event AuthorizedAddressAdded(
address indexed target,
address indexed caller
);
An AuthorizedAddressRemoved
event is emitted when an address becomes unauthorized to call an AssetProxy
contract's transfer functions.
event AuthorizedAddressRemoved(
address indexed target,
address indexed caller
);
The following events must precede the execution of any function called by AssetProxyOwner
(with the exception of removeAuthorizedAddressAtIndex
).
A Submission
event is emitted when a new transaction is submitted to the AssetProxyOwner
.
event Submission(uint256 indexed transactionId);
A Confirmation
event is emitted when a transaction is confirmed by an individual owner of the AssetProxyOwner
.
event Confirmation(
address indexed sender,
uint256 indexed transactionId
);
A ConfirmationTimeSet
event is emitted when a transaction has been fully confirmed. The 2 week timelock begins at this time, after which the transaction becomes executable.
event ConfirmationTimeSet(
uint256 indexed transactionId,
uint256 confirmationTime
);
struct Order {
address makerAddress; // Address that created the order.
address takerAddress; // Address that is allowed to fill the order. If set to 0, any address is allowed to fill the order.
address feeRecipientAddress; // Address that will receive fees when order is filled.
address senderAddress; // Address that is allowed to call Exchange contract methods that affect this order. If set to 0, any address is allowed to call these methods.
uint256 makerAssetAmount; // Amount of makerAsset being offered by maker. Must be greater than 0.
uint256 takerAssetAmount; // Amount of takerAsset being bid on by maker. Must be greater than 0.
uint256 makerFee; // Amount of ZRX paid to feeRecipient by maker when order is filled. If set to 0, no transfer of ZRX from maker to feeRecipient will be attempted.
uint256 takerFee; // Amount of ZRX paid to feeRecipient by taker when order is filled. If set to 0, no transfer of ZRX from taker to feeRecipient will be attempted.
uint256 expirationTimeSeconds; // Timestamp in seconds at which order expires.
uint256 salt; // Arbitrary number to facilitate uniqueness of the order's hash.
bytes makerAssetData; // Encoded data that can be decoded by a specified proxy contract when transferring makerAsset. The last byte references the id of this proxy.
bytes takerAssetData; // Encoded data that can be decoded by a specified proxy contract when transferring takerAsset. The last byte references the id of this proxy.
}
Fill methods that return a value will return a FillResults instance if successful.
struct FillResults {
uint256 makerAssetFilledAmount; // Total amount of makerAsset(s) filled.
uint256 takerAssetFilledAmount; // Total amount of takerAsset(s) filled.
uint256 makerFeePaid; // Total amount of ZRX paid by maker(s) to feeRecipient(s).
uint256 takerFeePaid; // Total amount of ZRX paid by taker to feeRecipients(s).
}
The matchOrders
method returns a MatchedFillResults instance if successful.
struct MatchedFillResults {
FillResults left; // Amounts filled and fees paid of left order.
FillResults right; // Amounts filled and fees paid of right order.
uint256 leftMakerAssetSpreadAmount; // Spread between price of left and right order, denominated in the left order's makerAsset, paid to taker.
}
The getOrderInfo
method returns an OrderInfo
instance.
struct OrderInfo {
uint8 orderStatus; // Status that describes order's validity and fillability.
bytes32 orderHash; // EIP712 hash of the order (see LibOrder.getOrderHash).
uint256 orderTakerAssetFilledAmount; // Amount of order that has already been filled.
}
For a full specification of how orders are intended to be posted to and retrieved from relayers, see the SRA v2 specification.
Hashes of orders and transactions are calculated according to the EIP712 specification.
The domain separator for the Exchange contract can be calculated with:
// EIP191 header for EIP712 prefix
string constant internal EIP191_HEADER = "\x19\x01";
// Hash of the EIP712 Domain Separator Schema
bytes32 constant internal EIP712_DOMAIN_SEPARATOR_SCHEMA_HASH = keccak256(abi.encodePacked(
"EIP712Domain(",
"string name,",
"string version,",
"address verifyingContract",
")"
));
bytes32 EIP712_DOMAIN_HASH = keccak256(abi.encodePacked(
EIP712_DOMAIN_SEPARATOR_SCHEMA_HASH,
keccak256(bytes("0x Protocol")),
keccak256(bytes("2")),
bytes32(address(this))
));
For more information about how this is used, see hashing an order and hashing a transaction.
Calldata is expensive. As per Appendix G of the Ethereum Yellowpaper, every non-zero byte of calldata costs 68 gas, and every zero byte costs 4 gas. There are certain off-chain optimizations that can be made in order to maximize the amount of zeroes included in calldata.
When an order is filled, it will attempt to fill the minimum of the amount submitted and the amount remaining. Therefore, if a user attempts to fill a very large amount such as 0xF000000000000000000000000000000000000000000000000000000000000000
, then the order will almost always be maximally filled while using minimal extra calldata.
When filling an order, the signature is only validated the first time the order is filled. Because of this, signatures should not be resubmitted after an order has already been partially filled. For a standard 65 byte ECDSA signature, this can save well over 4000 gas.
When creating an order, a full 32 byte salt is generally unecessary to facilitate uniqueness of the order's hash. Using a salt value with as many leading zeroes as possible will increase gas efficiency. It is recommended to use a timestamp or incrementing nonce for the salt value, which will generally be small enough to optimize gas while also working well with cancelOrdersUpTo
.
The matchOrders
, marketSellOrders
, marketSellOrdersNoThrow
, marketBuyOrders
, and marketBuyOrdersNoThrow
functions all require that certain parameters of the later passed in orders match the same parameters of the first passed in order. Rather than checking equality, these functions all assume that the parameters are equal. This means users may pass in zero values for those parameters and the functions will still execute as if the values had been passed in as calldata.
If frequently trading from a single address, it may make sense to generate a vanity address with as many zero bytes as possible.
The ecrecover
precompile available in Solidity expects v
to always have a value of 27
or 28
. Some signers and clients assume that v
will have a value of 0
or 1
, so it may be necessary to add 27
to v
before submitting it to the Exchange
contract.
The following functions within the Exchange
contract contain a mutex that prevents them from called via reentrancy:
fillOrder
fillOrKillOrder
batchFillOrders
batchFillOrKillOrders
marketBuyOrders
marketSellOrders
matchOrders
cancelOrder
batchCancelOrders
cancelOrdersUpTo
setSignatureValidatorApproval
fillOrderNoThrow
and all of its variations do not explicitly have a mutex, but will fail gracefully if any reentrancy is attempted.
The mutex is implemented with the following nonReentrant
modifier:
contract ReentrancyGuard {
// Locked state of mutex
bool private locked = false;
/// @dev Functions with this modifer cannot be reentered. The mutex will be locked
/// before function execution and unlocked after.
modifier nonReentrant() {
// Ensure mutex is unlocked
require(
!locked,
"REENTRANCY_ILLEGAL"
);
// Lock mutex before function call
locked = true;
// Perform function call
_;
// Unlock mutex after function call
locked = false;
}
}