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

delvtech / hyperdrive / 12940495014

24 Jan 2025 12:10AM UTC coverage: 89.489% (+0.02%) from 89.471%
12940495014

push

github

web-flow
Ensure that Morpho is the caller of `onMorphoFlashLoan` (#1227)

* Ensure that Morpho is the caller of `onMorphoFlashLoan`

* Updated coverage job

* Addressed a front-running vulnerability in the matching engine (#1228)

* Allow traders to specify the counterparty and fee recipient of their trades

* Added a non-reentrancy modifier to `cancelOrders` (#1229)

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

3 existing lines in 1 file now uncovered.

3014 of 3368 relevant lines covered (89.49%)

328347.25 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(
80
        OrderIntent[] calldata _orders
81
    ) external nonReentrant {
82
        // Cancel all of the orders in the batch.
83
        bytes32[] memory orderHashes = new bytes32[](_orders.length);
14✔
84
        for (uint256 i = 0; i < _orders.length; i++) {
14✔
85
            // Ensure that the sender is the trader in the order.
86
            if (msg.sender != _orders[i].trader) {
18✔
87
                revert InvalidSender();
2✔
88
            }
89

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

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

101
        emit OrdersCancelled(msg.sender, orderHashes);
8✔
102
    }
103

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

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

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

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

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

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

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

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

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

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

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

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

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

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

313
        return true;
419✔
314
    }
315

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

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

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

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

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

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

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

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

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

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

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

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

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

500
        // Ensure that the destination of the add/remove liquidity options is
501
        // this contract.
502
        if (
503
            _addLiquidityOptions.destination != address(this) ||
205✔
504
            _removeLiquidityOptions.destination != address(this)
203✔
505
        ) {
3✔
506
            revert InvalidDestination();
3✔
507
        }
508
    }
509
}
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