• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

delvtech / hyperdrive / 12939603715

23 Jan 2025 10:55PM UTC coverage: 89.255% (+0.02%) from 89.235%
12939603715

Pull #1228

github

web-flow
Merge f65359190 into 688eacdbe
Pull Request #1228: Addressed a front-running vulnerability in the matching engine

7 of 7 new or added lines in 1 file covered. (100.0%)

3 existing lines in 1 file now uncovered.

2924 of 3276 relevant lines covered (89.26%)

310514.33 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

95.0
/contracts/src/matching/HyperdriveMatchingEngine.sol
1
// SPDX-License-Identifier: Apache-2.0
2
pragma solidity 0.8.24;
3

4
import { IMorpho } from "morpho-blue/src/interfaces/IMorpho.sol";
5
import { IERC1271 } from "openzeppelin/interfaces/IERC1271.sol";
6
import { ERC20 } from "openzeppelin/token/ERC20/ERC20.sol";
7
import { SafeERC20 } from "openzeppelin/token/ERC20/utils/SafeERC20.sol";
8
import { ECDSA } from "openzeppelin/utils/cryptography/ECDSA.sol";
9
import { EIP712 } from "openzeppelin/utils/cryptography/EIP712.sol";
10
import { ReentrancyGuard } from "openzeppelin/utils/ReentrancyGuard.sol";
11
import { IHyperdrive } from "../interfaces/IHyperdrive.sol";
12
import { IHyperdriveMatchingEngine } from "../interfaces/IHyperdriveMatchingEngine.sol";
13
import { AssetId } from "../libraries/AssetId.sol";
14
import { HYPERDRIVE_MATCHING_ENGINE_KIND, VERSION } from "../libraries/Constants.sol";
15
import { FixedPointMath } from "../libraries/FixedPointMath.sol";
16

17
/// @author DELV
18
/// @title HyperdriveMatchingEngine
19
/// @notice A matching engine that processes order intents and settles trades on
20
///         the Hyperdrive AMM.
21
/// @custom:disclaimer The language used in this code is for coding convenience
22
///                    only, and is not intended to, and does not, have any
23
///                    particular legal or regulatory significance.
24
contract HyperdriveMatchingEngine is
25
    IHyperdriveMatchingEngine,
26
    ReentrancyGuard,
27
    EIP712
28
{
29
    using FixedPointMath for uint256;
30
    using SafeERC20 for ERC20;
31

32
    /// @notice The EIP712 typehash of the `IHyperdriveMatchingEngine.OrderIntent`
33
    ///         struct.
34
    bytes32 public constant ORDER_INTENT_TYPEHASH =
35
        keccak256(
36
            "OrderIntent(address trader,address counterparty,address feeRecipient,address hyperdrive,uint256 amount,uint256 slippageGuard,uint256 minVaultSharePrice,Options options,uint8 orderType,uint256 expiry,bytes32 salt)"
37
        );
38

39
    /// @notice The EIP712 typehash of the `IHyperdrive.Options` struct.
40
    /// @dev We exclude extra data from the options hashing since it has no
41
    ///      effect on execution.
42
    bytes32 public constant OPTIONS_TYPEHASH =
43
        keccak256("Options(address destination,bool asBase)");
44

45
    /// @notice The name of this matching engine.
46
    string public name;
47

48
    /// @notice The kind of this matching engine.
49
    string public constant kind = HYPERDRIVE_MATCHING_ENGINE_KIND;
50

51
    /// @notice The version of this matching engine.
52
    string public constant version = VERSION;
53

54
    /// @notice The morpho market that this matching engine connects to as a
55
    ///         flash loan provider.
56
    IMorpho public immutable morpho;
57

58
    /// @notice A mapping from order hashes to their cancellation status.
59
    mapping(bytes32 => bool) public isCancelled;
60

61
    /// @notice Initializes the matching engine.
62
    /// @param _name The name of this matching engine.
63
    /// @param _morpho The Morpho pool.
64
    constructor(string memory _name, IMorpho _morpho) EIP712(_name, VERSION) {
65
        name = _name;
×
66
        morpho = _morpho;
×
67
    }
68

69
    /// @dev Ensures that the caller is Morpho.
70
    modifier onlyMorpho() {
71
        if (msg.sender != address(morpho)) {
203✔
72
            revert SenderNotMorpho();
1✔
73
        }
74
        _;
75
    }
76

77
    /// @notice Allows a trader to cancel a list of their orders.
78
    /// @param _orders The orders to cancel.
79
    function cancelOrders(OrderIntent[] calldata _orders) external {
80
        // Cancel all of the orders in the batch.
81
        bytes32[] memory orderHashes = new bytes32[](_orders.length);
14✔
82
        for (uint256 i = 0; i < _orders.length; i++) {
14✔
83
            // Ensure that the sender is the trader in the order.
84
            if (msg.sender != _orders[i].trader) {
18✔
85
                revert InvalidSender();
2✔
86
            }
87

88
            // Ensure that the sender signed each order.
89
            bytes32 orderHash = hashOrderIntent(_orders[i]);
16✔
90
            if (!verifySignature(orderHash, _orders[i].signature, msg.sender)) {
16✔
91
                revert InvalidSignature();
2✔
92
            }
93

94
            // Cancel the order.
95
            isCancelled[orderHash] = true;
12✔
96
            orderHashes[i] = orderHash;
12✔
97
        }
98

99
        emit OrdersCancelled(msg.sender, orderHashes);
8✔
100
    }
101

102
    /// @notice Directly matches a long and a short order. To avoid the need for
103
    ///         liquidity, this function will open a flash loan on Morpho to
104
    ///         ensure that the pool is appropriately capitalized.
105
    /// @dev This function isn't marked as nonReentrant because this contract
106
    ///      will be reentered when the Morpho flash-loan callback is processed.
107
    ///      `onMorphoFlashLoan` has been marked as non reentrant to ensure that
108
    ///      the trading logic can't be reentered.
109
    /// @param _longOrder The order intent to open a long.
110
    /// @param _shortOrder The order intent to open a short.
111
    /// @param _lpAmount The amount to flash borrow and LP.
112
    /// @param _addLiquidityOptions The options used when adding liquidity.
113
    /// @param _removeLiquidityOptions The options used when removing liquidity.
114
    /// @param _feeRecipient The address that receives the LP fees from matching
115
    ///        the trades.
116
    /// @param _isLongFirst A flag indicating whether the long or short should
117
    ///        be opened first.
118
    function matchOrders(
119
        OrderIntent calldata _longOrder,
120
        OrderIntent calldata _shortOrder,
121
        uint256 _lpAmount,
122
        IHyperdrive.Options calldata _addLiquidityOptions,
123
        IHyperdrive.Options calldata _removeLiquidityOptions,
124
        address _feeRecipient,
125
        bool _isLongFirst
126
    ) external {
127
        // Validate the order intents and the add and remove liquidity options
128
        // in preparation of matching the orders.
129
        (bytes32 longOrderHash, bytes32 shortOrderHash) = _validateOrders(
227✔
130
            _longOrder,
131
            _shortOrder,
132
            _addLiquidityOptions,
133
            _removeLiquidityOptions,
134
            _feeRecipient
135
        );
136

137
        // Cancel the orders so that they can't be used again.
138
        isCancelled[longOrderHash] = true;
202✔
139
        isCancelled[shortOrderHash] = true;
202✔
140

141
        // Send off the flash loan call to Morpho. The remaining execution logic
142
        // will be executed in the `onMorphoFlashLoan` callback.
143
        morpho.flashLoan(
202✔
144
            // NOTE: The loan token is always the base token since we require
145
            // `asBase` to be true.
146
            _longOrder.hyperdrive.baseToken(),
147
            _lpAmount,
148
            abi.encode(
149
                _longOrder,
150
                _shortOrder,
151
                _addLiquidityOptions,
152
                _removeLiquidityOptions,
153
                _feeRecipient,
154
                _isLongFirst
155
            )
156
        );
157

158
        // Emit an `OrdersMatched` event.
159
        emit OrdersMatched(
202✔
160
            _longOrder.hyperdrive,
161
            longOrderHash,
162
            shortOrderHash,
163
            _longOrder.trader,
164
            _shortOrder.trader
165
        );
166
    }
167

168
    /// @notice Callback called when a flash loan occurs.
169
    /// @dev This can only be called by Morpho. This ensures that the flow goes
170
    ///      through `matchOrders`, which is required to verify that the
171
    ///      validation checks are performed.
172
    /// @dev The callback is called only if data is not empty.
173
    /// @param _lpAmount The amount of assets that were flash loaned.
174
    /// @param _data Arbitrary data passed to the `flashLoan` function.
175
    function onMorphoFlashLoan(
176
        uint256 _lpAmount,
177
        bytes calldata _data
178
    ) external onlyMorpho nonReentrant {
179
        // Decode the execution parameters. This encodes the information
180
        // required to execute the LP, long, and short operations.
181
        (
202✔
182
            OrderIntent memory longOrder,
183
            OrderIntent memory shortOrder,
184
            IHyperdrive.Options memory addLiquidityOptions,
185
            IHyperdrive.Options memory removeLiquidityOptions,
186
            address feeRecipient,
187
            bool isLongFirst
188
        ) = abi.decode(
202✔
189
                _data,
190
                (
191
                    OrderIntent,
192
                    OrderIntent,
193
                    IHyperdrive.Options,
194
                    IHyperdrive.Options,
195
                    address,
196
                    bool
197
                )
198
            );
199

200
        // Add liquidity to the pool.
201
        IHyperdrive hyperdrive = longOrder.hyperdrive;
202✔
202
        ERC20 baseToken = ERC20(hyperdrive.baseToken());
202✔
203
        uint256 lpAmount = _lpAmount; // avoid stack-too-deep
202✔
204
        uint256 lpShares = _addLiquidity(
202✔
205
            hyperdrive,
206
            baseToken,
207
            lpAmount,
208
            addLiquidityOptions
209
        );
210

211
        // If the long should be executed first, execute the long and then the
212
        // short.
213
        if (isLongFirst) {
202✔
214
            _openLong(hyperdrive, baseToken, longOrder);
1✔
215
            _openShort(hyperdrive, baseToken, shortOrder);
1✔
216
        }
217
        // Otherwise, execute the short and then the long.
218
        else {
219
            _openShort(hyperdrive, baseToken, shortOrder);
201✔
220
            _openLong(hyperdrive, baseToken, longOrder);
201✔
221
        }
222

223
        // Remove liquidity. This will repay the flash loan.
224
        (uint256 proceeds, uint256 withdrawalShares) = hyperdrive
202✔
225
            .removeLiquidity(lpShares, 0, removeLiquidityOptions);
226

227
        // If the withdrawal shares are greater than zero, send them to the fee
228
        // recipient.
229
        if (withdrawalShares > 0) {
202✔
UNCOV
230
            hyperdrive.transferFrom(
×
231
                AssetId._WITHDRAWAL_SHARE_ASSET_ID,
232
                address(this),
233
                feeRecipient,
234
                withdrawalShares
235
            );
236
        }
237

238
        // If the proceeds are greater than the LP amount, we send the difference
239
        // to the fee recipient.
240
        if (proceeds > lpAmount) {
202✔
241
            baseToken.safeTransfer(feeRecipient, proceeds - lpAmount);
202✔
242
        }
243

244
        // Approve Morpho Blue to take back the assets that were provided.
245
        baseToken.forceApprove(address(morpho), lpAmount);
202✔
246
    }
247

248
    /// @notice Hashes an order intent according to EIP-712.
249
    /// @param _order The order intent to hash.
250
    /// @return The hash of the order intent.
251
    function hashOrderIntent(
252
        OrderIntent calldata _order
253
    ) public view returns (bytes32) {
254
        return
874✔
255
            _hashTypedDataV4(
874✔
256
                keccak256(
257
                    abi.encode(
258
                        ORDER_INTENT_TYPEHASH,
259
                        _order.trader,
260
                        _order.counterparty,
261
                        _order.feeRecipient,
262
                        _order.hyperdrive,
263
                        _order.amount,
264
                        _order.slippageGuard,
265
                        _order.minVaultSharePrice,
266
                        keccak256(
267
                            abi.encode(
268
                                OPTIONS_TYPEHASH,
269
                                _order.options.destination,
270
                                _order.options.asBase
271
                            )
272
                        ),
273
                        uint8(_order.orderType),
274
                        _order.expiry,
275
                        _order.salt
276
                    )
277
                )
278
            );
279
    }
280

281
    /// @notice Verifies a signature for a known signer. Returns a flag
282
    ///         indicating whether signature verification was successful.
283
    /// @param _hash The EIP-712 hash of the order.
284
    /// @param _signature The signature bytes.
285
    /// @param _signer The expected signer.
286
    /// @return A flag inidicating whether signature verification was successful.
287
    function verifySignature(
288
        bytes32 _hash,
289
        bytes calldata _signature,
290
        address _signer
291
    ) public view returns (bool) {
292
        // For contracts, we use EIP-1271 signatures.
293
        if (_signer.code.length > 0) {
430✔
294
            try IERC1271(_signer).isValidSignature(_hash, _signature) returns (
6✔
295
                bytes4 magicValue
296
            ) {
6✔
297
                if (magicValue != IERC1271.isValidSignature.selector) {
6✔
298
                    return false;
2✔
299
                }
300
                return true;
4✔
UNCOV
301
            } catch {
×
UNCOV
302
                return false;
×
303
            }
304
        }
305

306
        // For EOAs, verify the ECDSA signature.
307
        if (ECDSA.recover(_hash, _signature) != _signer) {
424✔
308
            return false;
3✔
309
        }
310

311
        return true;
419✔
312
    }
313

314
    /// @dev Adds liquidity to the Hyperdrive pool.
315
    /// @param _hyperdrive The Hyperdrive pool.
316
    /// @param _baseToken The base token of the pool.
317
    /// @param _lpAmount The amount of base to LP.
318
    /// @param _options The options that configures how the deposit will be
319
    ///        settled.
320
    /// @return The amount of LP shares received.
321
    function _addLiquidity(
322
        IHyperdrive _hyperdrive,
323
        ERC20 _baseToken,
324
        uint256 _lpAmount,
325
        IHyperdrive.Options memory _options
326
    ) internal returns (uint256) {
327
        _baseToken.forceApprove(address(_hyperdrive), _lpAmount + 1);
202✔
328
        return
202✔
329
            _hyperdrive.addLiquidity(
202✔
330
                _lpAmount,
331
                0,
332
                0,
333
                type(uint256).max,
334
                _options
335
            );
336
    }
337

338
    /// @dev Opens a long position in the Hyperdrive pool.
339
    /// @param _hyperdrive The Hyperdrive pool.
340
    /// @param _baseToken The base token of the pool.
341
    /// @param _order The order containing the trade parameters.
342
    function _openLong(
343
        IHyperdrive _hyperdrive,
344
        ERC20 _baseToken,
345
        OrderIntent memory _order
346
    ) internal {
347
        _baseToken.safeTransferFrom(
202✔
348
            _order.trader,
349
            address(this),
350
            _order.amount
351
        );
352
        _baseToken.forceApprove(address(_hyperdrive), _order.amount + 1);
202✔
353
        _hyperdrive.openLong(
202✔
354
            _order.amount,
355
            _order.slippageGuard,
356
            _order.minVaultSharePrice,
357
            _order.options
358
        );
359
    }
360

361
    /// @dev Opens a short position in the Hyperdrive pool.
362
    /// @param _hyperdrive The Hyperdrive pool.
363
    /// @param _baseToken The base token of the pool.
364
    /// @param _order The order containing the trade parameters.
365
    function _openShort(
366
        IHyperdrive _hyperdrive,
367
        ERC20 _baseToken,
368
        OrderIntent memory _order
369
    ) internal {
370
        _baseToken.safeTransferFrom(
202✔
371
            _order.trader,
372
            address(this),
373
            _order.slippageGuard
374
        );
375
        _baseToken.forceApprove(address(_hyperdrive), _order.slippageGuard + 1);
202✔
376
        (, uint256 shortPaid) = _hyperdrive.openShort(
202✔
377
            _order.amount,
378
            _order.slippageGuard,
379
            _order.minVaultSharePrice,
380
            _order.options
381
        );
382
        if (_order.slippageGuard > shortPaid) {
202✔
383
            _baseToken.safeTransfer(
202✔
384
                _order.trader,
385
                _order.slippageGuard - shortPaid
386
            );
387
        }
388
    }
389

390
    /// @dev Validates orders and returns their hashes.
391
    /// @param _longOrder The order intent to open a long.
392
    /// @param _shortOrder The order intent to open a short.
393
    /// @param _addLiquidityOptions The options used when adding liquidity.
394
    /// @param _removeLiquidityOptions The options used when removing liquidity.
395
    /// @param _feeRecipient The fee recipient of the match.
396
    /// @return longOrderHash The hash of the long order.
397
    /// @return shortOrderHash The hash of the short order.
398
    function _validateOrders(
399
        OrderIntent calldata _longOrder,
400
        OrderIntent calldata _shortOrder,
401
        IHyperdrive.Options calldata _addLiquidityOptions,
402
        IHyperdrive.Options calldata _removeLiquidityOptions,
403
        address _feeRecipient
404
    ) internal view returns (bytes32 longOrderHash, bytes32 shortOrderHash) {
405
        // Ensure that the long and short orders are the correct type.
406
        if (
407
            _longOrder.orderType != OrderType.OpenLong ||
227✔
408
            _shortOrder.orderType != OrderType.OpenShort
225✔
409
        ) {
3✔
410
            revert InvalidOrderType();
3✔
411
        }
412

413
        // Ensure that the counterparties are compatible.
414
        if (
415
            (_longOrder.counterparty != address(0) &&
224✔
416
                _longOrder.counterparty != _shortOrder.trader) ||
417
            (_shortOrder.counterparty != address(0) &&
418
                _shortOrder.counterparty != _longOrder.trader)
419
        ) {
2✔
420
            revert InvalidCounterparty();
2✔
421
        }
422

423
        // Ensure that the fee recipients are compatible.
424
        if (
425
            (_longOrder.feeRecipient != address(0) &&
222✔
426
                _longOrder.feeRecipient != _feeRecipient) ||
427
            (_shortOrder.feeRecipient != address(0) &&
428
                _shortOrder.feeRecipient != _feeRecipient)
429
        ) {
2✔
430
            revert InvalidFeeRecipient();
2✔
431
        }
432

433
        // Ensure that neither order has expired.
434
        if (
435
            _longOrder.expiry <= block.timestamp ||
220✔
436
            _shortOrder.expiry <= block.timestamp
218✔
437
        ) {
3✔
438
            revert AlreadyExpired();
3✔
439
        }
440

441
        // Ensure that both orders refer to the same Hyperdrive pool.
442
        if (_longOrder.hyperdrive != _shortOrder.hyperdrive) {
217✔
443
            revert MismatchedHyperdrive();
1✔
444
        }
445

446
        // Ensure that all of the transactions should be settled with base.
447
        if (
448
            !_longOrder.options.asBase ||
216✔
449
            !_shortOrder.options.asBase ||
215✔
450
            !_addLiquidityOptions.asBase ||
214✔
451
            !_removeLiquidityOptions.asBase
213✔
452
        ) {
4✔
453
            revert InvalidSettlementAsset();
4✔
454
        }
455

456
        // Ensure that the orders cross. We can calculate a worst-case price
457
        // for the long and short using the `amount` and `slippageGuard` fields.
458
        // In order for the orders to cross, the price of the long should be
459
        // equal to or higher than the price of the short. This implies that the
460
        // long is willing to buy bonds at a price equal or higher than the
461
        // short is selling bonds, which ensures that the trade is valid.
462
        if (
463
            _longOrder.slippageGuard != 0 &&
212✔
464
            _shortOrder.slippageGuard < _shortOrder.amount &&
201✔
465
            _longOrder.amount.divDown(_longOrder.slippageGuard) <
201✔
466
            (_shortOrder.amount - _shortOrder.slippageGuard).divDown(
201✔
467
                _shortOrder.amount
468
            )
469
        ) {
1✔
470
            revert InvalidMatch();
1✔
471
        }
472

473
        // Hash both orders.
474
        longOrderHash = hashOrderIntent(_longOrder);
211✔
475
        shortOrderHash = hashOrderIntent(_shortOrder);
211✔
476

477
        // Ensure that neither order has been cancelled.
478
        if (isCancelled[longOrderHash] || isCancelled[shortOrderHash]) {
211✔
479
            revert AlreadyCancelled();
3✔
480
        }
481

482
        // Ensure that the order intents were signed correctly.
483
        if (
484
            !verifySignature(
208✔
485
                longOrderHash,
486
                _longOrder.signature,
487
                _longOrder.trader
488
            ) ||
489
            !verifySignature(
206✔
490
                shortOrderHash,
491
                _shortOrder.signature,
492
                _shortOrder.trader
493
            )
494
        ) {
3✔
495
            revert InvalidSignature();
3✔
496
        }
497

498
        // Ensure that the destination of the add/remove liquidity options is
499
        // this contract.
500
        if (
501
            _addLiquidityOptions.destination != address(this) ||
205✔
502
            _removeLiquidityOptions.destination != address(this)
203✔
503
        ) {
3✔
504
            revert InvalidDestination();
3✔
505
        }
506
    }
507
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc