diff --git a/contracts/ChannelAuction.sol b/contracts/ChannelAuction.sol new file mode 100644 index 0000000..32ea86a --- /dev/null +++ b/contracts/ChannelAuction.sol @@ -0,0 +1,635 @@ +pragma solidity >=0.6.8; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/SafeERC20.sol"; +import "./libraries/IterableOrderedOrderList.sol"; +import "@openzeppelin/contracts/math/Math.sol"; +import "@openzeppelin/contracts/math/SafeMath.sol"; +import "./libraries/IdToAddressBiMap.sol"; +import "./libraries/SafeCast.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; + +contract ChannelAuction is Ownable { + using SafeERC20 for IERC20; + using SafeMath for uint64; + using SafeMath for uint96; + using SafeMath for uint256; + using SafeCast for uint256; + using IterableOrderedOrderList for IterableOrderedOrderList.Data; + using IterableOrderedOrderList for bytes32; + using IdToAddressBiMap for IdToAddressBiMap.Data; + + modifier atStageOrderPlacement(uint256 auctionId) { + { + uint256 auctionStartDate = auctionData[auctionId].auctionStartDate; + require( + block.timestamp > auctionStartDate, + "not yet in order placement phase" + ); + require( + block.timestamp < + auctionStartDate.add(auctionData[auctionId].maxDuration), + "auction finished or not yet started" + ); + require( + bytes32(0) == auctionData[auctionId].clearingPriceOrder, + "no longer in order placement phase" + ); + } + _; + } + + modifier atStagePriceCalculation(uint256 auctionId) { + require( + auctionData[auctionId].clearingPriceOrder == bytes32(0), + "Auction already finished" + ); + _; + } + + modifier atStageFinished(uint256 auctionId) { + require( + auctionData[auctionId].clearingPriceOrder != bytes32(0), + "Auction not yet finished" + ); + _; + } + + event NewSellOrder( + uint256 indexed auctionId, + uint64 indexed userId, + uint96 buyAmount, + uint96 sellAmount + ); + event ClaimedFromOrder( + uint256 indexed auctionId, + uint64 indexed userId, + uint96 buyAmount, + uint96 sellAmount + ); + event NewUser(uint64 indexed userId, address indexed userAddress); + event NewAuction( + uint256 indexed auctionId, + IERC20 indexed _auctioningToken, + IERC20 indexed _biddingToken, + uint64 userId, + uint96 auctionStartDate, + uint96 _auctionedSellAmount, + uint96 _auctioneerBuyAmountMinimum, + uint96 _auctioneerBuyAmountMaximum, + uint96 minimumBiddingAmountPerOrder, + uint96 maxDuration + ); + event AuctionCleared( + uint256 indexed auctionId, + uint96 soldAuctioningTokens, + uint96 soldBiddingTokens, + bytes32 clearingPriceOrder + ); + event UserRegistration(address indexed user, uint64 userId); + + struct AuctionData { + IERC20 auctioningToken; + IERC20 biddingToken; + uint96 auctionStartDate; + bytes32 initialAuctionOrder; + uint96 minimumBiddingAmountPerOrder; + bytes32 clearingPriceOrder; + uint96 volumeClearingPriceOrder; + uint96 maxDuration; + uint96 auctioneerBuyAmountMaximum; + bytes32 interimOrder; + uint96 volumeInterimOrder; + } + mapping(uint256 => IterableOrderedOrderList.Data) internal sellOrders; + mapping(uint256 => AuctionData) public auctionData; + + IdToAddressBiMap.Data private registeredUsers; + uint64 public numUsers; + uint256 public auctionCounter; + + constructor() public Ownable() {} + + uint256 public feeNumerator = 0; + uint256 public constant FEE_DENOMINATOR = 1000; + uint64 public feeReceiverUserId = 1; + + function setFeeParameters( + uint256 newFeeNumerator, + address newfeeReceiverAddress + ) public onlyOwner() { + require( + newFeeNumerator <= 5, + "Fee is not allowed to be set higher than 0.5%" + ); + // caution: for currently running auctions, the feeReceiverUserId is changing as well. + feeReceiverUserId = getUserId(newfeeReceiverAddress); + feeNumerator = newFeeNumerator; + } + + // @dev: function to intiate a new auction + // Warning: In case the auction is expected to raise more than + // 2^96 units of the biddingToken, don't start the auction, as + // it will not be settlable. This corresponds to about 79 + // billion DAI. + // + // Prices between biddingToken and auctioningToken are expressed by a + // fraction whose components are stored as uint96. + function initiateAuction( + IERC20 _auctioningToken, + IERC20 _biddingToken, + uint96 _auctionedSellAmount, + uint96 _auctioneerBuyAmountMinimum, + uint96 _auctioneerBuyAmountMaximum, + uint96 _auctionStartDate, + uint96 _minimumBiddingAmountPerOrder, + uint96 _maxDuration + ) public returns (uint256) { + // withdraws sellAmount + fees + _auctioningToken.safeTransferFrom( + msg.sender, + address(this), + _auctionedSellAmount //[0] + ); + if (feeNumerator > 0) { + _auctioningToken.safeTransferFrom( + msg.sender, + registeredUsers.getAddressAt(feeReceiverUserId), + _auctionedSellAmount.mul(feeNumerator).div(FEE_DENOMINATOR) //[1] + ); + } + require(_auctionedSellAmount > 0, "cannot auction zero tokens"); + require( + _auctioneerBuyAmountMinimum > 0, + "_auctioneerBuyAmountMinimum must be positive" + ); + require( + _auctioneerBuyAmountMaximum > _auctioneerBuyAmountMinimum, + "_auctioneerBuyAmountMaximum must be higher than _auctioneerBuyAmountMinimum" + ); + require( + _minimumBiddingAmountPerOrder > 0, + "minimumBiddingAmountPerOrder is not allowed to be zero" + ); + require( + _auctionStartDate + _maxDuration >= now, + "time periods are not configured correctly" + ); + auctionCounter = auctionCounter.add(1); + sellOrders[auctionCounter].initializeEmptyList(); + uint64 userId = getUserId(msg.sender); + auctionData[auctionCounter] = AuctionData( + _auctioningToken, + _biddingToken, + _auctionStartDate, + IterableOrderedOrderList.encodeOrder( + userId, + _auctioneerBuyAmountMinimum, + _auctionedSellAmount + ), + _minimumBiddingAmountPerOrder, + bytes32(0), + 0, + _maxDuration, + _auctioneerBuyAmountMaximum, + bytes32(0), + 0 + ); + emit NewAuction( + auctionCounter, + _auctioningToken, + _biddingToken, + userId, + _auctionStartDate, + _auctionedSellAmount, + _auctioneerBuyAmountMinimum, + _auctioneerBuyAmountMaximum, + _minimumBiddingAmountPerOrder, + _maxDuration + ); + return auctionCounter; + } + + function placeSellOrders( + uint256 auctionId, + uint96 _minBuyAmounts, + uint96 _sellAmounts, + bytes32 _prevSellOrders + ) external atStageOrderPlacement(auctionId) { + _placeSellOrders( + auctionId, + _minBuyAmounts, + _sellAmounts, + _prevSellOrders, + msg.sender + ); + } + + function placeSellOrdersOnBehalf( + uint256 auctionId, + uint96 _minBuyAmounts, + uint96 _sellAmounts, + bytes32 _prevSellOrders, + address orderSubmitter + ) external atStageOrderPlacement(auctionId) { + _placeSellOrders( + auctionId, + _minBuyAmounts, + _sellAmounts, + _prevSellOrders, + orderSubmitter + ); + } + + function _placeSellOrders( + uint256 auctionId, + uint96 _minBuyAmount, + uint96 _sellAmount, + bytes32 _prevSellOrder, + address orderSubmitter + ) internal { + require(_minBuyAmount > 0, "_minBuyAmounts must be greater than 0"); + uint256 currentMinBuyAmountFromAuctioneer = 0; + ( + , + uint96 buyAmountOfInitialAuctionOrder, + uint96 sellAmountOfInitialAuctionOrder + ) = auctionData[auctionId].initialAuctionOrder.decodeOrder(); + { + uint96 auctioneerBuyAmountMaximum = + auctionData[auctionId].auctioneerBuyAmountMaximum; + currentMinBuyAmountFromAuctioneer = getCurrentMinBuyAmountFromAuctioneer( + buyAmountOfInitialAuctionOrder, + auctioneerBuyAmountMaximum, + block + .timestamp + .sub(auctionData[auctionId].auctionStartDate) + .toUint96(), + auctionData[auctionId].maxDuration + ); + } + uint96 minBuyAmount = _minBuyAmount; + { + require( + _minBuyAmount.mul(buyAmountOfInitialAuctionOrder) < + sellAmountOfInitialAuctionOrder.mul(_sellAmount), + "limit price not better than mimimal offer" + ); + if ( + _minBuyAmount.mul(currentMinBuyAmountFromAuctioneer) < + sellAmountOfInitialAuctionOrder.mul(_sellAmount) + ) { + minBuyAmount = _sellAmount + .mul(currentMinBuyAmountFromAuctioneer) + .div(sellAmountOfInitialAuctionOrder) + .toUint96(); + } + } + uint64 userId = getUserId(orderSubmitter); + // orders should have a minimum bid size in order to limit the gas + // required to compute the final price of the auction. + require( + _sellAmount > auctionData[auctionId].minimumBiddingAmountPerOrder, + "order too small" + ); + require( + sellOrders[auctionId].insert( + IterableOrderedOrderList.encodeOrder( + userId, + minBuyAmount, + _sellAmount + ), + _prevSellOrder + ), + "could not insert order" + ); + auctionData[auctionId].biddingToken.safeTransferFrom( + msg.sender, + address(this), + _sellAmount + ); //[1] + emit NewSellOrder(auctionId, userId, minBuyAmount, _sellAmount); + } + + function settleAuctionWithAdditionalOrder( + uint256 auctionId, + uint96 _minBuyAmount, + uint96 _sellAmount, + bytes32 _prevSellOrder + ) public atStagePriceCalculation(auctionId) { + //claculate the maximal outstanding volume and adjust sell amount + _placeSellOrders( + auctionId, + _minBuyAmount, + _sellAmount, + _prevSellOrder, + msg.sender + ); + settleAuction(auctionId); + bytes32[] memory claimOrder = new bytes32[](1); + claimOrder[0] = IterableOrderedOrderList.encodeOrder( + getUserId(msg.sender), + _minBuyAmount, + _sellAmount + ); + claimFromParticipantOrder(auctionId, claimOrder); + } + + function getCurrentMinBuyAmountFromAuctioneer( + uint96 buyAmountMinimum, + uint96 buyAmountMaximum, + uint96 passedTime, + uint96 maxDuration + ) public pure returns (uint96) { + if (passedTime > maxDuration) { + return buyAmountMinimum; + } else { + return + buyAmountMaximum + .sub( + (buyAmountMaximum.sub(buyAmountMinimum)) + .mul(passedTime) + .div(maxDuration) + ) + .toUint96(); + } + } + + // @dev function settling the auction and calculating the price + function settleAuction(uint256 auctionId) + public + atStagePriceCalculation(auctionId) + returns (bytes32 clearingOrder) + { + (uint64 auctioneerId, , uint96 fullAuctionedAmount) = + auctionData[auctionId].initialAuctionOrder.decodeOrder(); + + uint96 minAuctionedBuyAmount = 0; + { + (, uint96 auctioneerBuyAmountMinimum, ) = + auctionData[auctionId].initialAuctionOrder.decodeOrder(); + minAuctionedBuyAmount = getCurrentMinBuyAmountFromAuctioneer( + auctioneerBuyAmountMinimum, + auctionData[auctionId].auctioneerBuyAmountMaximum, + block + .timestamp + .sub(auctionData[auctionId].auctionStartDate) + .toUint96(), + auctionData[auctionId].maxDuration + ); + } + uint256 currentBidSum = 0; + bytes32 currentOrder = IterableOrderedOrderList.QUEUE_START; + uint256 buyAmountOfIter; + uint256 sellAmountOfIter; + uint96 fillVolumeOfAuctioneerOrder = fullAuctionedAmount; + // Sum order up, until fullAuctionedAmount is fully bought or queue end is reached + do { + bytes32 nextOrder = sellOrders[auctionId].next(currentOrder); + if (nextOrder == IterableOrderedOrderList.QUEUE_END) { + if ( + now > + auctionData[auctionId].auctionStartDate.add( + auctionData[auctionId].maxDuration + ) + ) break; + // fractional fillements are accepted, as price won't decrease more + else { + // fractional fillments are not accepted, as prices will continue to decrease + revert("Auction can not yet be settled"); + } + } + currentOrder = nextOrder; + (, buyAmountOfIter, sellAmountOfIter) = currentOrder.decodeOrder(); + currentBidSum = currentBidSum.add(sellAmountOfIter); + } while ( + currentBidSum.mul(buyAmountOfIter) < + fullAuctionedAmount.mul(sellAmountOfIter) + ); + + if ( + currentBidSum > 0 && + currentBidSum.mul(buyAmountOfIter) >= + fullAuctionedAmount.mul(sellAmountOfIter) + ) { + // All considered/summed orders are sufficient to close the auction fully + // at price between current and previous orders. + uint256 uncoveredBids = + currentBidSum.sub( + fullAuctionedAmount.mul(sellAmountOfIter).div( + buyAmountOfIter + ) + ); + + if (sellAmountOfIter >= uncoveredBids) { + //[13] + // Auction fully filled via partial match of currentOrder + uint256 sellAmountClearingOrder = + sellAmountOfIter.sub(uncoveredBids); + auctionData[auctionId] + .volumeClearingPriceOrder = sellAmountClearingOrder + .toUint96(); + currentBidSum = currentBidSum.sub(uncoveredBids); + clearingOrder = currentOrder; + } else { + //[14] + // Auction fully filled via price strictly between currentOrder and the order + // immediately before. For a proof, see the security-considerations.md + currentBidSum = currentBidSum.sub(sellAmountOfIter); + clearingOrder = IterableOrderedOrderList.encodeOrder( + 0, + fullAuctionedAmount, + currentBidSum.toUint96() + ); + } + } else { + // All considered/summed orders are not sufficient to close the auction fully at price of last order //[18] + // Either a higher price must be used or auction is only partially filled + + if (currentBidSum > minAuctionedBuyAmount) { + //[15] + // Price higher than last order would fill the auction + clearingOrder = IterableOrderedOrderList.encodeOrder( + 0, + fullAuctionedAmount, + currentBidSum.toUint96() + ); + } else { + //[16] + // Even at the initial auction price, the auction is partially filled + clearingOrder = IterableOrderedOrderList.encodeOrder( + 0, + fullAuctionedAmount, + minAuctionedBuyAmount + ); + fillVolumeOfAuctioneerOrder = currentBidSum + .mul(fullAuctionedAmount) + .div(minAuctionedBuyAmount) + .toUint96(); + } + } + auctionData[auctionId].clearingPriceOrder = clearingOrder; + processAuctioneerFunds( + auctionId, + fillVolumeOfAuctioneerOrder, + auctioneerId, + fullAuctionedAmount + ); + emit AuctionCleared( + auctionId, + fillVolumeOfAuctioneerOrder, + uint96(currentBidSum), + clearingOrder + ); + } + + function claimFromParticipantOrder( + uint256 auctionId, + bytes32[] memory orders + ) + public + atStageFinished(auctionId) + returns ( + uint256 sumAuctioningTokenAmount, + uint256 sumBiddingTokenAmount + ) + { + for (uint256 i = 0; i < orders.length; i++) { + // Note: we don't need to keep any information about the node since + // no new elements need to be inserted. + require( + sellOrders[auctionId].remove(orders[i]), + "order is no longer claimable" + ); + } + AuctionData memory auction = auctionData[auctionId]; + (, uint96 priceNumerator, uint96 priceDenominator) = + auction.clearingPriceOrder.decodeOrder(); + (uint64 userId, , ) = orders[0].decodeOrder(); + for (uint256 i = 0; i < orders.length; i++) { + (uint64 userIdOrder, uint96 buyAmount, uint96 sellAmount) = + orders[i].decodeOrder(); + require( + userIdOrder == userId, + "only allowed to claim for same user" + ); + //[23] + if (orders[i] == auction.clearingPriceOrder) { + //[25] + sumAuctioningTokenAmount = sumAuctioningTokenAmount.add( + auction.volumeClearingPriceOrder.mul(priceNumerator).div( + priceDenominator + ) + ); + sumBiddingTokenAmount = sumBiddingTokenAmount.add( + sellAmount.sub(auction.volumeClearingPriceOrder) + ); + } else { + if (orders[i].smallerThan(auction.clearingPriceOrder)) { + //[17] + sumAuctioningTokenAmount = sumAuctioningTokenAmount.add( + sellAmount.mul(priceNumerator).div(priceDenominator) + ); + } else { + //[24] + sumBiddingTokenAmount = sumBiddingTokenAmount.add( + sellAmount + ); + } + } + emit ClaimedFromOrder(auctionId, userId, buyAmount, sellAmount); + } + sendOutTokens( + auctionId, + sumAuctioningTokenAmount, + sumBiddingTokenAmount, + userId + ); //[3] + } + + function processAuctioneerFunds( + uint256 auctionId, + uint256 fillVolumeOfAuctioneerOrder, + uint64 auctioneerId, + uint96 fullAuctionedAmount + ) internal { + //[11] + (, uint96 priceNumerator, uint96 priceDenominator) = + auctionData[auctionId].clearingPriceOrder.decodeOrder(); + uint256 unsettledAuctionTokens = + fullAuctionedAmount.sub(fillVolumeOfAuctioneerOrder); + uint256 auctioningTokenAmount = unsettledAuctionTokens; + uint256 biddingTokenAmount = + fillVolumeOfAuctioneerOrder.mul(priceDenominator).div( + priceNumerator + ); + sendOutTokens( + auctionId, + auctioningTokenAmount, + biddingTokenAmount, + auctioneerId + ); //[5] + } + + function sendOutTokens( + uint256 auctionId, + uint256 auctioningTokenAmount, + uint256 biddingTokenAmount, + uint64 userId + ) internal { + address userAddress = registeredUsers.getAddressAt(userId); + if (auctioningTokenAmount > 0) { + auctionData[auctionId].auctioningToken.safeTransfer( + userAddress, + auctioningTokenAmount + ); + } + if (biddingTokenAmount > 0) { + auctionData[auctionId].biddingToken.safeTransfer( + userAddress, + biddingTokenAmount + ); + } + } + + function registerUser(address user) public returns (uint64 userId) { + numUsers = numUsers.add(1).toUint64(); + require( + registeredUsers.insert(numUsers, user), + "User already registered" + ); + userId = numUsers; + emit UserRegistration(user, userId); + } + + function getUserId(address user) public returns (uint64 userId) { + if (registeredUsers.hasAddress(user)) { + userId = registeredUsers.getId(user); + } else { + userId = registerUser(user); + emit NewUser(userId, user); + } + } + + function getSecondsRemainingUntilLastPossibleClose(uint256 auctionId) + public + view + returns (uint256) + { + uint256 auctionEndDate = + auctionData[auctionId].auctionStartDate.add( + auctionData[auctionId].maxDuration + ); + if (auctionEndDate < block.timestamp) { + return 0; + } + return auctionEndDate.sub(block.timestamp); + } + + function containsOrder(uint256 auctionId, bytes32 order) + public + view + returns (bool) + { + return sellOrders[auctionId].contains(order); + } +} diff --git a/contracts/libraries/IterableOrderedOrderList.sol b/contracts/libraries/IterableOrderedOrderList.sol new file mode 100644 index 0000000..e0dc46e --- /dev/null +++ b/contracts/libraries/IterableOrderedOrderList.sol @@ -0,0 +1,194 @@ +pragma solidity >=0.6.8; + +import "@openzeppelin/contracts/math/SafeMath.sol"; + +library IterableOrderedOrderList { + using SafeMath for uint96; + using IterableOrderedOrderList for bytes32; + + // represents smallest possible value for an order under comparison of fn smallerThan() + bytes32 internal constant QUEUE_START = + 0x0000000000000000000000000000000000000000000000000000000000000001; + // represents highest possible value for an order under comparison of fn smallerThan() + bytes32 internal constant QUEUE_END = + 0xffffffffffffffffffffffffffffffffffffffff000000000000000000000001; + + /// The struct is used to implement a modified version of a linked + /// list with sorted elements. The list starts from QUEUE_START to + /// QUEUE_END, and each node keeps track of its successor. + /// Nodes can be added or removed. + /// + /// The list is supposed to be + /// traversed with `next`. If `next` is empty, the node is not part of the + /// list. + struct Data { + mapping(bytes32 => bytes32) nextMap; + } + + struct Order { + uint64 owner; + uint96 buyAmount; + uint96 sellAmount; + } + + function initializeEmptyList(Data storage self) internal { + self.nextMap[QUEUE_START] = QUEUE_END; + } + + function isEmpty(Data storage self) internal view returns (bool) { + return self.nextMap[QUEUE_START] == QUEUE_END; + } + + function insert( + Data storage self, + bytes32 elementToInsert, + bytes32 elementBeforeNewOne + ) internal returns (bool) { + (, , uint96 denominator) = decodeOrder(elementToInsert); + require(denominator != uint96(0), "Inserting zero is not supported"); + require( + elementToInsert != QUEUE_START && elementToInsert != QUEUE_END, + "Inserting element is not valid" + ); + if (contains(self, elementToInsert)) { + return false; + } + if ( + elementBeforeNewOne != QUEUE_START && + !contains(self, elementBeforeNewOne) + ) { + return false; + } + if (!elementBeforeNewOne.smallerThan(elementToInsert)) { + return false; + } + + // `elementBeforeNewOne` belongs now to the linked list. We search the + // largest entry that is smaller than the element to insert. + bytes32 previous; + bytes32 current = elementBeforeNewOne; + do { + previous = current; + current = self.nextMap[current]; + } while (current.smallerThan(elementToInsert)); + // Note: previous < elementToInsert < current + self.nextMap[previous] = elementToInsert; + self.nextMap[elementToInsert] = current; + + return true; + } + + /// Remove an element from the chain, clearing all related storage. + function remove(Data storage self, bytes32 elementToRemove) + internal + returns (bool) + { + if (!contains(self, elementToRemove)) { + return false; + } + self.nextMap[elementToRemove] = bytes32(0); + return true; + } + + function contains(Data storage self, bytes32 value) + internal + view + returns (bool) + { + if (value == QUEUE_START) { + return false; + } + // Note: QUEUE_END is not contained in the list since it has no + // successor. + return self.nextMap[value] != bytes32(0); + } + + // @dev orders are ordered by + // 1. their price - buyAmount/sellAmount + // 2. by the sellAmount + // 3. their userId, + function smallerThan(bytes32 orderLeft, bytes32 orderRight) + internal + pure + returns (bool) + { + ( + uint64 userIdLeft, + uint96 priceNumeratorLeft, + uint96 priceDenominatorLeft + ) = decodeOrder(orderLeft); + ( + uint64 userIdRight, + uint96 priceNumeratorRight, + uint96 priceDenominatorRight + ) = decodeOrder(orderRight); + + if ( + priceNumeratorLeft.mul(priceDenominatorRight) < + priceNumeratorRight.mul(priceDenominatorLeft) + ) return true; + if ( + priceNumeratorLeft.mul(priceDenominatorRight) > + priceNumeratorRight.mul(priceDenominatorLeft) + ) return false; + + if (priceNumeratorLeft < priceNumeratorRight) return true; + if (priceNumeratorLeft > priceNumeratorRight) return false; + require( + userIdLeft != userIdRight, + "user is not allowed to place same order twice" + ); + if (userIdLeft < userIdRight) { + return true; + } + return false; + } + + function first(Data storage self) internal view returns (bytes32) { + require(!isEmpty(self), "Trying to get first from empty set"); + return self.nextMap[QUEUE_START]; + } + + function next(Data storage self, bytes32 value) + internal + view + returns (bytes32) + { + require(value != QUEUE_END, "Trying to get next of last element"); + bytes32 nextElement = self.nextMap[value]; + require( + nextElement != bytes32(0), + "Trying to get next of non-existent element" + ); + return nextElement; + } + + function decodeOrder(bytes32 _orderData) + internal + pure + returns ( + uint64 userId, + uint96 buyAmount, + uint96 sellAmount + ) + { + // Note: converting to uint discards the binary digits that do not fit + // the type. + userId = uint64(uint256(_orderData) >> 192); + buyAmount = uint96(uint256(_orderData) >> 96); + sellAmount = uint96(uint256(_orderData)); + } + + function encodeOrder( + uint64 userId, + uint96 buyAmount, + uint96 sellAmount + ) internal pure returns (bytes32) { + return + bytes32( + (uint256(userId) << 192) + + (uint256(buyAmount) << 96) + + uint256(sellAmount) + ); + } +} diff --git a/contracts/test/IterableOrderedOrderListWrapper.sol b/contracts/test/IterableOrderedOrderListWrapper.sol new file mode 100644 index 0000000..0a905b2 --- /dev/null +++ b/contracts/test/IterableOrderedOrderListWrapper.sol @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: LGPL-3.0-or-newer +pragma solidity ^0.6.0; +import "../libraries/IterableOrderedOrderList.sol"; + +contract IterableOrderedOrderListWrapper { + using IterableOrderedOrderList for IterableOrderedOrderList.Data; + + IterableOrderedOrderList.Data internal data; + + function initializeEmptyList() public { + data.initializeEmptyList(); + } + + function insert(bytes32 value) public returns (bool) { + return data.insert(value, IterableOrderedOrderList.QUEUE_START); + } + + function insertAt(bytes32 value, bytes32 at) public returns (bool) { + return data.insert(value, at); + } + + function remove(bytes32 value) public returns (bool) { + return data.remove(value); + } + + function contains(bytes32 value) public view returns (bool) { + return data.contains(value); + } + + function isEmpty() public view returns (bool) { + return data.isEmpty(); + } + + function first() public view returns (bytes32) { + return data.first(); + } + + function next(bytes32 value) public view returns (bytes32) { + return data.next(value); + } + + function nextMap(bytes32 value) public view returns (bytes32) { + return data.nextMap[value]; + } + + function decodeOrder(bytes32 value) + public + pure + returns ( + uint64, + uint96, + uint96 + ) + { + return IterableOrderedOrderList.decodeOrder(value); + } + + function encodeOrder( + uint64 userId, + uint96 sellAmount, + uint96 buyAmount + ) public pure returns (bytes32) { + return + IterableOrderedOrderList.encodeOrder(userId, sellAmount, buyAmount); + } + + function smallerThan(bytes32 orderLeft, bytes32 orderRight) + public + pure + returns (bool) + { + return IterableOrderedOrderList.smallerThan(orderLeft, orderRight); + } +} diff --git a/contracts/wrappers/DepositAndPlaceOrderForChannelAuction.sol b/contracts/wrappers/DepositAndPlaceOrderForChannelAuction.sol new file mode 100644 index 0000000..a8a32a6 --- /dev/null +++ b/contracts/wrappers/DepositAndPlaceOrderForChannelAuction.sol @@ -0,0 +1,35 @@ +pragma solidity >=0.6.8; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "../ChannelAuction.sol"; +import "../interfaces/IWETH.sol"; + +contract DepositAndPlaceOrderForChannelAuction { + ChannelAuction public immutable channelAuction; + IWETH public immutable nativeTokenWrapper; + + constructor(address channelAuctionAddress, address _nativeTokenWrapper) + public + { + nativeTokenWrapper = IWETH(_nativeTokenWrapper); + channelAuction = ChannelAuction(channelAuctionAddress); + IERC20(_nativeTokenWrapper).approve(channelAuctionAddress, uint256(-1)); + } + + function depositAndPlaceOrder( + uint256 auctionId, + uint96 _minBuyAmount, + bytes32 _prevSellOrder + ) external payable { + require(msg.value < 2**96, "too much value sent"); + nativeTokenWrapper.deposit{value: msg.value}(); + uint96 sellAmount = uint96(msg.value); + + channelAuction.placeSellOrdersOnBehalf( + auctionId, + _minBuyAmount, + sellAmount, + _prevSellOrder, + msg.sender + ); + } +} diff --git a/src/priceCalculation.ts b/src/priceCalculation.ts index bd2291b..067e3fd 100644 --- a/src/priceCalculation.ts +++ b/src/priceCalculation.ts @@ -163,7 +163,9 @@ function printOrders(orders: Order[], isInitialOrder: boolean, debug = false) { " for ", order.buyAmount.toString(), " at price of", - order.sellAmount.div(order.buyAmount).toString(), + order.buyAmount.gt(BigNumber.from("0")) + ? order.sellAmount.div(order.buyAmount).toString() + : "infinity", ); }); } else { @@ -175,7 +177,9 @@ function printOrders(orders: Order[], isInitialOrder: boolean, debug = false) { " for ", order.buyAmount.toString(), " at price of", - order.buyAmount.div(order.sellAmount).toString(), + order.buyAmount.gt(BigNumber.from("0")) + ? order.sellAmount.div(order.buyAmount).toString() + : "infinity", ); }); } @@ -260,6 +264,10 @@ export async function getAllSellOrders( }); const filterOrderCancellations = easyAuction.filters.CancellationSellOrder; + if (filterOrderCancellations == undefined) { + // For ChannelAuctions, there are no cancellations and hence, we return all sell orders + return sellOrders; + } const logsForCancellations = await easyAuction.queryFilter( filterOrderCancellations(), 0, @@ -283,7 +291,7 @@ export async function getAllSellOrders( } export async function createTokensAndMintAndApprove( - easyAuction: Contract, + auctionContract: Contract, users: Wallet[], hre: HardhatRuntimeEnvironment, ): Promise<{ auctioningToken: Contract; biddingToken: Contract }> { @@ -292,15 +300,15 @@ export async function createTokensAndMintAndApprove( const auctioningToken = await ERC20.deploy("BT", "BT"); for (const user of users) { - await biddingToken.mint(user.address, BigNumber.from(10).pow(30)); + await biddingToken.mint(user.address, BigNumber.from(10).pow(32)); await biddingToken .connect(user) - .approve(easyAuction.address, BigNumber.from(10).pow(30)); + .approve(auctionContract.address, BigNumber.from(10).pow(32)); - await auctioningToken.mint(user.address, BigNumber.from(10).pow(30)); + await auctioningToken.mint(user.address, BigNumber.from(10).pow(32)); await auctioningToken .connect(user) - .approve(easyAuction.address, BigNumber.from(10).pow(30)); + .approve(auctionContract.address, BigNumber.from(10).pow(32)); } return { auctioningToken: auctioningToken, biddingToken: biddingToken }; } @@ -332,3 +340,30 @@ export async function placeOrders( ); } } +export async function placeOrdersForChannelAuction( + easyAuction: Contract, + sellOrders: Order[], + auctionId: BigNumber, + hre: HardhatRuntimeEnvironment, +): Promise { + const newSellOrders = []; + for (const sellOrder of sellOrders) { + const tx = await easyAuction + .connect( + await hre.waffle.provider.getWallets()[sellOrder.userId.toNumber() - 1], + ) + .placeSellOrders( + auctionId, + sellOrder.buyAmount, + sellOrder.sellAmount, + queueStartElement, + ); + const newSellOrderEventData = (await tx.wait()).events.pop(); + newSellOrders.push({ + sellAmount: newSellOrderEventData.args.sellAmount, + buyAmount: newSellOrderEventData.args.buyAmount, + userId: newSellOrderEventData.args.userId, + }); + } + return newSellOrders; +} diff --git a/src/ts/types.ts b/src/ts/types.ts index 87dc7f3..0b9873a 100644 --- a/src/ts/types.ts +++ b/src/ts/types.ts @@ -13,3 +13,14 @@ export interface InitiateAuctionInput { allowListManager: BytesLike; allowListData: BytesLike; } + +export interface InitiateChannelAuctionInput { + auctioningToken: Contract; + biddingToken: Contract; + _auctionedSellAmount: BigNumberish; + _auctioneerBuyAmountMinimum: BigNumberish; + _auctioneerBuyAmountMaximum: BigNumberish; + _auctionStartDate: BigNumberish; + _minimumBiddingAmountPerOrder: BigNumberish; + _maxDuration: BigNumberish; +} diff --git a/test/contract/Channelauction.spec.ts b/test/contract/Channelauction.spec.ts new file mode 100644 index 0000000..fc06436 --- /dev/null +++ b/test/contract/Channelauction.spec.ts @@ -0,0 +1,2730 @@ +import { expect } from "chai"; +import { Contract, BigNumber } from "ethers"; +import hre, { ethers, waffle } from "hardhat"; +import "@nomiclabs/hardhat-ethers"; + +import { + toReceivedFunds, + encodeOrder, + queueStartElement, + createTokensAndMintAndApprove, + placeOrdersForChannelAuction, + calculateClearingPrice, +} from "../../src/priceCalculation"; + +import { + createChannelAuctionWithDefaults, + createChannelAuctionWithDefaultsAndReturnId, +} from "./defaultContractInteractions"; +import { + sendTxAndGetReturnValue, + closeChannelAuction, + claimFromAllOrders, + startChannelAuction, +} from "./utilities"; + +///////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Some tests use different test cases 1,..,10. These test cases are illustrated in the following jam board: +// https://jamboard.google.com/d/1DMgMYCQQzsSLKPq_hlK3l32JNBbRdIhsOrLB1oHaEYY/edit?usp=sharing +///////////////////////////////////////////////////////////////////////////////////////////////////////////// + +describe("ChannelAuction", async () => { + const [user_1, user_2, user_3, user_4] = await waffle.provider.getWallets(); + let channelAuction: Contract; + beforeEach(async () => { + const ChannelAuction = await ethers.getContractFactory("ChannelAuction"); + channelAuction = await ChannelAuction.deploy(); + }); + describe("initiate Auction", async () => { + it("throws if minimumBiddingAmountPerOrder is zero", async () => { + const { + auctioningToken, + biddingToken, + } = await createTokensAndMintAndApprove( + channelAuction, + [user_1, user_2], + hre, + ); + + await expect( + createChannelAuctionWithDefaults(channelAuction, { + auctioningToken, + biddingToken, + _minimumBiddingAmountPerOrder: 0, + }), + ).to.be.revertedWith( + "minimumBiddingAmountPerOrder is not allowed to be zero", + ); + }); + it("throws if auctioned amount is zero", async () => { + const { + auctioningToken, + biddingToken, + } = await createTokensAndMintAndApprove( + channelAuction, + [user_1, user_2], + hre, + ); + + await expect( + createChannelAuctionWithDefaults(channelAuction, { + auctioningToken, + biddingToken, + _auctionedSellAmount: 0, + }), + ).to.be.revertedWith("cannot auction zero tokens"); + }); + it("throws if auction is a giveaway", async () => { + const { + auctioningToken, + biddingToken, + } = await createTokensAndMintAndApprove( + channelAuction, + [user_1, user_2], + hre, + ); + + await expect( + createChannelAuctionWithDefaults(channelAuction, { + auctioningToken, + biddingToken, + _auctioneerBuyAmountMinimum: 0, + }), + ).to.be.revertedWith("_auctioneerBuyAmountMinimum must be positive"); + }); + it("throws if auction end is not in the future", async () => { + const { + auctioningToken, + biddingToken, + } = await createTokensAndMintAndApprove( + channelAuction, + [user_1, user_2], + hre, + ); + + const now = (await ethers.provider.getBlock("latest")).timestamp; + await expect( + createChannelAuctionWithDefaults(channelAuction, { + auctioningToken, + biddingToken, + _auctionStartDate: now - 60 * 60, + _maxDuration: 60, + }), + ).to.be.revertedWith("time periods are not configured correctly"); + }); + it("initiateAuction stores the parameters correctly", async () => { + const { + auctioningToken, + biddingToken, + } = await createTokensAndMintAndApprove( + channelAuction, + [user_1, user_2], + hre, + ); + + const now = (await ethers.provider.getBlock("latest")).timestamp; + const auctionStartDate = now + 1337; + const _maxDuration = BigNumber.from(2000); + + const initialAuctionOrder = { + sellAmount: ethers.utils.parseEther("1"), + buyAmount: ethers.utils.parseEther("1"), + userId: BigNumber.from(1), + }; + const auctionId: BigNumber = await createChannelAuctionWithDefaultsAndReturnId( + channelAuction, + { + auctioningToken, + biddingToken, + _auctionedSellAmount: initialAuctionOrder.sellAmount, + _auctioneerBuyAmountMaximum: initialAuctionOrder.buyAmount.mul(2), + _auctioneerBuyAmountMinimum: initialAuctionOrder.buyAmount, + _auctionStartDate: auctionStartDate, + _maxDuration, + }, + ); + const auctionData = await channelAuction.auctionData(auctionId); + expect(auctionData.auctioningToken).to.equal(auctioningToken.address); + expect(auctionData.biddingToken).to.equal(biddingToken.address); + expect(auctionData.initialAuctionOrder).to.equal( + encodeOrder(initialAuctionOrder), + ); + expect(auctionData.auctionStartDate).to.be.equal(auctionStartDate); + expect(auctionData.maxDuration).to.be.equal(_maxDuration); + expect(auctionData.auctioneerBuyAmountMaximum).to.be.equal( + initialAuctionOrder.buyAmount.mul(2), + ); + await expect(auctionData.clearingPriceOrder).to.equal( + encodeOrder({ + userId: BigNumber.from(0), + sellAmount: ethers.utils.parseEther("0"), + buyAmount: ethers.utils.parseEther("0"), + }), + ); + expect(auctionData.volumeClearingPriceOrder).to.be.equal(0); + + // expect(await auctioningToken.balanceOf(channelAuction.address)).to.equal( + // ethers.utils.parseEther("1"), + // ); + }); + }); + + describe("getUserId", async () => { + it("creates new userIds", async () => { + expect( + await sendTxAndGetReturnValue( + channelAuction, + "getUserId(address)", + user_1.address, + ), + ).to.equal(1); + expect( + await sendTxAndGetReturnValue( + channelAuction, + "getUserId(address)", + user_2.address, + ), + ).to.equal(2); + expect( + await sendTxAndGetReturnValue( + channelAuction, + "getUserId(address)", + user_1.address, + ), + ).to.equal(1); + }); + }); + // Maybe we have to disable that functionality + // describe("placeOrdersOnBehalf", async () => { + // it("places a new order and checks that tokens were transferred", async () => { + // const { + // auctioningToken, + // biddingToken, + // } = await createTokensAndMintAndApprove( + // channelAuction, + // [user_1, user_2], + // hre, + // ); + // const now = (await ethers.provider.getBlock("latest")).timestamp; + // const auctionId: BigNumber = await createChannelAuctionWithDefaultsAndReturnId( + // channelAuction, + // { + // auctioningToken, + // biddingToken, + // _auctionStartDate: now + 3600, + // }, + // ); + + // const balanceBeforeOrderPlacement = await biddingToken.balanceOf( + // user_1.address, + // ); + // const balanceBeforeOrderPlacementOfUser2 = await biddingToken.balanceOf( + // user_2.address, + // ); + // const sellAmount = ethers.utils.parseEther("2").add(1); + // const buyAmount = ethers.utils.parseEther("1"); + // await startChannelAuction(channelAuction, auctionId); + + // await channelAuction + // .connect(user_1) + // .placeSellOrdersOnBehalf( + // auctionId, + // [buyAmount], + // [sellAmount], + // [queueStartElement], + // user_2.address, + // ); + + // expect(await biddingToken.balanceOf(channelAuction.address)).to.equal( + // sellAmount, + // ); + // expect(await biddingToken.balanceOf(user_1.address)).to.equal( + // balanceBeforeOrderPlacement.sub(sellAmount), + // ); + // expect(await biddingToken.balanceOf(user_2.address)).to.equal( + // balanceBeforeOrderPlacementOfUser2, + // ); + // }); + // }); + + describe("placeOrders", async () => { + it("one can not place orders, if auction is not yet initiated", async () => { + await expect( + channelAuction.placeSellOrders( + 0, + ethers.utils.parseEther("1"), + ethers.utils.parseEther("1").add(1), + queueStartElement, + ), + ).to.be.revertedWith("auction finished or not yet started"); + }); + it("one can not place orders, if auction is not yet started", async () => { + const { + auctioningToken, + biddingToken, + } = await createTokensAndMintAndApprove( + channelAuction, + [user_1, user_2], + hre, + ); + const now = (await ethers.provider.getBlock("latest")).timestamp; + const auctionId: BigNumber = await createChannelAuctionWithDefaultsAndReturnId( + channelAuction, + { + auctioningToken, + biddingToken, + _auctionStartDate: now + 360000, + }, + ); + + await expect( + channelAuction.placeSellOrders( + auctionId, + ethers.utils.parseEther("4").add(1), + ethers.utils.parseEther("1"), + queueStartElement, + ), + ).to.be.revertedWith("not yet in order placement phase"); + }); + it("one can not place orders, if auction is over", async () => { + const { + auctioningToken, + biddingToken, + } = await createTokensAndMintAndApprove( + channelAuction, + [user_1, user_2], + hre, + ); + const auctionId: BigNumber = await createChannelAuctionWithDefaultsAndReturnId( + channelAuction, + { + auctioningToken, + biddingToken, + }, + ); + await closeChannelAuction(channelAuction, auctionId); + await expect( + channelAuction.placeSellOrders( + auctionId, + ethers.utils.parseEther("1"), + ethers.utils.parseEther("1").add(1), + queueStartElement, + ), + ).to.be.revertedWith("auction finished or not yet started"); + }); + it("one can not place orders, with a worser or same rate", async () => { + const { + auctioningToken, + biddingToken, + } = await createTokensAndMintAndApprove( + channelAuction, + [user_1, user_2], + hre, + ); + const _auctioneerBuyAmountMaximum = ethers.utils.parseEther("2"); + const _auctioneerBuyAmountMinimum = ethers.utils.parseEther("1"); + const _auctionedSellAmount = ethers.utils.parseEther("1"); + const auctionId: BigNumber = await createChannelAuctionWithDefaultsAndReturnId( + channelAuction, + { + auctioningToken, + biddingToken, + _auctionedSellAmount, + _auctioneerBuyAmountMaximum, + _auctioneerBuyAmountMinimum, + }, + ); + await startChannelAuction(channelAuction, auctionId); + // todo: maybe test is not accurate + await expect( + channelAuction.placeSellOrders( + auctionId, + _auctioneerBuyAmountMinimum, + _auctionedSellAmount, + queueStartElement, + ), + ).to.be.revertedWith("limit price not better than mimimal offer"); + await expect( + channelAuction.placeSellOrders( + auctionId, + _auctioneerBuyAmountMaximum, + _auctionedSellAmount, + queueStartElement, + ), + ).to.be.revertedWith("limit price not better than mimimal offer"); + }); + it("one can not place orders with buyAmount == 0", async () => { + const { + auctioningToken, + biddingToken, + } = await createTokensAndMintAndApprove( + channelAuction, + [user_1, user_2], + hre, + ); + const auctionId: BigNumber = await createChannelAuctionWithDefaultsAndReturnId( + channelAuction, + { + auctioningToken, + biddingToken, + }, + ); + await startChannelAuction(channelAuction, auctionId); + + await expect( + channelAuction.placeSellOrders( + auctionId, + ethers.utils.parseEther("0"), + ethers.utils.parseEther("1"), + queueStartElement, + ), + ).to.be.revertedWith("_minBuyAmounts must be greater than 0"); + }); + it("can't place an order twice", async () => { + const { + auctioningToken, + biddingToken, + } = await createTokensAndMintAndApprove( + channelAuction, + [user_1, user_2], + hre, + ); + const auctionId: BigNumber = await createChannelAuctionWithDefaultsAndReturnId( + channelAuction, + { + auctioningToken, + biddingToken, + }, + ); + await startChannelAuction(channelAuction, auctionId); + + await expect(() => + channelAuction.placeSellOrders( + auctionId, + ethers.utils.parseEther("1").sub(1), + ethers.utils.parseEther("1"), + queueStartElement, + ), + ).to.changeTokenBalances( + biddingToken, + [user_1], + [ethers.utils.parseEther("-1")], + ); + await expect( + channelAuction.placeSellOrders( + auctionId, + ethers.utils.parseEther("1").sub(1), + ethers.utils.parseEther("1"), + queueStartElement, + ), + ).to.be.revertedWith("could not insert order"); + }); + it("places a new order and checks that tokens were transferred", async () => { + const { + auctioningToken, + biddingToken, + } = await createTokensAndMintAndApprove( + channelAuction, + [user_1, user_2], + hre, + ); + const auctionId: BigNumber = await createChannelAuctionWithDefaultsAndReturnId( + channelAuction, + { + auctioningToken, + biddingToken, + }, + ); + + const balanceBeforeOrderPlacement = await biddingToken.balanceOf( + user_1.address, + ); + const sellAmount = ethers.utils.parseEther("1").add(1); + const buyAmount = ethers.utils.parseEther("1"); + await startChannelAuction(channelAuction, auctionId); + + await channelAuction.placeSellOrders( + auctionId, + buyAmount, + sellAmount, + queueStartElement, + ); + const transferredbiddingTokenAmount = sellAmount; + + expect(await biddingToken.balanceOf(channelAuction.address)).to.equal( + transferredbiddingTokenAmount, + ); + expect(await biddingToken.balanceOf(user_1.address)).to.equal( + balanceBeforeOrderPlacement.sub(transferredbiddingTokenAmount), + ); + }); + it("throws, if DDOS attack with small order amounts is started", async () => { + const initialAuctionOrder = { + sellAmount: ethers.utils.parseEther("1"), + buyAmount: ethers.utils.parseEther("1"), + userId: BigNumber.from(1), + }; + const sellOrders = [ + { + sellAmount: ethers.utils.parseEther("1").div(5000), + buyAmount: ethers.utils.parseEther("1").div(10000), + userId: BigNumber.from(1), + }, + ]; + + const { + auctioningToken, + biddingToken, + } = await createTokensAndMintAndApprove( + channelAuction, + [user_1, user_2], + hre, + ); + + const auctionId: BigNumber = await createChannelAuctionWithDefaultsAndReturnId( + channelAuction, + { + auctioningToken, + biddingToken, + _auctionedSellAmount: initialAuctionOrder.sellAmount, + _auctioneerBuyAmountMinimum: initialAuctionOrder.buyAmount, + _minimumBiddingAmountPerOrder: ethers.utils.parseEther("1").div(100), + }, + ); + await startChannelAuction(channelAuction, auctionId); + await expect( + channelAuction.placeSellOrders( + auctionId, + sellOrders[0].buyAmount, + sellOrders[0].sellAmount, + queueStartElement, + ), + ).to.be.revertedWith("order too small"); + }); + it("fails, if transfers are failing", async () => { + const { + auctioningToken, + biddingToken, + } = await createTokensAndMintAndApprove( + channelAuction, + [user_1, user_2], + hre, + ); + const auctionId: BigNumber = await createChannelAuctionWithDefaultsAndReturnId( + channelAuction, + { + auctioningToken, + biddingToken, + }, + ); + const sellAmount = ethers.utils.parseEther("1").add(1); + const buyAmount = ethers.utils.parseEther("1"); + await biddingToken.approve( + channelAuction.address, + ethers.utils.parseEther("0"), + ); + await startChannelAuction(channelAuction, auctionId); + await expect( + channelAuction.placeSellOrders( + auctionId, + buyAmount, + sellAmount, + queueStartElement, + ), + ).to.be.revertedWith("ERC20: transfer amount exceeds allowance"); + }); + }); + // describe("precalculateSellAmountSum", async () => { + // it("fails if too many orders are considered", async () => { + // const initialAuctionOrder = { + // sellAmount: ethers.utils.parseEther("1"), + // buyAmount: ethers.utils.parseEther("1"), + // userId: BigNumber.from(1), + // }; + // const sellOrders = [ + // { + // sellAmount: ethers.utils.parseEther("1").mul(2), + // buyAmount: ethers.utils.parseEther("1").div(5), + // userId: BigNumber.from(1), + // }, + // { + // sellAmount: ethers.utils.parseEther("1").mul(2), + // buyAmount: ethers.utils.parseEther("1"), + // userId: BigNumber.from(1), + // }, + + // { + // sellAmount: ethers.utils.parseEther("1").mul(2).add(1), + // buyAmount: ethers.utils.parseEther("1").mul(2), + // userId: BigNumber.from(1), + // }, + // ]; + // const { + // auctioningToken, + // biddingToken, + // } = await createTokensAndMintAndApprove( + // channelAuction, + // [user_1, user_2], + // hre, + // ); + + // const auctionId: BigNumber = await createChannelAuctionWithDefaultsAndReturnId( + // channelAuction, + // { + // auctioningToken, + // biddingToken, + // _auctionedSellAmount: initialAuctionOrder.sellAmount, + // _auctioneerBuyAmountMinimum: initialAuctionOrder.buyAmount, + // }, + // ); + // await placeOrdersForChannelAuction( + // channelAuction, + // sellOrders, + // auctionId, + // hre, + // ); + + // await closeChannelAuction(channelAuction, auctionId); + // await expect( + // channelAuction.precalculateSellAmountSum(auctionId, 3), + // ).to.be.revertedWith("too many orders summed up"); + // }); + // it("fails if queue end is reached", async () => { + // const initialAuctionOrder = { + // sellAmount: ethers.utils.parseEther("1"), + // buyAmount: ethers.utils.parseEther("1"), + // userId: BigNumber.from(1), + // }; + // const sellOrders = [ + // { + // sellAmount: ethers.utils.parseEther("1").mul(2), + // buyAmount: ethers.utils.parseEther("1").div(5), + // userId: BigNumber.from(1), + // }, + // ]; + // const { + // auctioningToken, + // biddingToken, + // } = await createTokensAndMintAndApprove( + // channelAuction, + // [user_1, user_2], + // hre, + // ); + + // const auctionId: BigNumber = await createChannelAuctionWithDefaultsAndReturnId( + // channelAuction, + // { + // auctioningToken, + // biddingToken, + // _auctionedSellAmount: initialAuctionOrder.sellAmount, + // _auctioneerBuyAmountMinimum: initialAuctionOrder.buyAmount, + // }, + // ); + // await placeOrdersForChannelAuction( + // channelAuction, + // sellOrders, + // auctionId, + // hre, + // ); + + // await closeChannelAuction(channelAuction, auctionId); + // await expect( + // channelAuction.precalculateSellAmountSum(auctionId, 2), + // ).to.be.revertedWith("reached end of order list"); + // }); + // it("verifies that interimSumBidAmount and iterOrder is set correctly", async () => { + // const initialAuctionOrder = { + // sellAmount: ethers.utils.parseEther("1"), + // buyAmount: ethers.utils.parseEther("1"), + // userId: BigNumber.from(1), + // }; + // const sellOrders = [ + // { + // sellAmount: ethers.utils.parseEther("1").mul(2), + // buyAmount: ethers.utils.parseEther("1").div(5), + // userId: BigNumber.from(1), + // }, + // { + // sellAmount: ethers.utils.parseEther("1").mul(2), + // buyAmount: ethers.utils.parseEther("1"), + // userId: BigNumber.from(1), + // }, + + // { + // sellAmount: ethers.utils.parseEther("1").mul(2).add(1), + // buyAmount: ethers.utils.parseEther("1").mul(2), + // userId: BigNumber.from(1), + // }, + // ]; + // const { + // auctioningToken, + // biddingToken, + // } = await createTokensAndMintAndApprove( + // channelAuction, + // [user_1, user_2], + // hre, + // ); + + // const auctionId: BigNumber = await createChannelAuctionWithDefaultsAndReturnId( + // channelAuction, + // { + // auctioningToken, + // biddingToken, + // _auctionedSellAmount: initialAuctionOrder.sellAmount, + // _auctioneerBuyAmountMinimum: initialAuctionOrder.buyAmount, + // }, + // ); + // await placeOrdersForChannelAuction( + // channelAuction, + // sellOrders, + // auctionId, + // hre, + // ); + + // await closeChannelAuction(channelAuction, auctionId); + + // await channelAuction.precalculateSellAmountSum(auctionId, 1); + // const auctionData = await channelAuction.auctionData(auctionId); + // expect(auctionData.interimSumBidAmount).to.equal( + // sellOrders[0].sellAmount, + // ); + + // expect(auctionData.interimOrder).to.equal(encodeOrder(sellOrders[0])); + // }); + // it("verifies that interimSumBidAmount and iterOrder takes correct starting values by applying twice", async () => { + // const initialAuctionOrder = { + // sellAmount: ethers.utils.parseEther("1"), + // buyAmount: ethers.utils.parseEther("1"), + // userId: BigNumber.from(1), + // }; + // const sellOrders = [ + // { + // sellAmount: ethers.utils.parseEther("1"), + // buyAmount: ethers.utils.parseEther("1").div(10), + // userId: BigNumber.from(1), + // }, + // { + // sellAmount: ethers.utils.parseEther("1"), + // buyAmount: ethers.utils.parseEther("1").div(10), + // userId: BigNumber.from(2), + // }, + // { + // sellAmount: ethers.utils.parseEther("1").mul(2), + // buyAmount: ethers.utils.parseEther("1"), + // userId: BigNumber.from(1), + // }, + + // { + // sellAmount: ethers.utils.parseEther("1").mul(2).add(1), + // buyAmount: ethers.utils.parseEther("1").mul(2), + // userId: BigNumber.from(1), + // }, + // ]; + // const { + // auctioningToken, + // biddingToken, + // } = await createTokensAndMintAndApprove( + // channelAuction, + // [user_1, user_2], + // hre, + // ); + + // const auctionId: BigNumber = await createChannelAuctionWithDefaultsAndReturnId( + // channelAuction, + // { + // auctioningToken, + // biddingToken, + // _auctionedSellAmount: initialAuctionOrder.sellAmount, + // _auctioneerBuyAmountMinimum: initialAuctionOrder.buyAmount, + // }, + // ); + // await startChannelAuction(channelAuction, auctionId); + + // await placeOrdersForChannelAuction( + // channelAuction, + // sellOrders, + // auctionId, + // hre, + // ); + + // await closeChannelAuction(channelAuction, auctionId); + + // await channelAuction.precalculateSellAmountSum(auctionId, 1); + // await channelAuction.precalculateSellAmountSum(auctionId, 1); + // const auctionData = await channelAuction.auctionData(auctionId); + // expect(auctionData.interimSumBidAmount).to.equal( + // sellOrders[0].sellAmount.add(sellOrders[0].sellAmount), + // ); + + // expect(auctionData.interimOrder).to.equal(encodeOrder(sellOrders[1])); + // }); + // }); + describe("settleAuction", async () => { + it("checks case 4, it verifies the price in case of clearing order == initialAuctionOrder", async () => { + const { + auctioningToken, + biddingToken, + } = await createTokensAndMintAndApprove( + channelAuction, + [user_1, user_2], + hre, + ); + const auctionInitParameters = { + auctioningToken, + biddingToken, + _auctionedSellAmount: ethers.utils.parseEther("1"), + _auctioneerBuyAmountMinimum: ethers.utils.parseEther("1"), + _auctioneerBuyAmountMaximum: ethers.utils.parseEther("2"), + _auctioneerUserId: BigNumber.from(0), + }; + const sellOrders = [ + { + sellAmount: ethers.utils.parseEther("1").div(10), + buyAmount: ethers.utils.parseEther("1").div(20), + userId: BigNumber.from(1), + }, + ]; + const auctionId: BigNumber = await createChannelAuctionWithDefaultsAndReturnId( + channelAuction, + auctionInitParameters, + ); + await startChannelAuction(channelAuction, auctionId); + await placeOrdersForChannelAuction( + channelAuction, + sellOrders, + auctionId, + hre, + ); + await closeChannelAuction(channelAuction, auctionId); + const { clearingOrder: price } = await calculateClearingPrice( + channelAuction, + auctionId, + ); + await expect(channelAuction.settleAuction(auctionId)) + .to.emit(channelAuction, "AuctionCleared") + .withArgs( + auctionId, + sellOrders[0].sellAmount.mul(price.buyAmount).div(price.sellAmount), + sellOrders[0].sellAmount, + encodeOrder({ + sellAmount: auctionInitParameters._auctioneerBuyAmountMinimum, + buyAmount: auctionInitParameters._auctionedSellAmount, + userId: auctionInitParameters._auctioneerUserId, + }), + ); + const auctionData = await channelAuction.auctionData(auctionId); + expect(auctionData.clearingPriceOrder).to.equal(encodeOrder(price)); + }); + it("checks case 4, it verifies the price in case of clearingOrder == initialAuctionOrder with 3 orders", async () => { + const { + auctioningToken, + biddingToken, + } = await createTokensAndMintAndApprove( + channelAuction, + [user_1, user_2, user_3, user_4], + hre, + ); + const auctionInitParameters = { + auctioningToken, + biddingToken, + _auctionedSellAmount: ethers.utils.parseEther("2"), + _auctioneerBuyAmountMinimum: ethers.utils.parseEther("0.5"), + _auctioneerBuyAmountMaximum: ethers.utils.parseEther("1"), + _auctioneerUserId: BigNumber.from(0), + }; + let sellOrders = [ + { + sellAmount: ethers.utils.parseEther("0.1"), + buyAmount: ethers.utils.parseEther("0.1"), + userId: BigNumber.from(1), + }, + { + sellAmount: ethers.utils.parseEther("0.1"), + buyAmount: ethers.utils.parseEther("0.099"), + userId: BigNumber.from(2), + }, + { + sellAmount: ethers.utils.parseEther("0.1"), + buyAmount: ethers.utils.parseEther("0.1"), + userId: BigNumber.from(3), + }, + ]; + + const auctionId: BigNumber = await createChannelAuctionWithDefaultsAndReturnId( + channelAuction, + auctionInitParameters, + ); + await startChannelAuction(channelAuction, auctionId); + sellOrders = await placeOrdersForChannelAuction( + channelAuction, + sellOrders, + auctionId, + hre, + ); + + await closeChannelAuction(channelAuction, auctionId); + const { clearingOrder: price } = await calculateClearingPrice( + channelAuction, + auctionId, + ); + await expect(channelAuction.settleAuction(auctionId)) + .to.emit(channelAuction, "AuctionCleared") + .withArgs( + auctionId, + sellOrders[0].sellAmount + .mul(3) + .mul(price.buyAmount) + .div(price.sellAmount), + sellOrders[0].sellAmount.mul(3), + encodeOrder({ + sellAmount: auctionInitParameters._auctioneerBuyAmountMinimum, + buyAmount: auctionInitParameters._auctionedSellAmount, + userId: auctionInitParameters._auctioneerUserId, + }), + ); + const auctionData = await channelAuction.auctionData(auctionId); + expect(auctionData.clearingPriceOrder).to.equal(encodeOrder(price)); + await claimFromAllOrders(channelAuction, auctionId, sellOrders); + }); + it("checks case 6, it verifies the price in case of clearingOrder == initialOrder, although last iterOrder would also be possible", async () => { + // This test demonstrates the case 6, + // where price could be either the auctioningOrder or sellOrder + const { + auctioningToken, + biddingToken, + } = await createTokensAndMintAndApprove( + channelAuction, + [user_1, user_2], + hre, + ); + const auctionInitParameters = { + auctioningToken, + biddingToken, + _auctionedSellAmount: ethers.utils.parseEther("500"), + _auctioneerBuyAmountMinimum: ethers.utils.parseEther("1"), + _auctioneerBuyAmountMaximum: ethers.utils.parseEther("2"), + _auctioneerUserId: BigNumber.from(0), + }; + const sellOrders = [ + { + sellAmount: ethers.utils.parseEther("1"), + buyAmount: ethers.utils.parseEther("260"), + userId: BigNumber.from(1), + }, + ]; + + const auctionId: BigNumber = await createChannelAuctionWithDefaultsAndReturnId( + channelAuction, + auctionInitParameters, + ); + await startChannelAuction(channelAuction, auctionId); + await placeOrdersForChannelAuction( + channelAuction, + sellOrders, + auctionId, + hre, + ); + + await closeChannelAuction(channelAuction, auctionId); + + await expect(channelAuction.settleAuction(auctionId)) + .to.emit(channelAuction, "AuctionCleared") + .withArgs( + auctionId, + auctionInitParameters._auctionedSellAmount, + sellOrders[0].sellAmount, + encodeOrder({ + sellAmount: auctionInitParameters._auctioneerBuyAmountMinimum, + buyAmount: auctionInitParameters._auctionedSellAmount, + userId: auctionInitParameters._auctioneerUserId, + }), + ); + const auctionData = await channelAuction.auctionData(auctionId); + expect(auctionData.clearingPriceOrder).to.equal( + encodeOrder({ + sellAmount: auctionInitParameters._auctioneerBuyAmountMinimum, + buyAmount: auctionInitParameters._auctionedSellAmount, + userId: auctionInitParameters._auctioneerUserId, + }), + ); + await claimFromAllOrders(channelAuction, auctionId, sellOrders); + }); + it("checks case 12, it verifies that price can not be the initial auction price (Adam's case)", async () => { + const { + auctioningToken, + biddingToken, + } = await createTokensAndMintAndApprove( + channelAuction, + [user_1, user_2], + hre, + ); + const auctionInitParameters = { + auctioningToken, + biddingToken, + _auctionedSellAmount: ethers.utils.parseEther("1"), + _auctioneerBuyAmountMinimum: ethers.utils.parseEther("0.1"), + _auctioneerBuyAmountMaximum: ethers.utils.parseEther("0.8"), + _auctioneerUserId: BigNumber.from(0), + }; + const sellOrders = [ + { + sellAmount: ethers.utils.parseEther("0.5"), + buyAmount: ethers.utils.parseEther("1"), + userId: BigNumber.from(1), + }, + { + sellAmount: BigNumber.from(2), + buyAmount: BigNumber.from(4), + userId: BigNumber.from(2), + }, + ]; + + const auctionId: BigNumber = await createChannelAuctionWithDefaultsAndReturnId( + channelAuction, + auctionInitParameters, + ); + await startChannelAuction(channelAuction, auctionId); + + await placeOrdersForChannelAuction( + channelAuction, + sellOrders, + auctionId, + hre, + ); + + await closeChannelAuction(channelAuction, auctionId); + + await channelAuction.settleAuction(auctionId); + await channelAuction.auctionData(auctionId); + await claimFromAllOrders(channelAuction, auctionId, sellOrders); + }); + it("checks case 3, it verifies the price in case of clearingOrder != placed order with 3x participation", async () => { + const { + auctioningToken, + biddingToken, + } = await createTokensAndMintAndApprove( + channelAuction, + [user_1, user_2, user_3], + hre, + ); + const auctionInitParameters = { + auctioningToken, + biddingToken, + _auctionedSellAmount: ethers.utils.parseEther("500"), + _auctioneerBuyAmountMinimum: ethers.utils.parseEther("1"), + _auctioneerBuyAmountMaximum: ethers.utils.parseEther("1000"), + _auctioneerUserId: BigNumber.from(0), + }; + const sellOrders = [ + { + sellAmount: ethers.utils.parseEther("1"), + buyAmount: ethers.utils.parseEther("1"), + userId: BigNumber.from(1), + }, + { + sellAmount: ethers.utils.parseEther("1"), + buyAmount: ethers.utils.parseEther("1"), + userId: BigNumber.from(3), + }, + { + sellAmount: ethers.utils.parseEther("1"), + buyAmount: ethers.utils.parseEther("1"), + userId: BigNumber.from(2), + }, + ]; + + const auctionId: BigNumber = await createChannelAuctionWithDefaultsAndReturnId( + channelAuction, + auctionInitParameters, + ); + await startChannelAuction(channelAuction, auctionId); + + await placeOrdersForChannelAuction( + channelAuction, + sellOrders, + auctionId, + hre, + ); + + await closeChannelAuction(channelAuction, auctionId); + + await channelAuction.settleAuction(auctionId); + const auctionData = await channelAuction.auctionData(auctionId); + expect(auctionData.clearingPriceOrder).to.equal( + encodeOrder({ + sellAmount: ethers.utils.parseEther("3"), + buyAmount: auctionInitParameters._auctionedSellAmount, + userId: BigNumber.from(0), + }), + ); + expect(auctionData.volumeClearingPriceOrder).to.equal(0); + await claimFromAllOrders(channelAuction, auctionId, sellOrders); + }); + it("checks case 8, it verifies the price in case of no participation of the auction", async () => { + const { + auctioningToken, + biddingToken, + } = await createTokensAndMintAndApprove( + channelAuction, + [user_1, user_2, user_3], + hre, + ); + const auctionInitParameters = { + auctioningToken, + biddingToken, + _auctionedSellAmount: ethers.utils.parseEther("500"), + _auctioneerBuyAmountMinimum: ethers.utils.parseEther("1"), + _auctioneerBuyAmountMaximum: ethers.utils.parseEther("1000"), + _auctioneerUserId: BigNumber.from(0), + }; + + const auctionId: BigNumber = await createChannelAuctionWithDefaultsAndReturnId( + channelAuction, + auctionInitParameters, + ); + + await closeChannelAuction(channelAuction, auctionId); + + await channelAuction.settleAuction(auctionId); + const auctionData = await channelAuction.auctionData(auctionId); + expect(auctionData.clearingPriceOrder).to.equal( + encodeOrder({ + sellAmount: auctionInitParameters._auctioneerBuyAmountMinimum, + buyAmount: auctionInitParameters._auctionedSellAmount, + userId: auctionInitParameters._auctioneerUserId, + }), + ); + expect(auctionData.volumeClearingPriceOrder).to.equal(BigNumber.from(0)); + }); + it("checks case 2, it verifies the price in case without a partially filled order", async () => { + const { + auctioningToken, + biddingToken, + } = await createTokensAndMintAndApprove( + channelAuction, + [user_1, user_2, user_3], + hre, + ); + + const auctionInitParameters = { + auctioningToken, + biddingToken, + _auctionedSellAmount: ethers.utils.parseEther("1"), + _auctioneerBuyAmountMinimum: ethers.utils.parseEther("1"), + _auctioneerBuyAmountMaximum: ethers.utils.parseEther("5"), + _auctioneerUserId: BigNumber.from(1), + }; + + const sellOrders = [ + { + sellAmount: ethers.utils.parseEther("1").mul(2), + buyAmount: ethers.utils.parseEther("1").div(2), + userId: BigNumber.from(1), + }, + { + sellAmount: ethers.utils.parseEther("1").add(1), + buyAmount: ethers.utils.parseEther("1"), + userId: BigNumber.from(1), + }, + ]; + + const auctionId: BigNumber = await createChannelAuctionWithDefaultsAndReturnId( + channelAuction, + auctionInitParameters, + ); + await startChannelAuction(channelAuction, auctionId); + await placeOrdersForChannelAuction( + channelAuction, + sellOrders, + auctionId, + hre, + ); + + await closeChannelAuction(channelAuction, auctionId); + await channelAuction.settleAuction(auctionId); + const auctionData = await channelAuction.auctionData(auctionId); + expect(auctionData.clearingPriceOrder).to.equal( + encodeOrder({ + sellAmount: sellOrders[0].sellAmount, + buyAmount: auctionInitParameters._auctionedSellAmount, + userId: BigNumber.from(0), + }), + ); + expect(auctionData.volumeClearingPriceOrder).to.equal(0); + await claimFromAllOrders(channelAuction, auctionId, sellOrders); + }); + it("checks case 10, verifies the price in case one sellOrder is eating initialAuctionOrder completely", async () => { + const { + auctioningToken, + biddingToken, + } = await createTokensAndMintAndApprove( + channelAuction, + [user_1, user_2, user_3], + hre, + ); + const auctionInitParameters = { + auctioningToken, + biddingToken, + _auctionedSellAmount: ethers.utils.parseEther("1"), + _auctioneerBuyAmountMinimum: ethers.utils.parseEther("1"), + _auctioneerBuyAmountMaximum: ethers.utils.parseEther("5"), + _auctioneerUserId: BigNumber.from(1), + }; + const sellOrders = [ + { + sellAmount: ethers.utils.parseEther("1").mul(20), + buyAmount: ethers.utils.parseEther("1").mul(10), + userId: BigNumber.from(1), + }, + ]; + + const auctionId: BigNumber = await createChannelAuctionWithDefaultsAndReturnId( + channelAuction, + auctionInitParameters, + ); + await startChannelAuction(channelAuction, auctionId); + + await placeOrdersForChannelAuction( + channelAuction, + sellOrders, + auctionId, + hre, + ); + + await closeChannelAuction(channelAuction, auctionId); + await channelAuction.settleAuction(auctionId); + const auctionData = await channelAuction.auctionData(auctionId); + expect(auctionData.clearingPriceOrder).to.equal( + encodeOrder(sellOrders[0]), + ); + expect(auctionData.volumeClearingPriceOrder).to.equal( + auctionInitParameters._auctionedSellAmount + .mul(sellOrders[0].sellAmount) + .div(sellOrders[0].buyAmount), + ); + await claimFromAllOrders(channelAuction, auctionId, sellOrders); + }); + it("checks case 5, bidding amount matches min buyAmount of initialOrder perfectly", async () => { + const { + auctioningToken, + biddingToken, + } = await createTokensAndMintAndApprove( + channelAuction, + [user_1, user_2, user_3], + hre, + ); + const auctionInitParameters = { + auctioningToken, + biddingToken, + _auctionedSellAmount: ethers.utils.parseEther("1"), + _auctioneerBuyAmountMinimum: ethers.utils.parseEther("0.5"), + _auctioneerBuyAmountMaximum: ethers.utils.parseEther("5"), + _auctioneerUserId: BigNumber.from(1), + }; + const sellOrders = [ + { + sellAmount: ethers.utils.parseEther("0.5"), + buyAmount: ethers.utils.parseEther("0.5"), + userId: BigNumber.from(2), + }, + { + sellAmount: ethers.utils.parseEther("0.5"), + buyAmount: ethers.utils.parseEther("0.5"), + userId: BigNumber.from(3), + }, + ]; + + const auctionId: BigNumber = await createChannelAuctionWithDefaultsAndReturnId( + channelAuction, + auctionInitParameters, + ); + await startChannelAuction(channelAuction, auctionId); + await placeOrdersForChannelAuction( + channelAuction, + sellOrders, + auctionId, + hre, + ); + + await closeChannelAuction(channelAuction, auctionId); + await channelAuction.settleAuction(auctionId); + const auctionData = await channelAuction.auctionData(auctionId); + expect(auctionData.clearingPriceOrder).to.eql(encodeOrder(sellOrders[1])); + expect(auctionData.volumeClearingPriceOrder).to.equal( + sellOrders[1].sellAmount, + ); + await expect(() => + channelAuction.claimFromParticipantOrder(auctionId, [ + encodeOrder(sellOrders[0]), + ]), + ).to.changeTokenBalances( + auctioningToken, + [user_2], + [sellOrders[0].sellAmount], + ); + await expect(() => + channelAuction.claimFromParticipantOrder(auctionId, [ + encodeOrder(sellOrders[1]), + ]), + ).to.changeTokenBalances( + auctioningToken, + [user_3], + [sellOrders[1].sellAmount], + ); + }); + it("checks case 7, bidding amount matches min buyAmount of initialOrder perfectly with additional order", async () => { + const { + auctioningToken, + biddingToken, + } = await createTokensAndMintAndApprove( + channelAuction, + [user_1, user_2, user_3], + hre, + ); + const auctionInitParameters = { + auctioningToken, + biddingToken, + _auctionedSellAmount: ethers.utils.parseEther("1"), + _auctioneerBuyAmountMinimum: ethers.utils.parseEther("0.5"), + _auctioneerBuyAmountMaximum: ethers.utils.parseEther("5"), + _auctioneerUserId: BigNumber.from(1), + }; + const sellOrders = [ + { + sellAmount: ethers.utils.parseEther("0.5"), + buyAmount: ethers.utils.parseEther("0.5"), + userId: BigNumber.from(2), + }, + { + sellAmount: ethers.utils.parseEther("0.5"), + buyAmount: ethers.utils.parseEther("0.5"), + userId: BigNumber.from(3), + }, + { + sellAmount: ethers.utils.parseEther("0.5"), + buyAmount: ethers.utils.parseEther("0.6"), + userId: BigNumber.from(3), + }, + ]; + const auctionId: BigNumber = await createChannelAuctionWithDefaultsAndReturnId( + channelAuction, + auctionInitParameters, + ); + await startChannelAuction(channelAuction, auctionId); + await placeOrdersForChannelAuction( + channelAuction, + sellOrders, + auctionId, + hre, + ); + await closeChannelAuction(channelAuction, auctionId); + await channelAuction.settleAuction(auctionId); + const auctionData = await channelAuction.auctionData(auctionId); + expect(auctionData.clearingPriceOrder).to.eql(encodeOrder(sellOrders[1])); + expect(auctionData.volumeClearingPriceOrder).to.equal( + sellOrders[1].sellAmount, + ); + await expect(() => + channelAuction.claimFromParticipantOrder(auctionId, [ + encodeOrder(sellOrders[0]), + ]), + ).to.changeTokenBalances( + auctioningToken, + [user_2], + [sellOrders[0].sellAmount], + ); + await expect(() => + channelAuction.claimFromParticipantOrder(auctionId, [ + encodeOrder(sellOrders[1]), + ]), + ).to.changeTokenBalances( + auctioningToken, + [user_3], + [sellOrders[1].sellAmount], + ); + await expect(() => + channelAuction.claimFromParticipantOrder(auctionId, [ + encodeOrder(sellOrders[2]), + ]), + ).to.changeTokenBalances( + biddingToken, + [user_3], + [sellOrders[2].sellAmount], + ); + }); + it("checks case 10: it shows an example why userId should always be given: 2 orders with the same price", async () => { + const { + auctioningToken, + biddingToken, + } = await createTokensAndMintAndApprove( + channelAuction, + [user_1, user_2, user_3], + hre, + ); + const auctionInitParameters = { + auctioningToken, + biddingToken, + _auctionedSellAmount: ethers.utils.parseEther("1"), + _auctioneerBuyAmountMinimum: ethers.utils.parseEther("0.5"), + _auctioneerBuyAmountMaximum: ethers.utils.parseEther("5"), + _auctioneerUserId: BigNumber.from(1), + }; + const sellOrders = [ + { + sellAmount: ethers.utils.parseEther("0.5"), + buyAmount: ethers.utils.parseEther("0.5"), + userId: BigNumber.from(2), + }, + { + sellAmount: ethers.utils.parseEther("0.5"), + buyAmount: ethers.utils.parseEther("0.5"), + userId: BigNumber.from(3), + }, + { + sellAmount: ethers.utils.parseEther("0.5"), + buyAmount: ethers.utils.parseEther("0.4"), + userId: BigNumber.from(3), + }, + ]; + const auctionId: BigNumber = await createChannelAuctionWithDefaultsAndReturnId( + channelAuction, + auctionInitParameters, + ); + await startChannelAuction(channelAuction, auctionId); + await placeOrdersForChannelAuction( + channelAuction, + sellOrders, + auctionId, + hre, + ); + + await closeChannelAuction(channelAuction, auctionId); + await channelAuction.settleAuction(auctionId); + const auctionData = await channelAuction.auctionData(auctionId); + expect(auctionData.clearingPriceOrder).to.equal( + encodeOrder(sellOrders[0]), + ); + expect(auctionData.volumeClearingPriceOrder).to.equal( + sellOrders[1].sellAmount, + ); + await expect(() => + channelAuction.claimFromParticipantOrder(auctionId, [ + encodeOrder(sellOrders[0]), + ]), + ).to.changeTokenBalances( + auctioningToken, + [user_2], + [sellOrders[0].sellAmount], + ); + await expect(() => + channelAuction.claimFromParticipantOrder(auctionId, [ + encodeOrder(sellOrders[1]), + ]), + ).to.changeTokenBalances( + biddingToken, + [user_3], + [sellOrders[1].sellAmount], + ); + await expect(() => + channelAuction.claimFromParticipantOrder(auctionId, [ + encodeOrder(sellOrders[2]), + ]), + ).to.changeTokenBalances( + auctioningToken, + [user_3], + [sellOrders[2].sellAmount], + ); + }); + it("checks case 1, it verifies the price in case of 2 of 3 sellOrders eating initialAuctionOrder completely", async () => { + const { + auctioningToken, + biddingToken, + } = await createTokensAndMintAndApprove( + channelAuction, + [user_1, user_2, user_3], + hre, + ); + const auctionInitParameters = { + auctioningToken, + biddingToken, + _auctionedSellAmount: ethers.utils.parseEther("1"), + _auctioneerBuyAmountMinimum: ethers.utils.parseEther("1"), + _auctioneerBuyAmountMaximum: ethers.utils.parseEther("20"), + _auctioneerUserId: BigNumber.from(1), + }; + const sellOrders = [ + { + sellAmount: ethers.utils.parseEther("1").mul(2), + buyAmount: ethers.utils.parseEther("1").div(5), + userId: BigNumber.from(1), + }, + { + sellAmount: ethers.utils.parseEther("1").mul(2), + buyAmount: ethers.utils.parseEther("1"), + userId: BigNumber.from(1), + }, + + { + sellAmount: ethers.utils.parseEther("1").mul(2).add(1), + buyAmount: ethers.utils.parseEther("1").mul(2), + userId: BigNumber.from(1), + }, + ]; + const auctionId: BigNumber = await createChannelAuctionWithDefaultsAndReturnId( + channelAuction, + auctionInitParameters, + ); + await startChannelAuction(channelAuction, auctionId); + await placeOrdersForChannelAuction( + channelAuction, + sellOrders, + auctionId, + hre, + ); + + await closeChannelAuction(channelAuction, auctionId); + await channelAuction.settleAuction(auctionId); + const auctionData = await channelAuction.auctionData(auctionId); + expect(auctionData.clearingPriceOrder).to.eql(encodeOrder(sellOrders[1])); + expect(auctionData.volumeClearingPriceOrder).to.equal(0); + await claimFromAllOrders(channelAuction, auctionId, sellOrders); + }); + // it("verifies the price in case of 2 of 3 sellOrders eating initialAuctionOrder completely - with precalculateSellAmountSum step", async () => { + // const { + // auctioningToken, + // biddingToken, + // } = await createTokensAndMintAndApprove( + // channelAuction, + // [user_1, user_2, user_3], + // hre, + // ); + // const auctionInitParameters = { + // auctioningToken, + // biddingToken, + // _auctionedSellAmount: ethers.utils.parseEther("1"), + // _auctioneerBuyAmountMinimum: ethers.utils.parseEther("1"), + // _auctioneerBuyAmountMaximum: ethers.utils.parseEther("5"), + // _auctioneerUserId: BigNumber.from(1), + // }; + // const sellOrders = [ + // { + // sellAmount: ethers.utils.parseEther("1").mul(2), + // buyAmount: ethers.utils.parseEther("1").div(5), + // userId: BigNumber.from(1), + // }, + // { + // sellAmount: ethers.utils.parseEther("1").mul(2), + // buyAmount: ethers.utils.parseEther("1"), + // userId: BigNumber.from(1), + // }, + + // { + // sellAmount: ethers.utils.parseEther("1").mul(2).add(1), + // buyAmount: ethers.utils.parseEther("1").mul(2), + // userId: BigNumber.from(1), + // }, + // ]; + + // const auctionId: BigNumber = await createChannelAuctionWithDefaultsAndReturnId( + // channelAuction, + // auctionInitParameters, + // ); + // await startChannelAuction(channelAuction, auctionId); + + // await placeOrdersForChannelAuction( + // channelAuction, + // sellOrders, + // auctionId, + // hre, + // ); + + // await closeChannelAuction(channelAuction, auctionId); + // // this is the additional step + // await channelAuction.precalculateSellAmountSum(auctionId, 1); + + // await channelAuction.settleAuction(auctionId); + // const auctionData = await channelAuction.auctionData(auctionId); + // expect(auctionData.clearingPriceOrder).to.eql(encodeOrder(sellOrders[1])); + // expect(auctionData.volumeClearingPriceOrder).to.equal(0); + // }); + // it("verifies the price in case of 2 of 4 sellOrders eating initialAuctionOrder completely - with precalculateSellAmountSum step and one more step within settleAuction", async () => { + // const initialAuctionOrder = { + // sellAmount: ethers.utils.parseEther("1"), + // buyAmount: ethers.utils.parseEther("1"), + // userId: BigNumber.from(1), + // }; + // const sellOrders = [ + // { + // sellAmount: ethers.utils.parseEther("1"), + // buyAmount: ethers.utils.parseEther("1").div(5), + // userId: BigNumber.from(1), + // }, + // { + // sellAmount: ethers.utils.parseEther("1"), + // buyAmount: ethers.utils.parseEther("1").div(5), + // userId: BigNumber.from(2), + // }, + // { + // sellAmount: ethers.utils.parseEther("1").mul(2), + // buyAmount: ethers.utils.parseEther("1"), + // userId: BigNumber.from(1), + // }, + + // { + // sellAmount: ethers.utils.parseEther("1").mul(2).add(1), + // buyAmount: ethers.utils.parseEther("1").mul(2), + // userId: BigNumber.from(1), + // }, + // ]; + // const { + // auctioningToken, + // biddingToken, + // } = await createTokensAndMintAndApprove( + // channelAuction, + // [user_1, user_2], + // hre, + // ); + + // const auctionId: BigNumber = await createChannelAuctionWithDefaultsAndReturnId( + // channelAuction, + // { + // auctioningToken, + // biddingToken, + // _auctionedSellAmount: initialAuctionOrder.sellAmount, + // _auctioneerBuyAmountMinimum: initialAuctionOrder.buyAmount, + // }, + // ); + // await placeOrdersForChannelAuction( + // channelAuction, + // sellOrders, + // auctionId, + // hre, + // ); + + // await closeChannelAuction(channelAuction, auctionId); + // // this is the additional step + // await channelAuction.precalculateSellAmountSum(auctionId, 1); + + // const auctionData = await channelAuction.auctionData(auctionId); + // expect(auctionData.interimSumBidAmount).to.equal( + // sellOrders[0].sellAmount, + // ); + // expect(auctionData.interimOrder).to.equal(encodeOrder(sellOrders[0])); + // await channelAuction.settleAuction(auctionId); + // const auctionData2 = await channelAuction.auctionData(auctionId); + // expect(auctionData2.clearingPriceOrder).to.eql( + // encodeOrder(sellOrders[2]), + // ); + // expect(auctionData2.volumeClearingPriceOrder).to.equal(0); + // await claimFromAllOrders(channelAuction, auctionId, sellOrders); + // }); + it("verifies the price in case of clearing order is decided by userId", async () => { + const { + auctioningToken, + biddingToken, + } = await createTokensAndMintAndApprove( + channelAuction, + [user_1, user_2, user_3], + hre, + ); + const auctionInitParameters = { + auctioningToken, + biddingToken, + _auctionedSellAmount: ethers.utils.parseEther("1"), + _auctioneerBuyAmountMinimum: ethers.utils.parseEther("1"), + _auctioneerBuyAmountMaximum: ethers.utils.parseEther("20"), + _auctioneerUserId: BigNumber.from(1), + }; + const sellOrders = [ + { + sellAmount: ethers.utils.parseEther("1").mul(2), + buyAmount: ethers.utils.parseEther("1").div(5), + userId: BigNumber.from(1), + }, + { + sellAmount: ethers.utils.parseEther("1").mul(2), + buyAmount: ethers.utils.parseEther("1"), + userId: BigNumber.from(1), + }, + + { + sellAmount: ethers.utils.parseEther("1").mul(2), + buyAmount: ethers.utils.parseEther("1"), + userId: BigNumber.from(2), + }, + ]; + + const auctionId: BigNumber = await createChannelAuctionWithDefaultsAndReturnId( + channelAuction, + auctionInitParameters, + ); + await startChannelAuction(channelAuction, auctionId); + await placeOrdersForChannelAuction( + channelAuction, + sellOrders, + auctionId, + hre, + ); + + await closeChannelAuction(channelAuction, auctionId); + await channelAuction.settleAuction(auctionId); + const auctionData = await channelAuction.auctionData(auctionId); + expect(auctionData.clearingPriceOrder).to.be.equal( + encodeOrder(sellOrders[1]), + ); + expect(auctionData.volumeClearingPriceOrder).to.equal(0); + await claimFromAllOrders(channelAuction, auctionId, sellOrders); + }); + }); + describe("claimFromAuctioneerOrder", async () => { + it("checks the claimed amounts for a fully matched initialAuctionOrder and buyOrder", async () => { + const { + auctioningToken, + biddingToken, + } = await createTokensAndMintAndApprove( + channelAuction, + [user_1, user_2, user_3], + hre, + ); + const auctionInitParameters = { + auctioningToken, + biddingToken, + _auctionedSellAmount: ethers.utils.parseEther("1"), + _auctioneerBuyAmountMinimum: ethers.utils.parseEther("1"), + _auctioneerBuyAmountMaximum: ethers.utils.parseEther("20"), + _auctioneerUserId: BigNumber.from(1), + }; + const sellOrders = [ + { + sellAmount: ethers.utils.parseEther("1").add(1), + buyAmount: ethers.utils.parseEther("1"), + userId: BigNumber.from(1), + }, + ]; + + const auctionId: BigNumber = await createChannelAuctionWithDefaultsAndReturnId( + channelAuction, + auctionInitParameters, + ); + await startChannelAuction(channelAuction, auctionId); + await placeOrdersForChannelAuction( + channelAuction, + sellOrders, + auctionId, + hre, + ); + await closeChannelAuction(channelAuction, auctionId); + const { clearingOrder: price } = await calculateClearingPrice( + channelAuction, + auctionId, + ); + const callPromise = channelAuction.settleAuction(auctionId); + // auctioneer reward check: + await expect(() => callPromise).to.changeTokenBalances( + auctioningToken, + [user_1], + [0], + ); + await expect(callPromise) + .to.emit(biddingToken, "Transfer") + .withArgs(channelAuction.address, user_1.address, price.sellAmount); + }); + it("checks the claimed amounts for a partially matched initialAuctionOrder and buyOrder", async () => { + const { + auctioningToken, + biddingToken, + } = await createTokensAndMintAndApprove( + channelAuction, + [user_1, user_2, user_3], + hre, + ); + const auctionInitParameters = { + auctioningToken, + biddingToken, + _auctionedSellAmount: ethers.utils.parseEther("1"), + _auctioneerBuyAmountMinimum: ethers.utils.parseEther("1"), + _auctioneerBuyAmountMaximum: ethers.utils.parseEther("20"), + _auctioneerUserId: BigNumber.from(1), + }; + const sellOrders = [ + { + sellAmount: ethers.utils.parseEther("1").div(2).add(1), + buyAmount: ethers.utils.parseEther("1").div(2), + userId: BigNumber.from(1), + }, + ]; + + const auctionId: BigNumber = await createChannelAuctionWithDefaultsAndReturnId( + channelAuction, + auctionInitParameters, + ); + await startChannelAuction(channelAuction, auctionId); + await placeOrdersForChannelAuction( + channelAuction, + sellOrders, + auctionId, + hre, + ); + await closeChannelAuction(channelAuction, auctionId); + const callPromise = channelAuction.settleAuction(auctionId); + // auctioneer reward check: + await expect(callPromise) + .to.emit(auctioningToken, "Transfer") + .withArgs( + channelAuction.address, + user_1.address, + auctionInitParameters._auctionedSellAmount.sub( + sellOrders[0].sellAmount, + ), + ); + await expect(callPromise) + .to.emit(biddingToken, "Transfer") + .withArgs( + channelAuction.address, + user_1.address, + sellOrders[0].sellAmount, + ); + }); + }); + describe("claimFromParticipantOrder", async () => { + it("checks that claiming only works after the finishing of the auction", async () => { + const { + auctioningToken, + biddingToken, + } = await createTokensAndMintAndApprove( + channelAuction, + [user_1, user_2, user_3], + hre, + ); + const auctionInitParameters = { + auctioningToken, + biddingToken, + _auctionedSellAmount: ethers.utils.parseEther("1"), + _auctioneerBuyAmountMinimum: ethers.utils.parseEther("1"), + _auctioneerBuyAmountMaximum: ethers.utils.parseEther("20"), + _auctioneerUserId: BigNumber.from(1), + }; + const sellOrders = [ + { + sellAmount: ethers.utils.parseEther("1").add(1), + buyAmount: ethers.utils.parseEther("1"), + userId: BigNumber.from(1), + }, + ]; + + const auctionId: BigNumber = await createChannelAuctionWithDefaultsAndReturnId( + channelAuction, + auctionInitParameters, + ); + await startChannelAuction(channelAuction, auctionId); + await placeOrdersForChannelAuction( + channelAuction, + sellOrders, + auctionId, + hre, + ); + + await expect( + channelAuction.claimFromParticipantOrder( + auctionId, + sellOrders.map((order) => encodeOrder(order)), + ), + ).to.be.revertedWith("Auction not yet finished"); + await closeChannelAuction(channelAuction, auctionId); + await expect( + channelAuction.claimFromParticipantOrder( + auctionId, + sellOrders.map((order) => encodeOrder(order)), + ), + ).to.be.revertedWith("Auction not yet finished"); + }); + it("checks the claimed amounts for a partially matched buyOrder", async () => { + const { + auctioningToken, + biddingToken, + } = await createTokensAndMintAndApprove( + channelAuction, + [user_1, user_2, user_3], + hre, + ); + const auctionInitParameters = { + auctioningToken, + biddingToken, + _auctionedSellAmount: ethers.utils.parseEther("1"), + _auctioneerBuyAmountMinimum: ethers.utils.parseEther("1"), + _auctioneerBuyAmountMaximum: ethers.utils.parseEther("20"), + _auctioneerUserId: BigNumber.from(1), + }; + const sellOrders = [ + { + sellAmount: ethers.utils.parseEther("1").div(2).add(1), + buyAmount: ethers.utils.parseEther("1").div(2), + userId: BigNumber.from(1), + }, + { + sellAmount: ethers.utils.parseEther("1").mul(2).div(3).add(1), + buyAmount: ethers.utils.parseEther("1").mul(2).div(3), + userId: BigNumber.from(1), + }, + ]; + + const auctionId: BigNumber = await createChannelAuctionWithDefaultsAndReturnId( + channelAuction, + auctionInitParameters, + ); + await startChannelAuction(channelAuction, auctionId); + await placeOrdersForChannelAuction( + channelAuction, + sellOrders, + auctionId, + hre, + ); + + await closeChannelAuction(channelAuction, auctionId); + const { clearingOrder: price } = await calculateClearingPrice( + channelAuction, + auctionId, + ); + await channelAuction.settleAuction(auctionId); + + const receivedAmounts = toReceivedFunds( + await channelAuction.callStatic.claimFromParticipantOrder(auctionId, [ + encodeOrder(sellOrders[1]), + ]), + ); + const settledBuyAmount = sellOrders[1].sellAmount + .mul(price.buyAmount) + .div(price.sellAmount) + .sub( + sellOrders[0].sellAmount + .add(sellOrders[1].sellAmount) + .mul(price.buyAmount) + .div(price.sellAmount) + .sub(auctionInitParameters._auctionedSellAmount), + ) + .sub(1); + expect(receivedAmounts.auctioningTokenAmount).to.equal(settledBuyAmount); + expect(receivedAmounts.biddingTokenAmount).to.equal( + sellOrders[1].sellAmount + .sub(settledBuyAmount.mul(price.sellAmount).div(price.buyAmount)) + .sub(1), + ); + }); + it("checks the claimed amounts for a fully not-matched buyOrder", async () => { + const { + auctioningToken, + biddingToken, + } = await createTokensAndMintAndApprove( + channelAuction, + [user_1, user_2, user_3], + hre, + ); + const auctionInitParameters = { + auctioningToken, + biddingToken, + _auctionedSellAmount: ethers.utils.parseEther("1"), + _auctioneerBuyAmountMinimum: ethers.utils.parseEther("1"), + _auctioneerBuyAmountMaximum: ethers.utils.parseEther("20"), + _auctioneerUserId: BigNumber.from(1), + }; + const sellOrders = [ + { + sellAmount: ethers.utils.parseEther("1").div(2).add(1), + buyAmount: ethers.utils.parseEther("1").div(2), + userId: BigNumber.from(1), + }, + { + sellAmount: ethers.utils.parseEther("1").mul(2).div(3).add(1), + buyAmount: ethers.utils.parseEther("1").mul(2).div(3), + userId: BigNumber.from(1), + }, + { + sellAmount: ethers.utils.parseEther("1").mul(2).div(3).add(1), + buyAmount: ethers.utils.parseEther("1").mul(2).div(3), + userId: BigNumber.from(2), + }, + ]; + const auctionId: BigNumber = await createChannelAuctionWithDefaultsAndReturnId( + channelAuction, + auctionInitParameters, + ); + await startChannelAuction(channelAuction, auctionId); + await placeOrdersForChannelAuction( + channelAuction, + sellOrders, + auctionId, + hre, + ); + await closeChannelAuction(channelAuction, auctionId); + await channelAuction.settleAuction(auctionId); + const receivedAmounts = toReceivedFunds( + await channelAuction.callStatic.claimFromParticipantOrder(auctionId, [ + encodeOrder(sellOrders[2]), + ]), + ); + expect(receivedAmounts.biddingTokenAmount).to.equal( + sellOrders[2].sellAmount, + ); + expect(receivedAmounts.auctioningTokenAmount).to.equal("0"); + }); + it("checks the claimed amounts for a fully matched buyOrder", async () => { + const { + auctioningToken, + biddingToken, + } = await createTokensAndMintAndApprove( + channelAuction, + [user_1, user_2, user_3], + hre, + ); + const auctionInitParameters = { + auctioningToken, + biddingToken, + _auctionedSellAmount: ethers.utils.parseEther("1"), + _auctioneerBuyAmountMinimum: ethers.utils.parseEther("1"), + _auctioneerBuyAmountMaximum: ethers.utils.parseEther("20"), + _auctioneerUserId: BigNumber.from(1), + }; + const sellOrders = [ + { + sellAmount: ethers.utils.parseEther("1").div(2).add(1), + buyAmount: ethers.utils.parseEther("1").div(2), + userId: BigNumber.from(1), + }, + { + sellAmount: ethers.utils.parseEther("1").mul(2).div(3).add(1), + buyAmount: ethers.utils.parseEther("1").mul(2).div(3), + userId: BigNumber.from(1), + }, + ]; + + const auctionId: BigNumber = await createChannelAuctionWithDefaultsAndReturnId( + channelAuction, + auctionInitParameters, + ); + await startChannelAuction(channelAuction, auctionId); + await placeOrdersForChannelAuction( + channelAuction, + sellOrders, + auctionId, + hre, + ); + + await closeChannelAuction(channelAuction, auctionId); + const { clearingOrder: price } = await calculateClearingPrice( + channelAuction, + auctionId, + ); + await channelAuction.settleAuction(auctionId); + + const receivedAmounts = toReceivedFunds( + await channelAuction.callStatic.claimFromParticipantOrder(auctionId, [ + encodeOrder(sellOrders[0]), + ]), + ); + expect(receivedAmounts.biddingTokenAmount).to.equal("0"); + expect(receivedAmounts.auctioningTokenAmount).to.equal( + sellOrders[0].sellAmount.mul(price.buyAmount).div(price.sellAmount), + ); + }); + it("checks that an order can not be used for claiming twice", async () => { + const { + auctioningToken, + biddingToken, + } = await createTokensAndMintAndApprove( + channelAuction, + [user_1, user_2, user_3], + hre, + ); + const auctionInitParameters = { + auctioningToken, + biddingToken, + _auctionedSellAmount: ethers.utils.parseEther("1"), + _auctioneerBuyAmountMinimum: ethers.utils.parseEther("1"), + _auctioneerBuyAmountMaximum: ethers.utils.parseEther("20"), + _auctioneerUserId: BigNumber.from(1), + }; + const sellOrders = [ + { + sellAmount: ethers.utils.parseEther("1").div(2).add(1), + buyAmount: ethers.utils.parseEther("1").div(2), + userId: BigNumber.from(1), + }, + { + sellAmount: ethers.utils.parseEther("1").mul(2).div(3).add(1), + buyAmount: ethers.utils.parseEther("1").mul(2).div(3), + userId: BigNumber.from(1), + }, + ]; + + const auctionId: BigNumber = await createChannelAuctionWithDefaultsAndReturnId( + channelAuction, + auctionInitParameters, + ); + await startChannelAuction(channelAuction, auctionId); + await placeOrdersForChannelAuction( + channelAuction, + sellOrders, + auctionId, + hre, + ); + + await closeChannelAuction(channelAuction, auctionId); + await channelAuction.settleAuction(auctionId); + await channelAuction.claimFromParticipantOrder(auctionId, [ + encodeOrder(sellOrders[0]), + ]), + await expect( + channelAuction.claimFromParticipantOrder(auctionId, [ + encodeOrder(sellOrders[0]), + ]), + ).to.be.revertedWith("order is no longer claimable"); + }); + }); + it("checks that orders from different users can not be claimed at once", async () => { + const { + auctioningToken, + biddingToken, + } = await createTokensAndMintAndApprove( + channelAuction, + [user_1, user_2, user_3], + hre, + ); + const auctionInitParameters = { + auctioningToken, + biddingToken, + _auctionedSellAmount: ethers.utils.parseEther("1"), + _auctioneerBuyAmountMinimum: ethers.utils.parseEther("1"), + _auctioneerBuyAmountMaximum: ethers.utils.parseEther("20"), + _auctioneerUserId: BigNumber.from(1), + }; + const sellOrders = [ + { + sellAmount: ethers.utils.parseEther("1").div(2).add(1), + buyAmount: ethers.utils.parseEther("1").div(2), + userId: BigNumber.from(1), + }, + { + sellAmount: ethers.utils.parseEther("1").mul(2).div(3).add(1), + buyAmount: ethers.utils.parseEther("1").mul(2).div(3), + userId: BigNumber.from(2), + }, + ]; + + const auctionId: BigNumber = await createChannelAuctionWithDefaultsAndReturnId( + channelAuction, + auctionInitParameters, + ); + await startChannelAuction(channelAuction, auctionId); + await placeOrdersForChannelAuction( + channelAuction, + sellOrders, + auctionId, + hre, + ); + + await closeChannelAuction(channelAuction, auctionId); + await channelAuction.settleAuction(auctionId); + await expect( + channelAuction.claimFromParticipantOrder(auctionId, [ + encodeOrder(sellOrders[0]), + encodeOrder(sellOrders[1]), + ]), + ).to.be.revertedWith("only allowed to claim for same user"); + }); + it("checks the claimed amounts are summed up correctly for two orders", async () => { + const { + auctioningToken, + biddingToken, + } = await createTokensAndMintAndApprove( + channelAuction, + [user_1, user_2, user_3], + hre, + ); + const auctionInitParameters = { + auctioningToken, + biddingToken, + _auctionedSellAmount: ethers.utils.parseEther("1"), + _auctioneerBuyAmountMinimum: ethers.utils.parseEther("1"), + _auctioneerBuyAmountMaximum: ethers.utils.parseEther("20"), + _auctioneerUserId: BigNumber.from(1), + }; + const sellOrders = [ + { + sellAmount: ethers.utils.parseEther("1").div(2).add(1), + buyAmount: ethers.utils.parseEther("1").div(2), + userId: BigNumber.from(1), + }, + { + sellAmount: ethers.utils.parseEther("1").mul(2).div(3).add(1), + buyAmount: ethers.utils.parseEther("1").mul(2).div(3), + userId: BigNumber.from(1), + }, + ]; + + const auctionId: BigNumber = await createChannelAuctionWithDefaultsAndReturnId( + channelAuction, + auctionInitParameters, + ); + await startChannelAuction(channelAuction, auctionId); + + await placeOrdersForChannelAuction( + channelAuction, + sellOrders, + auctionId, + hre, + ); + + await closeChannelAuction(channelAuction, auctionId); + const { clearingOrder: price } = await calculateClearingPrice( + channelAuction, + auctionId, + ); + await channelAuction.settleAuction(auctionId); + + const receivedAmounts = toReceivedFunds( + await channelAuction.callStatic.claimFromParticipantOrder(auctionId, [ + encodeOrder(sellOrders[0]), + encodeOrder(sellOrders[1]), + ]), + ); + expect(receivedAmounts.biddingTokenAmount).to.equal( + sellOrders[0].sellAmount + .add(sellOrders[1].sellAmount) + .sub( + auctionInitParameters._auctionedSellAmount + .mul(price.sellAmount) + .div(price.buyAmount), + ), + ); + expect(receivedAmounts.auctioningTokenAmount).to.equal( + auctionInitParameters._auctionedSellAmount.sub(1), + ); + }); + // describe("settleAuctionAtomically", async () => { + // it("can not settle atomically, if it is not allowed", async () => { + // const initialAuctionOrder = { + // sellAmount: ethers.utils.parseEther("1"), + // buyAmount: ethers.utils.parseEther("0.5"), + // userId: BigNumber.from(1), + // }; + // const sellOrders = [ + // { + // sellAmount: ethers.utils.parseEther("0.5"), + // buyAmount: ethers.utils.parseEther("0.5"), + // userId: BigNumber.from(1), + // }, + // ]; + // const atomicSellOrders = [ + // { + // sellAmount: ethers.utils.parseEther("0.499"), + // buyAmount: ethers.utils.parseEther("0.4999"), + // userId: BigNumber.from(2), + // }, + // ]; + // const { + // auctioningToken, + // biddingToken, + // } = await createTokensAndMintAndApprove( + // channelAuction, + // [user_1, user_2], + // hre, + // ); + + // const auctionId: BigNumber = await createChannelAuctionWithDefaultsAndReturnId( + // channelAuction, + // { + // auctioningToken, + // biddingToken, + // _auctionedSellAmount: initialAuctionOrder.sellAmount, + // _auctioneerBuyAmountMinimum: initialAuctionOrder.buyAmount, + // isAtomicClosureAllowed: false, + // }, + // ); + // await placeOrdersForChannelAuction( + // channelAuction, + // sellOrders, + // auctionId, + // hre, + // ); + + // await closeChannelAuction(channelAuction, auctionId); + // await expect( + // channelAuction.settleAuctionAtomically( + // auctionId, + // [atomicSellOrders[0].sellAmount], + // [atomicSellOrders[0].buyAmount], + // [queueStartElement], + // "0x", + // ), + // ).to.be.revertedWith("not allowed to settle auction atomically"); + // }); + // it("reverts, if more than one order is intended to be settled atomically", async () => { + // const initialAuctionOrder = { + // sellAmount: ethers.utils.parseEther("1"), + // buyAmount: ethers.utils.parseEther("0.5"), + // userId: BigNumber.from(1), + // }; + // const sellOrders = [ + // { + // sellAmount: ethers.utils.parseEther("0.5"), + // buyAmount: ethers.utils.parseEther("0.5"), + // userId: BigNumber.from(1), + // }, + // ]; + // const atomicSellOrders = [ + // { + // sellAmount: ethers.utils.parseEther("0.49"), + // buyAmount: ethers.utils.parseEther("0.49"), + // userId: BigNumber.from(2), + // }, + // { + // sellAmount: ethers.utils.parseEther("0.4"), + // buyAmount: ethers.utils.parseEther("0.4"), + // userId: BigNumber.from(2), + // }, + // ]; + // const { + // auctioningToken, + // biddingToken, + // } = await createTokensAndMintAndApprove( + // channelAuction, + // [user_1, user_2], + // hre, + // ); + + // const auctionId: BigNumber = await createChannelAuctionWithDefaultsAndReturnId( + // channelAuction, + // { + // auctioningToken, + // biddingToken, + // _auctionedSellAmount: initialAuctionOrder.sellAmount, + // _auctioneerBuyAmountMinimum: initialAuctionOrder.buyAmount, + // isAtomicClosureAllowed: true, + // }, + // ); + // await placeOrdersForChannelAuction( + // channelAuction, + // sellOrders, + // auctionId, + // hre, + // ); + + // await closeChannelAuction(channelAuction, auctionId); + // await expect( + // channelAuction.settleAuctionAtomically( + // auctionId, + // [atomicSellOrders[0].sellAmount, atomicSellOrders[1].sellAmount], + // [atomicSellOrders[0].buyAmount, atomicSellOrders[1].buyAmount], + // [queueStartElement, queueStartElement], + // "0x", + // ), + // ).to.be.revertedWith("Only one order can be placed atomically"); + // }); + // it("can not settle atomically, if precalculateSellAmountSum was used", async () => { + // const initialAuctionOrder = { + // sellAmount: ethers.utils.parseEther("1"), + // buyAmount: ethers.utils.parseEther("0.5"), + // userId: BigNumber.from(1), + // }; + // const sellOrders = [ + // { + // sellAmount: ethers.utils.parseEther("0.5"), + // buyAmount: ethers.utils.parseEther("0.5"), + // userId: BigNumber.from(1), + // }, + // ]; + // const atomicSellOrders = [ + // { + // sellAmount: ethers.utils.parseEther("0.499"), + // buyAmount: ethers.utils.parseEther("0.4999"), + // userId: BigNumber.from(2), + // }, + // ]; + // const { + // auctioningToken, + // biddingToken, + // } = await createTokensAndMintAndApprove( + // channelAuction, + // [user_1, user_2], + // hre, + // ); + + // const auctionId: BigNumber = await createChannelAuctionWithDefaultsAndReturnId( + // channelAuction, + // { + // auctioningToken, + // biddingToken, + // _auctionedSellAmount: initialAuctionOrder.sellAmount, + // _auctioneerBuyAmountMinimum: initialAuctionOrder.buyAmount, + // isAtomicClosureAllowed: true, + // }, + // ); + // await placeOrdersForChannelAuction( + // channelAuction, + // sellOrders, + // auctionId, + // hre, + // ); + + // await closeChannelAuction(channelAuction, auctionId); + // await channelAuction.precalculateSellAmountSum(auctionId, 1); + + // await expect( + // channelAuction.settleAuctionAtomically( + // auctionId, + // [atomicSellOrders[0].sellAmount], + // [atomicSellOrders[0].buyAmount], + // [queueStartElement], + // "0x", + // ), + // ).to.be.revertedWith("precalculateSellAmountSum is already too advanced"); + // }); + // it("allows an atomic settlement, if the precalculation are not yet beyond the price of the inserted order", async () => { + // const initialAuctionOrder = { + // sellAmount: ethers.utils.parseEther("1"), + // buyAmount: ethers.utils.parseEther("0.5"), + // userId: BigNumber.from(1), + // }; + // const sellOrders = [ + // { + // sellAmount: ethers.utils.parseEther("0.5"), + // buyAmount: ethers.utils.parseEther("0.5"), + // userId: BigNumber.from(1), + // }, + // ]; + // const atomicSellOrders = [ + // { + // sellAmount: ethers.utils.parseEther("0.5"), + // buyAmount: ethers.utils.parseEther("0.55"), + // userId: BigNumber.from(1), + // }, + // ]; + // const { + // auctioningToken, + // biddingToken, + // } = await createTokensAndMintAndApprove( + // channelAuction, + // [user_1, user_2], + // hre, + // ); + + // const auctionId: BigNumber = await createChannelAuctionWithDefaultsAndReturnId( + // channelAuction, + // { + // auctioningToken, + // biddingToken, + // _auctionedSellAmount: initialAuctionOrder.sellAmount, + // _auctioneerBuyAmountMinimum: initialAuctionOrder.buyAmount, + // isAtomicClosureAllowed: true, + // }, + // ); + // await placeOrdersForChannelAuction( + // channelAuction, + // sellOrders, + // auctionId, + // hre, + // ); + + // await closeChannelAuction(channelAuction, auctionId); + // await channelAuction.precalculateSellAmountSum(auctionId, 1); + + // await channelAuction.settleAuctionAtomically( + // auctionId, + // [atomicSellOrders[0].buyAmount], + // [atomicSellOrders[0].sellAmount], + // [queueStartElement], + // "0x", + // ); + // await claimFromAllOrders(channelAuction, auctionId, sellOrders); + // await claimFromAllOrders(channelAuction, auctionId, atomicSellOrders); + // }); + // it("can settle atomically, if it is allowed", async () => { + // const initialAuctionOrder = { + // sellAmount: ethers.utils.parseEther("1"), + // buyAmount: ethers.utils.parseEther("0.5"), + // userId: BigNumber.from(1), + // }; + // const sellOrders = [ + // { + // sellAmount: ethers.utils.parseEther("0.5"), + // buyAmount: ethers.utils.parseEther("0.5"), + // userId: BigNumber.from(1), + // }, + // ]; + // const atomicSellOrders = [ + // { + // sellAmount: ethers.utils.parseEther("0.4999"), + // buyAmount: ethers.utils.parseEther("0.4999"), + // userId: BigNumber.from(2), + // }, + // ]; + // const { + // auctioningToken, + // biddingToken, + // } = await createTokensAndMintAndApprove( + // channelAuction, + // [user_1, user_2], + // hre, + // ); + + // const auctionId: BigNumber = await createChannelAuctionWithDefaultsAndReturnId( + // channelAuction, + // { + // auctioningToken, + // biddingToken, + // _auctionedSellAmount: initialAuctionOrder.sellAmount, + // _auctioneerBuyAmountMinimum: initialAuctionOrder.buyAmount, + // isAtomicClosureAllowed: true, + // }, + // ); + // await placeOrdersForChannelAuction( + // channelAuction, + // sellOrders, + // auctionId, + // hre, + // ); + + // await closeChannelAuction(channelAuction, auctionId); + // await channelAuction + // .connect(user_2) + // .settleAuctionAtomically( + // auctionId, + // [atomicSellOrders[0].sellAmount], + // [atomicSellOrders[0].buyAmount], + // [queueStartElement], + // "0x", + // ); + // const auctionData = await channelAuction.auctionData(auctionId); + // expect(auctionData.clearingPriceOrder).to.equal( + // encodeOrder({ + // sellAmount: sellOrders[0].sellAmount.add( + // atomicSellOrders[0].sellAmount, + // ), + // buyAmount: initialAuctionOrder.sellAmount, + // userId: BigNumber.from(0), + // }), + // ); + // }); + // it("can not settle auctions atomically, before auction finished", async () => { + // const initialAuctionOrder = { + // sellAmount: ethers.utils.parseEther("1"), + // buyAmount: ethers.utils.parseEther("0.5"), + // userId: BigNumber.from(1), + // }; + // const sellOrders = [ + // { + // sellAmount: ethers.utils.parseEther("0.5"), + // buyAmount: ethers.utils.parseEther("0.5"), + // userId: BigNumber.from(1), + // }, + // ]; + // const atomicSellOrders = [ + // { + // sellAmount: ethers.utils.parseEther("0.4999"), + // buyAmount: ethers.utils.parseEther("0.4999"), + // userId: BigNumber.from(2), + // }, + // ]; + // const { + // auctioningToken, + // biddingToken, + // } = await createTokensAndMintAndApprove( + // channelAuction, + // [user_1, user_2], + // hre, + // ); + + // const auctionId: BigNumber = await createChannelAuctionWithDefaultsAndReturnId( + // channelAuction, + // { + // auctioningToken, + // biddingToken, + // _auctionedSellAmount: initialAuctionOrder.sellAmount, + // _auctioneerBuyAmountMinimum: initialAuctionOrder.buyAmount, + // isAtomicClosureAllowed: true, + // }, + // ); + // await placeOrdersForChannelAuction( + // channelAuction, + // sellOrders, + // auctionId, + // hre, + // ); + + // await expect( + // channelAuction + // .connect(user_2) + // .settleAuctionAtomically( + // auctionId, + // [atomicSellOrders[0].sellAmount], + // [atomicSellOrders[0].buyAmount], + // [queueStartElement], + // "0x", + // ), + // ).to.be.revertedWith("Auction not in solution submission phase"); + // }); + // }); + describe("registerUser", async () => { + it("registers a user only once", async () => { + await channelAuction.registerUser(user_1.address); + await expect( + channelAuction.registerUser(user_1.address), + ).to.be.revertedWith("User already registered"); + }); + }); + describe("containsOrder", async () => { + it("returns true, if it contains order", async () => { + const initialAuctionOrder = { + sellAmount: ethers.utils.parseEther("1"), + buyAmount: ethers.utils.parseEther("1"), + userId: BigNumber.from(1), + }; + const sellOrders = [ + { + sellAmount: ethers.utils.parseEther("1").div(2).add(1), + buyAmount: ethers.utils.parseEther("1").div(2), + userId: BigNumber.from(1), + }, + ]; + const { + auctioningToken, + biddingToken, + } = await createTokensAndMintAndApprove( + channelAuction, + [user_1, user_2], + hre, + ); + + const auctionId: BigNumber = await createChannelAuctionWithDefaultsAndReturnId( + channelAuction, + { + auctioningToken, + biddingToken, + _auctionedSellAmount: initialAuctionOrder.sellAmount, + _auctioneerBuyAmountMinimum: initialAuctionOrder.buyAmount, + }, + ); + await startChannelAuction(channelAuction, auctionId); + await placeOrdersForChannelAuction( + channelAuction, + sellOrders, + auctionId, + hre, + ); + + await closeChannelAuction(channelAuction, auctionId); + expect( + await channelAuction.callStatic.containsOrder( + auctionId, + encodeOrder(sellOrders[0]), + ), + ).to.be.equal(true); + }); + }); + describe("transfers fees", async () => { + it("transfers fees to feeReceiver", async () => { + const { + auctioningToken, + biddingToken, + } = await createTokensAndMintAndApprove( + channelAuction, + [user_1, user_2, user_3], + hre, + ); + const auctionInitParameters = { + auctioningToken, + biddingToken, + _auctionedSellAmount: ethers.utils.parseEther("1"), + _auctioneerBuyAmountMinimum: ethers.utils.parseEther("1"), + _auctioneerBuyAmountMaximum: ethers.utils.parseEther("20"), + _auctioneerUserId: BigNumber.from(1), + _minimumBiddingAmount: ethers.utils.parseEther("0.01"), + }; + let sellOrders = [ + { + sellAmount: ethers.utils.parseEther("1").div(2).add(1), + buyAmount: ethers.utils.parseEther("1").div(2), + userId: BigNumber.from(1), + }, + { + sellAmount: ethers.utils.parseEther("1").mul(2).div(3).add(1), + buyAmount: ethers.utils.parseEther("1").mul(2).div(3), + userId: BigNumber.from(1), + }, + ]; + + const feeReceiver = user_3; + const feeNumerator = 4; + await channelAuction + .connect(user_1) + .setFeeParameters(feeNumerator, feeReceiver.address); + + const now = (await ethers.provider.getBlock("latest")).timestamp; + + await expect(() => + channelAuction.initiateAuction( + auctionInitParameters.auctioningToken.address, + auctionInitParameters.biddingToken.address, + auctionInitParameters._auctionedSellAmount, + auctionInitParameters._auctioneerBuyAmountMinimum, + auctionInitParameters._auctioneerBuyAmountMaximum, + now + 3600, + auctionInitParameters._minimumBiddingAmount, + 3600, + ), + ).to.changeTokenBalances( + auctioningToken, + [feeReceiver], + [ + auctionInitParameters._auctionedSellAmount + .mul(feeNumerator) + .div("1000"), + ], + ); + const auctionId = BigNumber.from(1); + await startChannelAuction(channelAuction, auctionId); + sellOrders = await placeOrdersForChannelAuction( + channelAuction, + sellOrders, + auctionId, + hre, + ); + + await closeChannelAuction(channelAuction, auctionId); + await channelAuction.settleAuction(auctionId); + // contract still holds sufficient funds to pay the participants fully + await channelAuction.callStatic.claimFromParticipantOrder( + auctionId, + sellOrders.map((order) => encodeOrder(order)), + ); + }); + }); + describe("setFeeParameters", async () => { + it("changing the paramter works", async () => { + const feeReceiver = user_3; + const feeNumerator = 4; + await channelAuction + .connect(user_1) + .setFeeParameters(feeNumerator, feeReceiver.address); + expect(await channelAuction.callStatic.feeNumerator()).to.be.equal(4); + }); + it("can only be called by owner", async () => { + const feeReceiver = user_3; + const feeNumerator = 4; + await expect( + channelAuction + .connect(user_2) + .setFeeParameters(feeNumerator, feeReceiver.address), + ).to.be.revertedWith("Ownable: caller is not the owner"); + }); + it("does not allow fees higher than 1.5%", async () => { + const feeReceiver = user_3; + const feeNumerator = 16; + await expect( + channelAuction + .connect(user_1) + .setFeeParameters(feeNumerator, feeReceiver.address), + ).to.be.revertedWith("Fee is not allowed to be set higher than 0.5%"); + }); + }); +}); diff --git a/test/contract/IterableOrderList.spec.ts b/test/contract/IterableOrderList.spec.ts new file mode 100644 index 0000000..16364b6 --- /dev/null +++ b/test/contract/IterableOrderList.spec.ts @@ -0,0 +1,283 @@ +import { expect } from "chai"; +import { Contract, BigNumber } from "ethers"; +import { ethers } from "hardhat"; + +import { + queueLastElement, + queueStartElement, + encodeOrder, +} from "../../src/priceCalculation"; + +const BYTES32_ZERO = encodeOrder({ + userId: BigNumber.from(1), + sellAmount: BigNumber.from(0), + buyAmount: BigNumber.from(0), +}); +const BYTES32_ONE = encodeOrder({ + userId: BigNumber.from(2), + sellAmount: BigNumber.from(2), + buyAmount: BigNumber.from(2), +}); +const BYTES32_ONE_DIFFERENT = encodeOrder({ + userId: BigNumber.from(2), + sellAmount: BigNumber.from(3), + buyAmount: BigNumber.from(3), +}); +const BYTES32_ONE_BEST_USER = encodeOrder({ + userId: BigNumber.from(1), + sellAmount: BigNumber.from(2), + buyAmount: BigNumber.from(2), +}); +const BYTES32_ONE_BEST_AMOUNT = encodeOrder({ + userId: BigNumber.from(2), + sellAmount: BigNumber.from(1), + buyAmount: BigNumber.from(1), +}); +const BYTES32_TWO = encodeOrder({ + userId: BigNumber.from(2), + buyAmount: BigNumber.from(8), + sellAmount: BigNumber.from(4), +}); +const BYTES32_THREE = encodeOrder({ + userId: BigNumber.from(2), + buyAmount: BigNumber.from(6), + sellAmount: BigNumber.from(2), +}); + +async function getSetContent(set: Contract) { + const result = []; + if (!(await set.isEmpty())) { + const last_element = queueLastElement; + let current = await set.first(); + while (current != last_element) { + result.push(current); + current = await set.next(current); + } + } + return result; +} + +describe("IterableOrderedOrderList", function () { + let set: Contract; + beforeEach(async () => { + const IterableOrderedOrderListWrapper = await ethers.getContractFactory( + "IterableOrderedOrderListWrapper", + ); + + set = await IterableOrderedOrderListWrapper.deploy(); + await set.initializeEmptyList(); + }); + + it("should contain the added values", async () => { + expect(await getSetContent(set)).to.be.empty; + expect(await set.contains(BYTES32_ONE)).to.equal(false); + expect(await set.callStatic.insert(BYTES32_ONE)).to.equal(true); + await set.insert(BYTES32_ONE); + expect(await set.contains(BYTES32_ONE)).to.equal(true); + + expect(await getSetContent(set)).to.eql([BYTES32_ONE]); + }); + + it("should insert the same value only once", async () => { + expect(await set.callStatic.insert(BYTES32_ONE)).to.equal(true); + await set.insert(BYTES32_ONE); + expect(await getSetContent(set)).to.eql([BYTES32_ONE]); + + expect(await set.callStatic.insert(BYTES32_ONE)).to.equal(false); + await set.insert(BYTES32_ONE); + expect(await getSetContent(set)).to.eql([BYTES32_ONE]); + }); + + it("should return first", async () => { + await set.insert(BYTES32_ONE); + expect(await set.first()).to.equal(BYTES32_ONE); + }); + + it("should allow to iterate over content and check order - part 1", async () => { + await set.insert(BYTES32_ONE); + await set.insert(BYTES32_TWO); + await set.insert(BYTES32_THREE); + await set.insert(BYTES32_ONE_BEST_USER); + await set.insert(BYTES32_ONE_BEST_AMOUNT); + + const first = await set.first(); + const second = await set.next(first); + const third = await set.next(second); + const fourth = await set.next(third); + const fifth = await set.next(fourth); + + expect(first).to.equal(BYTES32_ONE_BEST_AMOUNT); + expect(second).to.equal(BYTES32_ONE_BEST_USER); + expect(third).to.equal(BYTES32_ONE); + expect(fourth).to.equal(BYTES32_TWO); + expect(fifth).to.equal(BYTES32_THREE); + }); + it("should allow to iterate over content and check order - part 2", async () => { + await set.insert(BYTES32_ONE); + await set.insert(BYTES32_ONE_BEST_AMOUNT); + await set.insert(BYTES32_ONE_BEST_USER); + await set.insert(BYTES32_TWO); + await set.insert(BYTES32_THREE); + + const first = await set.first(); + const second = await set.next(first); + const third = await set.next(second); + const fourth = await set.next(third); + const fifth = await set.next(fourth); + + expect(first).to.equal(BYTES32_ONE_BEST_AMOUNT); + expect(second).to.equal(BYTES32_ONE_BEST_USER); + expect(third).to.equal(BYTES32_ONE); + expect(fourth).to.equal(BYTES32_TWO); + expect(fifth).to.equal(BYTES32_THREE); + }); + it("should allow to insert same limit price with different amount with same user", async () => { + await set.insert(BYTES32_ONE); + expect(await set.callStatic.insert(BYTES32_ONE_DIFFERENT)).to.equal(true); + await set.insert(BYTES32_ONE_DIFFERENT); + expect(await set.callStatic.insert(BYTES32_ONE_DIFFERENT)).to.equal(false); + }); + it("should throw if the same orders are compared with smallerThan", async () => { + await expect(set.smallerThan(BYTES32_ONE, BYTES32_ONE)).to.be.revertedWith( + "user is not allowed to place same order twice", + ); + }); + + it("should allow to insert element at certain element", async () => { + await set.insert(BYTES32_ONE); + await set.insert(BYTES32_THREE); + + expect(await set.callStatic.insertAt(BYTES32_TWO, BYTES32_ONE)).to.equal( + true, + ); + expect(await set.callStatic.insertAt(BYTES32_TWO, BYTES32_THREE)).to.equal( + false, + ); + }); + + it("should not allow to insert element with non-containing element-Before-New-One", async () => { + await set.insert(BYTES32_THREE); + + expect(await set.callStatic.insertAt(BYTES32_TWO, BYTES32_ONE)).to.equal( + false, + ); + }); + + it("should not allow to insert element with element not in front of other element", async () => { + expect(await set.callStatic.insertAt(BYTES32_TWO, BYTES32_THREE)).to.equal( + false, + ); + }); + + it("should not allow to insert queue start element", async () => { + await expect( + set.callStatic.insertAt(queueStartElement, queueStartElement), + ).to.be.revertedWith("Inserting element is not valid"); + }); + it("should not allow to insert queue end element", async () => { + await expect( + set.callStatic.insertAt(queueLastElement, queueStartElement), + ).to.be.revertedWith("Inserting element is not valid"); + }); + + it("should insert element according to rate", async () => { + await set.insert(BYTES32_THREE); + await set.insert(BYTES32_ONE); + await set.insert(BYTES32_TWO); + + const first = await set.first(); + const second = await set.next(first); + const third = await set.next(second); + + expect(first).to.equal(BYTES32_ONE); + expect(second).to.equal(BYTES32_TWO); + expect(third).to.equal(BYTES32_THREE); + }); + + it("does not allow to get next of the queue end element", async () => { + await set.insert(BYTES32_THREE); + + const first = await set.first(); + const second = await set.next(first); + await expect(set.next(second)).to.be.revertedWith( + "Trying to get next of last element", + ); + }); + + it("doesn't allow to insert a number with denominator == 0", async () => { + await expect(set.insert(BYTES32_ZERO)).to.be.revertedWith( + "Inserting zero is not supported", + ); + }); + + it("cannot get first of empty list", async () => { + await expect(set.first()).to.be.revertedWith( + "Trying to get first from empty set", + ); + }); + + it("cannot get next of non-existent element", async () => { + await set.insert(BYTES32_ONE); + await expect(set.next(BYTES32_TWO)).to.be.reverted; + }); + + it("should remove element", async () => { + await set.insert(BYTES32_THREE); + await set.insert(BYTES32_ONE); + await set.insert(BYTES32_TWO); + + await set.remove(BYTES32_TWO); + + const first = await set.first(); + const second = await set.next(first); + const next_of_removed = await set.nextMap(BYTES32_TWO); + + expect(first).to.equal(BYTES32_ONE); + expect(second).to.equal(BYTES32_TWO); + expect(next_of_removed).to.equal(ethers.constants.Zero); + }); + + it("should allow to remove element twice", async () => { + await set.insert(BYTES32_ONE); + await set.insert(BYTES32_TWO); + await set.insert(BYTES32_THREE); + + expect(await set.callStatic.remove(BYTES32_TWO)).to.equal(true); + await set.remove(BYTES32_TWO); + expect(await set.callStatic.remove(BYTES32_TWO)).to.equal(false); + }); + + it("should recognize empty sets", async () => { + expect(await set.callStatic.isEmpty()).to.equal(true); + await set.insert(BYTES32_ONE); + expect(await set.callStatic.isEmpty()).to.equal(false); + }); + + it("cannot contain queue start element or queue end element", async () => { + expect(await set.callStatic.contains(queueLastElement)).to.equal(false); + expect(await set.callStatic.contains(queueStartElement)).to.equal(false); + }); + + it("cannot contain queue start element or queue end element", async () => { + expect(await set.callStatic.contains(queueLastElement)).to.equal(false); + expect(await set.callStatic.contains(queueStartElement)).to.equal(false); + }); + + it("does not allow to remove element not contained", async () => { + await set.insert(BYTES32_ONE); + await set.insert(BYTES32_TWO); + + expect(await set.callStatic.remove(BYTES32_THREE)).to.equal(false); + }); + + it("can add element BYTES32_ONE => rate of startingElement ==0", async () => { + expect(await set.callStatic.insert(BYTES32_ONE)).to.equal(true); + }); + + it("encodeOrder reverses decodeOrder", async () => { + const ans = await set.decodeOrder(BYTES32_THREE); + expect(await set.callStatic.encodeOrder(ans[0], ans[1], ans[2])).to.equal( + BYTES32_THREE, + ); + }); +}); diff --git a/test/contract/defaultContractInteractions.ts b/test/contract/defaultContractInteractions.ts index 19cbd78..7eb1877 100644 --- a/test/contract/defaultContractInteractions.ts +++ b/test/contract/defaultContractInteractions.ts @@ -1,13 +1,19 @@ import { Contract, BigNumber } from "ethers"; import { ethers } from "hardhat"; -import { InitiateAuctionInput } from "../../src/ts/types"; +import { + InitiateAuctionInput, + InitiateChannelAuctionInput, +} from "../../src/ts/types"; import { sendTxAndGetReturnValue } from "./utilities"; type PartialAuctionInput = Partial & Pick; +type PartialChannelAuctionInput = Partial & + Pick; + async function createAuctionInputWithDefaults( parameters: PartialAuctionInput, ): Promise { @@ -26,6 +32,21 @@ async function createAuctionInputWithDefaults( parameters.allowListData ?? "0x", ]; } +async function createChannelAuctionInputWithDefaults( + parameters: PartialChannelAuctionInput, +): Promise { + const now = (await ethers.provider.getBlock("latest")).timestamp; + return [ + parameters.auctioningToken.address, + parameters.biddingToken.address, + parameters._auctionedSellAmount ?? ethers.utils.parseEther("1"), + parameters._auctioneerBuyAmountMinimum ?? ethers.utils.parseEther("1"), + parameters._auctioneerBuyAmountMaximum ?? ethers.utils.parseEther("2"), + parameters._auctionStartDate ?? now + 3600, + parameters._minimumBiddingAmountPerOrder ?? 1, + parameters._maxDuration ?? 3600, + ]; +} export async function createAuctionWithDefaults( easyAuction: Contract, @@ -35,6 +56,25 @@ export async function createAuctionWithDefaults( ...(await createAuctionInputWithDefaults(parameters)), ); } +export async function createChannelAuctionWithDefaults( + channelAuction: Contract, + parameters: PartialChannelAuctionInput, +): Promise { + return channelAuction.initiateAuction( + ...(await createChannelAuctionInputWithDefaults(parameters)), + ); +} + +export async function createChannelAuctionWithDefaultsAndReturnId( + channelAuction: Contract, + parameters: PartialChannelAuctionInput, +): Promise { + return sendTxAndGetReturnValue( + channelAuction, + "initiateAuction(address,address,uint96,uint96,uint96,uint96,uint96,uint96)", + ...(await createChannelAuctionInputWithDefaults(parameters)), + ); +} export async function createAuctionWithDefaultsAndReturnId( easyAuction: Contract, diff --git a/test/contract/utilities.ts b/test/contract/utilities.ts index ff51432..add9389 100644 --- a/test/contract/utilities.ts +++ b/test/contract/utilities.ts @@ -14,6 +14,26 @@ export async function closeAuction( ).toNumber(); await increaseTime(time_remaining + 1); } +export async function closeChannelAuction( + instance: Contract, + auctionId: BigNumber, +): Promise { + const time_remaining = ( + await instance.callStatic.getSecondsRemainingUntilLastPossibleClose( + auctionId, + ) + ).toNumber(); + await increaseTime(time_remaining + 1); +} + +export async function startChannelAuction( + instance: Contract, + auctionId: BigNumber, +): Promise { + const now = (await ethers.provider.getBlock("latest")).timestamp; + const startTime = (await instance.auctionData(auctionId)).auctionStartDate; + await increaseTime(startTime - now); +} export async function claimFromAllOrders( easyAuction: Contract,