diff --git a/contracts/src/interfaces/IHyperdriveMatchingEngineV2.sol b/contracts/src/interfaces/IHyperdriveMatchingEngineV2.sol new file mode 100644 index 000000000..b3d108547 --- /dev/null +++ b/contracts/src/interfaces/IHyperdriveMatchingEngineV2.sol @@ -0,0 +1,224 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.20; + +import { IHyperdrive } from "../interfaces/IHyperdrive.sol"; + +/// @title IHyperdriveMatchingEngine +/// @notice Interface for the Hyperdrive matching engine. +interface IHyperdriveMatchingEngineV2 { + /// @notice Thrown when an order is already cancelled. + error AlreadyCancelled(); + + /// @notice Thrown when an order is already expired. + error AlreadyExpired(); + + /// @notice Thrown when the counterparty doesn't match the counterparty + /// signed into the order. + error InvalidCounterparty(); + + /// @notice Thrown when the destination for the add or remove liquidity + /// options isn't configured to this contract. + error InvalidDestination(); + + /// @notice Thrown when an address that didn't create an order tries to + /// cancel it. + error InvalidSender(); + + /// @notice Thrown when `asBase = false` is used. This implementation is + /// opinionated to keep the implementation simple. + error InvalidSettlementAsset(); + + /// @notice Thrown when the signature for an order intent doesn't recover to + /// the expected signer address. + error InvalidSignature(); + + /// @notice Thrown when the long and short orders don't refer to the same + /// Hyperdrive instance. + error MismatchedHyperdrive(); + + /// @notice Thrown when the bond match amount is zero. + error NoBondMatchAmount(); + + /// @notice Thrown when the used fund amount is greater than the order specified. + error InvalidFundAmount(); + + /// @notice Thrown when the maturity time is not within the asked range + /// in minting or transferring situations; or the maturity time + /// is not the same for both orders in burning situations. + error InvalidMaturityTime(); + + /// @notice Thrown when the funding amount is insufficient to cover the cost. + error InsufficientFunding(); + + /// @notice Thrown when the order combination is invalid. + error InvalidOrderCombination(); + + /// @notice Thrown when the order is already fully executed. + error AlreadyFullyExecuted(); + + /// @notice Emitted when orders are cancelled. + /// @param trader The address of the trader who cancelled the orders. + /// @param orderHashes The hashes of the cancelled orders. + event OrdersCancelled(address indexed trader, bytes32[] orderHashes); + + /// @notice Emitted when orders are matched. + /// @param hyperdrive The Hyperdrive contract where the trade occurred. + /// @param order1Hash The hash of the first order. + /// @param order2Hash The hash of the second order. + /// @param order1Trader The trader of the first order. + /// @param order2Trader The trader of the second order. + /// @param order1BondAmountUsed The amount of bonds used for the first order. + /// @param order2BondAmountUsed The amount of bonds used for the second order. + /// @param order1FundAmountUsed The amount of funds used for the first order. + /// @param order2FundAmountUsed The amount of funds used for the second order. + event OrdersMatched( + IHyperdrive indexed hyperdrive, + bytes32 indexed order1Hash, + bytes32 indexed order2Hash, + address order1Trader, + address order2Trader, + uint256 order1BondAmountUsed, + uint256 order2BondAmountUsed, + uint256 order1FundAmountUsed, + uint256 order2FundAmountUsed + ); + + /// @notice The type of an order intent. + enum OrderType { + OpenLong, + OpenShort, + CloseLong, + CloseShort + } + + /// @notice The order intent struct that encodes a trader's desire to trade. + /// @dev All monetary values use the same decimals as the base token. + struct OrderIntent { + /// @dev The trader address that will be charged when orders are matched. + address trader; + /// @dev The counterparty of the trade. If left as zero, the validation + /// is skipped. + address counterparty; + /// @dev The fee recipient of the trade. This is the address that will + /// receive any excess trading fees on the match. If left as zero, + /// the validation is skipped. + address feeRecipient; + /// @dev The Hyperdrive address where the trade will be executed. + IHyperdrive hyperdrive; + /// @dev The amount to be used in the trade. In the case of `OpenLong` or + /// `OpenShort`, this is the amount of funds to deposit; and in the + /// case of `CloseLong` or `CloseShort`, this is the min amount of + /// funds to receive. + uint256 fundAmount; + /// @dev The minimum output amount expected from the trade. In the case of + /// `OpenLong` or `OpenShort`, this is the min amount of bonds to + /// receive; and in the case of `CloseLong` or `CloseShort`, this is + /// the amount of bonds to close. + uint256 bondAmount; + /// @dev The minimum vault share price. This protects traders against + /// the sudden accrual of negative interest in a yield source. + uint256 minVaultSharePrice; + /// @dev The options that configure how the trade will be settled. + /// `asBase` is required to be true, the `destination` is the + /// address that receives the long or short position that is + /// purchased, and the extra data is configured for the yield + /// source that is being used. Since the extra data isn't included + /// in the order's hash, it can be updated between the order being + /// signed and executed. This is helpful for applications like DFB + /// that rely on the extra data field to record metadata in events. + IHyperdrive.Options options; + /// @dev The type of the order. Legal values are `OpenLong`, `OpenShort`, + /// `CloseLong`, or `CloseShort`. + OrderType orderType; + /// @dev The minimum and maximum maturity time for the order. + /// For `OpenLong` or `OpenShort` orders where the `onlyNewPositions` + /// is false, these values are checked for match validation. + /// For `CloseLong` or `CloseShort` orders, these values must be equal + /// and specify the maturity time of the position to close. + uint256 minMaturityTime; + uint256 maxMaturityTime; + /// @dev The signature that demonstrates the source's intent to complete + /// the trade. + bytes signature; + /// @dev The order's expiry timestamp. At or after this timestamp, the + /// order can't be filled. + uint256 expiry; + /// @dev The order's salt. This introduces some randomness which ensures + /// that duplicate orders don't collide. + bytes32 salt; + } + + /// @notice Get the name of this matching engine. + /// @return The name string. + function name() external view returns (string memory); + + /// @notice Get the kind of this matching engine. + /// @return The kind string. + function kind() external view returns (string memory); + + /// @notice Get the version of this matching engine. + /// @return The version string. + function version() external view returns (string memory); + + /// @notice Get the buffer amount used for cost calculations. + /// @return The buffer amount. + function TOKEN_AMOUNT_BUFFER() external view returns (uint256); + + /// @notice Returns whether or not an order has been cancelled. + /// @param orderHash The hash of the order. + /// @return True if the order was cancelled and false otherwise. + function isCancelled(bytes32 orderHash) external view returns (bool); + + /// @notice Get the amount of bonds used for a specific order. + /// @param orderHash The hash of the order. + /// @return The amount of bonds used. + function orderBondAmountUsed(bytes32 orderHash) external view returns (uint256); + + /// @notice Get the amount of funds used for a specific order. + /// @param orderHash The hash of the order. + /// @return The amount of funds used. + function orderFundAmountUsed(bytes32 orderHash) external view returns (uint256); + + /// @notice Get the EIP712 typehash for the + /// `IHyperdriveMatchingEngine.OrderIntent` struct. + /// @return The typehash. + function ORDER_INTENT_TYPEHASH() external view returns (bytes32); + + /// @notice Get the EIP712 typehash for the `IHyperdrive.Options` struct. + /// @return The typehash. + function OPTIONS_TYPEHASH() external view returns (bytes32); + + /// @notice Allows a trader to cancel a list of their orders. + /// @param _orders The orders to cancel. + function cancelOrders(OrderIntent[] calldata _orders) external; + + /// @notice Directly matches a long and a short order using a flash loan for + /// liquidity. + /// @param _order1 The order intent to open a long. + /// @param _order2 The order intent to open a short. + /// @param _surplusRecipient The address that receives the surplus funds from + /// matching the trades. + function matchOrders( + OrderIntent calldata _order1, + OrderIntent calldata _order2, + address _surplusRecipient + ) external; + + /// @notice Hashes an order intent according to EIP-712. + /// @param _order The order intent to hash. + /// @return The hash of the order intent. + function hashOrderIntent( + OrderIntent calldata _order + ) external view returns (bytes32); + + /// @notice Verifies a signature for a known signer. + /// @param _hash The EIP-712 hash of the order. + /// @param _signature The signature bytes. + /// @param _signer The expected signer. + /// @return True if signature is valid, false otherwise. + function verifySignature( + bytes32 _hash, + bytes calldata _signature, + address _signer + ) external view returns (bool); +} diff --git a/contracts/src/matching/HyperdriveMatchingEngineV2.sol b/contracts/src/matching/HyperdriveMatchingEngineV2.sol new file mode 100644 index 000000000..9a97f9fd2 --- /dev/null +++ b/contracts/src/matching/HyperdriveMatchingEngineV2.sol @@ -0,0 +1,645 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.24; + +import { ECDSA } from "lib/openzeppelin-contracts/contracts/utils/cryptography/ECDSA.sol"; +import { EIP712 } from "lib/openzeppelin-contracts/contracts/utils/cryptography/EIP712.sol"; +import { ERC20 } from "lib/openzeppelin-contracts/contracts/token/ERC20/ERC20.sol"; +import { IERC1271 } from "lib/openzeppelin-contracts/contracts/interfaces/IERC1271.sol"; +import { ReentrancyGuard } from "lib/openzeppelin-contracts/contracts/utils/ReentrancyGuard.sol"; +import { SafeERC20 } from "lib/openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; +import { AssetId } from "../libraries/AssetId.sol"; +import { FixedPointMath } from "../libraries/FixedPointMath.sol"; +import { HYPERDRIVE_MATCHING_ENGINE_KIND, VERSION } from "../libraries/Constants.sol"; +import { HyperdriveMath } from "../libraries/HyperdriveMath.sol"; +import { IHyperdrive } from "../interfaces/IHyperdrive.sol"; +import { IHyperdriveMatchingEngineV2 } from "../interfaces/IHyperdriveMatchingEngineV2.sol"; + +/// @author DELV +/// @title HyperdriveMatchingEngine +/// @notice A matching engine that processes order intents and settles trades on +/// the Hyperdrive AMM. +/// @dev This version uses direct Hyperdrive mint/burn functions instead of flash +/// loans. +/// @custom:disclaimer The language used in this code is for coding convenience +/// only, and is not intended to, and does not, have any +/// particular legal or regulatory significance. +contract HyperdriveMatchingEngineV2 is + IHyperdriveMatchingEngineV2, + ReentrancyGuard, + EIP712 +{ + using FixedPointMath for uint256; + using SafeERC20 for ERC20; + + /// @notice The EIP712 typehash of the OrderIntent struct. + bytes32 public constant ORDER_INTENT_TYPEHASH = + keccak256( + "OrderIntent(address trader,address counterparty,address feeRecipient,address hyperdrive,uint256 fundAmount,uint256 bondAmount,uint256 minVaultSharePrice,Options options,uint8 orderType,uint256 minMaturityTime,uint256 maxMaturityTime,uint256 expiry,bytes32 salt)" + ); + + /// @notice The EIP712 typehash of the Options struct. + bytes32 public constant OPTIONS_TYPEHASH = + keccak256("Options(address destination,bool asBase)"); + + /// @notice The name of this matching engine. + string public name; + + /// @notice The kind of this matching engine. + string public constant kind = HYPERDRIVE_MATCHING_ENGINE_KIND; + + /// @notice The version of this matching engine. + string public constant version = VERSION; + + /// @notice The buffer amount used for cost related calculations. + /// @dev TODO: The buffer amount needs more testing. + uint256 public constant TOKEN_AMOUNT_BUFFER = 10; + + /// @notice Mapping to track cancelled orders. + mapping(bytes32 => bool) public isCancelled; + + /// @notice Mapping to track the bond amount used for each order. + mapping(bytes32 => uint256) public orderBondAmountUsed; + + /// @notice Mapping to track the amount of base used for each order. + mapping(bytes32 => uint256) public orderFundAmountUsed; + + /// @notice Initializes the matching engine. + /// @param _name The name of this matching engine. + constructor(string memory _name) EIP712(_name, VERSION) { + name = _name; + } + + /// @notice Matches two orders. + /// @param _order1 The first order to match. + /// @param _order2 The second order to match. + /// @param _surplusRecipient The address that receives the surplus funds + /// from matching the trades. + function matchOrders( + OrderIntent calldata _order1, + OrderIntent calldata _order2, + address _surplusRecipient + ) external nonReentrant { + // Validate orders. + (bytes32 order1Hash, bytes32 order2Hash) = _validateOrders( + _order1, + _order2 + ); + + IHyperdrive hyperdrive = _order1.hyperdrive; + + // Handle different order type combinations. + // Case 1: Long + Short creation using mint(). + if (_order1.orderType == OrderType.OpenLong && + _order2.orderType == OrderType.OpenShort) { + // Get necessary pool parameters. + IHyperdrive.PoolConfig memory config = hyperdrive.getPoolConfig(); + + uint256 latestCheckpoint = _latestCheckpoint(config.checkpointDuration); + // @dev TODO: there is another way to get the info without calling + // getPoolInfo()? + uint256 vaultSharePrice = hyperdrive.getPoolInfo().vaultSharePrice; + + // Calculate the amount of base tokens to transfer based on the + // bondMatchAmount. + uint256 openVaultSharePrice = hyperdrive.getCheckpoint(latestCheckpoint).vaultSharePrice; + if (openVaultSharePrice == 0) { + openVaultSharePrice = vaultSharePrice; + } + + // Stack cycling to avoid stack-too-deep. + OrderIntent calldata order1 = _order1; + OrderIntent calldata order2 = _order2; + bytes32 order1Hash_ = order1Hash; + bytes32 order2Hash_ = order2Hash; + address surplusRecipient = _surplusRecipient; + + // Calculate matching amount. + // @dev This could have been placed before the control flow for + // shorter code, but it's put here to avoid stack-too-deep. + uint256 bondMatchAmount = _calculateBondMatchAmount( + order1, + order2, + order1Hash_, + order2Hash_ + ); + + + // Get the sufficient funding amount to mint the bonds. + // NOTE: Round the required fund amount up to overestimate the cost. + // Round the flat fee calculation up and the governance fee + // calculation down to match the rounding used in the other flows. + uint256 cost = bondMatchAmount.mulDivUp( + vaultSharePrice.max(openVaultSharePrice), + openVaultSharePrice) + + bondMatchAmount.mulUp(config.fees.flat) + + 2 * bondMatchAmount.mulUp(config.fees.flat).mulDown(config.fees.governanceLP); + + // Calculate the amount of base tokens to transfer based on the + // bondMatchAmount using dynamic pricing. During a series of partial + // matching, the pricing requirements can go easier as needed for each + // new match, hence increasing the match likelihood. + // NOTE: Round the required fund amount down to prevent overspending + // and possible reverting at a later step. + uint256 baseTokenAmountOrder1 = (order1.fundAmount - orderFundAmountUsed[order1Hash_]).mulDivDown(bondMatchAmount, (order1.bondAmount - orderBondAmountUsed[order1Hash_])); + uint256 baseTokenAmountOrder2 = (order2.fundAmount - orderFundAmountUsed[order2Hash_]).mulDivDown(bondMatchAmount, (order2.bondAmount - orderBondAmountUsed[order2Hash_])); + + // Update order fund amount used. + orderFundAmountUsed[order1Hash_] += baseTokenAmountOrder1; + orderFundAmountUsed[order2Hash_] += baseTokenAmountOrder2; + + // Check if the fund amount used is greater than the order amount. + if (orderFundAmountUsed[order1Hash_] > order1.fundAmount || + orderFundAmountUsed[order2Hash_] > order2.fundAmount) { + revert InvalidFundAmount(); + } + + // Calculate the maturity time of newly minted positions. + uint256 maturityTime = latestCheckpoint + config.positionDuration; + + // Check if the maturity time is within the range. + if (maturityTime < order1.minMaturityTime || maturityTime > order1.maxMaturityTime || + maturityTime < order2.minMaturityTime || maturityTime > order2.maxMaturityTime) { + revert InvalidMaturityTime(); + } + + // @dev This could have been placed before the control flow for + // shorter code, but it's put here to avoid stack-too-deep. + IHyperdrive hyperdrive_ = order1.hyperdrive; + ERC20 baseToken = ERC20(hyperdrive_.baseToken()); + + // Mint the bonds. + uint256 bondAmount = _handleMint( + order1, + order2, + baseTokenAmountOrder1, + baseTokenAmountOrder2, + cost, + bondMatchAmount, + baseToken, + hyperdrive_); + + // Update order bond amount used. + orderBondAmountUsed[order1Hash_] += bondAmount; + orderBondAmountUsed[order2Hash_] += bondAmount; + + // Transfer the remaining base tokens back to the surplus recipient. + baseToken.safeTransfer( + surplusRecipient, + baseToken.balanceOf(address(this)) + ); + } + + // Case 2: Long + Short closing using burn(). + else if (_order1.orderType == OrderType.CloseLong && + _order2.orderType == OrderType.CloseShort) { + // Verify both orders have the same maturity time. + if (_order1.maxMaturityTime != _order2.maxMaturityTime) { + revert InvalidMaturityTime(); + } + + // Calculate matching amount. + uint256 bondMatchAmount = _calculateBondMatchAmount( + _order1, + _order2, + order1Hash, + order2Hash + ); + + // Get the min fund output according to the bondMatchAmount. + // NOTE: Round the required fund amount up to respect the order specified + // min fund output. + uint256 minFundAmountOrder1 = (_order1.fundAmount - orderFundAmountUsed[order1Hash]).mulDivUp(bondMatchAmount, (_order1.bondAmount - orderBondAmountUsed[order1Hash])); + uint256 minFundAmountOrder2 = (_order2.fundAmount - orderFundAmountUsed[order2Hash]).mulDivUp(bondMatchAmount, (_order2.bondAmount - orderBondAmountUsed[order2Hash])); + + // Update order bond amount used. + // @dev After the update, there is no need to check if the bond + // amount used is greater than the order amount, as the order + // amount is already used to calculate the bondMatchAmount. + orderBondAmountUsed[order1Hash] += bondMatchAmount; + orderBondAmountUsed[order2Hash] += bondMatchAmount; + + // Get the base token. + ERC20 baseToken = ERC20(hyperdrive.baseToken()); + + // Handle burn operation through helper function. + _handleBurn( + _order1, + _order2, + minFundAmountOrder1, + minFundAmountOrder2, + bondMatchAmount, + baseToken, + hyperdrive + ); + + // Update order fund amount used. + orderFundAmountUsed[order1Hash] += minFundAmountOrder1; + orderFundAmountUsed[order2Hash] += minFundAmountOrder2; + + // Transfer the remaining base tokens back to the surplus recipient. + baseToken.safeTransfer( + _surplusRecipient, + baseToken.balanceOf(address(this)) + ); + } + + // Case 3: Long transfer between traders. + else if (_order1.orderType == OrderType.OpenLong && + _order2.orderType == OrderType.CloseLong) { + _handleLongTransfer(); + } + + // Case 4: Short transfer between traders. + else if (_order1.orderType == OrderType.OpenShort && + _order2.orderType == OrderType.CloseShort) { + _handleShortTransfer(); + } + + // All other cases are invalid. + else { + revert InvalidOrderCombination(); + } + + emit OrdersMatched( + hyperdrive, + order1Hash, + order2Hash, + _order1.trader, + _order2.trader, + orderBondAmountUsed[order1Hash], + orderBondAmountUsed[order2Hash], + orderFundAmountUsed[order1Hash], + orderFundAmountUsed[order2Hash] + ); + } + + /// @notice Allows traders to cancel their orders. + /// @param _orders Array of orders to cancel. + function cancelOrders(OrderIntent[] calldata _orders) external nonReentrant { + bytes32[] memory orderHashes = new bytes32[](_orders.length); + + for (uint256 i = 0; i < _orders.length; i++) { + // Ensure sender is the trader. + if (msg.sender != _orders[i].trader) { + revert InvalidSender(); + } + + // Verify signature. + bytes32 orderHash = hashOrderIntent(_orders[i]); + if (!verifySignature(orderHash, _orders[i].signature, msg.sender)) { + revert InvalidSignature(); + } + + // Cancel the order. + isCancelled[orderHash] = true; + orderHashes[i] = orderHash; + } + + emit OrdersCancelled(msg.sender, orderHashes); + } + + /// @notice Hashes an order intent according to EIP-712. + /// @param _order The order intent to hash. + /// @return The hash of the order intent. + function hashOrderIntent( + OrderIntent calldata _order + ) public view returns (bytes32) { + + return _hashTypedDataV4( + keccak256( + abi.encode( + ORDER_INTENT_TYPEHASH, + _order.trader, + _order.counterparty, + _order.feeRecipient, + address(_order.hyperdrive), + _order.fundAmount, + _order.bondAmount, + _order.minVaultSharePrice, + keccak256( + abi.encode( + OPTIONS_TYPEHASH, + _order.options.destination, + _order.options.asBase + ) + ), + uint8(_order.orderType), + _order.minMaturityTime, + _order.maxMaturityTime, + _order.expiry, + _order.salt + ) + ) + ); + } + + /// @notice Verifies a signature for a given signer. + /// @param _hash The EIP-712 hash of the order. + /// @param _signature The signature bytes. + /// @param _signer The expected signer. + /// @return True if signature is valid, false otherwise. + function verifySignature( + bytes32 _hash, + bytes calldata _signature, + address _signer + ) public view returns (bool) { + // For contracts, use EIP-1271. + if (_signer.code.length > 0) { + try IERC1271(_signer).isValidSignature(_hash, _signature) returns ( + bytes4 magicValue + ) { + return magicValue == IERC1271.isValidSignature.selector; + } catch { + return false; + } + } + + // For EOAs, verify ECDSA signature. + return ECDSA.recover(_hash, _signature) == _signer; + } + + /// @dev Validates orders before matching them. + /// @param _order1 The first order to validate. + /// @param _order2 The second order to validate. + /// @return order1Hash The hash of the first order. + /// @return order2Hash The hash of the second order. + function _validateOrders( + OrderIntent calldata _order1, + OrderIntent calldata _order2 + ) internal view returns (bytes32 order1Hash, bytes32 order2Hash) { + // Verify counterparties. + if ( + (_order1.counterparty != address(0) && + _order1.counterparty != _order2.trader) || + (_order2.counterparty != address(0) && + _order2.counterparty != _order1.trader) + ) { + revert InvalidCounterparty(); + } + + // Check expiry. + if ( + _order1.expiry <= block.timestamp || + _order2.expiry <= block.timestamp + ) { + revert AlreadyExpired(); + } + + // Verify Hyperdrive instance. + if (_order1.hyperdrive != _order2.hyperdrive) { + revert MismatchedHyperdrive(); + } + + // Verify settlement asset. + if ( + !_order1.options.asBase || + !_order2.options.asBase + ) { + revert InvalidSettlementAsset(); + } + + // Verify valid maturity time. + if (_order1.minMaturityTime > _order1.maxMaturityTime || + _order2.minMaturityTime > _order2.maxMaturityTime + ) { + revert InvalidMaturityTime(); + } + + // For close orders, minMaturityTime must equal maxMaturityTime. + if (_order1.orderType == OrderType.CloseLong || + _order1.orderType == OrderType.CloseShort) { + if (_order1.minMaturityTime != _order1.maxMaturityTime) { + revert InvalidMaturityTime(); + } + } + if (_order2.orderType == OrderType.CloseLong || + _order2.orderType == OrderType.CloseShort) { + if (_order2.minMaturityTime != _order2.maxMaturityTime) { + revert InvalidMaturityTime(); + } + } + + // Check that the destination is not the zero address. + if (_order1.options.destination == address(0) || + _order2.options.destination == address(0)) { + revert InvalidDestination(); + } + + // Hash orders. + order1Hash = hashOrderIntent(_order1); + order2Hash = hashOrderIntent(_order2); + + // Check if orders are fully executed. + if (orderBondAmountUsed[order1Hash] >= _order1.bondAmount || + orderFundAmountUsed[order1Hash] >= _order1.fundAmount) { + revert AlreadyFullyExecuted(); + } + if (orderBondAmountUsed[order2Hash] >= _order2.bondAmount || + orderFundAmountUsed[order2Hash] >= _order2.fundAmount) { + revert AlreadyFullyExecuted(); + } + + // Check if orders are cancelled. + if (isCancelled[order1Hash] || isCancelled[order2Hash]) { + revert AlreadyCancelled(); + } + + // Verify signatures. + if ( + !verifySignature( + order1Hash, + _order1.signature, + _order1.trader + ) || + !verifySignature( + order2Hash, + _order2.signature, + _order2.trader + ) + ) { + revert InvalidSignature(); + } + } + + /// @dev Calculates the amount of bonds that can be matched between two orders. + /// @param _order1 The first order to match. + /// @param _order2 The second order to match. + /// @param _order1Hash The hash of the first order. + /// @param _order2Hash The hash of the second order. + /// @return bondMatchAmount The amount of bonds that can be matched. + function _calculateBondMatchAmount( + OrderIntent calldata _order1, + OrderIntent calldata _order2, + bytes32 _order1Hash, + bytes32 _order2Hash + ) internal view returns ( + uint256 bondMatchAmount + ) { + uint256 order1BondAmountUsed = orderBondAmountUsed[_order1Hash]; + uint256 order2BondAmountUsed = orderBondAmountUsed[_order2Hash]; + + if (order1BondAmountUsed >= _order1.bondAmount || order2BondAmountUsed >= _order2.bondAmount) { + revert NoBondMatchAmount(); + } + + uint256 _order1BondAmount = _order1.bondAmount - order1BondAmountUsed; + uint256 _order2BondAmount = _order2.bondAmount - order2BondAmountUsed; + + bondMatchAmount = _order1BondAmount.min(_order2BondAmount); + } + + /// @dev Handles the minting of matching positions. + /// @param _longOrder The order for opening a long position. + /// @param _shortOrder The order for opening a short position. + /// @param _baseTokenAmountLongOrder The amount of base tokens from the long + /// order. + /// @param _baseTokenAmountShortOrder The amount of base tokens from the short + /// order. + /// @param _cost The total cost of the operation. + /// @param _bondMatchAmount The amount of bonds to mint. + /// @param _baseToken The base token being used. + /// @param _hyperdrive The Hyperdrive contract instance. + /// @return The amount of bonds minted. + function _handleMint( + OrderIntent calldata _longOrder, + OrderIntent calldata _shortOrder, + uint256 _baseTokenAmountLongOrder, + uint256 _baseTokenAmountShortOrder, + uint256 _cost, + uint256 _bondMatchAmount, + ERC20 _baseToken, + IHyperdrive _hyperdrive + ) internal returns (uint256) { + // Transfer base tokens from long trader. + _baseToken.safeTransferFrom( + _longOrder.trader, + address(this), + _baseTokenAmountLongOrder + ); + + // Transfer base tokens from short trader. + _baseToken.safeTransferFrom( + _shortOrder.trader, + address(this), + _baseTokenAmountShortOrder + ); + + // Approve Hyperdrive. + // @dev Use balanceOf to get the total amount of base tokens instead of + // summing up the two amounts, in order to open the door for any + // potential donation to help match orders. + uint256 totalBaseTokenAmount = _baseToken.balanceOf(address(this)); + uint256 baseTokenAmountToUse = _cost + TOKEN_AMOUNT_BUFFER; + if (totalBaseTokenAmount < baseTokenAmountToUse) { + revert InsufficientFunding(); + } + + // @dev Add 1 wei of approval so that the storage slot stays hot. + _baseToken.forceApprove(address(_hyperdrive), baseTokenAmountToUse + 1); + + // Create PairOptions. + IHyperdrive.PairOptions memory pairOptions = IHyperdrive.PairOptions({ + longDestination: _longOrder.options.destination, + shortDestination: _shortOrder.options.destination, + asBase: true, + extraData: "" + }); + + // Calculate minVaultSharePrice. + // @dev Take the larger of the two minVaultSharePrice as the min guard + // price to prevent slippage, so that it satisfies both orders. + uint256 minVaultSharePrice = _longOrder.minVaultSharePrice.max(_shortOrder.minVaultSharePrice); + + // Mint matching positions. + ( , uint256 bondAmount) = _hyperdrive.mint( + baseTokenAmountToUse, + _bondMatchAmount, + minVaultSharePrice, + pairOptions + ); + + // Return the bondAmount. + return bondAmount; + } + + /// @dev Handles the burning of matching positions. + /// @param _longOrder The first order (CloseLong). + /// @param _shortOrder The second order (CloseShort). + /// @param _minFundAmountLongOrder The minimum fund amount for the long order. + /// @param _minFundAmountShortOrder The minimum fund amount for the short order. + /// @param _bondMatchAmount The amount of bonds to burn. + /// @param _baseToken The base token being used. + /// @param _hyperdrive The Hyperdrive contract instance. + function _handleBurn( + OrderIntent calldata _longOrder, + OrderIntent calldata _shortOrder, + uint256 _minFundAmountLongOrder, + uint256 _minFundAmountShortOrder, + uint256 _bondMatchAmount, + ERC20 _baseToken, + IHyperdrive _hyperdrive + ) internal { + // Get asset IDs for the long and short positions. + uint256 longAssetId = AssetId.encodeAssetId( + AssetId.AssetIdPrefix.Long, + _longOrder.maxMaturityTime + ); + uint256 shortAssetId = AssetId.encodeAssetId( + AssetId.AssetIdPrefix.Short, + _shortOrder.maxMaturityTime + ); + + // This contract needs to take custody of the bonds before burning. + _hyperdrive.transferFrom( + longAssetId, + _longOrder.trader, + address(this), + _bondMatchAmount + ); + _hyperdrive.transferFrom( + shortAssetId, + _shortOrder.trader, + address(this), + _bondMatchAmount + ); + + // Calculate minOutput and consider the potential donation to help match + // orders. + uint256 minOutput = (_minFundAmountLongOrder + _minFundAmountShortOrder) > _baseToken.balanceOf(address(this)) ? + _minFundAmountLongOrder + _minFundAmountShortOrder - _baseToken.balanceOf(address(this)) : 0; + + // Burn the matching positions. + _hyperdrive.burn( + _longOrder.maxMaturityTime, + _bondMatchAmount, + minOutput, + IHyperdrive.Options({ + destination: address(this), + asBase: true, + extraData: "" + }) + ); + + // Transfer proceeds to traders. + _baseToken.safeTransfer(_longOrder.options.destination, _minFundAmountLongOrder); + _baseToken.safeTransfer(_shortOrder.options.destination, _minFundAmountShortOrder); + } + + // TODO: Implement these functions. + function _handleLongTransfer() internal {} + function _handleShortTransfer() internal {} + + /// @dev Gets the most recent checkpoint time. + /// @param _checkpointDuration The duration of the checkpoint. + /// @return latestCheckpoint The latest checkpoint. + function _latestCheckpoint(uint256 _checkpointDuration) + internal + view + returns (uint256 latestCheckpoint) + { + latestCheckpoint = HyperdriveMath.calculateCheckpointTime( + block.timestamp, + _checkpointDuration + ); + } +} diff --git a/test/units/matching/HyperdriveMatchingEngineV2Test.t.sol b/test/units/matching/HyperdriveMatchingEngineV2Test.t.sol new file mode 100644 index 000000000..1579a0526 --- /dev/null +++ b/test/units/matching/HyperdriveMatchingEngineV2Test.t.sol @@ -0,0 +1,543 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.24; + +import { ERC20 } from "openzeppelin/token/ERC20/ERC20.sol"; +import { SafeERC20 } from "openzeppelin/token/ERC20/utils/SafeERC20.sol"; +import { IHyperdrive } from "../../../contracts/src/interfaces/IHyperdrive.sol"; +import { IHyperdriveMatchingEngineV2 } from "../../../contracts/src/interfaces/IHyperdriveMatchingEngineV2.sol"; +import { AssetId } from "../../../contracts/src/libraries/AssetId.sol"; +import { FixedPointMath } from "../../../contracts/src/libraries/FixedPointMath.sol"; +import { HyperdriveMatchingEngineV2 } from "../../../contracts/src/matching/HyperdriveMatchingEngineV2.sol"; +import { HyperdriveTest } from "../../utils/HyperdriveTest.sol"; +import { HyperdriveUtils } from "../../utils/HyperdriveUtils.sol"; +import { Lib } from "../../utils/Lib.sol"; + +contract HyperdriveMatchingEngineV2Test is HyperdriveTest { + using FixedPointMath for uint256; + using HyperdriveUtils for IHyperdrive; + using SafeERC20 for ERC20; + using Lib for *; + + /// @dev A salt used to help create orders. + bytes32 internal constant salt = bytes32(uint256(0xdeadbeef)); + + /// @dev The deployed Hyperdrive matching engine. + HyperdriveMatchingEngineV2 internal matchingEngine; + + /// @notice Sets up the matching engine test with the following actions: + /// + /// 1. Deploy and initialize Hyperdrive pool with fees. + /// 2. Deploy matching engine. + /// 3. Fund accounts and approve matching engine. + function setUp() public override { + super.setUp(); + + // Deploy and initialize a Hyperdrive pool with fees. + IHyperdrive.PoolConfig memory config = testConfig(0.05e18, POSITION_DURATION); + config.fees.curve = 0.01e18; + config.fees.flat = 0.0005e18; + config.fees.governanceLP = 0.15e18; + deploy(alice, config); + initialize(alice, 0.05e18, 100_000e18); + + // Deploy matching engine. + matchingEngine = new HyperdriveMatchingEngineV2("Hyperdrive Matching Engine V2"); + + // Fund accounts and approve matching engine. + address[3] memory accounts = [alice, bob, celine]; + for (uint256 i = 0; i < accounts.length; i++) { + vm.stopPrank(); + vm.startPrank(accounts[i]); + baseToken.mint(100_000_000e18); + baseToken.approve(address(matchingEngine), type(uint256).max); + baseToken.approve(address(hyperdrive), type(uint256).max); + } + + vm.recordLogs(); + } + + /// @dev Tests matching orders with open long and open short orders. + function test_matchOrders_openLongAndOpenShort() public { + // Create orders. + IHyperdriveMatchingEngineV2.OrderIntent memory longOrder = _createOrderIntent( + alice, + address(0), + address(0), + 100_000e18, // fundAmount. + 95_000e18, // bondAmount. + IHyperdriveMatchingEngineV2.OrderType.OpenLong + ); + + IHyperdriveMatchingEngineV2.OrderIntent memory shortOrder = _createOrderIntent( + bob, + address(0), + address(0), + 101_000e18, // fundAmount. + 95_000e18, // bondAmount. + IHyperdriveMatchingEngineV2.OrderType.OpenShort + ); + + // Sign orders. + longOrder.signature = _signOrderIntent(longOrder, alicePK); + shortOrder.signature = _signOrderIntent(shortOrder, bobPK); + + // Record balances before. + uint256 aliceBaseBalanceBefore = baseToken.balanceOf(alice); + uint256 bobBaseBalanceBefore = baseToken.balanceOf(bob); + uint256 aliceLongBalanceBefore = _getLongBalance(alice); + uint256 bobShortBalanceBefore = _getShortBalance(bob); + + // Match orders. + matchingEngine.matchOrders(longOrder, shortOrder, celine); + + // Verify balances after. + assertLt(baseToken.balanceOf(alice), aliceBaseBalanceBefore); + assertLt(baseToken.balanceOf(bob), bobBaseBalanceBefore); + assertGt(_getLongBalance(alice), aliceLongBalanceBefore); + assertGt(_getShortBalance(bob), bobShortBalanceBefore); + } + + /// @dev Tests matching orders with close long and close short orders. + function test_matchOrders_closeLongAndCloseShort() public { + // First create and match open orders to create positions. + test_matchOrders_openLongAndOpenShort(); + + uint256 maturityTime = hyperdrive.latestCheckpoint() + hyperdrive.getPoolConfig().positionDuration; + + // Approve Hyperdrive bonds positions to the matching engine. + uint256 longAssetId = AssetId.encodeAssetId(AssetId.AssetIdPrefix.Long, maturityTime); + uint256 shortAssetId = AssetId.encodeAssetId(AssetId.AssetIdPrefix.Short, maturityTime); + + vm.startPrank(alice); + hyperdrive.setApproval(longAssetId, address(matchingEngine), type(uint256).max); + vm.stopPrank(); + + vm.startPrank(bob); + hyperdrive.setApproval(shortAssetId, address(matchingEngine), type(uint256).max); + vm.stopPrank(); + + // Create close orders. + IHyperdriveMatchingEngineV2.OrderIntent memory closeLongOrder = _createOrderIntent( + alice, + address(0), + address(0), + 90_000e18, // min fund amount to receive. + 95_000e18, // bond amount to close. + IHyperdriveMatchingEngineV2.OrderType.CloseLong + ); + closeLongOrder.minMaturityTime = maturityTime; + closeLongOrder.maxMaturityTime = maturityTime; + + IHyperdriveMatchingEngineV2.OrderIntent memory closeShortOrder = _createOrderIntent( + bob, + address(0), + address(0), + 5_001e18, // min fund amount to receive. + 95_000e18, // bond amount to close. + IHyperdriveMatchingEngineV2.OrderType.CloseShort + ); + closeShortOrder.minMaturityTime = maturityTime; + closeShortOrder.maxMaturityTime = maturityTime; + + // Sign orders. + closeLongOrder.signature = _signOrderIntent(closeLongOrder, alicePK); + closeShortOrder.signature = _signOrderIntent(closeShortOrder, bobPK); + + // Record balances before. + uint256 aliceBaseBalanceBefore = baseToken.balanceOf(alice); + uint256 bobBaseBalanceBefore = baseToken.balanceOf(bob); + uint256 aliceLongBalanceBefore = _getLongBalance(alice); + uint256 bobShortBalanceBefore = _getShortBalance(bob); + + // Match orders. + matchingEngine.matchOrders(closeLongOrder, closeShortOrder, celine); + + // Verify balances after. + assertGt(baseToken.balanceOf(alice), aliceBaseBalanceBefore); + assertGt(baseToken.balanceOf(bob), bobBaseBalanceBefore); + assertLt(_getLongBalance(alice), aliceLongBalanceBefore); + assertLt(_getShortBalance(bob), bobShortBalanceBefore); + } + + /// @dev Tests matching orders with close long and close short orders with + /// different maturity times. + function test_matchOrders_revertInvalidMaturityTime() public { + // Create close orders with different maturity times. + uint256 maturityTime = hyperdrive.latestCheckpoint() + hyperdrive.getPoolConfig().positionDuration; + + IHyperdriveMatchingEngineV2.OrderIntent memory closeLongOrder = _createOrderIntent( + alice, + address(0), + address(0), + 90_000e18, + 95_000e18, + IHyperdriveMatchingEngineV2.OrderType.CloseLong + ); + closeLongOrder.minMaturityTime = maturityTime; + closeLongOrder.maxMaturityTime = maturityTime; + + IHyperdriveMatchingEngineV2.OrderIntent memory closeShortOrder = _createOrderIntent( + bob, + address(0), + address(0), + 90_000e18, + 95_000e18, + IHyperdriveMatchingEngineV2.OrderType.CloseShort + ); + closeShortOrder.minMaturityTime = maturityTime + 1 days; + closeShortOrder.maxMaturityTime = maturityTime + 1 days; + + closeLongOrder.signature = _signOrderIntent(closeLongOrder, alicePK); + closeShortOrder.signature = _signOrderIntent(closeShortOrder, bobPK); + + vm.expectRevert(IHyperdriveMatchingEngineV2.InvalidMaturityTime.selector); + matchingEngine.matchOrders(closeLongOrder, closeShortOrder, celine); + } + + /// @dev Tests matching orders with insufficient funding. + function test_matchOrders_failure_insufficientFunding() public { + // Create orders with insufficient funding. + IHyperdriveMatchingEngineV2.OrderIntent memory longOrder = _createOrderIntent( + alice, + address(0), + address(0), + 1e18, // Very small fundAmount. + 95_000e18, + IHyperdriveMatchingEngineV2.OrderType.OpenLong + ); + + IHyperdriveMatchingEngineV2.OrderIntent memory shortOrder = _createOrderIntent( + bob, + address(0), + address(0), + 1e18, // Very small fundAmount. + 95_000e18, + IHyperdriveMatchingEngineV2.OrderType.OpenShort + ); + + longOrder.signature = _signOrderIntent(longOrder, alicePK); + shortOrder.signature = _signOrderIntent(shortOrder, bobPK); + + vm.expectRevert(IHyperdriveMatchingEngineV2.InsufficientFunding.selector); + matchingEngine.matchOrders(longOrder, shortOrder, celine); + } + + /// @dev Tests matching orders with valid but different bond amounts + /// (partial match). + function test_matchOrders_differentBondAmounts() public { + // Create orders with different bond amounts - this should succeed with + // partial matching. + IHyperdriveMatchingEngineV2.OrderIntent memory longOrder = _createOrderIntent( + alice, + address(0), + address(0), + 100_000e18, + 95_000e18, + IHyperdriveMatchingEngineV2.OrderType.OpenLong + ); + + IHyperdriveMatchingEngineV2.OrderIntent memory shortOrder = _createOrderIntent( + bob, + address(0), + address(0), + 100_000e18, + 90_000e18, // Different but valid bond amount. + IHyperdriveMatchingEngineV2.OrderType.OpenShort + ); + + longOrder.signature = _signOrderIntent(longOrder, alicePK); + shortOrder.signature = _signOrderIntent(shortOrder, bobPK); + + // Record balances before. + uint256 aliceLongBalanceBefore = _getLongBalance(alice); + uint256 bobShortBalanceBefore = _getShortBalance(bob); + + // Match orders - should succeed with partial match. + matchingEngine.matchOrders(longOrder, shortOrder, celine); + + // Verify partial fill - should match the smaller of the two amounts. + assertGe(_getLongBalance(alice) - aliceLongBalanceBefore, 90_000e18); + assertGe(_getShortBalance(bob) - bobShortBalanceBefore, 90_000e18); + } + + /// @dev Tests matching orders with invalid bond amounts (exceeds available + /// balance). + function test_matchOrders_failure_invalidBondAmount() public { + // First create some positions. + test_matchOrders_openLongAndOpenShort(); + + uint256 maturityTime = hyperdrive.latestCheckpoint() + hyperdrive.getPoolConfig().positionDuration; + + // Approve Hyperdrive bonds positions to the matching engine. + uint256 longAssetId = AssetId.encodeAssetId(AssetId.AssetIdPrefix.Long, maturityTime); + uint256 shortAssetId = AssetId.encodeAssetId(AssetId.AssetIdPrefix.Short, maturityTime); + + vm.startPrank(alice); + hyperdrive.setApproval(longAssetId, address(matchingEngine), type(uint256).max); + vm.stopPrank(); + + vm.startPrank(bob); + hyperdrive.setApproval(shortAssetId, address(matchingEngine), type(uint256).max); + vm.stopPrank(); + + // Try to close more bonds than available. + IHyperdriveMatchingEngineV2.OrderIntent memory closeLongOrder = _createOrderIntent( + alice, + address(0), + address(0), + 100_000e18, + 200_000e18, // More than what alice has. + IHyperdriveMatchingEngineV2.OrderType.CloseLong + ); + closeLongOrder.minMaturityTime = maturityTime; + closeLongOrder.maxMaturityTime = maturityTime; + + IHyperdriveMatchingEngineV2.OrderIntent memory closeShortOrder = _createOrderIntent( + bob, + address(0), + address(0), + 100_000e18, + 200_000e18, + IHyperdriveMatchingEngineV2.OrderType.CloseShort + ); + closeShortOrder.minMaturityTime = maturityTime; + closeShortOrder.maxMaturityTime = maturityTime; + + closeLongOrder.signature = _signOrderIntent(closeLongOrder, alicePK); + closeShortOrder.signature = _signOrderIntent(closeShortOrder, bobPK); + + // Should revert because traders don't have enough bonds. + // @dev TODO: Looks like there is no good error code to use for this + // expected revert, as the error is just an arithmetic underflow? + vm.expectRevert(); + matchingEngine.matchOrders(closeLongOrder, closeShortOrder, celine); + } + + /// @dev Tests matching orders with expired orders. + function test_matchOrders_failure_alreadyExpired() public { + IHyperdriveMatchingEngineV2.OrderIntent memory longOrder = _createOrderIntent( + alice, + address(0), + address(0), + 100_000e18, + 95_000e18, + IHyperdriveMatchingEngineV2.OrderType.OpenLong + ); + longOrder.expiry = block.timestamp - 1; // Already expired. + + IHyperdriveMatchingEngineV2.OrderIntent memory shortOrder = _createOrderIntent( + bob, + address(0), + address(0), + 100_000e18, + 95_000e18, + IHyperdriveMatchingEngineV2.OrderType.OpenShort + ); + + longOrder.signature = _signOrderIntent(longOrder, alicePK); + shortOrder.signature = _signOrderIntent(shortOrder, bobPK); + + vm.expectRevert(IHyperdriveMatchingEngineV2.AlreadyExpired.selector); + matchingEngine.matchOrders(longOrder, shortOrder, celine); + } + + /// @dev Tests matching orders with mismatched Hyperdrive instances. + function test_matchOrders_failure_mismatchedHyperdrive() public { + IHyperdriveMatchingEngineV2.OrderIntent memory longOrder = _createOrderIntent( + alice, + address(0), + address(0), + 100_000e18, + 95_000e18, + IHyperdriveMatchingEngineV2.OrderType.OpenLong + ); + + IHyperdriveMatchingEngineV2.OrderIntent memory shortOrder = _createOrderIntent( + bob, + address(0), + address(0), + 100_000e18, + 95_000e18, + IHyperdriveMatchingEngineV2.OrderType.OpenShort + ); + shortOrder.hyperdrive = IHyperdrive(address(0xdead)); // Different Hyperdrive instance. + + longOrder.signature = _signOrderIntent(longOrder, alicePK); + shortOrder.signature = _signOrderIntent(shortOrder, bobPK); + + vm.expectRevert(IHyperdriveMatchingEngineV2.MismatchedHyperdrive.selector); + matchingEngine.matchOrders(longOrder, shortOrder, celine); + } + + /// @dev Tests successful partial matching of orders. + function test_matchOrders_partialMatch() public { + // Create orders where one has larger amount than the other. + IHyperdriveMatchingEngineV2.OrderIntent memory longOrder = _createOrderIntent( + alice, + address(0), + address(0), + 100_000e18, + 95_000e18, + IHyperdriveMatchingEngineV2.OrderType.OpenLong + ); + + IHyperdriveMatchingEngineV2.OrderIntent memory shortOrder = _createOrderIntent( + bob, + address(0), + address(0), + 50_000e18, // Half the amount. + 47_500e18, // Half the bonds. + IHyperdriveMatchingEngineV2.OrderType.OpenShort + ); + + longOrder.signature = _signOrderIntent(longOrder, alicePK); + shortOrder.signature = _signOrderIntent(shortOrder, bobPK); + + // Record balances before. + uint256 aliceLongBalanceBefore = _getLongBalance(alice); + uint256 bobShortBalanceBefore = _getShortBalance(bob); + + // Match orders. + matchingEngine.matchOrders(longOrder, shortOrder, celine); + + // Verify partial fill. + assertGe(_getLongBalance(alice) - aliceLongBalanceBefore, 47_500e18); + assertGe(_getShortBalance(bob) - bobShortBalanceBefore, 47_500e18); + + // Verify order is not fully cancelled for alice. + bytes32 orderHash = matchingEngine.hashOrderIntent(longOrder); + assertFalse(matchingEngine.isCancelled(orderHash)); + } + + /// @dev Tests matching orders with invalid vault share price. + function test_matchOrders_failure_invalidVaultSharePrice() public { + IHyperdriveMatchingEngineV2.OrderIntent memory longOrder = _createOrderIntent( + alice, + address(0), + address(0), + 100_000e18, + 95_000e18, + IHyperdriveMatchingEngineV2.OrderType.OpenLong + ); + longOrder.minVaultSharePrice = type(uint256).max; // Unreasonably high min vault share price. + + IHyperdriveMatchingEngineV2.OrderIntent memory shortOrder = _createOrderIntent( + bob, + address(0), + address(0), + 100_000e18, + 95_000e18, + IHyperdriveMatchingEngineV2.OrderType.OpenShort + ); + shortOrder.minVaultSharePrice = type(uint256).max; // Unreasonably high min vault share price. + + longOrder.signature = _signOrderIntent(longOrder, alicePK); + shortOrder.signature = _signOrderIntent(shortOrder, bobPK); + + vm.expectRevert(IHyperdrive.MinimumSharePrice.selector); + matchingEngine.matchOrders(longOrder, shortOrder, celine); + } + + /// @dev Tests matching orders with invalid signatures. + function test_matchOrders_failure_invalidSignature() public { + IHyperdriveMatchingEngineV2.OrderIntent memory longOrder = _createOrderIntent( + alice, + address(0), + address(0), + 100_000e18, + 95_000e18, + IHyperdriveMatchingEngineV2.OrderType.OpenLong + ); + + IHyperdriveMatchingEngineV2.OrderIntent memory shortOrder = _createOrderIntent( + bob, + address(0), + address(0), + 100_000e18, + 95_000e18, + IHyperdriveMatchingEngineV2.OrderType.OpenShort + ); + + // Sign with wrong private keys. + longOrder.signature = _signOrderIntent(longOrder, bobPK); // Wrong signer. + shortOrder.signature = _signOrderIntent(shortOrder, bobPK); + + vm.expectRevert(IHyperdriveMatchingEngineV2.InvalidSignature.selector); + matchingEngine.matchOrders(longOrder, shortOrder, celine); + } + + // Helper functions. + + /// @dev Creates an order intent. + /// @param trader The address of the trader. + /// @param counterparty The address of the counterparty. + /// @param feeRecipient The address of the fee recipient. + /// @param fundAmount The amount of base tokens to fund the order. + /// @param bondAmount The amount of bonds to fund the order. + /// @param orderType The type of the order. + /// @return The order intent. + function _createOrderIntent( + address trader, + address counterparty, + address feeRecipient, + uint256 fundAmount, + uint256 bondAmount, + IHyperdriveMatchingEngineV2.OrderType orderType + ) internal view returns (IHyperdriveMatchingEngineV2.OrderIntent memory) { + return IHyperdriveMatchingEngineV2.OrderIntent({ + trader: trader, + counterparty: counterparty, + feeRecipient: feeRecipient, + hyperdrive: hyperdrive, + fundAmount: fundAmount, + bondAmount: bondAmount, + minVaultSharePrice: 0, + options: IHyperdrive.Options({ + destination: trader, + asBase: true, + extraData: "" + }), + orderType: orderType, + minMaturityTime: 0, + maxMaturityTime: type(uint256).max, + signature: "", + expiry: block.timestamp + 1 days, + salt: salt + }); + } + + /// @dev Gets the balance of long bonds for an account. + /// @param account The address of the account. + /// @return The balance of long bonds for the account. + function _getLongBalance(address account) internal view returns (uint256) { + uint256 maturityTime = hyperdrive.latestCheckpoint() + hyperdrive.getPoolConfig().positionDuration; + return hyperdrive.balanceOf( + AssetId.encodeAssetId(AssetId.AssetIdPrefix.Long, maturityTime), + account + ); + } + + /// @dev Gets the balance of short bonds for an account. + /// @param account The address of the account. + /// @return The balance of short bonds for the account. + function _getShortBalance(address account) internal view returns (uint256) { + uint256 maturityTime = hyperdrive.latestCheckpoint() + hyperdrive.getPoolConfig().positionDuration; + return hyperdrive.balanceOf( + AssetId.encodeAssetId(AssetId.AssetIdPrefix.Short, maturityTime), + account + ); + } + + /// @dev Signs an order intent. + /// @param order The order intent to sign. + /// @param privateKey The private key of the signer. + /// @return The signature of the order intent. + function _signOrderIntent( + IHyperdriveMatchingEngineV2.OrderIntent memory order, + uint256 privateKey + ) internal view returns (bytes memory) { + bytes32 digest = matchingEngine.hashOrderIntent(order); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, digest); + return abi.encodePacked(r, s, v); + } +} \ No newline at end of file