Skip to content
Closed
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
41 changes: 16 additions & 25 deletions contracts/AnchoredDutchAuction.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,11 @@ import { IOrderRegistrator } from "./interfaces/IOrderRegistrator.sol";

/**
* @title Announcement-anchored Dutch auction
* @notice Dutch auction whose curve and resolver exclusivity may be measured from an order's
* on-chain announcement, for orders whose submission time is unknown at build time.
* @notice Dutch auction whose curve and resolver exclusivity may run from the order's on-chain
* announcement instead of build-time timestamps.
* @dev Opt-in per order via the top bit of the uint32 timestamps the legacy encodings already
* carry, so a legacy blob parses unchanged: the top bit of `auctionStartTime` and of the whitelist
* `allowedTime` anchor the start and the exclusivity to the announcement (each taking the later of
* announcement and built time), and the top bit of the anchored delay adds a fill deadline. Free
* until 19 Jan 2038 (2^31 s) — replace before then, when real timestamps set it and anchored reads
* revert.
* carry (`auctionStartTime`, whitelist `allowedTime`); anchoring takes the later of announcement
* and built time. The bit is free until 19 Jan 2038.
*/
abstract contract AnchoredDutchAuction is DutchAuctionBase {
uint256 private constant _ANCHORED_FLAG = 1 << 31; // top bit of a uint32 timestamp
Expand All @@ -33,37 +30,33 @@ abstract contract AnchoredDutchAuction is DutchAuctionBase {
_ORDER_REGISTRATOR = orderRegistrator;
}

/// @dev Rate bump for the AuctionDetails of {DutchAuctionBase-_getRateBump}; when the top bit of
/// `auctionStartTime` is set, the start becomes max(announcement, built start).
function _auctionRateBump(bytes32 orderHash, bytes calldata auctionDetails) internal view returns (uint256, bytes calldata) {
/// @dev {DutchAuctionBase-_getNetBump} with an anchored start raised to the announcement time.
function _auctionNetBump(bytes32 orderHash, bytes calldata auctionDetails) internal view returns (int256 netBump, bytes calldata tail) {
unchecked {
uint256 auctionStartTime = uint32(bytes4(auctionDetails[7:11]));
if (auctionStartTime & _ANCHORED_FLAG == 0) {
return _getRateBump(auctionDetails);
return _getNetBump(auctionDetails);
}

uint256 gasBumpEstimate = uint24(bytes3(auctionDetails[0:3]));
uint256 gasPriceEstimate = uint32(bytes4(auctionDetails[3:7]));
uint256 auctionDuration = uint24(bytes3(auctionDetails[11:14]));
uint256 initialRateBump = uint24(bytes3(auctionDetails[14:17]));

auctionStartTime &= _TIMESTAMP_MASK;
uint256 anchoredStartTime = _announcedAt(orderHash);
if (anchoredStartTime > auctionStartTime) auctionStartTime = anchoredStartTime;

uint256 gasBump = gasBumpEstimate == 0 || gasPriceEstimate == 0 ? 0 : gasBumpEstimate * block.basefee / gasPriceEstimate / _GAS_PRICE_BASE;
(uint256 auctionBump, bytes calldata tail) = _getAuctionBump(auctionStartTime, auctionStartTime + auctionDuration, initialRateBump, auctionDetails[17:]);
return (auctionBump > gasBump ? auctionBump - gasBump : 0, tail);
uint256 auctionBump;
(auctionBump, tail) = _getAuctionBump(auctionStartTime, auctionStartTime + auctionDuration, initialRateBump, auctionDetails[_POINTS_COUNT_OFFSET:]);
netBump = int256(auctionBump) - int256(_getGasBump(auctionDetails));
}
}

/**
* @dev Anchored pre-checks for a fill; a no-op for a legacy blob. Walks the exclusivity ladder
* from the announcement and enforces the optional deadline; the caller walks the same ladder
* from the absolute allowed time, so a taker must clear both. `whitelistData` layout:
* @dev Announcement-based deadline and exclusivity checks; a no-op for a legacy blob.
* `whitelistData` layout:
* ```
* 4 bytes - allowed time; top bit: measure exclusivity from the announcement too
* 3 bytes - anchored allowed-time delay (present when set); its top bit adds the deadline
* 4 bytes - allowed time; top bit: anchored
* 3 bytes - anchored allowed-time delay (present when anchored); its top bit adds the deadline
* 3 bytes - announcement deadline delay (present when the deadline bit is set)
* 1 byte - size of the whitelist
* (bytes12)[N] — taker whitelist
Expand All @@ -85,8 +78,7 @@ abstract contract AnchoredDutchAuction is DutchAuctionBase {
}
}

/// @dev Absolute allowed time and the offset of the whitelist size byte, skipping the anchored
/// fields (enforced by `_validateAnchoredFill`) so a whitelist walk works anchored or not.
/// @dev Absolute allowed time and the offset of the whitelist size byte, anchored fields skipped.
function _skipAnchoredFields(bytes calldata whitelistData) internal pure returns (uint256 allowedTime, uint256 offset) {
unchecked {
allowedTime = uint32(bytes4(whitelistData));
Expand All @@ -98,8 +90,7 @@ abstract contract AnchoredDutchAuction is DutchAuctionBase {
}
}

/// @dev Walks the resolver ladder from the anchored allowed time, mirroring the settlement's
/// walk from the absolute one.
/// @dev The settlement's ladder walk, starting from the anchored allowed time.
function _checkAnchoredExclusivity(bytes calldata whitelistData, address taker, uint256 allowedTime) private view {
unchecked {
uint80 maskedTakerAddress = uint80(uint160(taker));
Expand Down
38 changes: 32 additions & 6 deletions contracts/DutchAuctionBase.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@ abstract contract DutchAuctionBase {
uint256 internal constant _BASE_POINTS = 10_000_000; // 100%
uint256 internal constant _GAS_PRICE_BASE = 1_000_000; // 1000 means 1 Gwei

/// @dev Offset of the auction points count byte, i.e. the fixed AuctionDetails header:
/// 3-byte gas bump estimate, 4-byte gas price estimate, 4-byte start time, 3-byte duration
/// and 3-byte initial rate bump.
uint256 internal constant _POINTS_COUNT_OFFSET = 17;

/// @dev The top bit of the points count is reserved for extensions to flag their own fields.
uint256 private constant _POINTS_COUNT_MASK = 0x7f;

/**
* @dev Parses auction rate bump data from the `auctionDetails` field.
* `gasBumpEstimate` and `gasPriceEstimate` are used to estimate the transaction costs
Expand All @@ -29,18 +37,36 @@ abstract contract DutchAuctionBase {
* @return Remaining calldata after parsing auction data.
*/
function _getRateBump(bytes calldata auctionDetails) internal view virtual returns (uint256, bytes calldata) {
(int256 netBump, bytes calldata tail) = _getNetBump(auctionDetails);
return (_clampBump(netBump), tail);
}

/// @dev Auction bump net of the gas bump; negative when the gas bump exceeds it.
function _getNetBump(bytes calldata auctionDetails) internal view virtual returns (int256 netBump, bytes calldata tail) {
unchecked {
uint256 gasBumpEstimate = uint24(bytes3(auctionDetails[0:3]));
uint256 gasPriceEstimate = uint32(bytes4(auctionDetails[3:7]));
uint256 gasBump = gasBumpEstimate == 0 || gasPriceEstimate == 0 ? 0 : gasBumpEstimate * block.basefee / gasPriceEstimate / _GAS_PRICE_BASE;
uint256 auctionStartTime = uint32(bytes4(auctionDetails[7:11]));
uint256 auctionFinishTime = auctionStartTime + uint24(bytes3(auctionDetails[11:14]));
uint256 initialRateBump = uint24(bytes3(auctionDetails[14:17]));
(uint256 auctionBump, bytes calldata tail) = _getAuctionBump(auctionStartTime, auctionFinishTime, initialRateBump, auctionDetails[17:]);
return (auctionBump > gasBump ? auctionBump - gasBump : 0, tail);
uint256 auctionBump;
(auctionBump, tail) = _getAuctionBump(auctionStartTime, auctionFinishTime, initialRateBump, auctionDetails[_POINTS_COUNT_OFFSET:]);
netBump = int256(auctionBump) - int256(_getGasBump(auctionDetails));
}
}

/// @dev The rate bump estimating the taker's transaction costs at the current base fee.
function _getGasBump(bytes calldata auctionDetails) internal view returns (uint256) {
unchecked {
uint256 gasBumpEstimate = uint24(bytes3(auctionDetails[0:3]));
uint256 gasPriceEstimate = uint32(bytes4(auctionDetails[3:7]));
return gasBumpEstimate == 0 || gasPriceEstimate == 0 ? 0 : gasBumpEstimate * block.basefee / gasPriceEstimate / _GAS_PRICE_BASE;
}
}

/// @dev Clamps a net bump to a non-negative rate bump.
function _clampBump(int256 netBump) internal pure returns (uint256) {
return netBump > 0 ? uint256(netBump) : 0;
}

/**
* @dev Calculates auction price bump. Auction is represented as a piecewise linear function with `N` points.
* Each point is represented as a pair of `(rateBump, timeDelta)`, where `rateBump` is the
Expand All @@ -60,7 +86,7 @@ abstract contract DutchAuctionBase {
unchecked {
uint256 currentPointTime = auctionStartTime;
uint256 currentRateBump = initialRateBump;
uint256 pointsCount = uint8(pointsAndTimeDeltas[0]);
uint256 pointsCount = uint8(pointsAndTimeDeltas[0]) & _POINTS_COUNT_MASK;
pointsAndTimeDeltas = pointsAndTimeDeltas[1:];
bytes calldata tail = pointsAndTimeDeltas[5 * pointsCount:];

Expand Down
103 changes: 103 additions & 0 deletions contracts/PartialFillPremiumAuction.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// SPDX-License-Identifier: MIT

pragma solidity 0.8.23;

import { Math } from "@openzeppelin/contracts/utils/math/Math.sol";

import { AnchoredDutchAuction } from "./AnchoredDutchAuction.sol";
import { IOrderRegistrator } from "./interfaces/IOrderRegistrator.sol";

/**
* @title Partial-fill premium auction
* @notice Dutch auction where a fill that leaves part of the order behind pays a premium, so
* sweeping the remainder is the cheapest way to fill.
* @dev Opt-in per order via the top bit of the auction points count, which a legacy blob never
* sets. The curve follows the time points, read over the fill's share of the remainder in `_SHARE_BASE`:
* ```
* 3 bytes - initial premium, paid by a vanishing fill
* 1 byte - points count
* (bytes3,bytes2)[M] — premiums and share deltas
* ```
*/
abstract contract PartialFillPremiumAuction is AnchoredDutchAuction {
uint256 private constant _FILL_CURVE_FLAG = 0x80; // top bit of the uint8 auction points count
uint256 private constant _SHARE_BASE = 10_000;

/// @dev The fill curve header is a 3-byte initial premium followed by the 1-byte points count.
uint256 private constant _CURVE_POINTS_COUNT_OFFSET = 3;
uint256 private constant _CURVE_HEADER_SIZE = 4;

/// @dev Each fill curve point is a 3-byte premium followed by a 2-byte share delta.
uint256 private constant _CURVE_POINT_SIZE = 5;

error NonMonotonicFillCurve();

constructor(IOrderRegistrator orderRegistrator) AnchoredDutchAuction(orderRegistrator) {}

/// @dev The auction's net bump and its fill curve, which is empty for an order without one.
function _parseAuctionDetails(bytes32 orderHash, bytes calldata auctionDetails)
internal view returns (int256 netBump, bytes calldata fillCurve, bytes calldata tail)
{
unchecked {
(netBump, tail) = _auctionNetBump(orderHash, auctionDetails);
if (uint8(auctionDetails[_POINTS_COUNT_OFFSET]) & _FILL_CURVE_FLAG == 0) {
fillCurve = tail[:0];
} else {
uint256 length = _CURVE_HEADER_SIZE + _CURVE_POINT_SIZE * uint256(uint8(tail[_CURVE_POINTS_COUNT_OFFSET]));
fillCurve = tail[:length];
tail = tail[length:];
}
}
}

/// @dev Rate bump for a fill of known size; a completing fill never reads the curve.
function _fillRateBump(int256 netBump, bytes calldata fillCurve, uint256 makingAmount, uint256 remainingMakingAmount)
internal pure returns (uint256)
{
unchecked {
if (fillCurve.length != 0 && makingAmount < remainingMakingAmount) {
netBump += int256(_fillPremium(makingAmount, remainingMakingAmount, fillCurve));
}
return _clampBump(netBump);
}
}

/// @dev Making-amount path: the fill size is estimated at the worst (initial) premium and
/// repriced, which can only overstate the bump.
function _estimatedFillRateBump(int256 netBump, bytes calldata fillCurve, uint256 unbumpedMakingAmount, uint256 remainingMakingAmount)
internal pure returns (uint256)
{
unchecked {
if (fillCurve.length == 0) return _clampBump(netBump);
uint256 worstRateBump = _clampBump(netBump + int256(uint256(uint24(bytes3(fillCurve[0:3])))));
uint256 estimatedMakingAmount = Math.mulDiv(unbumpedMakingAmount, _BASE_POINTS, _BASE_POINTS + worstRateBump);
return _fillRateBump(netBump, fillCurve, estimatedMakingAmount, remainingMakingAmount);
}
}

/// @dev Premium interpolated over the fill's share of the remainder, ending at zero for a full
/// sweep. A rising curve would reward splitting a fill, so the walk rejects it lazily.
function _fillPremium(uint256 makingAmount, uint256 remainingMakingAmount, bytes calldata fillCurve) private pure returns (uint256) {
unchecked {
uint256 currentPremium = uint24(bytes3(fillCurve[0:3]));
uint256 share = Math.mulDiv(makingAmount, _SHARE_BASE, remainingMakingAmount);
if (share == 0) return currentPremium;

uint256 currentShare = 0;
uint256 pointsCount = uint8(fillCurve[_CURVE_POINTS_COUNT_OFFSET]);
bytes calldata points = fillCurve[_CURVE_HEADER_SIZE:];
for (uint256 i = 0; i < pointsCount; i++) {
uint256 nextPremium = uint24(bytes3(points[:3]));
if (nextPremium > currentPremium) revert NonMonotonicFillCurve();
uint256 nextShare = currentShare + uint16(bytes2(points[3:5]));
if (share <= nextShare) {
return ((share - currentShare) * nextPremium + (nextShare - share) * currentPremium) / (nextShare - currentShare);
}
currentPremium = nextPremium;
currentShare = nextShare;
points = points[_CURVE_POINT_SIZE:];
}
return (_SHARE_BASE - share) * currentPremium / (_SHARE_BASE - currentShare);
}
}
}
Loading
Loading