diff --git a/src/adapters/MidnightAdapter.sol b/src/adapters/MidnightAdapter.sol index 05c1ec5be..a9e10104c 100644 --- a/src/adapters/MidnightAdapter.sol +++ b/src/adapters/MidnightAdapter.sol @@ -33,6 +33,7 @@ contract MidnightAdapter is IMidnightAdapter { address public immutable asset; address public immutable parentVault; address public immutable midnight; + address public immutable auctionRatifier; bytes32 public immutable adapterId; /// @dev Durations that can be used to cap the time to maturity. /// @dev Sorted in ascending order. @@ -59,11 +60,15 @@ contract MidnightAdapter is IMidnightAdapter { /* CONSTRUCTOR */ - constructor(address _parentVault, address _midnight, uint256[] memory _durations) { + constructor(address _parentVault, address _midnight, uint256[] memory _durations, address _auctionRatifier) { asset = IVaultV2(_parentVault).asset(); parentVault = _parentVault; midnight = _midnight; + auctionRatifier = _auctionRatifier; IMidnight(_midnight).setIsAuthorized(address(this), true, address(this)); + if (_auctionRatifier != address(0)) { + IMidnight(_midnight).setIsAuthorized(_auctionRatifier, true, address(this)); + } lastUpdate = block.timestamp.toUint48(); SafeERC20Lib.safeApprove(asset, _midnight, type(uint256).max); SafeERC20Lib.safeApprove(asset, _parentVault, type(uint256).max); diff --git a/src/adapters/MidnightAdapterFactory.sol b/src/adapters/MidnightAdapterFactory.sol index 370cba03c..51762e377 100644 --- a/src/adapters/MidnightAdapterFactory.sol +++ b/src/adapters/MidnightAdapterFactory.sol @@ -11,11 +11,13 @@ contract MidnightAdapterFactory is IMidnightAdapterFactory { mapping(address parentVault => mapping(address midnight => address)) public midnightAdapter; mapping(address account => bool) public isMidnightAdapter; uint256[] public durations; + address public immutable auctionRatifier; /* CONSTRUCTOR */ - constructor(uint256[] memory _durations) { + constructor(uint256[] memory _durations, address _auctionRatifier) { durations = _durations; + auctionRatifier = _auctionRatifier; } /* GETTERS */ @@ -27,7 +29,8 @@ contract MidnightAdapterFactory is IMidnightAdapterFactory { /* FUNCTIONS */ function createMidnightAdapter(address parentVault, address midnight) external returns (address) { - address _midnightAdapter = address(new MidnightAdapter{salt: bytes32(0)}(parentVault, midnight, durations)); + address _midnightAdapter = + address(new MidnightAdapter{salt: bytes32(0)}(parentVault, midnight, durations, auctionRatifier)); midnightAdapter[parentVault][midnight] = _midnightAdapter; isMidnightAdapter[_midnightAdapter] = true; emit CreateMidnightAdapter(parentVault, midnight, _midnightAdapter); diff --git a/src/adapters/interfaces/IMidnightAdapter.sol b/src/adapters/interfaces/IMidnightAdapter.sol index 49f5a7ac4..90a7c4309 100644 --- a/src/adapters/interfaces/IMidnightAdapter.sol +++ b/src/adapters/interfaces/IMidnightAdapter.sol @@ -65,6 +65,7 @@ interface IMidnightAdapter is IAdapter, IBuyCallback, ISellCallback, IRatifier { function availableMaturities() external view returns (uint8); function MAX_PENDING_MATURITIES() external view returns (uint8); function midnight() external view returns (address); + function auctionRatifier() external view returns (address); function adapterId() external view returns (bytes32); function packedDurations() external view returns (bytes32); function _markets(bytes32 marketId) external view returns (uint128 netCredit, uint128 growth); diff --git a/src/adapters/interfaces/IMidnightAdapterFactory.sol b/src/adapters/interfaces/IMidnightAdapterFactory.sol index b890c0746..338b9bc54 100644 --- a/src/adapters/interfaces/IMidnightAdapterFactory.sol +++ b/src/adapters/interfaces/IMidnightAdapterFactory.sol @@ -11,6 +11,7 @@ interface IMidnightAdapterFactory { function durations(uint256 index) external view returns (uint256); function durationsLength() external view returns (uint256); + function auctionRatifier() external view returns (address); function midnightAdapter(address parentVault, address midnight) external view returns (address); function isMidnightAdapter(address account) external view returns (bool); function createMidnightAdapter(address parentVault, address midnight) external returns (address); diff --git a/src/adapters/ratifiers/MidnightAuctionRatifier.sol b/src/adapters/ratifiers/MidnightAuctionRatifier.sol new file mode 100644 index 000000000..1f9a034cb --- /dev/null +++ b/src/adapters/ratifiers/MidnightAuctionRatifier.sol @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// Copyright (c) 2026 Morpho Association +pragma solidity 0.8.34; + +import {Offer} from "lib/midnight/src/interfaces/IMidnight.sol"; +import {MAX_TICK} from "lib/midnight/src/libraries/TickLib.sol"; +import {CALLBACK_SUCCESS} from "lib/midnight/src/libraries/ConstantsLib.sol"; +import {HashLib} from "lib/midnight/src/ratifiers/libraries/HashLib.sol"; +import {Signature, EIP712_DOMAIN_TYPEHASH} from "lib/midnight/src/ratifiers/interfaces/IEcrecoverRatifier.sol"; +import {IVaultV2} from "../../interfaces/IVaultV2.sol"; +import {IMidnightAdapter} from "../interfaces/IMidnightAdapter.sol"; +import {Auction, IMidnightAuctionRatifier} from "./interfaces/IMidnightAuctionRatifier.sol"; +import {AuctionHashLib} from "./libraries/AuctionHashLib.sol"; + +/// @dev Ratifies reduce-only sell offers of a MidnightAdapter that decay linearly, in tick space, from +/// (startTick, startTime) down to (endTick, endTime). +/// @dev Stateless and generic: works for any maker that implements IMidnightAdapter's `parentVault`, so a single +/// deployment can be authorized as a ratifier by any number of MidnightAdapter instances. +contract MidnightAuctionRatifier is IMidnightAuctionRatifier { + function isRatified(Offer memory offer, bytes memory data, address) external view returns (bytes32) { + (Signature memory sig, Auction memory auction) = abi.decode(data, (Signature, Auction)); + + require(auction.startTime < auction.endTime, InvalidAuctionWindow()); + require(auction.startTick <= MAX_TICK && auction.startTick >= auction.endTick, InvalidAuctionTicks()); + + Offer memory expected = Offer({ + market: auction.market, + buy: false, + maker: auction.maker, + start: auction.startTime, + expiry: auction.endTime, + tick: offer.tick, + group: auction.group, + callback: auction.callback, + callbackData: auction.callbackData, + receiverIfMakerIsSeller: auction.receiverIfMakerIsSeller, + ratifier: address(this), + reduceOnly: true, + maxUnits: auction.maxUnits, + maxAssets: auction.maxAssets, + continuousFeeCap: auction.continuousFeeCap + }); + require(HashLib.hashOffer(offer) == HashLib.hashOffer(expected), OfferMismatch()); + + // Midnight's `take` already enforces startTime <= block.timestamp <= endTime before calling the ratifier. + uint256 floorTick = auction.startTick - (auction.startTick - auction.endTick) + * (block.timestamp - auction.startTime) / (auction.endTime - auction.startTime); + require(offer.tick >= floorTick, BelowFloorTick()); + + bytes32 structHash = AuctionHashLib.hashAuction(auction); + bytes32 domainSeparator = keccak256(abi.encode(EIP712_DOMAIN_TYPEHASH, block.chainid, address(this))); + bytes32 digest = keccak256(bytes.concat("\x19\x01", domainSeparator, structHash)); + address signer = ecrecover(digest, sig.v, sig.r, sig.s); + require(signer != address(0), IncorrectSigner()); + require(IVaultV2(IMidnightAdapter(auction.maker).parentVault()).isAllocator(signer), IncorrectSigner()); + + return CALLBACK_SUCCESS; + } +} diff --git a/src/adapters/ratifiers/interfaces/IMidnightAuctionRatifier.sol b/src/adapters/ratifiers/interfaces/IMidnightAuctionRatifier.sol new file mode 100644 index 000000000..6b1421e8e --- /dev/null +++ b/src/adapters/ratifiers/interfaces/IMidnightAuctionRatifier.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// Copyright (c) 2026 Morpho Association +pragma solidity >=0.5.0; + +import {Market} from "lib/midnight/src/interfaces/IMidnight.sol"; +import {IRatifier} from "lib/midnight/src/interfaces/IRatifier.sol"; +import {Signature} from "lib/midnight/src/ratifiers/interfaces/IEcrecoverRatifier.sol"; + +/// @dev Describes a Dutch auction for a reduce-only sell offer of a MidnightAdapter. +/// @dev Every Offer field is fixed by the auction except `tick`, which is free to be anything at least as good for +/// the seller as the schedule's current floor tick, linearly interpolated between (startTick, startTime) and +/// (endTick, endTime). +struct Auction { + Market market; + address maker; + uint256 startTime; + uint256 endTime; + uint256 startTick; + uint256 endTick; + bytes32 group; + address callback; + bytes callbackData; + address receiverIfMakerIsSeller; + uint128 maxUnits; + uint128 maxAssets; + uint256 continuousFeeCap; +} + +interface IMidnightAuctionRatifier is IRatifier { + /// ERRORS /// + error OfferMismatch(); + error BelowFloorTick(); + error InvalidAuctionWindow(); + error InvalidAuctionTicks(); + error IncorrectSigner(); +} diff --git a/src/adapters/ratifiers/libraries/AuctionHashLib.sol b/src/adapters/ratifiers/libraries/AuctionHashLib.sol new file mode 100644 index 000000000..adf52d67f --- /dev/null +++ b/src/adapters/ratifiers/libraries/AuctionHashLib.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// Copyright (c) 2026 Morpho Association +pragma solidity ^0.8.0; + +import {HashLib} from "lib/midnight/src/ratifiers/libraries/HashLib.sol"; +import {Auction} from "../interfaces/IMidnightAuctionRatifier.sol"; + +/// @dev keccak256(bytes.concat(AUCTION_TYPE, COLLATERAL_PARAMS_TYPE, MARKET_TYPE)). +bytes32 constant AUCTION_TYPEHASH = keccak256( + bytes.concat( + "Auction(Market market,address maker,uint256 startTime,uint256 endTime,uint256 startTick,uint256 endTick,bytes32 group,address callback,bytes callbackData,address receiverIfMakerIsSeller,uint128 maxUnits,uint128 maxAssets,uint256 continuousFeeCap)", + "CollateralParams(address token,uint256 lltv,uint256 liquidationCursor,address oracle)", + "Market(uint256 chainId,address midnight,address loanToken,CollateralParams[] collateralParams,uint256 maturity,uint256 rcfThreshold,address enterGate,address liquidatorGate)" + ) +); + +library AuctionHashLib { + /// @dev Computes the EIP-712 hash struct of an Auction. + function hashAuction(Auction memory auction) internal pure returns (bytes32) { + return keccak256( + abi.encode( + AUCTION_TYPEHASH, + HashLib.hashMarket(auction.market), + auction.maker, + auction.startTime, + auction.endTime, + auction.startTick, + auction.endTick, + auction.group, + auction.callback, + keccak256(auction.callbackData), + auction.receiverIfMakerIsSeller, + auction.maxUnits, + auction.maxAssets, + auction.continuousFeeCap + ) + ); + } +} diff --git a/test/MidnightAdapterTest.sol b/test/MidnightAdapterTest.sol index 309843036..ca12d123f 100644 --- a/test/MidnightAdapterTest.sol +++ b/test/MidnightAdapterTest.sol @@ -104,6 +104,11 @@ contract MidnightAdapterTest is Test { uint256[] internal allDurations = [1 days, 7 days, 30 days, 90 days, 180 days]; + /// @dev Overridden by subclasses that need the adapter to authorize a specific ratifier at construction time. + function auctionRatifierAddress() internal virtual returns (address) { + return address(0); + } + function setUp() public virtual { owner = makeAddr("owner"); curator = makeAddr("curator"); @@ -122,7 +127,7 @@ contract MidnightAdapterTest is Test { parentVault = new VaultV2Mock(address(loanToken), owner, curator, signerAllocator, address(0)); - factory = new MidnightAdapterFactory(allDurations); + factory = new MidnightAdapterFactory(allDurations, auctionRatifierAddress()); adapter = MidnightAdapter(factory.createMidnightAdapter(address(parentVault), address(midnight))); // Adapter authorizes itself as ratifier @@ -829,7 +834,7 @@ contract MidnightAdapterTest is Test { /// forge-config: default.isolate = true /// @dev Runs on a real VaultV2, with a non-zero penalty, fees and maxRate, and with the adapter's allocator role /// revoked before the exit. - function testForceDeallocateRealVaultWithPenalty() public { + function testForceDeallocateRealVaultWithPenalty() public virtual { setUpRealVault(); Offer memory offer = buyOnRealVault(7 days, 1e18); submitAndCall(realVault, abi.encodeCall(IVaultV2.setIsAllocator, (address(adapter), false))); diff --git a/test/MidnightAuctionRatifierTest.sol b/test/MidnightAuctionRatifierTest.sol new file mode 100644 index 000000000..06ad6ed68 --- /dev/null +++ b/test/MidnightAuctionRatifierTest.sol @@ -0,0 +1,316 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// Copyright (c) 2026 Morpho Association +pragma solidity ^0.8.0; + +import {MidnightAdapterTest} from "./MidnightAdapterTest.sol"; +import {MidnightAuctionRatifier} from "../src/adapters/ratifiers/MidnightAuctionRatifier.sol"; +import {IMidnightAuctionRatifier, Auction} from "../src/adapters/ratifiers/interfaces/IMidnightAuctionRatifier.sol"; +import {AuctionHashLib} from "../src/adapters/ratifiers/libraries/AuctionHashLib.sol"; +import {Offer, Market} from "../lib/midnight/src/interfaces/IMidnight.sol"; +import {Signature, EIP712_DOMAIN_TYPEHASH} from "../lib/midnight/src/ratifiers/interfaces/IEcrecoverRatifier.sol"; +import {TickLib, MAX_TICK} from "../lib/midnight/src/libraries/TickLib.sol"; +import {CALLBACK_SUCCESS} from "../lib/midnight/src/libraries/ConstantsLib.sol"; +import {TakeAmountsLib} from "../lib/midnight/src/periphery/libraries/TakeAmountsLib.sol"; +import {IdLib} from "../lib/midnight/src/libraries/IdLib.sol"; + +contract MidnightAuctionRatifierTest is MidnightAdapterTest { + MidnightAuctionRatifier internal ratifier; + + function auctionRatifierAddress() internal override returns (address) { + ratifier = new MidnightAuctionRatifier(); + return address(ratifier); + } + + /// forge-config: default.isolate = true + function testForceDeallocateRealVaultWithPenalty() public override { + super.testForceDeallocateRealVaultWithPenalty(); + } + + /* HELPERS */ + + function makeAuction(Market memory market, uint256 startTick, uint256 endTick, uint256 startTime, uint256 endTime) + internal + view + returns (Auction memory auction) + { + auction.market = market; + auction.maker = address(adapter); + auction.startTime = startTime; + auction.endTime = endTime; + auction.startTick = startTick; + auction.endTick = endTick; + auction.group = bytes32(vm.randomUint()); + auction.callback = address(adapter); + auction.callbackData = hex""; + auction.receiverIfMakerIsSeller = address(adapter); + auction.maxUnits = type(uint128).max; + auction.maxAssets = 0; + auction.continuousFeeCap = type(uint256).max; + } + + function offerFromAuction(Auction memory auction, uint256 tick) internal view returns (Offer memory offer) { + offer.market = auction.market; + offer.buy = false; + offer.maker = auction.maker; + offer.start = auction.startTime; + offer.expiry = auction.endTime; + offer.tick = tick; + offer.group = auction.group; + offer.callback = auction.callback; + offer.callbackData = auction.callbackData; + offer.receiverIfMakerIsSeller = auction.receiverIfMakerIsSeller; + offer.ratifier = address(ratifier); + offer.reduceOnly = true; + offer.maxUnits = auction.maxUnits; + offer.maxAssets = auction.maxAssets; + offer.continuousFeeCap = auction.continuousFeeCap; + } + + function floorTick(Auction memory auction, uint256 timestamp) internal pure returns (uint256) { + return auction.startTick - (auction.startTick - auction.endTick) * (timestamp - auction.startTime) + / (auction.endTime - auction.startTime); + } + + function signAuctionData(Auction memory auction, address signer) internal view returns (bytes memory) { + bytes32 structHash = AuctionHashLib.hashAuction(auction); + bytes32 domainSeparator = keccak256(abi.encode(EIP712_DOMAIN_TYPEHASH, block.chainid, address(ratifier))); + bytes32 digest = keccak256(bytes.concat("\x19\x01", domainSeparator, structHash)); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey[signer], digest); + return abi.encode(Signature({v: v, r: r, s: s}), auction); + } + + /* SCHEDULE VALIDATION */ + + function testInvalidAuctionWindow(uint256 startTime, uint256 endTime) public { + endTime = bound(endTime, 0, type(uint48).max - 1); + startTime = bound(startTime, endTime, type(uint48).max); + Auction memory auction = makeAuction(storedOffer.market, MAX_TICK, 0, startTime, endTime); + Offer memory offer = offerFromAuction(auction, 0); + bytes memory data = signAuctionData(auction, signerAllocator); + vm.expectRevert(IMidnightAuctionRatifier.InvalidAuctionWindow.selector); + ratifier.isRatified(offer, data, taker); + } + + function testInvalidAuctionTicksEndAboveStart(uint256 startTick, uint256 endTick) public { + startTick = bound(startTick, 0, MAX_TICK - 1); + endTick = bound(endTick, startTick + 1, MAX_TICK); + Auction memory auction = + makeAuction(storedOffer.market, startTick, endTick, block.timestamp, block.timestamp + 1 days); + Offer memory offer = offerFromAuction(auction, endTick); + bytes memory data = signAuctionData(auction, signerAllocator); + vm.expectRevert(IMidnightAuctionRatifier.InvalidAuctionTicks.selector); + ratifier.isRatified(offer, data, taker); + } + + function testInvalidAuctionTicksAboveMax(uint256 startTick) public { + startTick = bound(startTick, MAX_TICK + 1, type(uint256).max); + Auction memory auction = + makeAuction(storedOffer.market, startTick, 0, block.timestamp, block.timestamp + 1 days); + Offer memory offer = offerFromAuction(auction, 0); + bytes memory data = signAuctionData(auction, signerAllocator); + vm.expectRevert(IMidnightAuctionRatifier.InvalidAuctionTicks.selector); + ratifier.isRatified(offer, data, taker); + } + + /* TICK SCHEDULE */ + + function testFloorTickAtStartTime(uint256 startTick, uint256 endTick, uint256 duration) public { + startTick = bound(startTick, 1, MAX_TICK); + endTick = bound(endTick, 0, startTick - 1); + duration = bound(duration, 1, 365 days); + Auction memory auction = + makeAuction(storedOffer.market, startTick, endTick, block.timestamp, block.timestamp + duration); + Offer memory offer = offerFromAuction(auction, startTick); + bytes memory data = signAuctionData(auction, signerAllocator); + assertEq(ratifier.isRatified(offer, data, taker), CALLBACK_SUCCESS, "at start, exactly startTick succeeds"); + + offer.tick = startTick - 1; + vm.expectRevert(IMidnightAuctionRatifier.BelowFloorTick.selector); + ratifier.isRatified(offer, data, taker); + } + + function testFloorTickAtEndTime(uint256 startTick, uint256 endTick, uint256 duration) public { + startTick = bound(startTick, 1, MAX_TICK); + endTick = bound(endTick, 0, startTick - 1); + duration = bound(duration, 1, 365 days); + Auction memory auction = + makeAuction(storedOffer.market, startTick, endTick, block.timestamp, block.timestamp + duration); + bytes memory data = signAuctionData(auction, signerAllocator); + + skip(duration); + Offer memory offer = offerFromAuction(auction, endTick); + assertEq(ratifier.isRatified(offer, data, taker), CALLBACK_SUCCESS, "at end, exactly endTick succeeds"); + + if (endTick > 0) { + offer.tick = endTick - 1; + vm.expectRevert(IMidnightAuctionRatifier.BelowFloorTick.selector); + ratifier.isRatified(offer, data, taker); + } + } + + function testFloorTickAtMidpoint() public { + uint256 startTick = 6000; + uint256 endTick = 0; + uint256 duration = 1000; + Auction memory auction = + makeAuction(storedOffer.market, startTick, endTick, block.timestamp, block.timestamp + duration); + bytes memory data = signAuctionData(auction, signerAllocator); + + skip(duration / 2); + uint256 expectedFloor = floorTick(auction, block.timestamp); + assertEq(expectedFloor, startTick / 2, "linear midpoint"); + + Offer memory offer = offerFromAuction(auction, expectedFloor); + assertEq(ratifier.isRatified(offer, data, taker), CALLBACK_SUCCESS, "at floor succeeds"); + + offer.tick = expectedFloor - 1; + vm.expectRevert(IMidnightAuctionRatifier.BelowFloorTick.selector); + ratifier.isRatified(offer, data, taker); + + offer.tick = expectedFloor + 1; + assertEq(ratifier.isRatified(offer, data, taker), CALLBACK_SUCCESS, "above floor succeeds"); + } + + /* OFFER MATCHING */ + + function testOfferMismatchOnMutatedField() public { + Auction memory auction = makeAuction(storedOffer.market, MAX_TICK, 0, block.timestamp, block.timestamp + 1 days); + bytes memory data = signAuctionData(auction, signerAllocator); + Offer memory offer = offerFromAuction(auction, MAX_TICK); + + // Sanity check: the untouched offer ratifies fine. + assertEq(ratifier.isRatified(offer, data, taker), CALLBACK_SUCCESS, "untouched offer succeeds"); + + Offer memory mutatedMaker = offer; + mutatedMaker.maker = address(this); + vm.expectRevert(IMidnightAuctionRatifier.OfferMismatch.selector); + ratifier.isRatified(mutatedMaker, data, taker); + + Offer memory mutatedGroup = offer; + mutatedGroup.group = bytes32(uint256(auction.group) + 1); + vm.expectRevert(IMidnightAuctionRatifier.OfferMismatch.selector); + ratifier.isRatified(mutatedGroup, data, taker); + + Offer memory mutatedCallback = offer; + mutatedCallback.callback = address(this); + vm.expectRevert(IMidnightAuctionRatifier.OfferMismatch.selector); + ratifier.isRatified(mutatedCallback, data, taker); + + Offer memory mutatedReceiver = offer; + mutatedReceiver.receiverIfMakerIsSeller = address(this); + vm.expectRevert(IMidnightAuctionRatifier.OfferMismatch.selector); + ratifier.isRatified(mutatedReceiver, data, taker); + + Offer memory mutatedBuy = offer; + mutatedBuy.buy = true; + vm.expectRevert(IMidnightAuctionRatifier.OfferMismatch.selector); + ratifier.isRatified(mutatedBuy, data, taker); + + Offer memory mutatedReduceOnly = offer; + mutatedReduceOnly.reduceOnly = false; + vm.expectRevert(IMidnightAuctionRatifier.OfferMismatch.selector); + ratifier.isRatified(mutatedReduceOnly, data, taker); + + Offer memory mutatedStart = offer; + mutatedStart.start = auction.startTime + 1; + vm.expectRevert(IMidnightAuctionRatifier.OfferMismatch.selector); + ratifier.isRatified(mutatedStart, data, taker); + + Offer memory mutatedExpiry = offer; + mutatedExpiry.expiry = auction.endTime - 1; + vm.expectRevert(IMidnightAuctionRatifier.OfferMismatch.selector); + ratifier.isRatified(mutatedExpiry, data, taker); + + Offer memory mutatedLoanToken = offer; + mutatedLoanToken.market.loanToken = address(this); + vm.expectRevert(IMidnightAuctionRatifier.OfferMismatch.selector); + ratifier.isRatified(mutatedLoanToken, data, taker); + } + + /* SIGNER VALIDATION */ + + function testIncorrectSigner(uint256 seed) public { + vm.setSeed(seed); + (address otherSigner, uint256 otherSignerKey) = makeAddrAndKey("otherAuctionSigner"); + privateKey[otherSigner] = otherSignerKey; + vm.assume(otherSigner != signerAllocator); + + Auction memory auction = makeAuction(storedOffer.market, MAX_TICK, 0, block.timestamp, block.timestamp + 1 days); + Offer memory offer = offerFromAuction(auction, MAX_TICK); + bytes memory data = signAuctionData(auction, otherSigner); + vm.expectRevert(IMidnightAuctionRatifier.IncorrectSigner.selector); + ratifier.isRatified(offer, data, taker); + } + + function testSignerNotAllocator(uint256 seed) public { + vm.setSeed(seed); + (address nonAllocatorSigner, uint256 nonAllocatorSignerKey) = makeAddrAndKey("nonAllocatorAuctionSigner"); + privateKey[nonAllocatorSigner] = nonAllocatorSignerKey; + assertFalse(parentVault.isAllocator(nonAllocatorSigner), "must not be allocator"); + + Auction memory auction = makeAuction(storedOffer.market, MAX_TICK, 0, block.timestamp, block.timestamp + 1 days); + Offer memory offer = offerFromAuction(auction, MAX_TICK); + bytes memory data = signAuctionData(auction, nonAllocatorSigner); + vm.expectRevert(IMidnightAuctionRatifier.IncorrectSigner.selector); + ratifier.isRatified(offer, data, taker); + } + + /* CONSTRUCTOR AUTHORIZATION */ + + function testConstructorAuthorizesAuctionRatifier() public view { + assertEq(adapter.auctionRatifier(), address(ratifier), "auctionRatifier"); + assertTrue(midnight.isAuthorized(address(adapter), address(ratifier)), "authorized at construction"); + } + + /* FULL INTEGRATION VIA MIDNIGHT.TAKE */ + + function testAuctionUnwindsExistingPositionOverTime() public { + Offer memory boughtOffer = buy(7 days, 1e18); + bytes32 marketId = IdLib.toId(boughtOffer.market); + + uint256 startTick = TickLib.priceToTick(0.99e18, 4); + uint256 endTick = TickLib.priceToTick(0.9e18, 4); + uint256 duration = 1 days; + Auction memory auction = + makeAuction(boughtOffer.market, startTick, endTick, block.timestamp, block.timestamp + duration); + + skip(duration / 2); + uint256 tick = floorTick(auction, block.timestamp); + Offer memory previewOffer = offerFromAuction(auction, tick); + auction.maxUnits = + uint128(TakeAmountsLib.sellerAssetsToUnits(address(midnight), marketId, previewOffer, 0.1e18)); + bytes memory data = signAuctionData(auction, signerAllocator); + Offer memory offer = offerFromAuction(auction, tick); + + parentVault.setTotalAssets(1e18); + vm.prank(taker); + midnight.take(offer, data, offer.maxUnits, taker, address(0), address(0), ""); + + (uint128 marketNetCredit,) = adapter._markets(marketId); + assertLt(marketNetCredit, 1e18, "position was partially unwound"); + } + + function testAuctionRevertsBelowFloorViaMidnightTake() public { + Offer memory boughtOffer = buy(7 days, 1e18); + bytes32 marketId = IdLib.toId(boughtOffer.market); + + uint256 startTick = TickLib.priceToTick(0.99e18, 4); + uint256 endTick = TickLib.priceToTick(0.9e18, 4); + uint256 duration = 1 days; + Auction memory auction = + makeAuction(boughtOffer.market, startTick, endTick, block.timestamp, block.timestamp + duration); + + skip(duration / 2); + uint256 belowFloorTick = floorTick(auction, block.timestamp) - 4; + Offer memory previewOffer = offerFromAuction(auction, belowFloorTick); + auction.maxUnits = + uint128(TakeAmountsLib.sellerAssetsToUnits(address(midnight), marketId, previewOffer, 0.1e18)); + bytes memory data = signAuctionData(auction, signerAllocator); + Offer memory offer = offerFromAuction(auction, belowFloorTick); + + parentVault.setTotalAssets(1e18); + vm.prank(taker); + vm.expectRevert(IMidnightAuctionRatifier.BelowFloorTick.selector); + midnight.take(offer, data, offer.maxUnits, taker, address(0), address(0), ""); + } +}