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

delvtech / hyperdrive / 13661090457

04 Mar 2025 07:08PM UTC coverage: 86.507% (+0.2%) from 86.309%
13661090457

Pull #1241

github

Sean329
Bug fix: mint cost when asBase is false ; Fixed integration tests
Pull Request #1241: WIP PR - Adding more tests to Matching Engine V2

2 of 3 new or added lines in 1 file covered. (66.67%)

2 existing lines in 1 file now uncovered.

3244 of 3750 relevant lines covered (86.51%)

295391.19 hits per line

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

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

4
import { ECDSA } from "openzeppelin/utils/cryptography/ECDSA.sol";
5
import { EIP712 } from "openzeppelin/utils/cryptography/EIP712.sol";
6
import { ERC20 } from "openzeppelin/token/ERC20/ERC20.sol";
7
import { IERC1271 } from "openzeppelin/interfaces/IERC1271.sol";
8
import { ReentrancyGuard } from "openzeppelin/utils/ReentrancyGuard.sol";
9
import { SafeERC20 } from "openzeppelin/token/ERC20/utils/SafeERC20.sol";
10
import { AssetId } from "../libraries/AssetId.sol";
11
import { FixedPointMath } from "../libraries/FixedPointMath.sol";
12
import { HYPERDRIVE_MATCHING_ENGINE_KIND, VERSION } from "../libraries/Constants.sol";
13
import { HyperdriveMath } from "../libraries/HyperdriveMath.sol";
14
import { IHyperdrive } from "../interfaces/IHyperdrive.sol";
15
import { IHyperdriveMatchingEngineV2 } from "../interfaces/IHyperdriveMatchingEngineV2.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
/// @dev This version uses direct Hyperdrive mint/burn functions instead of flash
22
///      loans.
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 HyperdriveMatchingEngineV2 is
27
    IHyperdriveMatchingEngineV2,
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 counterparty,address hyperdrive,uint256 fundAmount,uint256 bondAmount,uint256 minVaultSharePrice,Options options,uint8 orderType,uint256 minMaturityTime,uint256 maxMaturityTime,uint256 expiry,bytes32 salt)Options(address destination,bool asBase)"
38
        );
39

40
    /// @notice The EIP712 typehash of the Options struct.
41
    bytes32 public constant OPTIONS_TYPEHASH =
42
        keccak256("Options(address destination,bool asBase)");
43

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

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

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

53
    /// @notice The buffer amount used for cost related calculations.
54
    /// @dev TODO: The buffer amount needs more testing.
55
    uint256 public constant TOKEN_AMOUNT_BUFFER = 10;
56

57
    /// @notice Mapping to track cancelled orders.
58
    mapping(bytes32 => bool) public isCancelled;
59

60
    /// @notice Mapping to track the amounts used for each order.
61
    mapping(bytes32 => OrderAmounts) public orderAmountsUsed;
62

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

69
    /// @notice Matches two orders. The ordering of the inputs matters, and
70
    ///         the general rule is to put the open order before the close
71
    ///         order and the long order before the short order. For example,
72
    ///         OpenLong + CloseLong is valid, but CloseLong + OpenLong is
73
    ///         invalid; OpenLong + OpenShort is valid, but OpenShort +
74
    ///         OpenLong is invalid.
75
    /// @param _order1 The first order to match.
76
    /// @param _order2 The second order to match.
77
    /// @param _surplusRecipient The address that receives the surplus funds
78
    ///         from matching the trades.
79
    /// @dev In the case of _handleMint(), the matching logic is "exact price"
80
    ///      semantics according to the order intent, to within numerical precision,
81
    ///      with any surplus going to the party that executed the match.
82
    function matchOrders(
83
        OrderIntent calldata _order1,
84
        OrderIntent calldata _order2,
85
        address _surplusRecipient
86
    ) external nonReentrant {
87
        // Set the surplus recipient to the caller if not specified.
88
        if (_surplusRecipient == address(0)) {
155✔
89
            _surplusRecipient = msg.sender;
×
90
        }
91

92
        // Validate orders.
93
        (bytes32 order1Hash, bytes32 order2Hash) = _validateOrdersNoTaker(
155✔
94
            _order1,
95
            _order2
96
        );
97

98
        IHyperdrive hyperdrive = _order1.hyperdrive;
152✔
99
        ERC20 fundToken;
152✔
100
        if (_order1.options.asBase) {
152✔
101
            fundToken = ERC20(hyperdrive.baseToken());
152✔
102
        } else {
103
            fundToken = ERC20(hyperdrive.vaultSharesToken());
×
104
        }
105

106
        // Handle different order type combinations.
107
        // Case 1: Long + Short creation using mint().
108
        if (
109
            _order1.orderType == OrderType.OpenLong &&
152✔
110
            _order2.orderType == OrderType.OpenShort
149✔
111
        ) {
112
            // Calculate matching amount.
113
            // @dev This could have been placed before the control flow for
114
            //      shorter code, but it's put here to avoid stack-too-deep.
115
            uint256 bondMatchAmount = _calculateBondMatchAmount(
148✔
116
                _order1,
117
                _order2,
118
                order1Hash,
119
                order2Hash
120
            );
121

122
            // Calculate the amount of fund tokens to transfer based on the
123
            // bondMatchAmount using dynamic pricing. During a series of partial
124
            // matching, the pricing requirements can go easier as needed for each
125
            // new match, hence increasing the match likelihood.
126
            // NOTE: Round the required fund amount down to prevent overspending
127
            //       and possible reverting at a later step.
128
            uint256 fundTokenAmountOrder1 = (_order1.fundAmount -
148✔
129
                orderAmountsUsed[order1Hash].fundAmount).mulDivDown(
130
                    bondMatchAmount,
131
                    (_order1.bondAmount -
132
                        orderAmountsUsed[order1Hash].bondAmount)
133
                );
134
            uint256 fundTokenAmountOrder2 = (_order2.fundAmount -
148✔
135
                orderAmountsUsed[order2Hash].fundAmount).mulDivDown(
136
                    bondMatchAmount,
137
                    (_order2.bondAmount -
138
                        orderAmountsUsed[order2Hash].bondAmount)
139
                );
140

141
            // Update order fund amount used.
142
            _updateOrderAmount(order1Hash, fundTokenAmountOrder1, false);
148✔
143
            _updateOrderAmount(order2Hash, fundTokenAmountOrder2, false);
148✔
144

145
            // Check if the fund amount used is greater than the order amount.
146
            if (
147
                orderAmountsUsed[order1Hash].fundAmount > _order1.fundAmount ||
148✔
148
                orderAmountsUsed[order2Hash].fundAmount > _order2.fundAmount
148✔
149
            ) {
×
150
                revert InvalidFundAmount();
×
151
            }
152

153
            // Calculate costs and parameters.
154
            (uint256 maturityTime, uint256 cost) = _calculateMintCost(
148✔
155
                hyperdrive,
156
                bondMatchAmount,
157
                _order1.options.asBase
158
            );
159

160
            // Check if the maturity time is within the range.
161
            if (
162
                maturityTime < _order1.minMaturityTime ||
148✔
163
                maturityTime > _order1.maxMaturityTime ||
148✔
164
                maturityTime < _order2.minMaturityTime ||
148✔
165
                maturityTime > _order2.maxMaturityTime
148✔
166
            ) {
×
167
                revert InvalidMaturityTime();
×
168
            }
169

170
            // Mint the bonds.
171
            uint256 bondAmount = _handleMint(
148✔
172
                _order1,
173
                _order2,
174
                fundTokenAmountOrder1,
175
                fundTokenAmountOrder2,
176
                cost,
177
                bondMatchAmount,
178
                fundToken,
179
                hyperdrive
180
            );
181

182
            // Update order bond amount used.
183
            _updateOrderAmount(order1Hash, bondAmount, true);
146✔
184
            _updateOrderAmount(order2Hash, bondAmount, true);
146✔
185
        }
186
        // Case 2: Long + Short closing using burn().
187
        else if (
188
            _order1.orderType == OrderType.CloseLong &&
4✔
189
            _order2.orderType == OrderType.CloseShort
3✔
190
        ) {
191
            // Verify both orders have the same maturity time.
192
            if (_order1.maxMaturityTime != _order2.maxMaturityTime) {
3✔
193
                revert InvalidMaturityTime();
1✔
194
            }
195

196
            // Calculate matching amount.
197
            uint256 bondMatchAmount = _calculateBondMatchAmount(
2✔
198
                _order1,
199
                _order2,
200
                order1Hash,
201
                order2Hash
202
            );
203

204
            // Get the min fund output according to the bondMatchAmount.
205
            // NOTE: Round the required fund amount up to respect the order specified
206
            //       min fund output.
207
            uint256 minFundAmountOrder1 = (_order1.fundAmount -
2✔
208
                orderAmountsUsed[order1Hash].fundAmount).mulDivUp(
209
                    bondMatchAmount,
210
                    (_order1.bondAmount -
211
                        orderAmountsUsed[order1Hash].bondAmount)
212
                );
213
            uint256 minFundAmountOrder2 = (_order2.fundAmount -
2✔
214
                orderAmountsUsed[order2Hash].fundAmount).mulDivUp(
215
                    bondMatchAmount,
216
                    (_order2.bondAmount -
217
                        orderAmountsUsed[order2Hash].bondAmount)
218
                );
219

220
            // Update order bond amount used.
221
            // @dev After the update, there is no need to check if the bond
222
            //      amount used is greater than the order amount, as the order
223
            //      amount is already used to calculate the bondMatchAmount.
224
            _updateOrderAmount(order1Hash, bondMatchAmount, true);
2✔
225
            _updateOrderAmount(order2Hash, bondMatchAmount, true);
2✔
226

227
            // Handle burn operation through helper function.
228
            _handleBurn(
2✔
229
                _order1,
230
                _order2,
231
                minFundAmountOrder1,
232
                minFundAmountOrder2,
233
                bondMatchAmount,
234
                fundToken,
235
                hyperdrive
236
            );
237

238
            // Update order fund amount used.
239
            _updateOrderAmount(order1Hash, minFundAmountOrder1, false);
1✔
240
            _updateOrderAmount(order2Hash, minFundAmountOrder2, false);
1✔
241
        }
242
        // Case 3: Transfer positions between traders.
243
        else if (
244
            (_order1.orderType == OrderType.OpenLong &&
1✔
245
                _order2.orderType == OrderType.CloseLong) ||
246
            (_order1.orderType == OrderType.OpenShort &&
247
                _order2.orderType == OrderType.CloseShort)
248
        ) {
249
            // Verify that the maturity time of the close order matches the
250
            // open order's requirements.
251
            if (
252
                _order2.maxMaturityTime > _order1.maxMaturityTime ||
1✔
253
                _order2.maxMaturityTime < _order1.minMaturityTime
1✔
254
            ) {
×
255
                revert InvalidMaturityTime();
×
256
            }
257

258
            // Calculate matching amount.
259
            uint256 bondMatchAmount = _calculateBondMatchAmount(
1✔
260
                _order1,
261
                _order2,
262
                order1Hash,
263
                order2Hash
264
            );
265

266
            // Calculate the amount of fund tokens to transfer based on the
267
            // bondMatchAmount using dynamic pricing. During a series of partial
268
            // matching, the pricing requirements can go easier as needed for each
269
            // new match, hence increasing the match likelihood.
270
            // NOTE: Round the required fund amount down to prevent overspending
271
            //       and possible reverting at a later step.
272
            uint256 fundTokenAmountOrder1 = (_order1.fundAmount -
1✔
273
                orderAmountsUsed[order1Hash].fundAmount).mulDivDown(
274
                    bondMatchAmount,
275
                    (_order1.bondAmount -
276
                        orderAmountsUsed[order1Hash].bondAmount)
277
                );
278

279
            // Get the min fund output according to the bondMatchAmount.
280
            // NOTE: Round the required fund amount up to respect the order specified
281
            //       min fund output.
282
            uint256 minFundAmountOrder2 = (_order2.fundAmount -
1✔
283
                orderAmountsUsed[order2Hash].fundAmount).mulDivUp(
284
                    bondMatchAmount,
285
                    (_order2.bondAmount -
286
                        orderAmountsUsed[order2Hash].bondAmount)
287
                );
288

289
            // Check if trader 1 has enough fund to transfer to trader 2.
290
            // @dev Also considering any donations to help match the orders.
291
            if (
292
                fundTokenAmountOrder1 + fundToken.balanceOf(address(this)) <
1✔
293
                minFundAmountOrder2
294
            ) {
×
295
                revert InsufficientFunding();
×
296
            }
297

298
            // Update order bond amount used.
299
            // @dev After the update, there is no need to check if the bond
300
            //      amount used is greater than the order amount, as the order
301
            //      amount is already used to calculate the bondMatchAmount.
302
            _updateOrderAmount(order1Hash, bondMatchAmount, true);
1✔
303
            _updateOrderAmount(order2Hash, bondMatchAmount, true);
1✔
304

305
            _handleTransfer(
1✔
306
                _order1,
307
                _order2,
308
                fundTokenAmountOrder1,
309
                minFundAmountOrder2,
310
                bondMatchAmount,
311
                fundToken,
312
                hyperdrive
313
            );
314

315
            if (fundTokenAmountOrder1 < minFundAmountOrder2) {
1✔
316
                fundToken.safeTransfer(
×
317
                    _order2.options.destination,
318
                    minFundAmountOrder2 - fundTokenAmountOrder1
319
                );
320
            } else if (fundTokenAmountOrder1 > minFundAmountOrder2) {
1✔
321
                fundToken.safeTransferFrom(
1✔
322
                    _order1.trader,
323
                    _surplusRecipient,
324
                    fundTokenAmountOrder1 - minFundAmountOrder2
325
                );
326
            }
327

328
            // Update order fund amount used.
329
            _updateOrderAmount(order1Hash, fundTokenAmountOrder1, false);
1✔
330
            _updateOrderAmount(order2Hash, minFundAmountOrder2, false);
1✔
331
        }
332
        // All other cases are invalid.
333
        else {
334
            revert InvalidOrderCombination();
×
335
        }
336

337
        // Transfer the remaining fund tokens back to the surplus recipient.
338
        uint256 remainingBalance = fundToken.balanceOf(address(this));
148✔
339
        if (remainingBalance > 0) {
148✔
340
            fundToken.safeTransfer(_surplusRecipient, remainingBalance);
47✔
341
        }
342

343
        emit OrdersMatched(
148✔
344
            hyperdrive,
345
            order1Hash,
346
            order2Hash,
347
            _order1.trader,
348
            _order2.trader,
349
            orderAmountsUsed[order1Hash].bondAmount,
350
            orderAmountsUsed[order2Hash].bondAmount,
351
            orderAmountsUsed[order1Hash].fundAmount,
352
            orderAmountsUsed[order2Hash].fundAmount
353
        );
354
    }
355

356
    /// @notice Fills a maker order by the taker.
357
    /// @param _makerOrder The maker order to fill.
358
    /// @param _takerOrder The taker order created on the fly by the frontend.
359
    /// @dev The frontend will have to take some necessary values from the user
360
    ///      like the destination address, the orderType, and the maturity time
361
    ///      for the close position...etc., and create the minimal sufficient
362
    ///      struct as the _takerOrder argument. For example:
363
    ///
364
    /// OrderIntent({
365
    ///    trader: msg.sender, // Take the user's address.
366
    ///    counterparty: address(0), // Not needed for immediate fill.
367
    ///    hyperdrive: IHyperdrive(address(0)), // Not needed for immediate fill.
368
    ///    fundAmount: 0,  // Not needed for immediate fill.
369
    ///    bondAmount: _bondAmount,  // Take from the user's input.
370
    ///    minVaultSharePrice: _makerOrder.minVaultSharePrice, // From maker order.
371
    ///    options: IHyperdrive.Options({
372
    ///        // Take destination from the user's input; if null, set to msg.sender.
373
    ///        destination: _destination or msg.sender,
374
    ///        asBase: _makerOrder.options.asBase, // From maker order.
375
    ///        extraData: ""
376
    ///    }),
377
    ///    orderType: _takerOrderType, // Take from the user's input.
378
    ///    // For closing positions, take maturity time from the user's input;
379
    ///    // otherwise, use values from the maker order.
380
    ///    minMaturityTime: _closeOrderMaturityTime or _makerOrder.minMaturityTime,
381
    ///    maxMaturityTime: _closeOrderMaturityTime or _makerOrder.maxMaturityTime,
382
    ///    expiry: 0,  // Not needed for immediate fill.
383
    ///    salt: 0,                      // Not needed for immediate fill.
384
    ///    signature: ""                 // Not needed for immediate fill.
385
    /// })
386
    function fillOrder(
387
        OrderIntent calldata _makerOrder,
388
        OrderIntent calldata _takerOrder
389
    ) external nonReentrant {
390
        // Ensure sender is the trader.
391
        if (msg.sender != _takerOrder.trader) {
14✔
392
            revert InvalidSender();
×
393
        }
394

395
        // Validate maker order and taker order.
396
        bytes32 makerOrderHash = _validateOrdersWithTaker(
14✔
397
            _makerOrder,
398
            _takerOrder
399
        );
400

401
        // Calculates the amount of bonds that can be matched between two orders.
402
        OrderAmounts memory amountsMaker = orderAmountsUsed[makerOrderHash];
11✔
403
        uint256 makerBondAmount = _makerOrder.bondAmount -
11✔
404
            amountsMaker.bondAmount;
405
        uint256 bondMatchAmount = makerBondAmount.min(_takerOrder.bondAmount);
11✔
406

407
        IHyperdrive hyperdrive = _makerOrder.hyperdrive;
11✔
408
        ERC20 fundToken;
11✔
409
        if (_makerOrder.options.asBase) {
11✔
410
            fundToken = ERC20(hyperdrive.baseToken());
11✔
411
        } else {
412
            fundToken = ERC20(hyperdrive.vaultSharesToken());
×
413
        }
414

415
        // Handle different maker order types.
416
        if (_makerOrder.orderType == OrderType.OpenLong) {
11✔
417
            if (_takerOrder.orderType == OrderType.OpenShort) {
10✔
418
                // OpenLong + OpenShort: _handleMint().
419
                // Calculate the amount of fund tokens to transfer based on the
420
                // bondMatchAmount using dynamic pricing. During a series of partial
421
                // matching, the pricing requirements can go easier as needed for
422
                // each new match, hence increasing the match likelihood.
423
                // NOTE: Round the required fund amount down to prevent overspending
424
                //       and possible reverting at a later step.
425
                uint256 fundTokenAmountMaker = (_makerOrder.fundAmount -
9✔
426
                    orderAmountsUsed[makerOrderHash].fundAmount).mulDivDown(
427
                        bondMatchAmount,
428
                        (_makerOrder.bondAmount -
429
                            orderAmountsUsed[makerOrderHash].bondAmount)
430
                    );
431

432
                // Update order fund amount used.
433
                _updateOrderAmount(makerOrderHash, fundTokenAmountMaker, false);
9✔
434

435
                // Check if the fund amount used is greater than the order amount.
436
                if (
437
                    orderAmountsUsed[makerOrderHash].fundAmount >
9✔
438
                    _makerOrder.fundAmount
439
                ) {
×
440
                    revert InvalidFundAmount();
×
441
                }
442

443
                // Calculate costs and parameters.
444
                (uint256 maturityTime, uint256 cost) = _calculateMintCost(
9✔
445
                    hyperdrive,
446
                    bondMatchAmount,
447
                    _makerOrder.options.asBase
448
                );
449

450
                // Check if the maturity time is within the range.
451
                if (
452
                    maturityTime < _makerOrder.minMaturityTime ||
9✔
453
                    maturityTime > _makerOrder.maxMaturityTime
9✔
454
                ) {
1✔
455
                    revert InvalidMaturityTime();
1✔
456
                }
457

458
                // Calculate the amount of fund tokens the taker needs to pay.
459
                uint256 fundTokenAmountTaker = fundTokenAmountMaker >
8✔
460
                    cost + TOKEN_AMOUNT_BUFFER
461
                    ? 0
462
                    : cost + TOKEN_AMOUNT_BUFFER - fundTokenAmountMaker;
463

464
                // Mint the bonds.
465
                uint256 bondAmount = _handleMint(
8✔
466
                    _makerOrder,
467
                    _takerOrder,
468
                    fundTokenAmountMaker,
469
                    fundTokenAmountTaker,
470
                    cost,
471
                    bondMatchAmount,
472
                    fundToken,
473
                    hyperdrive
474
                );
475

476
                // Update order bond amount used.
477
                _updateOrderAmount(makerOrderHash, bondAmount, true);
8✔
478
            } else if (_takerOrder.orderType == OrderType.CloseLong) {
1✔
479
                // OpenLong + CloseLong: _handleTransfer().
480
                // Verify that the maturity time of the close order matches the
481
                // open order's requirements.
482
                if (
483
                    _takerOrder.maxMaturityTime > _makerOrder.maxMaturityTime ||
×
484
                    _takerOrder.maxMaturityTime < _makerOrder.minMaturityTime
×
485
                ) {
×
486
                    revert InvalidMaturityTime();
×
487
                }
488

489
                // Calculate the amount of fund tokens to transfer based on the
490
                // bondMatchAmount using dynamic pricing. During a series of partial
491
                // matching, the pricing requirements can go easier as needed for each
492
                // new match, hence increasing the match likelihood.
493
                // NOTE: Round the required fund amount down to prevent overspending
494
                //       and possible reverting at a later step.
495
                uint256 fundTokenAmountMaker = (_makerOrder.fundAmount -
×
496
                    orderAmountsUsed[makerOrderHash].fundAmount).mulDivDown(
497
                        bondMatchAmount,
498
                        (_makerOrder.bondAmount -
499
                            orderAmountsUsed[makerOrderHash].bondAmount)
500
                    );
501

502
                // The taker simply agrees with the maker's fund amount, and no
503
                // additional donation nor validations need to be considered
504
                uint256 minFundAmountTaker = fundTokenAmountMaker;
×
505

506
                // Update order bond amount used.
507
                // @dev After the update, there is no need to check if the bond
508
                //      amount used is greater than the order amount, as the order
509
                //      amount is already used to calculate the bondMatchAmount.
510
                _updateOrderAmount(makerOrderHash, bondMatchAmount, true);
×
511

512
                _handleTransfer(
×
513
                    _makerOrder,
514
                    _takerOrder,
515
                    fundTokenAmountMaker,
516
                    minFundAmountTaker,
517
                    bondMatchAmount,
518
                    fundToken,
519
                    hyperdrive
520
                );
521

522
                // Update order fund amount used.
523
                _updateOrderAmount(makerOrderHash, fundTokenAmountMaker, false);
×
524
            } else {
525
                revert InvalidOrderCombination();
1✔
526
            }
527
        } else if (_makerOrder.orderType == OrderType.OpenShort) {
1✔
528
            if (_takerOrder.orderType == OrderType.OpenLong) {
1✔
529
                // OpenShort + OpenLong: _handleMint() but reverse the order.
530
                // Calculate the amount of fund tokens to transfer based on the
531
                // bondMatchAmount using dynamic pricing. During a series of partial
532
                // matching, the pricing requirements can go easier as needed for
533
                // each new match, hence increasing the match likelihood.
534
                // NOTE: Round the required fund amount down to prevent overspending
535
                //       and possible reverting at a later step.
536
                uint256 fundTokenAmountMaker = (_makerOrder.fundAmount -
1✔
537
                    orderAmountsUsed[makerOrderHash].fundAmount).mulDivDown(
538
                        bondMatchAmount,
539
                        (_makerOrder.bondAmount -
540
                            orderAmountsUsed[makerOrderHash].bondAmount)
541
                    );
542

543
                // Update order fund amount used.
544
                _updateOrderAmount(makerOrderHash, fundTokenAmountMaker, false);
1✔
545

546
                // Check if the fund amount used is greater than the order amount.
547
                if (
548
                    orderAmountsUsed[makerOrderHash].fundAmount >
1✔
549
                    _makerOrder.fundAmount
550
                ) {
×
551
                    revert InvalidFundAmount();
×
552
                }
553

554
                // Calculate costs and parameters.
555
                (uint256 maturityTime, uint256 cost) = _calculateMintCost(
1✔
556
                    hyperdrive,
557
                    bondMatchAmount,
558
                    _makerOrder.options.asBase
559
                );
560

561
                // Check if the maturity time is within the range.
562
                if (
563
                    maturityTime < _makerOrder.minMaturityTime ||
1✔
564
                    maturityTime > _makerOrder.maxMaturityTime
1✔
565
                ) {
×
566
                    revert InvalidMaturityTime();
×
567
                }
568

569
                // Calculate the amount of fund tokens the taker needs to pay.
570
                uint256 fundTokenAmountTaker = fundTokenAmountMaker >
1✔
571
                    cost + TOKEN_AMOUNT_BUFFER
572
                    ? 0
573
                    : cost + TOKEN_AMOUNT_BUFFER - fundTokenAmountMaker;
574

575
                // Mint the bonds.
576
                uint256 bondAmount = _handleMint(
1✔
577
                    _takerOrder,
578
                    _makerOrder,
579
                    fundTokenAmountTaker,
580
                    fundTokenAmountMaker,
581
                    cost,
582
                    bondMatchAmount,
583
                    fundToken,
584
                    hyperdrive
585
                );
586

587
                // Update order bond amount used.
588
                _updateOrderAmount(makerOrderHash, bondAmount, true);
1✔
589
            } else if (_takerOrder.orderType == OrderType.CloseShort) {
×
590
                // OpenShort + CloseShort: _handleTransfer().
591
                // Verify that the maturity time of the close order matches the
592
                // open order's requirements.
593
                if (
594
                    _takerOrder.maxMaturityTime > _makerOrder.maxMaturityTime ||
×
595
                    _takerOrder.maxMaturityTime < _makerOrder.minMaturityTime
×
596
                ) {
×
597
                    revert InvalidMaturityTime();
×
598
                }
599

600
                // Calculate the amount of fund tokens to transfer based on the
601
                // bondMatchAmount using dynamic pricing. During a series of partial
602
                // matching, the pricing requirements can go easier as needed for each
603
                // new match, hence increasing the match likelihood.
604
                // NOTE: Round the required fund amount down to prevent overspending
605
                //       and possible reverting at a later step.
606
                uint256 fundTokenAmountMaker = (_makerOrder.fundAmount -
×
607
                    orderAmountsUsed[makerOrderHash].fundAmount).mulDivDown(
608
                        bondMatchAmount,
609
                        (_makerOrder.bondAmount -
610
                            orderAmountsUsed[makerOrderHash].bondAmount)
611
                    );
612

613
                // The taker simply agrees with the maker's fund amount, and no
614
                // additional donation nor validations need to be considered
615
                uint256 minFundAmountTaker = fundTokenAmountMaker;
×
616

617
                // Update order bond amount used.
618
                // @dev After the update, there is no need to check if the bond
619
                //      amount used is greater than the order amount, as the order
620
                //      amount is already used to calculate the bondMatchAmount.
621
                _updateOrderAmount(makerOrderHash, bondMatchAmount, true);
×
622

623
                _handleTransfer(
×
624
                    _makerOrder,
625
                    _takerOrder,
626
                    fundTokenAmountMaker,
627
                    minFundAmountTaker,
628
                    bondMatchAmount,
629
                    fundToken,
630
                    hyperdrive
631
                );
632

633
                // Update order fund amount used.
634
                _updateOrderAmount(makerOrderHash, fundTokenAmountMaker, false);
×
635
            } else {
636
                revert InvalidOrderCombination();
×
637
            }
638
        } else if (_makerOrder.orderType == OrderType.CloseLong) {
×
639
            if (_takerOrder.orderType == OrderType.OpenLong) {
×
640
                // CloseLong + OpenLong: _handleTransfer() but reverse the order.
641
                // Verify that the maturity time of the close order matches the
642
                // open order's requirements.
643
                if (
644
                    _makerOrder.maxMaturityTime > _takerOrder.maxMaturityTime ||
×
645
                    _makerOrder.maxMaturityTime < _takerOrder.minMaturityTime
×
646
                ) {
×
647
                    revert InvalidMaturityTime();
×
648
                }
649

650
                // Calculate the amount of fund tokens to transfer based on the
651
                // bondMatchAmount using dynamic pricing. During a series of partial
652
                // matching, the pricing requirements can go easier as needed for each
653
                // new match, hence increasing the match likelihood.
654
                // NOTE: Round the required fund amount down to prevent overspending
655
                //       and possible reverting at a later step.
656
                uint256 minFundAmountMaker = (_makerOrder.fundAmount -
×
657
                    orderAmountsUsed[makerOrderHash].fundAmount).mulDivDown(
658
                        bondMatchAmount,
659
                        (_makerOrder.bondAmount -
660
                            orderAmountsUsed[makerOrderHash].bondAmount)
661
                    );
662

663
                // The taker simply agrees with the maker's fund amount, and no
664
                // additional donation nor validations need to be considered
665
                uint256 fundTokenAmountTaker = minFundAmountMaker;
×
666

667
                // Update order bond amount used.
668
                // @dev After the update, there is no need to check if the bond
669
                //      amount used is greater than the order amount, as the order
670
                //      amount is already used to calculate the bondMatchAmount.
671
                _updateOrderAmount(makerOrderHash, bondMatchAmount, true);
×
672

673
                _handleTransfer(
×
674
                    _takerOrder,
675
                    _makerOrder,
676
                    fundTokenAmountTaker,
677
                    minFundAmountMaker,
678
                    bondMatchAmount,
679
                    fundToken,
680
                    hyperdrive
681
                );
682

683
                // Update order fund amount used.
684
                _updateOrderAmount(makerOrderHash, minFundAmountMaker, false);
×
685
            } else if (_takerOrder.orderType == OrderType.CloseShort) {
×
686
                // CloseLong + CloseShort: _handleBurn().
687
                // Verify both orders have the same maturity time.
688
                if (
689
                    _makerOrder.maxMaturityTime != _takerOrder.maxMaturityTime
×
690
                ) {
×
691
                    revert InvalidMaturityTime();
×
692
                }
693

694
                // Get the min fund output according to the bondMatchAmount.
695
                // NOTE: Round the required fund amount up to respect the order
696
                //       specified min fund output.
697
                uint256 minFundAmountMaker = (_makerOrder.fundAmount -
×
698
                    orderAmountsUsed[makerOrderHash].fundAmount).mulDivUp(
699
                        bondMatchAmount,
700
                        (_makerOrder.bondAmount -
701
                            orderAmountsUsed[makerOrderHash].bondAmount)
702
                    );
703

704
                // The taker takes whatever the leftover fund amount is.
705
                // @dev The taker will not receive proceeds inside the _handleBurn(),
706
                //      but will receive the leftover fund at the surplus distribution.
707
                uint256 minFundAmountTaker = 0;
×
708

709
                // Update order bond amount used.
710
                // @dev After the update, there is no need to check if the bond
711
                //      amount used is greater than the order amount, as the order
712
                //      amount is already used to calculate the bondMatchAmount.
713
                _updateOrderAmount(makerOrderHash, bondMatchAmount, true);
×
714

715
                // Handle burn operation through helper function.
716
                _handleBurn(
×
717
                    _makerOrder,
718
                    _takerOrder,
719
                    minFundAmountMaker,
720
                    minFundAmountTaker,
721
                    bondMatchAmount,
722
                    fundToken,
723
                    hyperdrive
724
                );
725

726
                // Update order fund amount used.
727
                _updateOrderAmount(makerOrderHash, minFundAmountMaker, false);
×
728
            } else {
729
                revert InvalidOrderCombination();
×
730
            }
731
        } else if (_makerOrder.orderType == OrderType.CloseShort) {
×
732
            if (_takerOrder.orderType == OrderType.OpenShort) {
×
733
                // CloseShort + OpenShort: _handleTransfer() but reverse the order.
734
                // Verify that the maturity time of the close order matches the
735
                // open order's requirements.
736
                if (
737
                    _makerOrder.maxMaturityTime > _takerOrder.maxMaturityTime ||
×
738
                    _makerOrder.maxMaturityTime < _takerOrder.minMaturityTime
×
739
                ) {
×
740
                    revert InvalidMaturityTime();
×
741
                }
742

743
                // Calculate the amount of fund tokens to transfer based on the
744
                // bondMatchAmount using dynamic pricing. During a series of partial
745
                // matching, the pricing requirements can go easier as needed for each
746
                // new match, hence increasing the match likelihood.
747
                // NOTE: Round the required fund amount down to prevent overspending
748
                //       and possible reverting at a later step.
749
                uint256 minFundAmountMaker = (_makerOrder.fundAmount -
×
750
                    orderAmountsUsed[makerOrderHash].fundAmount).mulDivDown(
751
                        bondMatchAmount,
752
                        (_makerOrder.bondAmount -
753
                            orderAmountsUsed[makerOrderHash].bondAmount)
754
                    );
755

756
                // The taker simply agrees with the maker's fund amount, and no
757
                // additional donation nor validations need to be considered
758
                uint256 fundTokenAmountTaker = minFundAmountMaker;
×
759

760
                // Update order bond amount used.
761
                // @dev After the update, there is no need to check if the bond
762
                //      amount used is greater than the order amount, as the order
763
                //      amount is already used to calculate the bondMatchAmount.
764
                _updateOrderAmount(makerOrderHash, bondMatchAmount, true);
×
765

766
                _handleTransfer(
×
767
                    _takerOrder,
768
                    _makerOrder,
769
                    fundTokenAmountTaker,
770
                    minFundAmountMaker,
771
                    bondMatchAmount,
772
                    fundToken,
773
                    hyperdrive
774
                );
775

776
                // Update order fund amount used.
777
                _updateOrderAmount(makerOrderHash, minFundAmountMaker, false);
×
778
            } else if (_takerOrder.orderType == OrderType.CloseLong) {
×
779
                // CloseShort + CloseLong: _handleBurn() but reverse the order.
780
                // Verify both orders have the same maturity time.
781
                if (
782
                    _makerOrder.maxMaturityTime != _takerOrder.maxMaturityTime
×
783
                ) {
×
784
                    revert InvalidMaturityTime();
×
785
                }
786

787
                // Get the min fund output according to the bondMatchAmount.
788
                // NOTE: Round the required fund amount up to respect the order
789
                //       specified min fund output.
790
                uint256 minFundAmountMaker = (_makerOrder.fundAmount -
×
791
                    orderAmountsUsed[makerOrderHash].fundAmount).mulDivUp(
792
                        bondMatchAmount,
793
                        (_makerOrder.bondAmount -
794
                            orderAmountsUsed[makerOrderHash].bondAmount)
795
                    );
796

797
                // The taker takes whatever the leftover fund amount is.
798
                // @dev The taker will not receive proceeds inside the _handleBurn(),
799
                //      but will receive the leftover fund at the surplus distribution.
800
                uint256 minFundAmountTaker = 0;
×
801

802
                // Update order bond amount used.
803
                // @dev After the update, there is no need to check if the bond
804
                //      amount used is greater than the order amount, as the order
805
                //      amount is already used to calculate the bondMatchAmount.
806
                _updateOrderAmount(makerOrderHash, bondMatchAmount, true);
×
807

808
                // Handle burn operation through helper function.
809
                _handleBurn(
×
810
                    _takerOrder,
811
                    _makerOrder,
812
                    minFundAmountTaker,
813
                    minFundAmountMaker,
814
                    bondMatchAmount,
815
                    fundToken,
816
                    hyperdrive
817
                );
818

819
                // Update order fund amount used.
820
                _updateOrderAmount(makerOrderHash, minFundAmountMaker, false);
×
821
            } else {
822
                revert InvalidOrderCombination();
×
823
            }
824
        } else {
825
            revert InvalidOrderCombination();
×
826
        }
827

828
        // Transfer any remaining fund tokens back to the taker's destination.
829
        uint256 remainingBalance = fundToken.balanceOf(address(this));
9✔
830
        if (remainingBalance > 0) {
9✔
831
            fundToken.safeTransfer(
7✔
832
                _takerOrder.options.destination,
833
                remainingBalance
834
            );
835
        }
836

837
        emit OrderFilled(
9✔
838
            _makerOrder.hyperdrive,
839
            makerOrderHash,
840
            _makerOrder.trader,
841
            _takerOrder.trader,
842
            orderAmountsUsed[makerOrderHash].bondAmount,
843
            orderAmountsUsed[makerOrderHash].fundAmount
844
        );
845
    }
846

847
    /// @notice Allows traders to cancel their orders.
848
    /// @param _orders Array of orders to cancel.
849
    function cancelOrders(
850
        OrderIntent[] calldata _orders
851
    ) external nonReentrant {
852
        bytes32[] memory orderHashes = new bytes32[](_orders.length);
×
853
        uint256 validOrderCount = 0;
×
854

855
        uint256 orderCount = _orders.length;
×
856
        for (uint256 i = 0; i < orderCount; i++) {
×
857
            // Skip if order is already fully executed or cancelled
858
            bytes32 orderHash = hashOrderIntent(_orders[i]);
×
859
            if (
860
                orderAmountsUsed[orderHash].bondAmount >=
×
861
                _orders[i].bondAmount ||
862
                orderAmountsUsed[orderHash].fundAmount >=
×
863
                _orders[i].fundAmount ||
864
                isCancelled[orderHash]
865
            ) {
×
866
                continue;
×
867
            }
868

869
            // Ensure sender is the trader.
870
            if (msg.sender != _orders[i].trader) {
×
871
                revert InvalidSender();
×
872
            }
873

874
            // Verify signature.
875
            if (!verifySignature(orderHash, _orders[i].signature, msg.sender)) {
×
876
                revert InvalidSignature();
×
877
            }
878

879
            // Cancel the order.
880
            isCancelled[orderHash] = true;
×
881
            orderHashes[validOrderCount] = orderHash;
×
882
            validOrderCount++;
×
883
        }
884

885
        // Emit event with assembly to truncate array to actual size
886
        assembly {
887
            mstore(orderHashes, validOrderCount)
×
888
        }
889
        emit OrdersCancelled(msg.sender, orderHashes);
×
890
    }
891

892
    /// @notice Hashes an order intent according to EIP-712.
893
    /// @param _order The order intent to hash.
894
    /// @return The hash of the order intent.
895
    /// @dev Use two helper functions to encode to avoid stack too deep.
896
    function hashOrderIntent(
897
        OrderIntent calldata _order
898
    ) public view returns (bytes32) {
899
        // Get the encoded parts.
900
        bytes memory encodedPart1 = _encodeOrderPart1(_order);
640✔
901
        bytes memory encodedPart2 = _encodeOrderPart2(_order);
640✔
902

903
        // Concatenate and calculate the final hash
904
        return
640✔
905
            _hashTypedDataV4(
640✔
906
                keccak256(bytes.concat(encodedPart1, encodedPart2))
907
            );
908
    }
909

910
    /// @notice Verifies a signature for a given signer.
911
    /// @param _hash The EIP-712 hash of the order.
912
    /// @param _signature The signature bytes.
913
    /// @param _signer The expected signer.
914
    /// @return True if signature is valid, false otherwise.
915
    function verifySignature(
916
        bytes32 _hash,
917
        bytes calldata _signature,
918
        address _signer
919
    ) public view returns (bool) {
920
        // For contracts, use EIP-1271.
921
        if (_signer.code.length > 0) {
316✔
922
            try IERC1271(_signer).isValidSignature(_hash, _signature) returns (
×
923
                bytes4 magicValue
924
            ) {
×
925
                return magicValue == IERC1271.isValidSignature.selector;
×
926
            } catch {
×
927
                return false;
×
928
            }
929
        }
930

931
        // For EOAs, verify ECDSA signature.
932
        return ECDSA.recover(_hash, _signature) == _signer;
316✔
933
    }
934

935
    /// @dev Encodes the first part of the order intent.
936
    /// @param _order The order intent to encode.
937
    /// @return The encoded part of the order intent.
938
    function _encodeOrderPart1(
939
        OrderIntent calldata _order
940
    ) internal pure returns (bytes memory) {
941
        return
640✔
942
            abi.encode(
640✔
943
                ORDER_INTENT_TYPEHASH,
944
                _order.trader,
945
                _order.counterparty,
946
                address(_order.hyperdrive),
947
                _order.fundAmount,
948
                _order.bondAmount
949
            );
950
    }
951

952
    /// @dev Encodes the second part of the order intent.
953
    /// @param _order The order intent to encode.
954
    /// @return The encoded part of the order intent.
955
    function _encodeOrderPart2(
956
        OrderIntent calldata _order
957
    ) internal pure returns (bytes memory) {
958
        bytes32 optionsHash = keccak256(
640✔
959
            abi.encode(
960
                OPTIONS_TYPEHASH,
961
                _order.options.destination,
962
                _order.options.asBase
963
            )
964
        );
965

966
        return
640✔
967
            abi.encode(
640✔
968
                _order.minVaultSharePrice,
969
                optionsHash,
970
                uint8(_order.orderType),
971
                _order.minMaturityTime,
972
                _order.maxMaturityTime,
973
                _order.expiry,
974
                _order.salt
975
            );
976
    }
977

978
    /// @dev Validates orders before matching them.
979
    /// @param _order1 The first order to validate.
980
    /// @param _order2 The second order to validate.
981
    /// @return order1Hash The hash of the first order.
982
    /// @return order2Hash The hash of the second order.
983
    function _validateOrdersNoTaker(
984
        OrderIntent calldata _order1,
985
        OrderIntent calldata _order2
986
    ) internal view returns (bytes32 order1Hash, bytes32 order2Hash) {
987
        // Verify counterparties.
988
        if (
989
            (_order1.counterparty != address(0) &&
155✔
990
                _order1.counterparty != _order2.trader) ||
991
            (_order2.counterparty != address(0) &&
992
                _order2.counterparty != _order1.trader)
993
        ) {
×
994
            revert InvalidCounterparty();
×
995
        }
996

997
        // Check expiry.
998
        if (
999
            _order1.expiry <= block.timestamp ||
155✔
1000
            _order2.expiry <= block.timestamp
154✔
1001
        ) {
1✔
1002
            revert AlreadyExpired();
1✔
1003
        }
1004

1005
        // Verify Hyperdrive instance.
1006
        if (_order1.hyperdrive != _order2.hyperdrive) {
154✔
1007
            revert MismatchedHyperdrive();
1✔
1008
        }
1009

1010
        // Verify settlement asset.
1011
        // @dev TODO: only supporting both true or both false for now.
1012
        //      Supporting mixed asBase values needs code changes on the Hyperdrive
1013
        //      instances.
1014
        if (_order1.options.asBase != _order2.options.asBase) {
153✔
1015
            revert InvalidSettlementAsset();
×
1016
        }
1017

1018
        // Verify valid maturity time.
1019
        if (
1020
            _order1.minMaturityTime > _order1.maxMaturityTime ||
153✔
1021
            _order2.minMaturityTime > _order2.maxMaturityTime
153✔
1022
        ) {
×
1023
            revert InvalidMaturityTime();
×
1024
        }
1025

1026
        // For close orders, minMaturityTime must equal maxMaturityTime.
1027
        if (
1028
            _order1.orderType == OrderType.CloseLong ||
153✔
1029
            _order1.orderType == OrderType.CloseShort
150✔
1030
        ) {
3✔
1031
            if (_order1.minMaturityTime != _order1.maxMaturityTime) {
3✔
1032
                revert InvalidMaturityTime();
×
1033
            }
1034
        }
1035
        if (
1036
            _order2.orderType == OrderType.CloseLong ||
153✔
1037
            _order2.orderType == OrderType.CloseShort
152✔
1038
        ) {
4✔
1039
            if (_order2.minMaturityTime != _order2.maxMaturityTime) {
4✔
1040
                revert InvalidMaturityTime();
×
1041
            }
1042
        }
1043

1044
        // Check that the destination is not the zero address.
1045
        if (
1046
            _order1.options.destination == address(0) ||
153✔
1047
            _order2.options.destination == address(0)
153✔
1048
        ) {
×
1049
            revert InvalidDestination();
×
1050
        }
1051

1052
        // Hash orders.
1053
        order1Hash = hashOrderIntent(_order1);
153✔
1054
        order2Hash = hashOrderIntent(_order2);
153✔
1055

1056
        // Check if orders are fully executed.
1057
        if (
1058
            orderAmountsUsed[order1Hash].bondAmount >= _order1.bondAmount ||
153✔
1059
            orderAmountsUsed[order1Hash].fundAmount >= _order1.fundAmount
153✔
1060
        ) {
×
1061
            revert AlreadyFullyExecuted();
×
1062
        }
1063
        if (
1064
            orderAmountsUsed[order2Hash].bondAmount >= _order2.bondAmount ||
153✔
1065
            orderAmountsUsed[order2Hash].fundAmount >= _order2.fundAmount
153✔
1066
        ) {
×
1067
            revert AlreadyFullyExecuted();
×
1068
        }
1069

1070
        // Check if orders are cancelled.
1071
        if (isCancelled[order1Hash] || isCancelled[order2Hash]) {
153✔
1072
            revert AlreadyCancelled();
×
1073
        }
1074

1075
        // Verify signatures.
1076
        if (
1077
            !verifySignature(order1Hash, _order1.signature, _order1.trader) ||
153✔
1078
            !verifySignature(order2Hash, _order2.signature, _order2.trader)
152✔
1079
        ) {
1✔
1080
            revert InvalidSignature();
1✔
1081
        }
1082
    }
1083

1084
    /// @dev Validates the maker and taker orders. This function has shrinked
1085
    ///      logic from the _validateOrdersNoTaker function, as the taker order
1086
    ///      is only a minimal sufficient struct created by the frontend, with
1087
    ///      some fields being faulty values.
1088
    /// @param _makerOrder The maker order to validate.
1089
    /// @param _takerOrder The taker order to validate.
1090
    /// @return makerOrderHash The hash of the maker order.
1091
    function _validateOrdersWithTaker(
1092
        OrderIntent calldata _makerOrder,
1093
        OrderIntent calldata _takerOrder
1094
    ) internal view returns (bytes32 makerOrderHash) {
1095
        // Verify the maker's counterparty is the taker.
1096
        if (
1097
            (_makerOrder.counterparty != address(0) &&
1098
                _makerOrder.counterparty != _takerOrder.trader)
1099
        ) {
1✔
1100
            revert InvalidCounterparty();
1✔
1101
        }
1102

1103
        // Check expiry.
1104
        if (_makerOrder.expiry <= block.timestamp) {
13✔
1105
            revert AlreadyExpired();
1✔
1106
        }
1107

1108
        // Verify settlement asset.
1109
        // @dev TODO: only supporting both true or both false for now.
1110
        //      Supporting mixed asBase values needs code changes on the Hyperdrive
1111
        //      instances.
1112
        if (_makerOrder.options.asBase != _takerOrder.options.asBase) {
12✔
1113
            revert InvalidSettlementAsset();
×
1114
        }
1115

1116
        // Verify valid maturity time.
1117
        if (
1118
            _makerOrder.minMaturityTime > _makerOrder.maxMaturityTime ||
12✔
1119
            _takerOrder.minMaturityTime > _takerOrder.maxMaturityTime
12✔
1120
        ) {
×
1121
            revert InvalidMaturityTime();
×
1122
        }
1123

1124
        // For the close order, minMaturityTime must equal maxMaturityTime.
1125
        if (
1126
            _makerOrder.orderType == OrderType.CloseLong ||
12✔
1127
            _makerOrder.orderType == OrderType.CloseShort
12✔
1128
        ) {
×
1129
            if (_makerOrder.minMaturityTime != _makerOrder.maxMaturityTime) {
×
1130
                revert InvalidMaturityTime();
×
1131
            }
1132
        }
1133
        if (
1134
            _takerOrder.orderType == OrderType.CloseLong ||
12✔
1135
            _takerOrder.orderType == OrderType.CloseShort
12✔
1136
        ) {
×
1137
            if (_takerOrder.minMaturityTime != _takerOrder.maxMaturityTime) {
×
1138
                revert InvalidMaturityTime();
×
1139
            }
1140
        }
1141

1142
        // Check that the destination is not the zero address.
1143
        if (
1144
            _makerOrder.options.destination == address(0) ||
12✔
1145
            _takerOrder.options.destination == address(0)
12✔
1146
        ) {
×
1147
            revert InvalidDestination();
×
1148
        }
1149

1150
        // Hash the order.
1151
        makerOrderHash = hashOrderIntent(_makerOrder);
12✔
1152

1153
        // Check if the maker order is fully executed.
1154
        if (
1155
            orderAmountsUsed[makerOrderHash].bondAmount >=
12✔
1156
            _makerOrder.bondAmount ||
1157
            orderAmountsUsed[makerOrderHash].fundAmount >=
11✔
1158
            _makerOrder.fundAmount
1159
        ) {
1✔
1160
            revert AlreadyFullyExecuted();
1✔
1161
        }
1162

1163
        // Check if the maker order is cancelled.
1164
        if (isCancelled[makerOrderHash]) {
×
1165
            revert AlreadyCancelled();
×
1166
        }
1167

1168
        // Verify the maker's signature.
1169
        if (
1170
            !verifySignature(
11✔
1171
                makerOrderHash,
1172
                _makerOrder.signature,
1173
                _makerOrder.trader
1174
            )
1175
        ) {
×
1176
            revert InvalidSignature();
×
1177
        }
1178
    }
1179

1180
    /// @dev Calculates the amount of bonds that can be matched between two orders.
1181
    /// @param _order1 The first order to match.
1182
    /// @param _order2 The second order to match.
1183
    /// @param _order1Hash The hash of the first order.
1184
    /// @param _order2Hash The hash of the second order.
1185
    /// @return bondMatchAmount The amount of bonds that can be matched.
1186
    function _calculateBondMatchAmount(
1187
        OrderIntent calldata _order1,
1188
        OrderIntent calldata _order2,
1189
        bytes32 _order1Hash,
1190
        bytes32 _order2Hash
1191
    ) internal view returns (uint256 bondMatchAmount) {
1192
        OrderAmounts memory amounts1 = orderAmountsUsed[_order1Hash];
151✔
1193
        OrderAmounts memory amounts2 = orderAmountsUsed[_order2Hash];
151✔
1194

1195
        uint256 order1BondAmount = _order1.bondAmount - amounts1.bondAmount;
151✔
1196
        uint256 order2BondAmount = _order2.bondAmount - amounts2.bondAmount;
151✔
1197

1198
        bondMatchAmount = order1BondAmount.min(order2BondAmount);
151✔
1199
    }
1200

1201
    /// @dev Handles the minting of matching positions.
1202
    /// @param _longOrder The order for opening a long position.
1203
    /// @param _shortOrder The order for opening a short position.
1204
    /// @param _fundTokenAmountLongOrder The amount of fund tokens from the long
1205
    ///        order.
1206
    /// @param _fundTokenAmountShortOrder The amount of fund tokens from the short
1207
    ///        order.
1208
    /// @param _cost The total cost of the operation.
1209
    /// @param _bondMatchAmount The amount of bonds to mint.
1210
    /// @param _fundToken The fund token being used.
1211
    /// @param _hyperdrive The Hyperdrive contract instance.
1212
    /// @return The amount of bonds minted.
1213
    function _handleMint(
1214
        OrderIntent calldata _longOrder,
1215
        OrderIntent calldata _shortOrder,
1216
        uint256 _fundTokenAmountLongOrder,
1217
        uint256 _fundTokenAmountShortOrder,
1218
        uint256 _cost,
1219
        uint256 _bondMatchAmount,
1220
        ERC20 _fundToken,
1221
        IHyperdrive _hyperdrive
1222
    ) internal returns (uint256) {
1223
        // Transfer fund tokens from long trader.
1224
        _fundToken.safeTransferFrom(
157✔
1225
            _longOrder.trader,
1226
            address(this),
1227
            _fundTokenAmountLongOrder
1228
        );
1229

1230
        // Transfer fund tokens from short trader.
1231
        _fundToken.safeTransferFrom(
157✔
1232
            _shortOrder.trader,
1233
            address(this),
1234
            _fundTokenAmountShortOrder
1235
        );
1236

1237
        // Approve Hyperdrive.
1238
        // @dev Use balanceOf to get the total amount of fund tokens instead of
1239
        //      summing up the two amounts, in order to open the door for any
1240
        //      potential donation to help match orders.
1241
        uint256 totalFundTokenAmount = _fundToken.balanceOf(address(this));
157✔
1242
        uint256 fundTokenAmountToUse = _cost + TOKEN_AMOUNT_BUFFER;
157✔
1243
        if (totalFundTokenAmount < fundTokenAmountToUse) {
157✔
1244
            revert InsufficientFunding();
1✔
1245
        }
1246

1247
        // @dev Add 1 wei of approval so that the storage slot stays hot.
1248
        _fundToken.forceApprove(address(_hyperdrive), fundTokenAmountToUse + 1);
156✔
1249

1250
        // Create PairOptions.
1251
        IHyperdrive.PairOptions memory pairOptions = IHyperdrive.PairOptions({
156✔
1252
            longDestination: _longOrder.options.destination,
1253
            shortDestination: _shortOrder.options.destination,
1254
            asBase: _longOrder.options.asBase,
1255
            extraData: ""
1256
        });
1257

1258
        // Calculate minVaultSharePrice.
1259
        // @dev Take the larger of the two minVaultSharePrice as the min guard
1260
        //      price to prevent slippage, so that it satisfies both orders.
1261
        uint256 minVaultSharePrice = _longOrder.minVaultSharePrice.max(
156✔
1262
            _shortOrder.minVaultSharePrice
1263
        );
1264

1265
        // Mint matching positions.
1266
        (, uint256 bondAmount) = _hyperdrive.mint(
156✔
1267
            fundTokenAmountToUse,
1268
            _bondMatchAmount,
1269
            minVaultSharePrice,
1270
            pairOptions
1271
        );
1272

1273
        // Return the bondAmount.
1274
        return bondAmount;
155✔
1275
    }
1276

1277
    /// @dev Handles the burning of matching positions.
1278
    /// @param _longOrder The first order (CloseLong).
1279
    /// @param _shortOrder The second order (CloseShort).
1280
    /// @param _minFundAmountLongOrder The minimum fund amount for the long order.
1281
    /// @param _minFundAmountShortOrder The minimum fund amount for the short order.
1282
    /// @param _bondMatchAmount The amount of bonds to burn.
1283
    /// @param _fundToken The fund token being used.
1284
    /// @param _hyperdrive The Hyperdrive contract instance.
1285
    function _handleBurn(
1286
        OrderIntent calldata _longOrder,
1287
        OrderIntent calldata _shortOrder,
1288
        uint256 _minFundAmountLongOrder,
1289
        uint256 _minFundAmountShortOrder,
1290
        uint256 _bondMatchAmount,
1291
        ERC20 _fundToken,
1292
        IHyperdrive _hyperdrive
1293
    ) internal {
1294
        // Get asset IDs for the long and short positions.
1295
        uint256 longAssetId = AssetId.encodeAssetId(
2✔
1296
            AssetId.AssetIdPrefix.Long,
1297
            _longOrder.maxMaturityTime
1298
        );
1299
        uint256 shortAssetId = AssetId.encodeAssetId(
2✔
1300
            AssetId.AssetIdPrefix.Short,
1301
            _shortOrder.maxMaturityTime
1302
        );
1303

1304
        // This contract needs to take custody of the bonds before burning.
1305
        _hyperdrive.safeTransferFrom(
2✔
1306
            _longOrder.trader,
1307
            address(this),
1308
            longAssetId,
1309
            _bondMatchAmount,
1310
            ""
1311
        );
1312
        _hyperdrive.safeTransferFrom(
1✔
1313
            _shortOrder.trader,
1314
            address(this),
1315
            shortAssetId,
1316
            _bondMatchAmount,
1317
            ""
1318
        );
1319

1320
        // Calculate minOutput and consider the potential donation to help match
1321
        // orders.
1322
        uint256 minOutput = (_minFundAmountLongOrder +
1✔
1323
            _minFundAmountShortOrder) > _fundToken.balanceOf(address(this))
1324
            ? _minFundAmountLongOrder +
1325
                _minFundAmountShortOrder -
1326
                _fundToken.balanceOf(address(this))
1327
            : 0;
1328

1329
        // Stack cycling to avoid stack-too-deep.
1330
        OrderIntent calldata longOrder = _longOrder;
1✔
1331
        OrderIntent calldata shortOrder = _shortOrder;
1✔
1332

1333
        // Burn the matching positions.
1334
        _hyperdrive.burn(
1✔
1335
            longOrder.maxMaturityTime,
1336
            _bondMatchAmount,
1337
            minOutput,
1338
            IHyperdrive.Options({
1339
                destination: address(this),
1340
                asBase: longOrder.options.asBase,
1341
                extraData: ""
1342
            })
1343
        );
1344

1345
        // Transfer proceeds to traders.
1346
        _fundToken.safeTransfer(
1✔
1347
            longOrder.options.destination,
1348
            _minFundAmountLongOrder
1349
        );
1350
        _fundToken.safeTransfer(
1✔
1351
            shortOrder.options.destination,
1352
            _minFundAmountShortOrder
1353
        );
1354
    }
1355

1356
    /// @dev Handles the transfer of positions between traders.
1357
    /// @param _openOrder The order for opening a position.
1358
    /// @param _closeOrder The order for closing a position.
1359
    /// @param _fundTokenAmountOpenOrder The amount of fund tokens from the
1360
    ///        open order.
1361
    /// @param _minFundAmountCloseOrder The minimum fund amount for the close
1362
    ///        order.
1363
    /// @param _bondMatchAmount The amount of bonds to transfer.
1364
    /// @param _fundToken The fund token being used.
1365
    /// @param _hyperdrive The Hyperdrive contract instance.
1366
    function _handleTransfer(
1367
        OrderIntent calldata _openOrder,
1368
        OrderIntent calldata _closeOrder,
1369
        uint256 _fundTokenAmountOpenOrder,
1370
        uint256 _minFundAmountCloseOrder,
1371
        uint256 _bondMatchAmount,
1372
        ERC20 _fundToken,
1373
        IHyperdrive _hyperdrive
1374
    ) internal {
1375
        // Get asset ID for the position.
1376
        uint256 assetId;
1✔
1377
        if (_openOrder.orderType == OrderType.OpenLong) {
1✔
1378
            assetId = AssetId.encodeAssetId(
1✔
1379
                AssetId.AssetIdPrefix.Long,
1380
                _closeOrder.maxMaturityTime
1381
            );
1382
        } else {
1383
            assetId = AssetId.encodeAssetId(
×
1384
                AssetId.AssetIdPrefix.Short,
1385
                _closeOrder.maxMaturityTime
1386
            );
1387
        }
1388

1389
        // Transfer the position from the close trader to the open trader.
1390
        _hyperdrive.safeTransferFrom(
1✔
1391
            _closeOrder.trader,
1392
            _openOrder.options.destination,
1393
            assetId,
1394
            _bondMatchAmount,
1395
            ""
1396
        );
1397

1398
        // Transfer fund tokens from open trader to the close trader.
1399
        _fundToken.safeTransferFrom(
1✔
1400
            _openOrder.trader,
1401
            _closeOrder.options.destination,
1402
            _fundTokenAmountOpenOrder.min(_minFundAmountCloseOrder)
1403
        );
1404
    }
1405

1406
    /// @dev Gets the most recent checkpoint time.
1407
    /// @param _checkpointDuration The duration of the checkpoint.
1408
    /// @return latestCheckpoint The latest checkpoint.
1409
    function _latestCheckpoint(
1410
        uint256 _checkpointDuration
1411
    ) internal view returns (uint256 latestCheckpoint) {
1412
        latestCheckpoint = HyperdriveMath.calculateCheckpointTime(
158✔
1413
            block.timestamp,
1414
            _checkpointDuration
1415
        );
1416
    }
1417

1418
    /// @dev Calculates the cost and parameters for minting positions.
1419
    /// @param _hyperdrive The Hyperdrive contract instance.
1420
    /// @param _bondMatchAmount The amount of bonds to mint.
1421
    /// @param _asBase Whether the cost is in terms of base.
1422
    /// @return maturityTime The maturity time for new positions.
1423
    /// @return cost The total cost including fees.
1424
    function _calculateMintCost(
1425
        IHyperdrive _hyperdrive,
1426
        uint256 _bondMatchAmount,
1427
        bool _asBase
1428
    ) internal view returns (uint256 maturityTime, uint256 cost) {
1429
        // Get pool configuration.
1430
        IHyperdrive.PoolConfig memory config = _hyperdrive.getPoolConfig();
158✔
1431

1432
        // Calculate checkpoint and maturity time.
1433
        uint256 latestCheckpoint = _latestCheckpoint(config.checkpointDuration);
158✔
1434
        maturityTime = latestCheckpoint + config.positionDuration;
158✔
1435

1436
        // Get vault share prices.
1437
        uint256 vaultSharePrice = _hyperdrive.convertToBase(1e18);
158✔
1438
        uint256 openVaultSharePrice = _hyperdrive
158✔
1439
            .getCheckpoint(latestCheckpoint)
1440
            .vaultSharePrice;
1441
        if (openVaultSharePrice == 0) {
158✔
1442
            openVaultSharePrice = vaultSharePrice;
2✔
1443
        }
1444

1445
        // Calculate the required fund amount.
1446
        // NOTE: Round the required fund amount up to overestimate the cost.
1447
        cost = _bondMatchAmount.mulDivUp(
158✔
1448
            vaultSharePrice.max(openVaultSharePrice),
1449
            openVaultSharePrice
1450
        );
1451

1452
        // Add flat fee.
1453
        // NOTE: Round the flat fee calculation up to match other flows.
1454
        uint256 flatFee = _bondMatchAmount.mulUp(config.fees.flat);
158✔
1455
        cost += flatFee;
158✔
1456

1457
        // Add governance fee.
1458
        // NOTE: Round the governance fee calculation down to match other flows.
1459
        uint256 governanceFee = 2 * flatFee.mulDown(config.fees.governanceLP);
158✔
1460
        cost += governanceFee;
158✔
1461

1462
        if (_asBase) {
158✔
1463
            // NOTE: Round up to overestimate the cost.
1464
            cost = _hyperdrive.convertToBase(cost.divUp(vaultSharePrice));
158✔
1465
        } else {
1466
            // NOTE: Round up to overestimate the cost.
NEW
1467
            cost = cost.divUp(vaultSharePrice);
×
1468
        }
1469
    }
1470

1471
    /// @dev Updates either the bond amount or fund amount used for a given order.
1472
    /// @param orderHash The hash of the order.
1473
    /// @param amount The amount to add.
1474
    /// @param updateBond If true, updates bond amount; if false, updates fund
1475
    ///        amount.
1476
    function _updateOrderAmount(
1477
        bytes32 orderHash,
1478
        uint256 amount,
1479
        bool updateBond
1480
    ) internal {
1481
        OrderAmounts memory amounts = orderAmountsUsed[orderHash];
617✔
1482

1483
        if (updateBond) {
617✔
1484
            // Check for overflow before casting to uint128
1485
            if (amounts.bondAmount + amount > type(uint128).max) {
307✔
UNCOV
1486
                revert AmountOverflow();
×
1487
            }
1488
            orderAmountsUsed[orderHash] = OrderAmounts({
307✔
1489
                bondAmount: uint128(amounts.bondAmount + amount),
1490
                fundAmount: amounts.fundAmount
1491
            });
1492
        } else {
1493
            // Check for overflow before casting to uint128.
1494
            if (amounts.fundAmount + amount > type(uint128).max) {
310✔
UNCOV
1495
                revert AmountOverflow();
×
1496
            }
1497
            orderAmountsUsed[orderHash] = OrderAmounts({
310✔
1498
                bondAmount: amounts.bondAmount,
1499
                fundAmount: uint128(amounts.fundAmount + amount)
1500
            });
1501
        }
1502
    }
1503

1504
    /// @notice Handles the receipt of a single ERC1155 token type. This
1505
    ///         function is called at the end of a `safeTransferFrom` after the
1506
    ///         balance has been updated.
1507
    /// @return The magic function selector if the transfer is allowed, and the
1508
    ///         the 0 bytes4 otherwise.
1509
    function onERC1155Received(
1510
        address,
1511
        address,
1512
        uint256,
1513
        uint256,
1514
        bytes calldata
1515
    ) external pure returns (bytes4) {
1516
        // This contract always accepts the transfer.
1517
        return
2✔
1518
            bytes4(
1519
                keccak256(
1520
                    "onERC1155Received(address,address,uint256,uint256,bytes)"
1521
                )
1522
            );
1523
    }
1524
}
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