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

delvtech / hyperdrive / 13497662436

24 Feb 2025 12:08PM UTC coverage: 86.309% (-0.2%) from 86.513%
13497662436

Pull #1240

github

Sean329
Resolve the EIP712 TypeHash issue
Pull Request #1240: To resolve the auditor's comments on Matching Engine V2

1 of 13 new or added lines in 1 file covered. (7.69%)

1 existing line in 1 file now uncovered.

3234 of 3747 relevant lines covered (86.31%)

295358.77 hits per line

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

57.27
/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)) {
115✔
89
            _surplusRecipient = msg.sender;
×
90
        }
91

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

98
        IHyperdrive hyperdrive = _order1.hyperdrive;
112✔
99
        ERC20 fundToken;
112✔
100
        if (_order1.options.asBase) {
112✔
101
            fundToken = ERC20(hyperdrive.baseToken());
112✔
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 &&
112✔
110
            _order2.orderType == OrderType.OpenShort
109✔
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(
108✔
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 -
108✔
129
                orderAmountsUsed[order1Hash].fundAmount).mulDivDown(
130
                    bondMatchAmount,
131
                    (_order1.bondAmount -
132
                        orderAmountsUsed[order1Hash].bondAmount)
133
                );
134
            uint256 fundTokenAmountOrder2 = (_order2.fundAmount -
108✔
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);
108✔
143
            _updateOrderAmount(order2Hash, fundTokenAmountOrder2, false);
108✔
144

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

442
                // Calculate costs and parameters.
443
                (uint256 maturityTime, uint256 cost) = _calculateMintCost(
1✔
444
                    hyperdrive,
445
                    bondMatchAmount
446
                );
447

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

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

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

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

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

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

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

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

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

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

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

552
                // Calculate costs and parameters.
553
                (uint256 maturityTime, uint256 cost) = _calculateMintCost(
1✔
554
                    hyperdrive,
555
                    bondMatchAmount
556
                );
557

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

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

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

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

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

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

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

620
                _handleTransfer(
×
621
                    _makerOrder,
622
                    _takerOrder,
623
                    fundTokenAmountMaker,
624
                    minFundAmountTaker,
625
                    bondMatchAmount,
626
                    fundToken,
627
                    hyperdrive
628
                );
629

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

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

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

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

670
                _handleTransfer(
×
671
                    _takerOrder,
672
                    _makerOrder,
673
                    fundTokenAmountTaker,
674
                    minFundAmountMaker,
675
                    bondMatchAmount,
676
                    fundToken,
677
                    hyperdrive
678
                );
679

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

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

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

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

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

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

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

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

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

763
                _handleTransfer(
×
764
                    _takerOrder,
765
                    _makerOrder,
766
                    fundTokenAmountTaker,
767
                    minFundAmountMaker,
768
                    bondMatchAmount,
769
                    fundToken,
770
                    hyperdrive
771
                );
772

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

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

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

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

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

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

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

834
        emit OrderFilled(
2✔
835
            _makerOrder.hyperdrive,
836
            makerOrderHash,
837
            _makerOrder.trader,
838
            _takerOrder.trader,
839
            orderAmountsUsed[makerOrderHash].bondAmount,
840
            orderAmountsUsed[makerOrderHash].fundAmount
841
        );
842
    }
843

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

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

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

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

876
            // Cancel the order.
877
            isCancelled[orderHash] = true;
×
NEW
878
            orderHashes[validOrderCount] = orderHash;
×
NEW
879
            validOrderCount++;
×
880
        }
881

882
        // Emit event with assembly to truncate array to actual size
883
        assembly {
NEW
884
            mstore(orderHashes, validOrderCount)
×
885
        }
UNCOV
886
        emit OrdersCancelled(msg.sender, orderHashes);
×
887
    }
888

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

900
        // Concatenate and calculate the final hash
901
        return
463✔
902
            _hashTypedDataV4(
463✔
903
                keccak256(bytes.concat(encodedPart1, encodedPart2))
904
            );
905
    }
906

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

928
        // For EOAs, verify ECDSA signature.
929
        return ECDSA.recover(_hash, _signature) == _signer;
228✔
930
    }
931

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

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

963
        return
463✔
964
            abi.encode(
463✔
965
                _order.minVaultSharePrice,
966
                optionsHash,
967
                uint8(_order.orderType),
968
                _order.minMaturityTime,
969
                _order.maxMaturityTime,
970
                _order.expiry,
971
                _order.salt
972
            );
973
    }
974

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

994
        // Check expiry.
995
        if (
996
            _order1.expiry <= block.timestamp ||
115✔
997
            _order2.expiry <= block.timestamp
114✔
998
        ) {
1✔
999
            revert AlreadyExpired();
1✔
1000
        }
1001

1002
        // Verify Hyperdrive instance.
1003
        if (_order1.hyperdrive != _order2.hyperdrive) {
114✔
1004
            revert MismatchedHyperdrive();
1✔
1005
        }
1006

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

1015
        // Verify valid maturity time.
1016
        if (
1017
            _order1.minMaturityTime > _order1.maxMaturityTime ||
113✔
1018
            _order2.minMaturityTime > _order2.maxMaturityTime
113✔
1019
        ) {
×
1020
            revert InvalidMaturityTime();
×
1021
        }
1022

1023
        // For close orders, minMaturityTime must equal maxMaturityTime.
1024
        if (
1025
            _order1.orderType == OrderType.CloseLong ||
113✔
1026
            _order1.orderType == OrderType.CloseShort
110✔
1027
        ) {
3✔
1028
            if (_order1.minMaturityTime != _order1.maxMaturityTime) {
3✔
1029
                revert InvalidMaturityTime();
×
1030
            }
1031
        }
1032
        if (
1033
            _order2.orderType == OrderType.CloseLong ||
113✔
1034
            _order2.orderType == OrderType.CloseShort
112✔
1035
        ) {
4✔
1036
            if (_order2.minMaturityTime != _order2.maxMaturityTime) {
4✔
1037
                revert InvalidMaturityTime();
×
1038
            }
1039
        }
1040

1041
        // Check that the destination is not the zero address.
1042
        if (
1043
            _order1.options.destination == address(0) ||
113✔
1044
            _order2.options.destination == address(0)
113✔
1045
        ) {
×
1046
            revert InvalidDestination();
×
1047
        }
1048

1049
        // Hash orders.
1050
        order1Hash = hashOrderIntent(_order1);
113✔
1051
        order2Hash = hashOrderIntent(_order2);
113✔
1052

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

1067
        // Check if orders are cancelled.
1068
        if (isCancelled[order1Hash] || isCancelled[order2Hash]) {
113✔
1069
            revert AlreadyCancelled();
×
1070
        }
1071

1072
        // Verify signatures.
1073
        if (
1074
            !verifySignature(order1Hash, _order1.signature, _order1.trader) ||
113✔
1075
            !verifySignature(order2Hash, _order2.signature, _order2.trader)
112✔
1076
        ) {
1✔
1077
            revert InvalidSignature();
1✔
1078
        }
1079
    }
1080

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

1100
        // Check expiry.
1101
        if (_makerOrder.expiry <= block.timestamp) {
4✔
1102
            revert AlreadyExpired();
1✔
1103
        }
1104

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

1113
        // Verify valid maturity time.
1114
        if (
1115
            _makerOrder.minMaturityTime > _makerOrder.maxMaturityTime ||
3✔
1116
            _takerOrder.minMaturityTime > _takerOrder.maxMaturityTime
3✔
1117
        ) {
×
1118
            revert InvalidMaturityTime();
×
1119
        }
1120

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

1139
        // Check that the destination is not the zero address.
1140
        if (
1141
            _makerOrder.options.destination == address(0) ||
3✔
1142
            _takerOrder.options.destination == address(0)
3✔
1143
        ) {
×
1144
            revert InvalidDestination();
×
1145
        }
1146

1147
        // Hash the order.
1148
        makerOrderHash = hashOrderIntent(_makerOrder);
3✔
1149

1150
        // Check if the maker order is fully executed.
1151
        if (
1152
            orderAmountsUsed[makerOrderHash].bondAmount >=
3✔
1153
            _makerOrder.bondAmount ||
1154
            orderAmountsUsed[makerOrderHash].fundAmount >=
3✔
1155
            _makerOrder.fundAmount
1156
        ) {
×
1157
            revert AlreadyFullyExecuted();
×
1158
        }
1159

1160
        // Check if the maker order is cancelled.
1161
        if (isCancelled[makerOrderHash]) {
×
1162
            revert AlreadyCancelled();
×
1163
        }
1164

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

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

1192
        uint256 order1BondAmount = _order1.bondAmount - amounts1.bondAmount;
111✔
1193
        uint256 order2BondAmount = _order2.bondAmount - amounts2.bondAmount;
111✔
1194

1195
        bondMatchAmount = order1BondAmount.min(order2BondAmount);
111✔
1196
    }
1197

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

1227
        // Transfer fund tokens from short trader.
1228
        _fundToken.safeTransferFrom(
110✔
1229
            _shortOrder.trader,
1230
            address(this),
1231
            _fundTokenAmountShortOrder
1232
        );
1233

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

1244
        // @dev Add 1 wei of approval so that the storage slot stays hot.
1245
        _fundToken.forceApprove(address(_hyperdrive), fundTokenAmountToUse + 1);
109✔
1246

1247
        // Create PairOptions.
1248
        IHyperdrive.PairOptions memory pairOptions = IHyperdrive.PairOptions({
109✔
1249
            longDestination: _longOrder.options.destination,
1250
            shortDestination: _shortOrder.options.destination,
1251
            asBase: _longOrder.options.asBase,
1252
            extraData: ""
1253
        });
1254

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

1262
        // Mint matching positions.
1263
        (, uint256 bondAmount) = _hyperdrive.mint(
109✔
1264
            fundTokenAmountToUse,
1265
            _bondMatchAmount,
1266
            minVaultSharePrice,
1267
            pairOptions
1268
        );
1269

1270
        // Return the bondAmount.
1271
        return bondAmount;
108✔
1272
    }
1273

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

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

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

1326
        // Stack cycling to avoid stack-too-deep.
1327
        OrderIntent calldata longOrder = _longOrder;
1✔
1328
        OrderIntent calldata shortOrder = _shortOrder;
1✔
1329

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

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

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

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

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

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

1415
    /// @dev Calculates the cost and parameters for minting positions.
1416
    /// @param _hyperdrive The Hyperdrive contract instance.
1417
    /// @param _bondMatchAmount The amount of bonds to mint.
1418
    /// @return maturityTime The maturity time for new positions.
1419
    /// @return cost The total cost including fees.
1420
    function _calculateMintCost(
1421
        IHyperdrive _hyperdrive,
1422
        uint256 _bondMatchAmount
1423
    ) internal view returns (uint256 maturityTime, uint256 cost) {
1424
        // Get pool configuration.
1425
        IHyperdrive.PoolConfig memory config = _hyperdrive.getPoolConfig();
110✔
1426

1427
        // Calculate checkpoint and maturity time.
1428
        uint256 latestCheckpoint = _latestCheckpoint(config.checkpointDuration);
110✔
1429
        maturityTime = latestCheckpoint + config.positionDuration;
110✔
1430

1431
        // Get vault share prices.
1432
        uint256 vaultSharePrice = _hyperdrive.convertToBase(1e18);
110✔
1433
        uint256 openVaultSharePrice = _hyperdrive
110✔
1434
            .getCheckpoint(latestCheckpoint)
1435
            .vaultSharePrice;
1436
        if (openVaultSharePrice == 0) {
110✔
1437
            openVaultSharePrice = vaultSharePrice;
×
1438
        }
1439

1440
        // Calculate the required fund amount.
1441
        // NOTE: Round the required fund amount up to overestimate the cost.
1442
        cost = _bondMatchAmount.mulDivUp(
110✔
1443
            vaultSharePrice.max(openVaultSharePrice),
1444
            openVaultSharePrice
1445
        );
1446

1447
        // Add flat fee.
1448
        // NOTE: Round the flat fee calculation up to match other flows.
1449
        uint256 flatFee = _bondMatchAmount.mulUp(config.fees.flat);
110✔
1450
        cost += flatFee;
110✔
1451

1452
        // Add governance fee.
1453
        // NOTE: Round the governance fee calculation down to match other flows.
1454
        uint256 governanceFee = 2 * flatFee.mulDown(config.fees.governanceLP);
110✔
1455
        cost += governanceFee;
110✔
1456
    }
1457

1458
    /// @dev Updates either the bond amount or fund amount used for a given order.
1459
    /// @param orderHash The hash of the order.
1460
    /// @param amount The amount to add.
1461
    /// @param updateBond If true, updates bond amount; if false, updates fund
1462
    ///        amount.
1463
    function _updateOrderAmount(
1464
        bytes32 orderHash,
1465
        uint256 amount,
1466
        bool updateBond
1467
    ) internal {
1468
        OrderAmounts memory amounts = orderAmountsUsed[orderHash];
442✔
1469

1470
        if (updateBond) {
442✔
1471
            // Check for overflow before casting to uint128
1472
            if (amounts.bondAmount + amount > type(uint128).max) {
220✔
1473
                revert AmountOverflow();
×
1474
            }
1475
            orderAmountsUsed[orderHash] = OrderAmounts({
220✔
1476
                bondAmount: uint128(amounts.bondAmount + amount),
1477
                fundAmount: amounts.fundAmount
1478
            });
1479
        } else {
1480
            // Check for overflow before casting to uint128.
1481
            if (amounts.fundAmount + amount > type(uint128).max) {
222✔
1482
                revert AmountOverflow();
×
1483
            }
1484
            orderAmountsUsed[orderHash] = OrderAmounts({
222✔
1485
                bondAmount: amounts.bondAmount,
1486
                fundAmount: uint128(amounts.fundAmount + amount)
1487
            });
1488
        }
1489
    }
1490

1491
    /// @notice Handles the receipt of a single ERC1155 token type. This
1492
    ///         function is called at the end of a `safeTransferFrom` after the
1493
    ///         balance has been updated.
1494
    /// @return The magic function selector if the transfer is allowed, and the
1495
    ///         the 0 bytes4 otherwise.
1496
    function onERC1155Received(
1497
        address,
1498
        address,
1499
        uint256,
1500
        uint256,
1501
        bytes calldata
1502
    ) external pure returns (bytes4) {
1503
        // This contract always accepts the transfer.
1504
        return
2✔
1505
            bytes4(
1506
                keccak256(
1507
                    "onERC1155Received(address,address,uint256,uint256,bytes)"
1508
                )
1509
            );
1510
    }
1511
}
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