From 5b13dfe581dd8afe82c3db3799454293c6f2431d Mon Sep 17 00:00:00 2001 From: gozzy Date: Sun, 9 Apr 2023 12:33:02 +0000 Subject: [PATCH] bump RDA version --- .gitattributes | 1 + src/RDA.sol | 279 +++++++++++++++++++++++++++++-------------------- 2 files changed, 165 insertions(+), 115 deletions(-) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..7cc88f0 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.sol linguist-language=Solidity \ No newline at end of file diff --git a/src/RDA.sol b/src/RDA.sol index cf7975b..a238d02 100755 --- a/src/RDA.sol +++ b/src/RDA.sol @@ -1,12 +1,10 @@ pragma solidity 0.8.13; -import { UD60x18 } from "@prb/math/UD60x18.sol"; +import { IRDA } from "@root/interfaces/IRDA.sol"; -import { IRDA } from "@interfaces/IRDA.sol"; -import { IERC20 } from "@openzeppelin/token/ERC20/IERC20.sol"; -import { IDelegatedVesting } from "@interfaces/IDelegatedVesting.sol"; - -import { add, sub, mul, wrap, unwrap, gt, mod, div } from "@prb/math/UD60x18.sol"; +import { ERC20 } from "@openzeppelin/token/ERC20/ERC20.sol"; +import { SafeERC20 } from "@openzeppelin/token/ERC20/utils/SafeERC20.sol"; +import { ReentrancyGuard } from "@openzeppelin/security/ReentrancyGuard.sol"; /* * @title Rolling Dutch Auction (RDA) @@ -14,7 +12,9 @@ import { add, sub, mul, wrap, unwrap, gt, mod, div } from "@prb/math/UD60x18.sol * @description A dutch auction derivative with composite decay */ -contract RDA is IRDA { +contract RDA is IRDA, ReentrancyGuard { + + using SafeERC20 for ERC20; /* @dev Address mapping for an auction's redeemable balances */ mapping(address => mapping(bytes => bytes)) public _claims; @@ -28,13 +28,11 @@ contract RDA is IRDA { /* @dev Auction mapping for the window index */ mapping(bytes => uint256) public _windows; - mapping(bytes => Vesting) public _vesting; - 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 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 */ @@ -49,17 +47,14 @@ contract RDA is IRDA { bool processed; /* @dev Window fuflfillment state */ } - struct Vesting { - address instance; - uint256 period; - } - /* * @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); + modifier activeAuction(bytes calldata auctionId) { + if (remainingWindowTime(auctionId) == 0 && remainingTime(auctionId) == 0) { + revert AuctionInactive(); + } _; } @@ -67,8 +62,10 @@ contract RDA is IRDA { * @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); + modifier inactiveAuction(bytes calldata auctionId) { + if (remainingWindowTime(auctionId) > 0 || remainingTime(auctionId) > 0) { + revert AuctionActive(); + } _; } @@ -76,7 +73,7 @@ contract RDA is IRDA { * @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) { + function operatorAddress(bytes calldata auctionId) public pure returns (address opAddress) { (opAddress,,,,) = abi.decode(auctionId, (address, address, address, uint256, bytes)); } @@ -84,15 +81,31 @@ contract RDA is IRDA { * @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) { + function purchaseToken(bytes calldata auctionId) public pure returns (address tokenAddress) { (,, tokenAddress,,) = abi.decode(auctionId, (address, address, address, uint256, bytes)); } + function isWindowInit(bytes calldata auctionId) public view returns (bool) { + return _window[auctionId][_windows[auctionId]].expiry != 0; + } + + function isWindowActive(bytes calldata auctionId) public view returns (bool) { + Window storage window = _window[auctionId][_windows[auctionId]]; + + return isWindowInit(auctionId) && window.expiry > block.timestamp; + } + + function isWindowExpired(bytes calldata auctionId) public view returns (bool) { + Window storage window = _window[auctionId][_windows[auctionId]]; + + return isWindowInit(auctionId) && window.expiry < block.timestamp; + } + /* * @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) { + function reserveToken(bytes calldata auctionId) public pure returns (address tokenAddress) { (, tokenAddress,,,) = abi.decode(auctionId, (address, address, address, uint256, bytes)); } @@ -124,7 +137,6 @@ contract RDA is IRDA { * @param w͟i͟n͟d͟o͟w͟D͟u͟r͟a͟t͟i͟o͟n͟ Uinx time window duration */ function createAuction( - address vestingAddress, address operatorAddress, address reserveToken, address purchaseToken, @@ -133,9 +145,8 @@ contract RDA is IRDA { uint256 startingOriginPrice, uint256 startTimestamp, uint256 endTimestamp, - uint256 windowDuration, - uint256 vestingDuration - ) external returns (bytes memory) { + uint256 windowDuration + ) override external returns (bytes memory) { bytes memory auctionId = abi.encode( operatorAddress, reserveToken, @@ -144,24 +155,38 @@ contract RDA is IRDA { abi.encodePacked(reserveAmount, startingOriginPrice, startTimestamp, endTimestamp, windowDuration) ); + ERC20 tokenReserve = ERC20(reserveToken); + ERC20 tokenPurchase = ERC20(purchaseToken); + Auction storage state = _auctions[auctionId]; + uint256 auctionDuration = endTimestamp - startTimestamp; + if (state.price != 0) { revert AuctionExists(); } + if (startingOriginPrice == 0) { + revert InvalidAuctionPrice(); + } + if (startTimestamp < block.timestamp) { + revert InvalidAuctionTimestamp(); + } + if (tokenReserve.decimals() != tokenPurchase.decimals()){ + revert InvalidTokenDecimals(); + } + if (auctionDuration < 1 days || windowDuration < 2 hours) { + revert InvalidAuctionDurations(); + } - _vesting[auctionId].instance = vestingAddress; - _vesting[auctionId].period = vestingDuration; + tokenReserve.safeTransferFrom(msg.sender, address(this), reserveAmount); - 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.reserves = reserveAmount; state.price = startingOriginPrice; + state.duration = auctionDuration; + state.reserves = reserveAmount; emit NewAuction(auctionId, reserveToken, reserveAmount, startingOriginPrice, endTimestamp); @@ -172,18 +197,10 @@ contract RDA is IRDA { * @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) { + function minimumPurchase(bytes calldata auctionId) public pure returns (uint256 minimumAmount) { (,,, minimumAmount,) = abi.decode(auctionId, (address, address, address, uint256, bytes)); } - /* - * @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 calldata auctionId) external returns (uint256) { - return unwrap(scalarPrice(auctionId)); - } - /* * @dev Active price decay proportional to time delta (t) between the current * timestamp and the window's start timestamp or if the window is expired; @@ -194,22 +211,23 @@ contract RDA is IRDA { * the origin price (y) and subtracted by y to result the decayed price * @param a͟u͟c͟t͟i͟o͟n͟I͟d͟ Encoded auction parameter identifier */ - function scalarPrice(bytes memory auctionId) public view returns (UD60x18) { + function scalarPrice(bytes calldata auctionId) + activeAuction(auctionId) + public view returns (uint256) { Auction storage state = _auctions[auctionId]; Window storage window = _window[auctionId][_windows[auctionId]]; - bool isInitialised = window.expiry != 0; - bool isExpired = window.expiry < block.timestamp && isInitialised; + uint256 ts = isWindowExpired(auctionId) ? window.expiry : state.windowTimestamp; + uint256 y = !isWindowInit(auctionId) ? state.price : window.price; - uint256 timestamp = isExpired ? window.expiry : state.windowTimestamp; + uint256 t = block.timestamp - ts; + uint256 t_r = state.duration - elapsedTimeFromWindow(auctionId); - UD60x18 t = wrap(block.timestamp - timestamp); - UD60x18 t_r = wrap(state.duration - elapsedTime(auctionId, timestamp)); + uint256 t_mod = t % (t_r - t); + uint256 x = (t + t_mod) * 1e18; + uint256 y_x = y * x / t_r; - UD60x18 x = div(add(t, mod(t, sub(t_r, t))), t_r); - UD60x18 y = !isInitialised ? wrap(state.price) : wrap(window.price); - - return sub(y, mul(y, x)); + return y - y_x / 1e18; } /* @@ -218,9 +236,11 @@ contract RDA is IRDA { * @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) + function commitBid(bytes calldata auctionId, uint256 price, uint256 volume) activeAuction(auctionId) - external returns (bytes memory) { + nonReentrant + override external returns (bytes memory bidId) { + Auction storage state = _auctions[auctionId]; Window storage window = _window[auctionId][_windows[auctionId]]; if (volume < minimumPurchase(auctionId)) { @@ -229,8 +249,8 @@ contract RDA is IRDA { bool hasExpired; - if (window.expiry != 0) { - if (remainingWindowTime(auctionId) > 0) { + if (isWindowInit(auctionId)) { + if (isWindowActive(auctionId)) { if (window.price < price) { if (volume < window.volume) { revert InvalidWindowVolume(); @@ -244,68 +264,85 @@ contract RDA is IRDA { } if (window.price == 0 || hasExpired) { - if (gt(scalarPrice(auctionId), wrap(price))) { + if (price < scalarPrice(auctionId)) { revert InvalidScalarPrice(); } } - IERC20(purchaseToken(auctionId)).transferFrom(msg.sender, address(this), volume); + uint256 orderVolume = volume - (volume % price); - if (_auctions[auctionId].reserves < (volume / price)) { + if (state.reserves < orderVolume * 1e18 / price) { revert InsufficientReserves(); } + if (volume < price) { + revert InvalidReserveVolume(); + } - bytes memory bidId = abi.encode(auctionId, msg.sender, price, volume); + bidId = abi.encode(auctionId, msg.sender, price, orderVolume); (uint256 refund, uint256 claim) = balancesOf(_claims[msg.sender][auctionId]); - _claims[msg.sender][auctionId] = abi.encode(refund + volume, claim); + _claims[msg.sender][auctionId] = abi.encode(refund + orderVolume, claim); if (hasExpired) { window = _window[auctionId][windowExpiration(auctionId)]; } - _auctions[auctionId].windowTimestamp = block.timestamp; - - window.expiry = block.timestamp + _auctions[auctionId].windowDuration; - window.volume = volume; + window.expiry = block.timestamp + state.windowDuration; + window.volume = orderVolume; window.price = price; window.bidId = bidId; - emit Offer(auctionId, msg.sender, window.bidId, window.expiry); + state.windowTimestamp = block.timestamp; - return bidId; + emit Offer(auctionId, msg.sender, bidId, window.expiry); + + ERC20 tokenPurchase = ERC20(purchaseToken(auctionId)); + + tokenPurchase.safeTransferFrom(msg.sender, address(this), orderVolume); } /* * @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) { + function windowExpiration(bytes calldata auctionId) internal returns (uint256) { uint256 windowIndex = _windows[auctionId]; - uint256 auctionElapsedTime = elapsedTime(auctionId, block.timestamp); - uint256 auctionRemainingTime = _auctions[auctionId].duration - auctionElapsedTime; - _auctions[auctionId].endTimestamp = block.timestamp + auctionRemainingTime; - _auctions[auctionId].price = _window[auctionId][windowIndex].price; + Auction storage state = _auctions[auctionId]; + Window storage window = _window[auctionId][windowIndex]; + + state.endTimestamp = block.timestamp + remainingTime(auctionId); + state.price = window.price; _windows[auctionId] = windowIndex + 1; - fulfillWindow(auctionId, windowIndex); + _fulfillWindow(auctionId, windowIndex); - emit Expiration(auctionId, _window[auctionId][windowIndex].bidId, windowIndex); + emit Expiration(auctionId, window.bidId, windowIndex); return windowIndex + 1; } /* - * @dev Fulfill a window index even if the auction is inactive + * @dev Fulfill a window index for an inactive auction * @param a͟u͟c͟t͟i͟o͟n͟I͟d͟ Encoded auction parameter identifier */ - function fulfillWindow(bytes memory auctionId, uint256 windowId) public { + function fulfillWindow(bytes calldata auctionId, uint256 windowId) + inactiveAuction(auctionId) + override public { + _fulfillWindow(auctionId, windowId); + } + + /* + * @dev Fulfill a window index + * @param a͟u͟c͟t͟i͟o͟n͟I͟d͟ Encoded auction parameter identifier + */ + function _fulfillWindow(bytes calldata auctionId, uint256 windowId) internal { + Auction storage state = _auctions[auctionId]; Window storage window = _window[auctionId][windowId]; - if (window.expiry > block.timestamp) { + if (isWindowActive(auctionId)) { revert WindowUnexpired(); } if (window.processed) { @@ -319,10 +356,12 @@ contract RDA is IRDA { window.processed = true; - _auctions[auctionId].reserves -= volume / price; - _auctions[auctionId].proceeds += volume; + uint256 orderAmount = volume * 1e18 / price; - _claims[bidder][auctionId] = abi.encode(refund - volume, claim + (volume / price)); + state.reserves -= orderAmount; + state.proceeds += volume; + + _claims[bidder][auctionId] = abi.encode(refund - volume, claim + orderAmount); emit Fulfillment(auctionId, window.bidId, windowId); } @@ -331,53 +370,64 @@ contract RDA is IRDA { * @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; - } + function remainingTime(bytes calldata auctionId) public view returns (uint256) { + return _auctions[auctionId].duration - elapsedTime(auctionId); } /* * @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 expiryTimestamp - block.timestamp; - } else { + function remainingWindowTime(bytes calldata auctionId) public view returns (uint256) { + if (!isWindowActive(auctionId)) { return 0; - } + } + + return _window[auctionId][_windows[auctionId]].expiry - 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 auctionElapsedTime = timestamp - _auctions[auctionId].startTimestamp; - uint256 windowElapsedTime = _auctions[auctionId].windowDuration * windowIndex; + function elapsedTime(bytes calldata auctionId) public view returns (uint256) { + return block.timestamp - windowElapsedTime(auctionId) - _auctions[auctionId].startTimestamp; + } - if (auctionElapsedTime > windowElapsedTime) { - return auctionElapsedTime - windowElapsedTime; - } else { - return auctionElapsedTime; + function windowElapsedTime(bytes calldata auctionId) public view returns (uint256) { + if (!isWindowInit(auctionId)) { + return 0; } + + uint256 windowIndex = _windows[auctionId]; + uint256 elapsedWindowsTime = _auctions[auctionId].windowDuration * (windowIndex + 1); + + return elapsedWindowsTime - remainingWindowTime(auctionId); + } + + function elapsedTimeFromWindow(bytes calldata auctionId) public view returns (uint256) { + Auction storage state = _auctions[auctionId]; + + uint256 endTimestamp = state.windowTimestamp; + + if (isWindowExpired(auctionId)) { + endTimestamp = _window[auctionId][_windows[auctionId]].expiry; + } + + + return endTimestamp - windowElapsedTime(auctionId) - state.startTimestamp; } /* * @dev Auction management redemption * @param a͟u͟c͟t͟i͟o͟n͟I͟d͟ Encoded auction parameter identifier */ - function withdraw(bytes memory auctionId) + function withdraw(bytes calldata auctionId) inactiveAuction(auctionId) - external { + override external { + ERC20 tokenReserve = ERC20(reserveToken(auctionId)); + ERC20 tokenPurchase = ERC20(purchaseToken(auctionId)); + uint256 proceeds = _auctions[auctionId].proceeds; uint256 reserves = _auctions[auctionId].reserves; @@ -385,10 +435,10 @@ contract RDA is IRDA { delete _auctions[auctionId].reserves; if (proceeds > 0) { - IERC20(purchaseToken(auctionId)).transfer(operatorAddress(auctionId), proceeds); + tokenPurchase.safeTransfer(operatorAddress(auctionId), proceeds); } if (reserves > 0) { - IERC20(reserveToken(auctionId)).transfer(operatorAddress(auctionId), reserves); + tokenReserve.safeTransfer(operatorAddress(auctionId), reserves); } emit Withdraw(auctionId); @@ -398,24 +448,23 @@ contract RDA is IRDA { * @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) + function redeem(address bidder, bytes calldata auctionId) inactiveAuction(auctionId) - external { - bytes memory claimHash = _claims[bidder][auctionId]; + override external { + ERC20 tokenReserve = ERC20(reserveToken(auctionId)); + ERC20 tokenPurchase = ERC20(purchaseToken(auctionId)); - delete _claims[bidder][auctionId]; + bytes memory claimHash = _claims[bidder][auctionId]; (uint256 refund, uint256 claim) = balancesOf(claimHash); - uint256 vestingTimestamp = block.timestamp + _vesting[auctionId].period; - address vestingAddress = _vesting[auctionId].instance; + delete _claims[bidder][auctionId]; if (refund > 0) { - IERC20(purchaseToken(auctionId)).transfer(bidder, refund); + tokenPurchase.safeTransfer(bidder, refund); } if (claim > 0) { - IERC20(reserveToken(auctionId)).approve(vestingAddress, claim); - IDelegatedVesting(vestingAddress).makeCommitment(bidder, claim, vestingTimestamp); + tokenReserve.safeTransfer(bidder, claim); } emit Claim(auctionId, claimHash);