Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion src/adapters/MidnightAdapter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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);
Expand Down
7 changes: 5 additions & 2 deletions src/adapters/MidnightAdapterFactory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand All @@ -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);
Expand Down
1 change: 1 addition & 0 deletions src/adapters/interfaces/IMidnightAdapter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
1 change: 1 addition & 0 deletions src/adapters/interfaces/IMidnightAdapterFactory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
59 changes: 59 additions & 0 deletions src/adapters/ratifiers/MidnightAuctionRatifier.sol
Original file line number Diff line number Diff line change
@@ -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;
}
}
36 changes: 36 additions & 0 deletions src/adapters/ratifiers/interfaces/IMidnightAuctionRatifier.sol
Original file line number Diff line number Diff line change
@@ -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();
}
39 changes: 39 additions & 0 deletions src/adapters/ratifiers/libraries/AuctionHashLib.sol
Original file line number Diff line number Diff line change
@@ -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
)
);
}
}
9 changes: 7 additions & 2 deletions test/MidnightAdapterTest.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand All @@ -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
Expand Down Expand Up @@ -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)));
Expand Down
Loading