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

delvtech / hyperdrive / 12015593165

25 Nov 2024 05:35PM UTC coverage: 89.263%. First build
12015593165

Pull #1201

github

web-flow
Merge f1104934e into ab4da7ec0
Pull Request #1201: Hyperdrive Matching Engine

87 of 92 new or added lines in 1 file covered. (94.57%)

2860 of 3204 relevant lines covered (89.26%)

309879.72 hits per line

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

94.57
/contracts/src/matching/HyperdriveMatchingEngine.sol
1
// SPDX-License-Identifier: Apache-2.0
2
pragma solidity 0.8.22;
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
// TODO: Document the simplifications that were made.
18
//
19
/// @author DELV
20
/// @title HyperdriveMatchingEngine
21
/// @notice A matching engine that processes order intents and settles trades on
22
///         the Hyperdrive AMM.
23
/// @custom:disclaimer The language used in this code is for coding convenience
24
///                    only, and is not intended to, and does not, have any
25
///                    particular legal or regulatory significance.
26
contract HyperdriveMatchingEngine is
27
    IHyperdriveMatchingEngine,
28
    ReentrancyGuard,
29
    EIP712
30
{
31
    using FixedPointMath for uint256;
32
    using SafeERC20 for ERC20;
33

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

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

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

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

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

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

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

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

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

81
            // Ensure that the sender signed each order.
82
            bytes32 orderHash = hashOrderIntent(_orders[i]);
16✔
83
            if (!verifySignature(orderHash, _orders[i].signature, msg.sender)) {
16✔
84
                revert InvalidSignature();
2✔
85
            }
86

87
            // Cancel the order.
88
            cancels[orderHash] = true;
12✔
89
            orderHashes[i] = orderHash;
12✔
90
        }
91

92
        emit OrdersCancelled(msg.sender, orderHashes);
8✔
93
    }
94

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

129
        // Cancel the orders so that they can't be used again.
130
        cancels[longOrderHash] = true;
202✔
131
        cancels[shortOrderHash] = true;
202✔
132

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

150
        // Emit an `OrdersMatched` event.
151
        emit OrdersMatched(
202✔
152
            _longOrder.hyperdrive,
153
            longOrderHash,
154
            shortOrderHash,
155
            _longOrder.trader,
156
            _shortOrder.trader
157
        );
158
    }
159

160
    /// @notice Callback called when a flash loan occurs.
161
    /// @dev The callback is called only if data is not empty.
162
    /// @param _lpAmount The amount of assets that were flash loaned.
163
    /// @param _data Arbitrary data passed to the `flashLoan` function.
164
    function onMorphoFlashLoan(
165
        uint256 _lpAmount,
166
        bytes calldata _data
167
    ) external nonReentrant {
168
        // Decode the execution parameters. This encodes the information
169
        // required to execute the LP, long, and short operations.
170
        (
202✔
171
            OrderIntent memory longOrder,
172
            OrderIntent memory shortOrder,
173
            IHyperdrive.Options memory addLiquidityOptions,
174
            IHyperdrive.Options memory removeLiquidityOptions,
175
            address feeRecipient,
176
            bool isLongFirst
177
        ) = abi.decode(
202✔
178
                _data,
179
                (
180
                    OrderIntent,
181
                    OrderIntent,
182
                    IHyperdrive.Options,
183
                    IHyperdrive.Options,
184
                    address,
185
                    bool
186
                )
187
            );
188

189
        // Add liquidity to the pool.
190
        IHyperdrive hyperdrive = longOrder.hyperdrive;
202✔
191
        ERC20 baseToken = ERC20(hyperdrive.baseToken());
202✔
192
        uint256 lpAmount = _lpAmount; // avoid stack-too-deep
202✔
193
        uint256 lpShares = _addLiquidity(
202✔
194
            hyperdrive,
195
            baseToken,
196
            lpAmount,
197
            addLiquidityOptions
198
        );
199

200
        // If the long should be executed first, execute the long and then the
201
        // short.
202
        if (isLongFirst) {
202✔
203
            _openLong(hyperdrive, baseToken, longOrder);
1✔
204
            _openShort(hyperdrive, baseToken, shortOrder);
1✔
205
        }
206
        // Otherwise, execute the short and then the long.
207
        else {
208
            _openShort(hyperdrive, baseToken, shortOrder);
201✔
209
            _openLong(hyperdrive, baseToken, longOrder);
201✔
210
        }
211

212
        // Remove liquidity. This will repay the flash loan. We revert if there
213
        // are any withdrawal shares.
214
        (uint256 proceeds, uint256 withdrawalShares) = hyperdrive
202✔
215
            .removeLiquidity(lpShares, 0, removeLiquidityOptions);
216

217
        // If the withdrawal shares are greater than zero, send them to the fee
218
        // recipient.
219
        if (withdrawalShares > 0) {
202✔
NEW
220
            hyperdrive.transferFrom(
×
221
                AssetId._WITHDRAWAL_SHARE_ASSET_ID,
222
                address(this),
223
                feeRecipient,
224
                withdrawalShares
225
            );
226
        }
227

228
        // If the proceeds are greater than the LP amount, we send the difference
229
        // to the fee recipient.
230
        if (proceeds > lpAmount) {
202✔
231
            baseToken.safeTransfer(feeRecipient, proceeds - lpAmount);
202✔
232
        }
233

234
        // Approve Morpho Blue to take back the assets that were provided.
235
        baseToken.forceApprove(address(morpho), lpAmount);
202✔
236
    }
237

238
    /// @notice Hashes an order intent according to EIP-712
239
    /// @param _order The order intent to hash
240
    /// @return The hash of the order intent
241
    function hashOrderIntent(
242
        OrderIntent calldata _order
243
    ) public view returns (bytes32) {
244
        return
872✔
245
            _hashTypedDataV4(
872✔
246
                keccak256(
247
                    abi.encode(
248
                        ORDER_INTENT_TYPEHASH,
249
                        _order.trader,
250
                        _order.hyperdrive,
251
                        _order.amount,
252
                        _order.slippageGuard,
253
                        _order.minVaultSharePrice,
254
                        keccak256(
255
                            abi.encode(
256
                                OPTIONS_TYPEHASH,
257
                                _order.options.destination,
258
                                _order.options.asBase
259
                            )
260
                        ),
261
                        uint8(_order.orderType),
262
                        _order.expiry,
263
                        _order.salt
264
                    )
265
                )
266
            );
267
    }
268

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

294
        // For EOAs, verify the ECDSA signature.
295
        if (ECDSA.recover(_hash, _signature) != _signer) {
424✔
296
            return false;
3✔
297
        }
298

299
        return true;
419✔
300
    }
301

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

326
    /// @dev Opens a long position in the Hyperdrive pool.
327
    /// @param _hyperdrive The Hyperdrive pool.
328
    /// @param _baseToken The base token of the pool.
329
    /// @param _order The order containing the trade parameters.
330
    function _openLong(
331
        IHyperdrive _hyperdrive,
332
        ERC20 _baseToken,
333
        OrderIntent memory _order
334
    ) internal {
335
        _baseToken.safeTransferFrom(
202✔
336
            _order.trader,
337
            address(this),
338
            _order.amount
339
        );
340
        _baseToken.forceApprove(address(_hyperdrive), _order.amount + 1);
202✔
341
        _hyperdrive.openLong(
202✔
342
            _order.amount,
343
            _order.slippageGuard,
344
            _order.minVaultSharePrice,
345
            _order.options
346
        );
347
    }
348

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

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

399
        // Ensure that neither order has expired.
400
        if (
401
            _longOrder.expiry <= block.timestamp ||
220✔
402
            _shortOrder.expiry <= block.timestamp
218✔
403
        ) {
3✔
404
            revert AlreadyExpired();
3✔
405
        }
406

407
        // Ensure that both orders refer to the same Hyperdrive pool.
408
        if (_longOrder.hyperdrive != _shortOrder.hyperdrive) {
217✔
409
            revert MismatchedHyperdrive();
1✔
410
        }
411

412
        // Ensure that all of the transactions should be settled with base.
413
        if (
414
            !_longOrder.options.asBase ||
216✔
415
            !_shortOrder.options.asBase ||
215✔
416
            !_addLiquidityOptions.asBase ||
214✔
417
            !_removeLiquidityOptions.asBase
213✔
418
        ) {
4✔
419
            revert InvalidSettlementAsset();
4✔
420
        }
421

422
        // Ensure that the order's cross. We can calculate a worst-case price
423
        // for the long and short using the `amount` and `slippageGuard` fields.
424
        // In order for the orders to cross, the price of the long should be
425
        // equal to or higher than the price of the short. This implies that the
426
        // long is willing to buy bonds at a price equal or higher than the
427
        // short is selling bonds, which ensures that the trade is valid.
428
        if (
429
            _longOrder.slippageGuard != 0 &&
212✔
430
            _shortOrder.slippageGuard < _shortOrder.amount &&
201✔
431
            _longOrder.amount.divDown(_longOrder.slippageGuard) <=
201✔
432
            (_shortOrder.amount - _shortOrder.slippageGuard).divDown(
201✔
433
                _shortOrder.amount
434
            )
435
        ) {
1✔
436
            revert InvalidMatch();
1✔
437
        }
438

439
        // Hash both orders
440
        longOrderHash = hashOrderIntent(_longOrder);
211✔
441
        shortOrderHash = hashOrderIntent(_shortOrder);
211✔
442

443
        // Ensure that neither order has been cancelled.
444
        if (cancels[longOrderHash] || cancels[shortOrderHash]) {
211✔
445
            revert AlreadyCancelled();
3✔
446
        }
447

448
        // Ensure that the order intents were signed correctly.
449
        if (
450
            !verifySignature(
208✔
451
                longOrderHash,
452
                _longOrder.signature,
453
                _longOrder.trader
454
            ) ||
455
            !verifySignature(
206✔
456
                shortOrderHash,
457
                _shortOrder.signature,
458
                _shortOrder.trader
459
            )
460
        ) {
3✔
461
            revert InvalidSignature();
3✔
462
        }
463

464
        // Ensure that the destination of the add/remove liquidity options is this contract.
465
        if (
466
            _addLiquidityOptions.destination != address(this) ||
205✔
467
            _removeLiquidityOptions.destination != address(this)
203✔
468
        ) {
3✔
469
            revert InvalidDestination();
3✔
470
        }
471
    }
472
}
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

© 2025 Coveralls, Inc