pragma solidity 0.8.13; import { UD60x18 } from "@prb/math/UD60x18.sol"; import { IERC20 } from "@root/interfaces/IERC20.sol"; import { inv, add, sub, mul, exp, ln, wrap, unwrap, gte, mod, div } from "@prb/math/UD60x18.sol"; /* * @title Rolling Dutch Auction * @author Samuel JJ Gosling * @description A dutch auction derivative with composite logarithimic decay */ contract RollingDutchAuction { /* @dev Address mapping for an auction's redeemable balances */ mapping(address => mapping(bytes => bytes)) public _claims; /* @dev Auction mapping translating to an indexed window */ mapping(bytes => mapping(uint256 => Window)) public _window; /* @dev Auction mapping for associated parameters */ mapping(bytes => Auction) public _auctions; /* @dev Auction mapping for the window index */ mapping(bytes => uint256) public _windows; struct Auction { uint256 windowDuration; /* @dev Unix time window duration */ uint256 windowTimestamp; /* @dev Unix timestamp for window start */ uint256 startTimestamp; /* @dev Unix auction start timestamp */ uint256 endTimestamp; /* @dev Unix auction end timestamp */ uint256 duration; /* @dev Unix time auction duration */ uint256 proceeds; /* @dev Auction proceeds balance */ uint256 reserves; /* @dev Auction reserves balance */ uint256 price; /* @dev Auction origin price */ } struct Window { bytes bidId; /* @dev Bid identifier */ uint256 expiry; /* @dev Unix timestamp window exipration */ uint256 price; /* @dev Window price */ uint256 volume; /* @dev Window volume */ bool processed; /* @dev Window fuflfillment state */ } /* * @dev Conditioner to ensure an auction is active * @param a͟u͟c͟t͟i͟o͟n͟I͟d͟ Encoded auction parameter identifier */ modifier activeAuction(bytes memory auctionId) { require(remainingWindowTime(auctionId) > 0 || remainingTime(auctionId) > 0); _; } /* * @dev Conditioner to ensure an auction is inactive * @param a͟u͟c͟t͟i͟o͟n͟I͟d͟ Encoded auction parameter identifier */ modifier inactiveAuction(bytes memory auctionId) { require(remainingWindowTime(auctionId) == 0 && remainingTime(auctionId) == 0); _; } /* * @dev Helper to view an auction's operator address * @param a͟u͟c͟t͟i͟o͟n͟I͟d͟ Encoded auction parameter identifier */ function operatorAddress(bytes memory auctionId) public pure returns (address opAddress) { (opAddress,,,,) = abi.decode(auctionId, (address, address, address, uint256, bytes)); } /* * @dev Helper to view an auction's purchase token address * @param a͟u͟c͟t͟i͟o͟n͟I͟d͟ Ancoded auction parameter identifier */ function purchaseToken(bytes memory auctionId) public pure returns (address tokenAddress) { (,, tokenAddress,,) = abi.decode(auctionId, (address, address, address, uint256, bytes)); } /* * @dev Helper to view an auction's reserve token address * @param a͟u͟c͟t͟i͟o͟n͟I͟d͟ Encoded auction parameter identifier */ function reserveToken(bytes memory auctionId) public pure returns (address tokenAddress) { (, tokenAddress,,,) = abi.decode(auctionId, (address, address, address, uint256, bytes)); } /* * @dev Helper to decode claim hash balances * @param c͟l͟a͟i͟m͟H͟a͟s͟h͟ Encoded (uint256, uint256) values */ function balancesOf(bytes memory claimHash) public pure returns (uint256, uint256) { uint256 refundBalance; uint256 claimBalance; if (keccak256(claimHash) != keccak256(bytes(""))) { (refundBalance, claimBalance) = abi.decode(claimHash, (uint256, uint256)); } return (refundBalance, claimBalance); } /* * @dev Auction deployment * @param o͟p͟e͟r͟a͟t͟o͟r͟A͟d͟r͟e͟s͟s͟ Auction management address * @param r͟e͟s͟e͟r͟v͟e͟T͟o͟k͟e͟n͟ Auctioning token address * @param p͟u͟r͟c͟h͟a͟s͟e͟T͟o͟k͟e͟n͟ Currency token address * @param r͟e͟s͟e͟r͟v͟e͟A͟m͟o͟u͟n͟t͟ Auctioning token amount * @param m͟i͟n͟i͟m͟u͟m͟P͟u͟r͟c͟h͟a͟s͟e͟A͟m͟o͟u͟n͟t͟ Minimum currency purchase amount * @param s͟t͟a͟r͟t͟i͟n͟g͟O͟r͟i͟g͟i͟n͟P͟r͟i͟c͟e͟ Auction starting price * @param s͟t͟a͟r͟t͟T͟i͟m͟e͟s͟t͟a͟m͟p͟ Unix timestamp auction initiation * @param e͟n͟d͟T͟i͟m͟e͟s͟t͟a͟m͟p͟ Unix timestamp auction expiration * @param w͟i͟n͟d͟o͟w͟D͟u͟r͟a͟t͟i͟o͟n͟ Uinx time window duration */ function createAuction( address operatorAddress, address reserveToken, address purchaseToken, uint256 reserveAmount, uint256 minimumPurchaseAmount, uint256 startingOriginPrice, uint256 startTimestamp, uint256 endTimestamp, uint256 windowDuration ) public returns (bytes memory) { bytes memory auctionId = abi.encode( operatorAddress, reserveToken, purchaseToken, minimumPurchaseAmount, abi.encodePacked(reserveAmount, startingPrice, startTimestamp, endTimestamp, windowDuration) ); Auction storage state = _auctions[auctionId]; require(state.price == 0, "AUCTION EXISTS"); IERC20(reserveToken).transferFrom(msg.sender, address(this), reserveAmount); state.duration = endTimestamp - startTimestamp; state.windowDuration = windowDuration; state.windowTimestamp = startTimestamp; state.startTimestamp = startTimestamp; state.endTimestamp = endTimestamp; state.price = startingOriginPrice; state.reserves = reserveAmount; emit NewAuction(auctionId, reserveToken, reserveAmount, startingPrice, endTimestamp); return auctionId; } /* * @dev Helper to view an auction's minimum purchase amount * @param a͟u͟c͟t͟i͟o͟n͟I͟d͟ Encoded auction parameter identifier */ function minimumPurchase(bytes memory auctionId) public pure returns (uint256 minimumAmount) { (,,, minimumAmount,) = abi.decode(auctionId, (address, address, address, uint256, bytes)); } /* * @dev Helper to view an auction's maximum order reserve amount * @param a͟u͟c͟t͟i͟o͟n͟I͟d͟ Encoded auction parameter identifier */ function maximumPurchase(bytes memory auctionId) public view returns (uint256) { return unwrap(inv(scalarPrice(auctionId))); } /* * @dev Helper to view an auction's active scalar price formatted to uint256 * @param a͟u͟c͟t͟i͟o͟n͟I͟d͟ Encoded auction parameter identifier */ function scalarPriceUint(bytes memory auctionId) public view returns (uint256) { return unwrap(scalarPrice(auctionId)); } /* * @dev Active price decay following time delta (x) between the current * timestamp and the window's start timestamp or if the window is expired; time * delta between the window's expiration. Which is the applied as the exponent * of Euler's number and subject to the natural logarithim. Finally applied as * as a product to the origin price (y) and substracted from itself * @param a͟u͟c͟t͟i͟o͟n͟I͟d͟ Encoded auction parameter identifier */ function scalarPrice(bytes memory auctionId) public view returns (UD60x18) { Auction storage state = _auctions[auctionId]; Window storage w = _window[auctionId][_windows[auctionId]]; bool isInitialised = w.expiry != 0; bool isExpired = w.expiry < block.timestamp && isInitialised; uint256 timestamp = isExpired ? w.expiry : state.windowTimestamp; UD60x18 t = wrap(block.timestamp - timestamp); UD60x18 t_r = wrap(state.endTimestamp - timestamp); UD60x18 x = div(add(t, mod(t, sub(t_r, t))), t_r); UD60x18 y = !isInitialised ? wrap(state.price) : wrap(w.price); return sub(y, mul(ln(exp(x)), y)); } /* * @dev Bid submission * @param a͟u͟c͟t͟i͟o͟n͟I͟d͟ Encoded auction parameter identifier * @param p͟r͟i͟c͟e͟ Bid order price * @param v͟o͟l͟u͟m͟e͟ Bid order volume */ function commitBid(bytes memory auctionId, uint256 price, uint256 volume) activeAuction(auctionId) public returns (bytes memory) { Window storage w = _window[auctionId][_windows[auctionId]]; require(minimumPurchase(auctionId) <= volume, "INSUFFICIENT VOLUME"); bool hasExpired; if (w.expiry != 0) { if (remainingWindowTime(auctionId) > 0) { if (w.price < price) { require(w.volume <= volume, "INSUFFICIENT WINDOW VOLUME"); } else { require(w.price < price, "INVALID WINDOW PRICE"); } } else { hasExpired = true; } } if (w.price == 0 || hasExpired) { require(gte(wrap(price), scalarPrice(auctionId)), "INVALID CURVE PRICE"); } IERC20(purchaseToken(auctionId)).transferFrom(msg.sender, address(this), volume); require(_auctions[auctionId].reserves >= (volume / price), "INSUFFICIENT RESERVES"); require(maximumPurchase(auctionId) >= (volume / price), "INVALID VOLUME"); bytes memory bidId = abi.encode(auctionId, msg.sender, price, volume); (uint256 refund, uint256 claim) = balancesOf(_claims[msg.sender][auctionId]); _claims[msg.sender][auctionId] = abi.encode(refund + volume, claim); if (hasExpired) { w = _window[auctionId][windowExpiration(auctionId)]; } _auctions[auctionId].windowTimestamp = block.timestamp; w.expiry = block.timestamp + _auctions[auctionId].windowDuration; w.volume = volume; w.price = price; w.bidId = bidId; emit Offer(auctionId, msg.sender, w.bidId, w.expiry); return bidId; } /* * @dev Expire and fulfill an auction's active window * @param a͟u͟c͟t͟i͟o͟n͟I͟d͟ Encoded auction parameter identifier */ function windowExpiration(bytes memory auctionId) internal returns (uint256) { uint256 windowIndex = _windows[auctionId]; uint256 auctionElapsedTime = elapsedTime(auctionId, block.timestamp); uint256 auctionRemainingTime = _auctions[auctionId].duration - auctionElapsedTime; bytes memory winningBidId = _window[auctionId][windowIndex].bidId; _auctions[auctionId].endTimestamp = block.timestamp + auctionRemainingTime; _auctions[auctionId].price = _window[auctionId][windowIndex].price; _windows[auctionId] = windowIndex + 1; fulfillWindow(auctionId, windowIndex); emit Expiration(auctionId, winningBidId, windowIndex); return windowIndex + 1; } /* * @dev Fulfill a window index even if the auction is inactive * @param a͟u͟c͟t͟i͟o͟n͟I͟d͟ Encoded auction parameter identifier */ function fulfillWindow(bytes memory auctionId, uint256 windowId) public { Window storage w = _window[auctionId][windowId]; require(w.expiry < block.timestamp, "WINDOW UNEXPIRED"); require(!w.processed, "WINDOW ALREADY FUFILLED"); (, address bidder, uint256 price, uint256 volume) = abi.decode(w.bidId, (bytes, address, uint256, uint256)); (uint256 refund, uint256 claim) = balancesOf(_claims[bidder][auctionId]); delete _claims[bidder][auctionId]; w.processed = true; _auctions[auctionId].reserves -= volume / price; _auctions[auctionId].proceeds += volume; _claims[bidder][auctionId] = abi.encode(refund - volume, claim + (volume / price)); emit Fufillment(auctionId, w.bidId, windowId); } /* * @dev Helper to view an auction's remaining duration * @param a͟u͟c͟t͟i͟o͟n͟I͟d͟ Encoded auction parameter identifier */ function remainingTime(bytes memory auctionId) public view returns (uint256) { uint256 endTimestamp = _auctions[auctionId].endTimestamp; if (endTimestamp > block.timestamp) { return endTimestamp - block.timestamp; } else { return 0; } } /* * @dev Helper to view an auction's active remaining window duration * @param a͟u͟c͟t͟i͟o͟n͟I͟d͟ Encoded auction parameter identifier */ function remainingWindowTime(bytes memory auctionId) public view returns (uint256) { uint256 expiryTimestamp = _window[auctionId][_windows[auctionId]].expiry; if (expiryTimestamp == 0 || block.timestamp > expiryTimestamp) { return 0; } else { return expiryTimestamp - block.timestamp; } } /* * @dev Helper to view an auction's progress in unix time * @param a͟u͟c͟t͟i͟o͟n͟I͟d͟ Encoded auction parameter identifier */ function elapsedTime(bytes memory auctionId, uint256 timestamp) public view returns (uint256) { uint256 windowIndex = _windows[auctionId] + 1; uint256 windowElapsedTime = _auctions[auctionId].windowDuration * windowIndex; return timestamp - _auctions[auctionId].startTimestamp - windowElapsedTime; } /* * @dev Auction management redemption * @param a͟u͟c͟t͟i͟o͟n͟I͟d͟ Encoded auction parameter identifier */ function withdraw(bytes memory auctionId) inactiveAuction(auctionId) public { uint256 proceeds = _auctions[auctionId].proceeds; uint256 reserves = _auctions[auctionId].reserves; delete _auctions[auctionId].proceeds; delete _auctions[auctionId].reserves; if (proceeds > 0) { IERC20(purchaseToken(auctionId)).transfer(operatorAddress(auctionId), proceeds); } if (reserves > 0) { IERC20(reserveToken(auctionId)).transfer(operatorAddress(auctionId), reserves); } emit Withdraw(auctionId); } /* * @dev Auction order and refund redemption * @param a͟u͟c͟t͟i͟o͟n͟I͟d͟ Encoded auction parameter identifier */ function redeem(address bidder, bytes memory auctionId) inactiveAuction(auctionId) public { bytes memory claimHash = _claims[bidder][auctionId]; (uint256 refund, uint256 claim) = balancesOf(claimHash); delete _claims[bidder][auctionId]; if (refund > 0) { IERC20(purchaseToken(auctionId)).transfer(bidder, refund); } if (claim > 0) { IERC20(reserveToken(auctionId)).transfer(bidder, claim); } emit Claim(auctionId, claimHash); } event NewAuction( bytes indexed auctionId, address reserveToken, uint256 reserves, uint256 price, uint256 endTimestamp ); event Offer(bytes indexed auctionId, address indexed owner, bytes indexed bidId, uint256 expiry); event Fufillment(bytes indexed auctionId, bytes indexed bidId, uint256 windowId); event Expiration(bytes indexed auctionId, bytes indexed bidId, uint256 windowId); event Claim(bytes indexed auctionId, bytes indexed bidId); event Withdraw(bytes indexed auctionId); }