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

delvtech / hyperdrive / 13148119986

05 Feb 2025 12:05AM UTC coverage: 89.245% (-0.2%) from 89.489%
13148119986

push

github

web-flow
ERC1155 Compatibility (#1238)

* Added the missing ERC1155 functions for compatability

* Made the Hyperdrive multi token ERC1155 compatible

* Fixed the zap tests

* Updated the MultiToken tests for ERC1155 compatibility

* Added more tests

* Added the remaining tests

* Addressed review feedback from @Sean329

* Fixed the broken tests

* Fixed the code size issue

70 of 82 new or added lines in 6 files covered. (85.37%)

1 existing line in 1 file now uncovered.

3037 of 3403 relevant lines covered (89.24%)

325484.76 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✔
NEW
232
            hyperdrive.safeTransferFrom(
×
233
                address(this),
234
                feeRecipient,
235
                AssetId._WITHDRAWAL_SHARE_ASSET_ID,
236
                withdrawalShares,
237
                ""
238
            );
239
        }
240

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

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

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

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

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

314
        return true;
419✔
315
    }
316

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

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

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

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

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

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

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

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

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

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

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

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

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

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