5/5
## The Standard GMX Order Creation Process Creating orders on decentralized exchanges like GMX, whether executing a market swap, opening a position, or closing one, follows a consistent operational pattern executed via smart contracts. This process typically involves three core steps performed sequentially: 1. **Send Execution Fee:** The required fee, often paid in the network's wrapped native token (e.g., WETH), is transferred to the appropriate contract. 2. **Send Collateral Tokens:** The specific tokens required for the order (e.g., DAI, WETH, USDC) are transferred as collateral. 3. **Call `createOrder`:** The main function responsible for initiating the order logic is invoked. Transaction analysis tools like Tenderly reveal that these steps are often bundled together for security and efficiency. A common pattern observed is the use of the `ExchangeRouter.multicall` function. This function acts as an entry point, executing multiple internal calls within a single transaction. A typical sequence within `multicall` for order creation might look like this: * `ExchangeRouter.sendWnt(...)`: Transfers the wrapped native token for the execution fee. * `ExchangeRouter.sendTokens(...)`: Transfers the designated collateral token (e.g., DAI). * `ExchangeRouter.createOrder(...)`: Calls the function that begins the order creation logic within the GMX protocol. Successful execution results in token transfers, such as the execution fee moving from the user (via the router) to the `OrderVault` and the collateral moving from the user's address to the `OrderVault`. ## Why Atomicity is Non-Negotiable for Order Creation It is critically important that the three steps outlined above – sending the execution fee, sending collateral tokens, and calling the `createOrder` function – occur within the boundaries of a **single, atomic transaction**. Atomicity ensures that either all steps complete successfully, or if any step fails, the entire sequence is reverted, leaving the blockchain state unchanged. Consider the severe vulnerability that would arise if these steps were performed in separate transactions: 1. **Transaction 1:** User sends the execution fee (WETH) to the `OrderVault`. 2. **Transaction 2:** User sends the collateral tokens (e.g., DAI) to the `OrderVault`. 3. **(Gap):** Before the user can submit Transaction 3... 4. **Attacker's Transaction:** A malicious user (an attacker) monitoring the blockchain observes the fee and collateral deposits. They quickly submit their *own* `createOrder` transaction. 5. **Transaction 3 (User):** The user finally submits their `createOrder` transaction. In this scenario, the attacker's transaction would likely be processed before the user's third transaction. When the attacker's `createOrder` call executes, the GMX contracts would detect the fees and collateral deposited by the *original user* and associate them with the *attacker's* order. The attacker effectively steals the user's funds to initiate their own position or swap, leaving insufficient funds for the user's subsequent `createOrder` call, which would then fail. This type of attack exploits the time gap between actions and is a form of front-running or state griefing. ## Securing Fund Accounting: The `recordTransferIn` Mechanism To correctly associate deposited funds with the specific order being created and prevent the misuse of funds across different potential orders, the GMX contracts employ a specific mechanism during the `createOrder` execution. The core of this is the `recordTransferIn` function located within the `OrderVault` contract (which inherits functionality from a base contract, likely `StrictBank`). When `createOrder` is called, its internal logic (handled by contracts like `OrderHandler` and `OrderUtils`) needs to determine precisely how much execution fee and collateral were transferred *as part of the current transaction*. It achieves this by calling `OrderVault.recordTransferIn(tokenAddress)` for both the fee token and the collateral token. The logic of the internal `recordTransferIn` function is crucial: ```solidity // Likely found within StrictBank.sol // @dev records a token transfer into the contract // @param token the token to record the transfer for // @return the amount of tokens transferred in function recordTransferIn(address token) internal returns (uint256) { uint256 prevBalance = tokenBalances[token]; // Reads previously stored balance for this token uint256 nextBalance = IERC20(token).balanceOf(address(this)); // Reads the contract's current actual balance of the token tokenBalances[token] = nextBalance; // Updates the stored balance to the current balance return nextBalance - prevBalance; // Returns the difference, representing the amount received } ``` Here's how it works: 1. **Get Previous Balance:** It retrieves the balance of the specified `token` as it was last recorded by this function, stored in the `tokenBalances` mapping (a state variable). 2. **Get Current Balance:** It queries the actual current balance of the `token` held by the contract (`address(this)`) using the standard `balanceOf` function from the token's contract (`IERC20`). 3. **Update Stored Balance:** It updates the `tokenBalances` mapping with the `nextBalance`. This ensures subsequent calls within the same transaction context or future transactions have an accurate starting point. 4. **Calculate and Return Delta:** It returns the difference between the `nextBalance` and `prevBalance`. This difference represents the amount of the `token` that was transferred *into* the contract since the last time `recordTransferIn` was called for that token, effectively measuring the funds received *within the current transactional context*. For example, if the `OrderVault` had a recorded balance of 0 DAI (`prevBalance = 0`), and the user's transaction transferred 10 DAI, the `balanceOf(address(this))` call would return 10 (`nextBalance = 10`). The function would update `tokenBalances[DAI]` to 10 and return `10 - 0 = 10`. If `recordTransferIn(DAI)` were called again immediately *within the same transaction* without any further DAI transfer, `prevBalance` would now be 10, `nextBalance` would still be 10, `tokenBalances[DAI]` would remain 10, and the function would return `10 - 10 = 0`. ## Integrating Fund Recording into the Order Logic The `OrderUtils.sol` contract, which orchestrates much of the `createOrder` logic, utilizes the `recordTransferIn` function to securely determine the amounts deposited for the specific order being created. First, it records the amount of collateral received: ```solidity // Within OrderUtils.sol during createOrder execution cache.initialCollateralDeltaAmount = orderVault.recordTransferIn(params.addresses.initialCollateralToken /*, ... */); // This captures the amount of collateral token transferred in this transaction. ``` Next, it records the amount of the execution fee token (typically WETH) received and validates it: ```solidity // Within OrderUtils.sol during createOrder execution uint256 wntAmount = orderVault.recordTransferIn(cache.wnt); // cache.wnt holds the WETH address if (wntAmount < params.numbers.executionFee) { revert Errors.InsufficientWntAmountForExecutionFee(/* ... */); } uint256 executionFee = params.numbers.executionFee; // Assuming validation passes // This captures the WETH transferred and checks if it meets the required fee amount. ``` These accurately measured amounts (`initialCollateralDeltaAmount` and the validated `executionFee`) are then stored within the data structure representing the new order: ```solidity // Within OrderUtils.sol, populating the order object order.setInitialCollateralDeltaAmount(cache.initialCollateralDeltaAmount); // ... other order parameters order.setExecutionFee(executionFee); ``` Finally, this fully populated `order` object, containing the correctly accounted-for funds, is saved persistently to the blockchain state, often managed by a contract like `DataStore` via a utility function: `OrderStoreUtils.set(dataStore, key, order);`. ## The `multicall` Pattern: Achieving Atomicity and Preventing Front-Running Revisiting the vulnerability scenario highlights why the `recordTransferIn` mechanism alone isn't sufficient without atomicity. If transfers and the `createOrder` call were separate transactions, an attacker's intervening `createOrder` call would trigger `recordTransferIn`. This function would correctly calculate the difference based on the *user's* deposits (as the balance increased since the last block), assign these funds to the *attacker's* order, and update the `tokenBalances` state. When the user's legitimate `createOrder` call finally executes, `recordTransferIn` would find no *new* balance increase relative to the state updated by the attacker's transaction, resulting in a returned amount of 0 and order failure for the user. This is precisely why secure integrations, including the GMX frontend, utilize patterns like `multicall`. By bundling the `sendWnt` (fee), `sendTokens` (collateral), and `createOrder` calls into a single operation submitted as one Ethereum transaction using `ExchangeRouter.multicall`, the system guarantees **atomicity**. The benefits are clear: * **All or Nothing:** All three internal calls must succeed for the transaction to be successful. If any part fails, the entire transaction reverts, including the token transfers. * **No Interruption:** An attacker cannot insert their transaction between the fee/collateral transfers and the `createOrder` call because these steps execute sequentially within the protected context of a single transaction. * **Correct Accounting:** The `recordTransferIn` calls within `createOrder` accurately measure the funds deposited *during that specific transaction*, correctly associating them with the user's intended order before the transaction concludes and state changes are finalized. Therefore, using `multicall` or similar atomic execution patterns is essential for the security and correct functioning of multi-step operations like order creation in decentralized finance protocols, effectively mitigating front-running risks associated with fund deposits.
Creating orders on decentralized exchanges like GMX, whether executing a market swap, opening a position, or closing one, follows a consistent operational pattern executed via smart contracts. This process typically involves three core steps performed sequentially:
Send Execution Fee: The required fee, often paid in the network's wrapped native token (e.g., WETH), is transferred to the appropriate contract.
Send Collateral Tokens: The specific tokens required for the order (e.g., DAI, WETH, USDC) are transferred as collateral.
Call createOrder
: The main function responsible for initiating the order logic is invoked.
Transaction analysis tools like Tenderly reveal that these steps are often bundled together for security and efficiency. A common pattern observed is the use of the ExchangeRouter.multicall
function. This function acts as an entry point, executing multiple internal calls within a single transaction. A typical sequence within multicall
for order creation might look like this:
ExchangeRouter.sendWnt(...)
: Transfers the wrapped native token for the execution fee.
ExchangeRouter.sendTokens(...)
: Transfers the designated collateral token (e.g., DAI).
ExchangeRouter.createOrder(...)
: Calls the function that begins the order creation logic within the GMX protocol.
Successful execution results in token transfers, such as the execution fee moving from the user (via the router) to the OrderVault
and the collateral moving from the user's address to the OrderVault
.
It is critically important that the three steps outlined above – sending the execution fee, sending collateral tokens, and calling the createOrder
function – occur within the boundaries of a single, atomic transaction. Atomicity ensures that either all steps complete successfully, or if any step fails, the entire sequence is reverted, leaving the blockchain state unchanged.
Consider the severe vulnerability that would arise if these steps were performed in separate transactions:
Transaction 1: User sends the execution fee (WETH) to the OrderVault
.
Transaction 2: User sends the collateral tokens (e.g., DAI) to the OrderVault
.
(Gap): Before the user can submit Transaction 3...
Attacker's Transaction: A malicious user (an attacker) monitoring the blockchain observes the fee and collateral deposits. They quickly submit their own createOrder
transaction.
Transaction 3 (User): The user finally submits their createOrder
transaction.
In this scenario, the attacker's transaction would likely be processed before the user's third transaction. When the attacker's createOrder
call executes, the GMX contracts would detect the fees and collateral deposited by the original user and associate them with the attacker's order. The attacker effectively steals the user's funds to initiate their own position or swap, leaving insufficient funds for the user's subsequent createOrder
call, which would then fail. This type of attack exploits the time gap between actions and is a form of front-running or state griefing.
recordTransferIn
MechanismTo correctly associate deposited funds with the specific order being created and prevent the misuse of funds across different potential orders, the GMX contracts employ a specific mechanism during the createOrder
execution. The core of this is the recordTransferIn
function located within the OrderVault
contract (which inherits functionality from a base contract, likely StrictBank
).
When createOrder
is called, its internal logic (handled by contracts like OrderHandler
and OrderUtils
) needs to determine precisely how much execution fee and collateral were transferred as part of the current transaction. It achieves this by calling OrderVault.recordTransferIn(tokenAddress)
for both the fee token and the collateral token.
The logic of the internal recordTransferIn
function is crucial:
Here's how it works:
Get Previous Balance: It retrieves the balance of the specified token
as it was last recorded by this function, stored in the tokenBalances
mapping (a state variable).
Get Current Balance: It queries the actual current balance of the token
held by the contract (address(this)
) using the standard balanceOf
function from the token's contract (IERC20
).
Update Stored Balance: It updates the tokenBalances
mapping with the nextBalance
. This ensures subsequent calls within the same transaction context or future transactions have an accurate starting point.
Calculate and Return Delta: It returns the difference between the nextBalance
and prevBalance
. This difference represents the amount of the token
that was transferred into the contract since the last time recordTransferIn
was called for that token, effectively measuring the funds received within the current transactional context.
For example, if the OrderVault
had a recorded balance of 0 DAI (prevBalance = 0
), and the user's transaction transferred 10 DAI, the balanceOf(address(this))
call would return 10 (nextBalance = 10
). The function would update tokenBalances[DAI]
to 10 and return 10 - 0 = 10
. If recordTransferIn(DAI)
were called again immediately within the same transaction without any further DAI transfer, prevBalance
would now be 10, nextBalance
would still be 10, tokenBalances[DAI]
would remain 10, and the function would return 10 - 10 = 0
.
The OrderUtils.sol
contract, which orchestrates much of the createOrder
logic, utilizes the recordTransferIn
function to securely determine the amounts deposited for the specific order being created.
First, it records the amount of collateral received:
Next, it records the amount of the execution fee token (typically WETH) received and validates it:
These accurately measured amounts (initialCollateralDeltaAmount
and the validated executionFee
) are then stored within the data structure representing the new order:
Finally, this fully populated order
object, containing the correctly accounted-for funds, is saved persistently to the blockchain state, often managed by a contract like DataStore
via a utility function: OrderStoreUtils.set(dataStore, key, order);
.
multicall
Pattern: Achieving Atomicity and Preventing Front-RunningRevisiting the vulnerability scenario highlights why the recordTransferIn
mechanism alone isn't sufficient without atomicity. If transfers and the createOrder
call were separate transactions, an attacker's intervening createOrder
call would trigger recordTransferIn
. This function would correctly calculate the difference based on the user's deposits (as the balance increased since the last block), assign these funds to the attacker's order, and update the tokenBalances
state. When the user's legitimate createOrder
call finally executes, recordTransferIn
would find no new balance increase relative to the state updated by the attacker's transaction, resulting in a returned amount of 0 and order failure for the user.
This is precisely why secure integrations, including the GMX frontend, utilize patterns like multicall
. By bundling the sendWnt
(fee), sendTokens
(collateral), and createOrder
calls into a single operation submitted as one Ethereum transaction using ExchangeRouter.multicall
, the system guarantees atomicity.
The benefits are clear:
All or Nothing: All three internal calls must succeed for the transaction to be successful. If any part fails, the entire transaction reverts, including the token transfers.
No Interruption: An attacker cannot insert their transaction between the fee/collateral transfers and the createOrder
call because these steps execute sequentially within the protected context of a single transaction.
Correct Accounting: The recordTransferIn
calls within createOrder
accurately measure the funds deposited during that specific transaction, correctly associating them with the user's intended order before the transaction concludes and state changes are finalized.
Therefore, using multicall
or similar atomic execution patterns is essential for the security and correct functioning of multi-step operations like order creation in decentralized finance protocols, effectively mitigating front-running risks associated with fund deposits.
A critical analysis to GMX Order Creation Atomicity - Discover the essential role of atomic transactions (`multicall`) in securing GMX orders. Learn how `recordTransferIn` verifies funds within a single transaction context to prevent front-running.
Previous lesson
Previous
Next lesson
Next
Give us feedback
Cyfrin Updraft has partnered with GMX to provide SSCD+ certifications vouchers to the first 25 students who demonstrate the completion of this course and are building on top of GMX. Join the GMX Discord server to redeem your coupon, or navigate to the docs to learn more!
Course Overview
About the course
Mechanics and contract architecture of the GMX protocol
Token pricing and fees
Liquidity: GM pools and GLV vaults
Math, funding rates, liquidation pricing, P&L calculations
Limit orders, take profit orders, stop loss, and stop market orders
Auto-cancel and auto-deleveraging
GLP, esGMX, GMX staking and delegation
DeFi Developer
$75,000 - $200,000 (avg. salary)
Smart Contract Engineer
$100,000 - $150,000 (avg. salary)
Web3 developer
$60,000 - $150,000 (avg. salary)
Web3 Developer Relations
$85,000 - $125,000 (avg. salary)
Smart Contract Auditor
$100,000 - $200,000 (avg. salary)
Security researcher
$49,999 - $120,000 (avg. salary)
Last updated on June 26, 2025
Duration: 8min
Duration: 1h 19min
Duration: 1h 24min
Duration: 16min
Duration: 11min
Duration: 11min
Duration: 6min
Course Overview
About the course
Mechanics and contract architecture of the GMX protocol
Token pricing and fees
Liquidity: GM pools and GLV vaults
Math, funding rates, liquidation pricing, P&L calculations
Limit orders, take profit orders, stop loss, and stop market orders
Auto-cancel and auto-deleveraging
GLP, esGMX, GMX staking and delegation
DeFi Developer
$75,000 - $200,000 (avg. salary)
Smart Contract Engineer
$100,000 - $150,000 (avg. salary)
Web3 developer
$60,000 - $150,000 (avg. salary)
Web3 Developer Relations
$85,000 - $125,000 (avg. salary)
Smart Contract Auditor
$100,000 - $200,000 (avg. salary)
Security researcher
$49,999 - $120,000 (avg. salary)
Last updated on June 26, 2025