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

delvtech / hyperdrive / 12938071562

23 Jan 2025 09:06PM UTC coverage: 89.235% (+0.006%) from 89.229%
12938071562

Pull #1227

github

jalextowle
Updated coverage job
Pull Request #1227: Ensure that Morpho is the caller of `onMorphoFlashLoan`

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

3 existing lines in 1 file now uncovered.

2918 of 3270 relevant lines covered (89.24%)

311083.08 hits per line

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

94.68
/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 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(
223✔
130
            _longOrder,
131
            _shortOrder,
132
            _addLiquidityOptions,
133
            _removeLiquidityOptions
134
        );
135

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

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

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

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

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

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

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

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

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

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

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

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

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

308
        return true;
419✔
309
    }
310

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

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

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

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

408
        // Ensure that neither order has expired.
409
        if (
410
            _longOrder.expiry <= block.timestamp ||
220✔
411
            _shortOrder.expiry <= block.timestamp
218✔
412
        ) {
3✔
413
            revert AlreadyExpired();
3✔
414
        }
415

416
        // Ensure that both orders refer to the same Hyperdrive pool.
417
        if (_longOrder.hyperdrive != _shortOrder.hyperdrive) {
217✔
418
            revert MismatchedHyperdrive();
1✔
419
        }
420

421
        // Ensure that all of the transactions should be settled with base.
422
        if (
423
            !_longOrder.options.asBase ||
216✔
424
            !_shortOrder.options.asBase ||
215✔
425
            !_addLiquidityOptions.asBase ||
214✔
426
            !_removeLiquidityOptions.asBase
213✔
427
        ) {
4✔
428
            revert InvalidSettlementAsset();
4✔
429
        }
430

431
        // Ensure that the orders cross. We can calculate a worst-case price
432
        // for the long and short using the `amount` and `slippageGuard` fields.
433
        // In order for the orders to cross, the price of the long should be
434
        // equal to or higher than the price of the short. This implies that the
435
        // long is willing to buy bonds at a price equal or higher than the
436
        // short is selling bonds, which ensures that the trade is valid.
437
        if (
438
            _longOrder.slippageGuard != 0 &&
212✔
439
            _shortOrder.slippageGuard < _shortOrder.amount &&
201✔
440
            _longOrder.amount.divDown(_longOrder.slippageGuard) <
201✔
441
            (_shortOrder.amount - _shortOrder.slippageGuard).divDown(
201✔
442
                _shortOrder.amount
443
            )
444
        ) {
1✔
445
            revert InvalidMatch();
1✔
446
        }
447

448
        // Hash both orders.
449
        longOrderHash = hashOrderIntent(_longOrder);
211✔
450
        shortOrderHash = hashOrderIntent(_shortOrder);
211✔
451

452
        // Ensure that neither order has been cancelled.
453
        if (isCancelled[longOrderHash] || isCancelled[shortOrderHash]) {
211✔
454
            revert AlreadyCancelled();
3✔
455
        }
456

457
        // Ensure that the order intents were signed correctly.
458
        if (
459
            !verifySignature(
208✔
460
                longOrderHash,
461
                _longOrder.signature,
462
                _longOrder.trader
463
            ) ||
464
            !verifySignature(
206✔
465
                shortOrderHash,
466
                _shortOrder.signature,
467
                _shortOrder.trader
468
            )
469
        ) {
3✔
470
            revert InvalidSignature();
3✔
471
        }
472

473
        // Ensure that the destination of the add/remove liquidity options is
474
        // this contract.
475
        if (
476
            _addLiquidityOptions.destination != address(this) ||
205✔
477
            _removeLiquidityOptions.destination != address(this)
203✔
478
        ) {
3✔
479
            revert InvalidDestination();
3✔
480
        }
481
    }
482
}
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