diff --git a/protocol/contracts/Constants.sol b/protocol/contracts/Constants.sol index a65bf88a..0da2351b 100644 --- a/protocol/contracts/Constants.sol +++ b/protocol/contracts/Constants.sol @@ -68,6 +68,8 @@ library Constants { /* Market */ uint256 private constant COUPON_EXPIRATION = 90; uint256 private constant DEBT_RATIO_CAP = 20e16; // 20% + uint256 private constant MAX_COUPON_YIELD_MULT = 2000; //2000 coupouns per 1 dollar burn + /* Regulator */ uint256 private constant SUPPLY_CHANGE_LIMIT = 3e16; // 3% @@ -165,6 +167,10 @@ library Constants { return COUPON_EXPIRATION; } + function getCouponMaxYieldToBurn() internal pure returns (uint256) { + return MAX_COUPON_YIELD_MULT; + } + function getDebtRatioCap() internal pure returns (Decimal.D256 memory) { return Decimal.D256({value: DEBT_RATIO_CAP}); } diff --git a/protocol/contracts/dao/Comptroller.sol b/protocol/contracts/dao/Comptroller.sol index fa05fa06..796ad748 100644 --- a/protocol/contracts/dao/Comptroller.sol +++ b/protocol/contracts/dao/Comptroller.sol @@ -35,6 +35,12 @@ contract Comptroller is Setters { balanceCheck(); } + function burnFromAccountSansDebt(address account, uint256 amount) internal { + dollar().transferFrom(account, address(this), amount); + dollar().burn(amount); + balanceCheck(); + } + function burnFromAccount(address account, uint256 amount) internal { dollar().transferFrom(account, address(this), amount); dollar().burn(amount); @@ -122,6 +128,10 @@ contract Comptroller is Setters { return 0; } + function acceptableBidCheck(address account, uint256 dollarAmount) internal returns (bool) { + return (dollar().balanceOf(account) >= balanceOfBonded(account).add(dollarAmount)); + } + function balanceCheck() private { Require.that( dollar().balanceOf(address(this)) >= totalBonded().add(totalStaged()).add(totalRedeemable()), diff --git a/protocol/contracts/dao/Getters.sol b/protocol/contracts/dao/Getters.sol index 46498b8a..69cdc9d2 100644 --- a/protocol/contracts/dao/Getters.sol +++ b/protocol/contracts/dao/Getters.sol @@ -191,6 +191,131 @@ contract Getters is State { return epoch <= Constants.getBootstrappingPeriod(); } + function getCouponAuctionAtEpoch(uint256 epoch) internal view returns (Epoch.AuctionState storage) { + return _state.epochs[epoch].auction; + } + + function getCouponAuctionBids(uint256 epoch) internal view returns (uint256) { + return _state.epochs[epoch].auction._totalBids; + } + + function getCouponBidderState(uint256 epoch, address bidder) internal view returns (Epoch.CouponBidderState storage) { + return _state.epochs[epoch].auction.couponBidderState[bidder]; + } + + function getCouponBidderStateSelected(uint256 epoch, address bidder) internal view returns (bool) { + return _state.epochs[epoch].auction.couponBidderState[bidder].selected; + } + + function getCouponBidderStateAssginedAtIndex(uint256 epoch, uint256 index) internal view returns (address) { + return _state.epochs[epoch].auction.seletedCouponBidder[index]; + } + + function getCouponBidderStateRejected(uint256 epoch, address bidder) internal view returns (bool) { + return _state.epochs[epoch].auction.couponBidderState[bidder].rejected; + } + + function getCouponBidderStateIndex(uint256 epoch, uint256 index) internal view returns (address) { + return _state.epochs[epoch].auction.couponBidder[index]; + } + + function isCouponAuctionFinished(uint256 epoch) internal view returns (bool){ + return _state.epochs[epoch].auction.finished; + } + + function isCouponAuctionCanceled(uint256 epoch) internal view returns (bool){ + return _state.epochs[epoch].auction.canceled; + } + + function getCouponAuctionMinExpiry(uint256 epoch) internal view returns (uint256) { + return _state.epochs[epoch].auction.minExpiry; + } + + function getCouponAuctionMaxExpiry(uint256 epoch) internal view returns (uint256) { + return _state.epochs[epoch].auction.maxExpiry; + } + + function getCouponAuctionMinYield(uint256 epoch) internal view returns (uint256) { + return _state.epochs[epoch].auction.minYield; + } + + function getCouponAuctionMaxYield(uint256 epoch) internal view returns (uint256) { + return _state.epochs[epoch].auction.maxYield; + } + + function getCouponAuctionMinDollarAmount(uint256 epoch) internal view returns (uint256) { + return _state.epochs[epoch].auction.minDollarAmount; + } + + function getCouponAuctionMaxDollarAmount(uint256 epoch) internal view returns (uint256) { + return _state.epochs[epoch].auction.maxDollarAmount; + } + + function getMinExpiryFilled(uint256 epoch) public view returns (uint256) { + return _state.epochs[epoch].auction.minExpiryFilled; + } + + function getMaxExpiryFilled(uint256 epoch) public view returns (uint256) { + return _state.epochs[epoch].auction.maxExpiryFilled; + } + + function getAvgExpiryFilled(uint256 epoch) public view returns (uint256) { + return _state.epochs[epoch].auction.avgExpiryFilled; + } + + function getMinYieldFilled(uint256 epoch) public view returns (uint256) { + return _state.epochs[epoch].auction.minYieldFilled; + } + + function getMaxYieldFilled(uint256 epoch) public view returns (uint256) { + return _state.epochs[epoch].auction.maxYieldFilled; + } + + function getAvgYieldFilled(uint256 epoch) public view returns (uint256) { + return _state.epochs[epoch].auction.avgYieldFilled; + } + + function getBidToCover(uint256 epoch) public view returns (uint256) { + return _state.epochs[epoch].auction.bidToCover; + } + + function getTotalFilled(uint256 epoch) public view returns (uint256) { + return _state.epochs[epoch].auction.totalFilled; + } + + function getTotalAuctioned(uint256 epoch) public view returns (uint256) { + return _state.epochs[epoch].auction.totalAuctioned; + } + + function getTotalBurned(uint256 epoch) public view returns (uint256) { + return _state.epochs[epoch].auction.totalBurned; + } + + function getEarliestDeadAuctionEpoch() public view returns (uint256) { + return _state.epoch.earliestDeadAuction; + } + + function getLatestCouponAuctionRedeemedSelectedBidderIndex(uint256 epoch) public view returns (uint256) { + return _state.epochs[epoch].auction.latestRedeemedSelectedBidderIndex; + } + + function getAvgAvgYieldAcrossCouponAuctions() public view returns (uint256) { + // loop over past epochs from the latest `dead` epoch to the current + uint256 sumYield = 0; + uint256 totalAvailableAuctions = 1; + for (uint256 d_idx = getEarliestDeadAuctionEpoch(); d_idx < uint256(epoch()); d_idx++) { + uint256 temp_coupon_auction_epoch = d_idx; + Epoch.AuctionState storage auction = getCouponAuctionAtEpoch(temp_coupon_auction_epoch); + // skip auctions that have been canceled, dead or not finished auction present? + if (!auction.canceled && !auction.dead && auction.isInit && auction.finished) { + sumYield += getAvgYieldFilled(temp_coupon_auction_epoch); + totalAvailableAuctions++; + } + } + + return sumYield.div(totalAvailableAuctions); + } + /** * Governance */ diff --git a/protocol/contracts/dao/Market.sol b/protocol/contracts/dao/Market.sol index 2740d674..f9668274 100644 --- a/protocol/contracts/dao/Market.sol +++ b/protocol/contracts/dao/Market.sol @@ -32,7 +32,8 @@ contract Market is Comptroller, Curve { event CouponRedemption(address indexed account, uint256 indexed epoch, uint256 couponAmount); event CouponTransfer(address indexed from, address indexed to, uint256 indexed epoch, uint256 value); event CouponApproval(address indexed owner, address indexed spender, uint256 value); - + event CouponBidPlaced(address indexed account, uint256 indexed epoch, uint256 dollarAmount, uint256 maxCouponAmount); + function step() internal { // Expire prior coupons for (uint256 i = 0; i < expiringCoupons(epoch()); i++) { @@ -117,4 +118,49 @@ contract Market is Comptroller, Curve { emit CouponTransfer(sender, recipient, epoch, amount); } + + function placeCouponAuctionBid(uint256 couponEpochExpiry, uint256 dollarAmount, uint256 maxCouponAmount) external returns (bool) { + Require.that( + couponEpochExpiry > 0, + FILE, + "Must have non-zero expiry" + ); + + Require.that( + dollarAmount > 0, + FILE, + "Must bid non-zero amount" + ); + + Require.that( + maxCouponAmount > 0, + FILE, + "Must bid on non-zero amount" + ); + + Require.that( + acceptableBidCheck(msg.sender, dollarAmount), + FILE, + "Must have enough in account" + ); + + uint256 yield = maxCouponAmount.div(dollarAmount); + uint256 maxYield = Constants.getCouponMaxYieldToBurn(); + + Require.that( + maxYield >= yield, + FILE, + "Must be under maxYield" + ); + + uint256 epochExpiry = epoch().add(couponEpochExpiry); + setCouponAuctionRelYield(maxCouponAmount.div(dollarAmount)); + setCouponAuctionRelDollarAmount(dollarAmount); + setCouponAuctionRelExpiry(epochExpiry); + setCouponBidderState(uint256(epoch()), msg.sender, couponEpochExpiry, dollarAmount, maxCouponAmount); + setCouponBidderStateIndex(uint256(epoch()), getCouponAuctionBids(uint256(epoch())), msg.sender); + incrementCouponAuctionBids(); + emit CouponBidPlaced(msg.sender, epochExpiry, dollarAmount, maxCouponAmount); + return true; + } } diff --git a/protocol/contracts/dao/Regulator.sol b/protocol/contracts/dao/Regulator.sol index 5920424c..70ab2f13 100644 --- a/protocol/contracts/dao/Regulator.sol +++ b/protocol/contracts/dao/Regulator.sol @@ -26,6 +26,21 @@ contract Regulator is Comptroller { using SafeMath for uint256; using Decimal for Decimal.D256; + bytes32 private constant FILE = "Regulator"; + Epoch.CouponBidderState[] private bids; + uint256 private totalFilled = 0; + uint256 private totalBurned = 0; + uint256 private yieldRelNorm = 1; + uint256 private expiryRelNorm = 1; + uint256 private dollarRelNorm = 1; + uint256 private totalAuctioned = 0; + uint256 private maxExpiryFilled = 0; + uint256 private sumExpiryFilled = 0; + uint256 private sumYieldFilled = 0; + uint256 private minExpiryFilled = 2**256 - 1; + Decimal.D256 private maxYieldFilled = Decimal.zero(); + Decimal.D256 private minYieldFilled = Decimal.D256(2**256 - 1); + event SupplyIncrease(uint256 indexed epoch, uint256 price, uint256 newRedeemable, uint256 lessDebt, uint256 newBonded); event SupplyDecrease(uint256 indexed epoch, uint256 price, uint256 newDebt); event SupplyNeutral(uint256 indexed epoch); @@ -33,12 +48,26 @@ contract Regulator is Comptroller { function step() internal { Decimal.D256 memory price = oracleCapture(); + //need to check previous epoch because by the time the Regulator.step function is fired, Bonding.step may have already incremented the epoch + Epoch.AuctionState storage auction = getCouponAuctionAtEpoch(epoch() - 1); + if (price.greaterThan(Decimal.one())) { + //check for outstanding auction, if exists cancel it + if (auction.isInit == true){ + cancelCouponAuctionAtEpoch(epoch() - 1); + } + growSupply(price); return; } if (price.lessThan(Decimal.one())) { + //check for outstanding auction, if exists settle it and start a new one + if (auction.isInit == true){ + bool isAuctionSettled = settleCouponAuction(epoch() - 1); + finishCouponAuctionAtEpoch(epoch() - 1); + } + initCouponAuction(); shrinkSupply(price); return; } @@ -58,8 +87,8 @@ contract Regulator is Comptroller { function growSupply(Decimal.D256 memory price) private { uint256 lessDebt = resetDebt(Decimal.zero()); - Decimal.D256 memory delta = limit(price.sub(Decimal.one()), price); - uint256 newSupply = delta.mul(totalNet()).asUint256(); + Decimal.D256 memory delta = Decimal.ratio(1, getAvgAvgYieldAcrossCouponAuctions()); + uint256 newSupply = delta.mul(dollar().totalSupply()).asUint256(); (uint256 newRedeemable, uint256 newBonded) = increaseSupply(newSupply); emit SupplyIncrease(epoch(), price.value, newRedeemable, lessDebt, newBonded); } @@ -89,5 +118,278 @@ contract Regulator is Comptroller { } return price; + } + + function sortBidsByDistance(Epoch.CouponBidderState[] storage bids) internal returns(Epoch.CouponBidderState[] storage) { + quickSort(bids, int(0), int(bids.length - 1)); + return bids; + } + + function quickSort(Epoch.CouponBidderState[] memory arr, int left, int right) internal { + int i = left; + int j = right; + if(i==j) return; + Decimal.D256 memory pivot = arr[uint256(left + (right - left) / 2)].distance; + while (i <= j) { + while (arr[uint256(i)].distance.lessThan(pivot)) i++; + while (pivot.lessThan(arr[uint256(j)].distance)) j--; + if (i <= j) { + (arr[uint256(i)], arr[uint256(j)]) = (arr[uint256(j)], arr[uint256(i)]); + i++; + j--; + } + } + if (left < j) + quickSort(arr, left, j); + if (i < right) + quickSort(arr, i, right); + } + + function sqrt(Decimal.D256 memory x) internal pure returns (Decimal.D256 memory y) { + Decimal.D256 memory z = x.add(1).div(2); + y = x; + while (z.lessThan(y)) { + y = z; + z = x.div(z.add(z)).div(2); + } + return y; + } + + function settleCouponAuction(uint256 settlementEpoch) internal returns (bool success) { + if (!isCouponAuctionFinished(settlementEpoch) && !isCouponAuctionCanceled(settlementEpoch)) { + yieldRelNorm = getCouponAuctionMaxYield(settlementEpoch) - getCouponAuctionMinYield(settlementEpoch); + expiryRelNorm = getCouponAuctionMaxExpiry(settlementEpoch) - getCouponAuctionMinExpiry(settlementEpoch); + dollarRelNorm = getCouponAuctionMaxDollarAmount(settlementEpoch) - getCouponAuctionMinDollarAmount(settlementEpoch); + + // loop over bids and compute distance + for (uint256 i = 0; i < getCouponAuctionBids(settlementEpoch); i++) { + Epoch.CouponBidderState storage bidder = getCouponBidderState(settlementEpoch, getCouponBidderStateIndex(settlementEpoch, i)); + Decimal.D256 memory yieldRel = Decimal.ratio( + Decimal.ratio( + bidder.couponAmount, + bidder.dollarAmount + ).asUint256(), + yieldRelNorm + ); + + Decimal.D256 memory expiryRel = Decimal.ratio( + bidder.couponExpiryEpoch, + expiryRelNorm + ); + + Decimal.D256 memory dollarRelMax = Decimal.ratio( + bidder.dollarAmount, + dollarRelNorm + ); + Decimal.D256 memory dollarRel = (Decimal.one().add(Decimal.one())).sub(dollarRelMax); + + Decimal.D256 memory yieldRelSquared = yieldRel.pow(2); + Decimal.D256 memory expiryRelSquared = expiryRel.pow(2); + Decimal.D256 memory dollarRelSquared = dollarRel.pow(2); + + Decimal.D256 memory sumOfSquared = yieldRelSquared.add(expiryRelSquared).add(dollarRelSquared); + Decimal.D256 memory distance; + if (sumOfSquared.greaterThan(Decimal.zero())) { + distance = sqrt(sumOfSquared); + } else { + distance = Decimal.zero(); + } + + setCouponBidderStateDistance(settlementEpoch, getCouponBidderStateIndex(settlementEpoch, i), distance); + bidder = getCouponBidderState(settlementEpoch, getCouponBidderStateIndex(settlementEpoch, i)); + bids.push(bidder); + } + + + // sort bids + bids = sortBidsByDistance(bids); + + // assign coupons in order of bid preference + for (uint256 i = 0; i < bids.length; i++) { + if (!getCouponBidderStateRejected(settlementEpoch, bids[i].bidder) && !getCouponBidderStateRejected(settlementEpoch, bids[i].bidder)) { + Decimal.D256 memory yield = Decimal.ratio( + bids[i].couponAmount, + bids[i].dollarAmount + ); + + //must check again if account is able to be assigned + if (acceptableBidCheck(bids[i].bidder, bids[i].dollarAmount)){ + if (yield.lessThan(minYieldFilled)) { + minYieldFilled = yield; + } else if (yield.greaterThan(maxYieldFilled)) { + maxYieldFilled = yield; + } + + if (bids[i].couponExpiryEpoch < minExpiryFilled) { + minExpiryFilled = bids[i].couponExpiryEpoch; + } else if (bids[i].couponExpiryEpoch > maxExpiryFilled) { + maxExpiryFilled = bids[i].couponExpiryEpoch; + } + + sumYieldFilled += yield.asUint256(); + sumExpiryFilled += bids[i].couponExpiryEpoch; + totalAuctioned += bids[i].couponAmount; + totalBurned += bids[i].dollarAmount; + + uint256 epochExpiry = epoch().add(bids[i].couponExpiryEpoch); + burnFromAccountSansDebt(bids[i].bidder, bids[i].dollarAmount); + incrementBalanceOfCoupons(bids[i].bidder, epochExpiry, bids[i].couponAmount); + setCouponBidderStateSelected(settlementEpoch, bids[i].bidder, i); + totalFilled++; + } else { + setCouponBidderStateRejected(settlementEpoch, bids[i].bidder); + } + } + + } + + // set auction internals + if (totalFilled > 0) { + Decimal.D256 memory avgYieldFilled = Decimal.ratio( + sumYieldFilled, + totalFilled + ); + Decimal.D256 memory avgExpiryFilled = Decimal.ratio( + sumExpiryFilled, + totalFilled + ); + + //mul(100) to avoid sub 0 results + Decimal.D256 memory bidToCover = Decimal.ratio( + bids.length, + totalFilled + ).mul(100); + + setMinExpiryFilled(settlementEpoch, minExpiryFilled); + setMaxExpiryFilled(settlementEpoch, maxExpiryFilled); + setAvgExpiryFilled(settlementEpoch, avgExpiryFilled.asUint256()); + setMinYieldFilled(settlementEpoch, minYieldFilled.asUint256()); + setMaxYieldFilled(settlementEpoch, maxYieldFilled.asUint256()); + setAvgYieldFilled(settlementEpoch, avgYieldFilled.asUint256()); + setBidToCover(settlementEpoch, bidToCover.asUint256()); + setTotalFilled(settlementEpoch, totalFilled); + setTotalAuctioned(settlementEpoch, totalAuctioned); + setTotalBurned(settlementEpoch, totalBurned); + } + + //clear bids and reset vars + delete bids; + totalFilled = 0; + totalBurned = 0; + yieldRelNorm = 1; + expiryRelNorm = 1; + dollarRelNorm = 1; + totalAuctioned = 0; + maxExpiryFilled = 0; + sumExpiryFilled = 0; + sumYieldFilled = 0; + minExpiryFilled = 2**256 - 1; + maxYieldFilled = Decimal.zero(); + minYieldFilled = Decimal.D256(2**256 - 1); + + return true; + } else { + return false; + } + } + + function autoRedeemFromCouponAuction() internal returns (bool success) { + /* + WARNING: may need fundemental constraints in order to cap max run time as epocs grow? (i.e totalRedeemable needs to be a function of auction internals of non dead auctions when twap > 1) + */ + + // this will allow us to reloop over best bidders in each auction + while (totalRedeemable() > 0) { + bool willRedeemableOverflow = false; + // loop over past epochs from the latest `dead` epoch to the current + for (uint256 d_idx = getEarliestDeadAuctionEpoch(); d_idx < uint256(epoch()); d_idx++) { + uint256 temp_coupon_auction_epoch = d_idx; + Epoch.AuctionState storage auction = getCouponAuctionAtEpoch(temp_coupon_auction_epoch); + + // skip auctions that have been canceled and or dead or no auction present? + if (!auction.canceled && !auction.dead && auction.isInit) { + if (auction.finished) { + + uint256 totalCurrentlyTriedRedeemed = 0; + // loop over bidders in order of assigned per epoch and redeem automatically untill capp is filled for epoch, mark those bids as redeemed, + + for (uint256 s_idx = getLatestCouponAuctionRedeemedSelectedBidderIndex(temp_coupon_auction_epoch); s_idx < getTotalFilled(temp_coupon_auction_epoch); s_idx++) { + address bidderAddress = getCouponBidderStateAssginedAtIndex(temp_coupon_auction_epoch, s_idx); + Epoch.CouponBidderState storage bidder = getCouponBidderState(temp_coupon_auction_epoch, bidderAddress); + + // skip over those bids that have already been redeemed + if (bidder.redeemed) { + totalCurrentlyTriedRedeemed++; + continue; + } + + uint256 totalRedeemable = totalRedeemable(); + + if (totalRedeemable > bidder.couponAmount) { + /* TODO + - need to make sure this is "safe" (i.e. it should NOT revert and undo all the previous redemptions, just break and skip while still incrementing total redeemed tried count) + */ + uint256 couponExpiryEpoch = temp_coupon_auction_epoch.add(bidder.couponExpiryEpoch); + + if (couponExpiryEpoch > uint256(couponExpiryEpoch)) { + //check if coupons for epoch are expired already + totalCurrentlyTriedRedeemed++; + setCouponBidderStateRedeemed(couponExpiryEpoch, bidderAddress); + continue; + } + + uint256 couponBalance = balanceOfCoupons(bidderAddress, couponExpiryEpoch); + + if (couponBalance > 0) { + uint256 minCouponAmount = 0; + if (couponBalance >= bidder.couponAmount) { + minCouponAmount = bidder.couponAmount; + } else { + minCouponAmount = couponBalance; + } + + decrementBalanceOfCoupons(bidderAddress, couponExpiryEpoch, minCouponAmount, "Regulator: Insufficient coupon balance"); + + redeemToAccount(bidderAddress, minCouponAmount); + + setCouponBidderStateRedeemed(couponExpiryEpoch, bidderAddress); + // set the next bidder in line + setLatestCouponAuctionRedeemedSelectedBidderIndex(temp_coupon_auction_epoch, s_idx + 1); + totalCurrentlyTriedRedeemed++; + + // time to jump into next auctions bidders + break; + } else { + // mark as redeemd if couponBalance is zero + setCouponBidderStateRedeemed(couponExpiryEpoch, bidderAddress); + // set the next bidder in line + setLatestCouponAuctionRedeemedSelectedBidderIndex(temp_coupon_auction_epoch, s_idx + 1); + totalCurrentlyTriedRedeemed++; + + // time to jump into next auctions bidders + break; + } + } else { + // no point in trying to redeem more if quota for epoch is done + willRedeemableOverflow = true; + break; + } + } + + // if all have been tried to be redeemd or expired, mark auction as `dead` + + if (totalCurrentlyTriedRedeemed == getTotalFilled(temp_coupon_auction_epoch)) { + setEarliestDeadAuctionEpoch(temp_coupon_auction_epoch); + setCouponAuctionStateDead(temp_coupon_auction_epoch); + } + } + } + } + + if (willRedeemableOverflow) { + // stop trying to redeem across auctions + break; + } + } } } diff --git a/protocol/contracts/dao/Setters.sol b/protocol/contracts/dao/Setters.sol index 8ca83c92..00e5b42a 100644 --- a/protocol/contracts/dao/Setters.sol +++ b/protocol/contracts/dao/Setters.sol @@ -149,6 +149,144 @@ contract Setters is State, Getters { _state.epochs[epoch].coupons.outstanding = 0; } + function initCouponAuction() internal { + if (_state.epochs[epoch()].auction.isInit == false) { + _state.epochs[epoch()].auction._totalBids = 0; + _state.epochs[epoch()].auction.minExpiry = 2**256 -1; + _state.epochs[epoch()].auction.maxExpiry = 0; + _state.epochs[epoch()].auction.minYield = 2**256 -1; + _state.epochs[epoch()].auction.maxYield = 0; + _state.epochs[epoch()].auction.minDollarAmount = 2**256 -1; + _state.epochs[epoch()].auction.maxDollarAmount = 0; + _state.epochs[epoch()].auction.isInit = true; + } + } + + function cancelCouponAuctionAtEpoch(uint256 epoch) internal { + _state.epochs[epoch].auction.canceled = true; + } + + function finishCouponAuctionAtEpoch(uint256 epoch) internal { + _state.epochs[epoch].auction.finished = true; + } + + function setCouponBidderState(uint256 epoch, address bidder, uint256 couponEpochExpiry, uint256 dollarAmount, uint256 maxCouponAmount) internal { + Epoch.CouponBidderState storage bidderState = _state.epochs[epoch].auction.couponBidderState[bidder]; + + bidderState.couponExpiryEpoch = couponEpochExpiry; + bidderState.dollarAmount = dollarAmount; + bidderState.couponAmount = maxCouponAmount; + bidderState.bidder = bidder; + } + + function setCouponBidderStateDistance(uint256 epoch, address bidder, Decimal.D256 memory distance) internal { + _state.epochs[epoch].auction.couponBidderState[bidder].distance = distance; + } + + function setCouponBidderStateSelected(uint256 epoch, address bidder, uint256 index) internal { + _state.epochs[epoch].auction.couponBidderState[bidder].selected = true; + _state.epochs[epoch].auction.seletedCouponBidder[index] = bidder; + } + + function setCouponBidderStateRejected(uint256 epoch, address bidder) internal { + _state.epochs[epoch].auction.couponBidderState[bidder].rejected = true; + } + + function setCouponBidderStateRedeemed(uint256 epoch, address bidder) internal { + _state.epochs[epoch].auction.couponBidderState[bidder].redeemed = true; + } + + function setCouponBidderStateIndex(uint256 epoch, uint256 index, address bidder) internal { + _state.epochs[epoch].auction.couponBidder[index] = bidder; + } + + function incrementCouponAuctionBids() internal { + _state.epochs[epoch()].auction._totalBids++; + } + + function setCouponAuctionRelYield(uint256 yield) internal { + if (yield > _state.epochs[epoch()].auction.maxYield) { + _state.epochs[epoch()].auction.maxYield = yield; + } + + if (_state.epochs[epoch()].auction.minYield > yield) { + _state.epochs[epoch()].auction.minYield = yield; + } + } + + function setCouponAuctionRelExpiry(uint256 couponEpochExpiry) internal { + if (couponEpochExpiry > _state.epochs[epoch()].auction.maxExpiry) { + _state.epochs[epoch()].auction.maxExpiry = couponEpochExpiry; + } + + if (couponEpochExpiry < _state.epochs[epoch()].auction.minExpiry) { + _state.epochs[epoch()].auction.minExpiry = couponEpochExpiry; + } + } + + function setCouponAuctionRelDollarAmount(uint256 couponDollarAmount) internal { + if (couponDollarAmount > _state.epochs[epoch()].auction.maxDollarAmount) { + _state.epochs[epoch()].auction.maxDollarAmount = couponDollarAmount; + } + + if (couponDollarAmount < _state.epochs[epoch()].auction.minDollarAmount) { + _state.epochs[epoch()].auction.minDollarAmount = couponDollarAmount; + } + } + + function setMinExpiryFilled(uint256 epoch, uint256 minExpiryFilled) internal { + _state.epochs[epoch].auction.minExpiryFilled = minExpiryFilled; + } + + function setMaxExpiryFilled(uint256 epoch, uint256 maxExpiryFilled) internal { + _state.epochs[epoch].auction.maxExpiryFilled = maxExpiryFilled; + } + + function setAvgExpiryFilled(uint256 epoch, uint256 avgExpiryFilled) internal { + _state.epochs[epoch].auction.avgExpiryFilled = avgExpiryFilled; + } + + function setMinYieldFilled(uint256 epoch, uint256 minYieldFilled) internal { + _state.epochs[epoch].auction.minYieldFilled = minYieldFilled; + } + + function setMaxYieldFilled(uint256 epoch, uint256 maxYieldFilled) internal { + _state.epochs[epoch].auction.maxYieldFilled = maxYieldFilled; + } + + function setAvgYieldFilled(uint256 epoch, uint256 avgYieldFilled) internal { + _state.epochs[epoch].auction.avgYieldFilled = avgYieldFilled; + } + + function setBidToCover(uint256 epoch, uint256 bidToCover) internal { + _state.epochs[epoch].auction.bidToCover = bidToCover; + } + + function setTotalFilled(uint256 epoch, uint256 totalFilled) internal { + _state.epochs[epoch].auction.totalFilled = totalFilled; + } + + function setTotalAuctioned(uint256 epoch, uint256 totalAuctioned) internal { + _state.epochs[epoch].auction.totalAuctioned = totalAuctioned; + } + + function setCouponAuctionStateDead(uint256 epoch) internal { + _state.epochs[epoch].auction.dead = true; + } + + function setTotalBurned(uint256 epoch, uint256 totalBurned) internal { + _state.epochs[epoch].auction.totalBurned = totalBurned; + } + + function setEarliestDeadAuctionEpoch(uint256 epoch) internal { + _state.epoch.earliestDeadAuction = epoch; + } + + function setLatestCouponAuctionRedeemedSelectedBidderIndex(uint256 epoch, uint256 index) internal { + _state.epochs[epoch].auction.latestRedeemedSelectedBidderIndex = index; + } + + /** * Governance */ diff --git a/protocol/contracts/dao/State.sol b/protocol/contracts/dao/State.sol index 50888867..b3d8abe7 100644 --- a/protocol/contracts/dao/State.sol +++ b/protocol/contracts/dao/State.sol @@ -44,6 +44,7 @@ contract Epoch { uint256 start; uint256 period; uint256 current; + uint256 earliestDeadAuction; } struct Coupons { @@ -52,10 +53,52 @@ contract Epoch { uint256[] expiring; } + struct CouponBidderState { + bool dead; + bool selected; + bool rejected; + bool redeemed; + address bidder; + uint256 dollarAmount; + uint256 couponAmount; + Decimal.D256 distance; + uint256 couponExpiryEpoch; + uint256 couponRedemptionIndex; + } + + struct AuctionState { + bool dead; + bool isInit; + bool canceled; + bool finished; + uint256 minExpiry; + uint256 maxExpiry; + uint256 minYield; + uint256 maxYield; + uint256 _totalBids; + uint256 bidToCover; + uint256 totalFilled; + uint256 totalBurned; + uint256 totalAuctioned; + uint256 minYieldFilled; + uint256 maxYieldFilled; + uint256 avgYieldFilled; + uint256 minExpiryFilled; + uint256 maxExpiryFilled; + uint256 avgExpiryFilled; + uint256 minDollarAmount; + uint256 maxDollarAmount; + mapping(uint256 => address) couponBidder; + uint256 latestRedeemedSelectedBidderIndex; + mapping(uint256 => address) seletedCouponBidder; + mapping(address => CouponBidderState) couponBidderState; + } + struct State { uint256 bonded; Coupons coupons; - } + AuctionState auction; + } } contract Candidate { diff --git a/protocol/contracts/mock/MockRegulator.sol b/protocol/contracts/mock/MockRegulator.sol index d8787664..b3844ed6 100644 --- a/protocol/contracts/mock/MockRegulator.sol +++ b/protocol/contracts/mock/MockRegulator.sol @@ -18,11 +18,14 @@ pragma solidity ^0.5.17; pragma experimental ABIEncoderV2; import "../dao/Regulator.sol"; +import "../dao/Market.sol"; import "../oracle/IOracle.sol"; import "./MockComptroller.sol"; import "./MockState.sol"; contract MockRegulator is MockComptroller, Regulator { + bytes32 private constant FILE = "MockRegulator"; + constructor (address oracle, address pool) MockComptroller(pool) public { _state.provider.oracle = IOracle(oracle); } @@ -34,4 +37,94 @@ contract MockRegulator is MockComptroller, Regulator { function bootstrappingAt(uint256 epoch) public view returns (bool) { return epoch <= 5; } + + function settleCouponAuctionE(uint256 epoch) external { + super.settleCouponAuction(epoch); + } + + function cancelCouponAuctionAtEpochE(uint256 epoch) external { + super.cancelCouponAuctionAtEpoch(epoch); + } + + function finishCouponAuctionAtEpochE(uint256 epoch) external { + super.finishCouponAuctionAtEpoch(epoch); + } + + function initCouponAuctionE() external { + super.initCouponAuction(); + } + + function getCouponAuctionBidsE(uint256 epoch) external returns (uint256) { + return super.getCouponAuctionBids(epoch); + } + + function getCouponAuctionMinExpiryE(uint256 epoch) external returns (uint256) { + return super.getCouponAuctionMinExpiry(epoch); + } + + function getCouponAuctionMaxExpiryE(uint256 epoch) external returns (uint256) { + return super.getCouponAuctionMaxExpiry(epoch); + } + + function getCouponAuctionMinYieldE(uint256 epoch) external returns (uint256) { + return super.getCouponAuctionMinYield(epoch); + } + + function getCouponAuctionMaxYieldE(uint256 epoch) external returns (uint256) { + return super.getCouponAuctionMaxYield(epoch); + } + + function getCouponAuctionMinDollarAmountE(uint256 epoch) external returns (uint256) { + return super.getCouponAuctionMinDollarAmount(epoch); + } + + function getCouponAuctionMaxDollarAmountE(uint256 epoch) external returns (uint256) { + return super.getCouponAuctionMaxDollarAmount(epoch); + } + + /* for testing only */ + + function placeCouponAuctionBid(uint256 couponEpochExpiry, uint256 dollarAmount, uint256 maxCouponAmount) external returns (bool) { + Require.that( + couponEpochExpiry > 0, + FILE, + "Must have non-zero expiry" + ); + + Require.that( + dollarAmount > 0, + FILE, + "Must bid non-zero amount" + ); + + Require.that( + maxCouponAmount > 0, + FILE, + "Must bid on non-zero amount" + ); + + Require.that( + acceptableBidCheck(msg.sender, dollarAmount), + FILE, + "Must have enough in account" + ); + + uint256 yield = maxCouponAmount.div(dollarAmount); + uint256 maxYield = Constants.getCouponMaxYieldToBurn(); + + Require.that( + maxYield >= yield, + FILE, + "Must be under maxYield" + ); + + uint256 epochExpiry = epoch().add(couponEpochExpiry); + setCouponAuctionRelYield(maxCouponAmount.div(dollarAmount)); + setCouponAuctionRelDollarAmount(dollarAmount); + setCouponAuctionRelExpiry(epochExpiry); + setCouponBidderState(uint256(epoch()), msg.sender, couponEpochExpiry, dollarAmount, maxCouponAmount); + setCouponBidderStateIndex(uint256(epoch()), getCouponAuctionBids(uint256(epoch())), msg.sender); + incrementCouponAuctionBids(); + return true; + } } diff --git a/protocol/contracts/mock/MockState.sol b/protocol/contracts/mock/MockState.sol index c745a3d0..b73c8097 100644 --- a/protocol/contracts/mock/MockState.sol +++ b/protocol/contracts/mock/MockState.sol @@ -119,6 +119,11 @@ contract MockState is Setters { super.eliminateOutstandingCoupons(epoch); } + function isCouponAuctionInitAtEpochE(uint256 epoch) external returns (bool) { + return super.getCouponAuctionAtEpoch(epoch).isInit; + } + + /** * Governance */ diff --git a/protocol/test/dao/Market.test.js b/protocol/test/dao/Market.test.js index 2d23b59f..e75dd358 100644 --- a/protocol/test/dao/Market.test.js +++ b/protocol/test/dao/Market.test.js @@ -142,6 +142,69 @@ describe('Market', function () { }); }); + describe('placeCouponAuctionBid', function () { + describe('before call', function () { + beforeEach(async function () { + await this.market.incrementTotalDebtE(100000); + }); + }); + + describe('zero expiry', function () { + it('reverts', async function () { + await expectRevert(this.market.placeCouponAuctionBid(0, 100, 500, {from: userAddress}), "Market: Must have non-zero expiry"); + }); + }); + + describe('no dollar amount', function () { + it('reverts', async function () { + await expectRevert(this.market.placeCouponAuctionBid(1, 0, 500, {from: userAddress}), "Market: Must bid non-zero amount"); + }); + }); + + describe('no coupon amount', function () { + it('reverts', async function () { + await expectRevert(this.market.placeCouponAuctionBid(1, 1, 0, {from: userAddress}), "Market: Must bid on non-zero amount"); + }); + }); + + describe('on single call', function () { + beforeEach(async function () { + await this.market.incrementTotalDebtE(100000); + this.result = await this.market.placeCouponAuctionBid(1, 100, 500, {from: userAddress}); + }); + + it('emits CouponBidPlaced event', async function () { + const event = await expectEvent(this.result, 'CouponBidPlaced', { + account: userAddress, + }); + expect(event.args.epoch).to.be.bignumber.equal(new BN(2)); + expect(event.args.dollarAmount).to.be.bignumber.equal(new BN(100)); + expect(event.args.maxCouponAmount).to.be.bignumber.equal(new BN(500)); + }); + }); + + describe('multiple calls', function () { + beforeEach(async function () { + await this.market.incrementTotalDebtE(1000000); + await this.market.placeCouponAuctionBid(1, 1000, 50000, {from: userAddress}); + await this.market.placeCouponAuctionBid(1, 2000, 50000, {from: userAddress}); + this.result = await this.market.placeCouponAuctionBid(2, 1000, 50000, {from: userAddress}); + }); + + + it('emits CouponBidPlaced event', async function () { + const event = await expectEvent(this.result, 'CouponBidPlaced', { + account: userAddress, + }); + + expect(event.args.epoch).to.be.bignumber.equal(new BN(3)); + expect(event.args.dollarAmount).to.be.bignumber.equal(new BN(1000)); + expect(event.args.maxCouponAmount).to.be.bignumber.equal(new BN(50000)); + }); + }); + + }); + describe('redeemCoupons', function () { beforeEach(async function () { await this.market.incrementTotalDebtE(100000); diff --git a/protocol/test/dao/Regulator.test.js b/protocol/test/dao/Regulator.test.js index 670cf683..5e2edc69 100644 --- a/protocol/test/dao/Regulator.test.js +++ b/protocol/test/dao/Regulator.test.js @@ -1,6 +1,6 @@ const { accounts, contract } = require('@openzeppelin/test-environment'); -const { BN, expectEvent } = require('@openzeppelin/test-helpers'); +const { BN, expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); const MockRegulator = contract.fromArtifact('MockRegulator'); @@ -24,11 +24,12 @@ function treasuryIncentive(newAmount) { } describe('Regulator', function () { - const [ ownerAddress, userAddress, poolAddress ] = accounts; + const [ ownerAddress, userAddress, poolAddress, userAddress2, userAddress3, userAddress4 ] = accounts; beforeEach(async function () { this.oracle = await MockSettableOracle.new({from: ownerAddress, gas: 8000000}); this.regulator = await MockRegulator.new(this.oracle.address, poolAddress, {from: ownerAddress, gas: 8000000}); + this.dollar = await Dollar.at(await this.regulator.dollar()); }); @@ -74,6 +75,11 @@ describe('Regulator', function () { expect(await this.regulator.totalCoupons()).to.be.bignumber.equal(new BN(0)); expect(await this.regulator.totalRedeemable()).to.be.bignumber.equal(new BN(0)); }); + it('has not created any auction in the past 7 epochs', async function () { + for(var a_idx = 1; a_idx<8; a_idx++){ + expect(await this.regulator.isCouponAuctionInitAtEpochE.call(a_idx)).equal(false); + } + }); it('emits SupplyIncrease event', async function () { const event = await expectEvent.inTransaction(this.txHash, MockRegulator, 'SupplyIncrease', {}); @@ -121,6 +127,12 @@ describe('Regulator', function () { expect(await this.regulator.totalRedeemable()).to.be.bignumber.equal(new BN(0)); }); + it('has not created any auction in the past 7 epochs', async function () { + for(var a_idx = 1; a_idx<8; a_idx++){ + expect(await this.regulator.isCouponAuctionInitAtEpochE.call(a_idx)).equal(false); + } + }); + it('emits SupplyIncrease event', async function () { const event = await expectEvent.inTransaction(this.txHash, MockRegulator, 'SupplyIncrease', {}); @@ -175,6 +187,12 @@ describe('Regulator', function () { expect(await this.regulator.totalRedeemable()).to.be.bignumber.equal(new BN(this.expectedRewardCoupons)); }); + it('has not created any auction in the past 7 epochs', async function () { + for(var a_idx = 1; a_idx<8; a_idx++){ + expect(await this.regulator.isCouponAuctionInitAtEpochE.call(a_idx)).equal(false); + } + }); + it('emits SupplyIncrease event', async function () { const event = await expectEvent.inTransaction(this.txHash, MockRegulator, 'SupplyIncrease', {}); @@ -230,6 +248,12 @@ describe('Regulator', function () { expect(await this.regulator.totalRedeemable()).to.be.bignumber.equal(new BN(2000)); }); + it('has not created any auction in the past 7 epochs', async function () { + for(var a_idx = 1; a_idx<8; a_idx++){ + expect(await this.regulator.isCouponAuctionInitAtEpochE.call(a_idx)).equal(false); + } + }); + it('emits SupplyIncrease event', async function () { const event = await expectEvent.inTransaction(this.txHash, MockRegulator, 'SupplyIncrease', {}); @@ -284,6 +308,12 @@ describe('Regulator', function () { expect(await this.regulator.totalRedeemable()).to.be.bignumber.equal(new BN(this.expectedRewardCoupons)); }); + it('has not created any auction in the past 7 epochs', async function () { + for(var a_idx = 1; a_idx<8; a_idx++){ + expect(await this.regulator.isCouponAuctionInitAtEpochE.call(a_idx)).equal(false); + } + }); + it('emits SupplyIncrease event', async function () { const event = await expectEvent.inTransaction(this.txHash, MockRegulator, 'SupplyIncrease', {}); @@ -323,6 +353,13 @@ describe('Regulator', function () { expect(await this.dollar.balanceOf(poolAddress)).to.be.bignumber.equal(new BN(0)); }); + it('has created 1 auction in the past 8 epochs', async function () { + for(var a_idx = 1; a_idx<8; a_idx++){ + expect(await this.regulator.isCouponAuctionInitAtEpochE.call(a_idx)).equal(false); + } + expect(await this.regulator.isCouponAuctionInitAtEpochE.call(8)).equal(true); + }); + it('updates totals', async function () { expect(await this.regulator.totalStaged()).to.be.bignumber.equal(new BN(0)); expect(await this.regulator.totalBonded()).to.be.bignumber.equal(new BN(1000000)); @@ -377,6 +414,13 @@ describe('Regulator', function () { expect(await this.regulator.totalRedeemable()).to.be.bignumber.equal(new BN(0)); }); + it('has created 1 auction in the past 7 epochs', async function () { + for(var a_idx = 1; a_idx<7; a_idx++){ + expect(await this.regulator.isCouponAuctionInitAtEpochE.call(a_idx)).equal(false); + } + expect(await this.regulator.isCouponAuctionInitAtEpochE.call(7)).equal(true); + }); + it('emits SupplyDecrease event', async function () { const event = await expectEvent.inTransaction(this.txHash, MockRegulator, 'SupplyDecrease', {}); @@ -425,6 +469,13 @@ describe('Regulator', function () { }); }); + it('has created 1 auction in the past 7 epochs', async function () { + for(var a_idx = 1; a_idx<7; a_idx++){ + expect(await this.regulator.isCouponAuctionInitAtEpochE.call(a_idx)).equal(false); + } + expect(await this.regulator.isCouponAuctionInitAtEpochE.call(7)).equal(true); + }); + it('emits SupplyDecrease event', async function () { const event = await expectEvent.inTransaction(this.txHash, MockRegulator, 'SupplyDecrease', {}); @@ -473,6 +524,13 @@ describe('Regulator', function () { }); }); + it('has created 1 auction in the past 7 epochs', async function () { + for(var a_idx = 1; a_idx<7; a_idx++){ + expect(await this.regulator.isCouponAuctionInitAtEpochE.call(a_idx)).equal(false); + } + expect(await this.regulator.isCouponAuctionInitAtEpochE.call(7)).equal(true); + }); + it('emits SupplyDecrease event', async function () { const event = await expectEvent.inTransaction(this.txHash, MockRegulator, 'SupplyDecrease', {}); @@ -521,6 +579,13 @@ describe('Regulator', function () { }); }); + it('has created 1 auction in the past 7 epochs', async function () { + for(var a_idx = 1; a_idx<7; a_idx++){ + expect(await this.regulator.isCouponAuctionInitAtEpochE.call(a_idx)).equal(false); + } + expect(await this.regulator.isCouponAuctionInitAtEpochE.call(7)).equal(true); + }); + it('emits SupplyDecrease event', async function () { const event = await expectEvent.inTransaction(this.txHash, MockRegulator, 'SupplyDecrease', {}); @@ -552,6 +617,125 @@ describe('Regulator', function () { this.txHash = this.result.tx; }); + describe('when settling auction', function () { + describe('auction is not finished and not canceled', function () { + beforeEach(async function () { + await this.regulator.mintToE(userAddress, 1000000); + await this.regulator.mintToE(userAddress2, 1000000); + await this.regulator.mintToE(userAddress3, 1000000); + //await this.regulator.mintToE(userAddress4, 1000000); + await this.dollar.approve(this.regulator.address, 1000000, {from: userAddress}); + await this.dollar.approve(this.regulator.address, 1000000, {from: userAddress2}); + await this.dollar.approve(this.regulator.address, 1000000, {from: userAddress3}); + //await this.dollar.approve(this.regulator.address, 1000000, {from: userAddress4}); + }); + + it('is able to settle auction and generated internals', async function () { + // add some bidders + this.result = await this.regulator.placeCouponAuctionBid(20, 1000, 50000, {from: userAddress}); + this.result1 = await this.regulator.placeCouponAuctionBid(5, 2000, 50000, {from: userAddress2}); + //thise bidders will be rejected + this.result2 = await this.regulator.placeCouponAuctionBid(1000, 900, 50000, {from: userAddress3}); + await expectRevert(this.regulator.placeCouponAuctionBid(100990, 900, 50000, {from: userAddress4}), "MockRegulator: Must have enough in account"); + this.auction_settlement = await this.regulator.settleCouponAuctionE(7); + + + expect(await this.regulator.getCouponAuctionBidsE.call(7)).to.be.bignumber.equal(new BN(3)); + expect(await this.regulator.getCouponAuctionMinExpiryE.call(7)).to.be.bignumber.equal(new BN(12)); + expect(await this.regulator.getCouponAuctionMaxExpiryE.call(7)).to.be.bignumber.equal(new BN(1007)); + expect(await this.regulator.getCouponAuctionMinYieldE.call(7)).to.be.bignumber.equal(new BN(25)); + expect(await this.regulator.getCouponAuctionMaxYieldE.call(7)).to.be.bignumber.equal(new BN(55)); + expect(await this.regulator.getCouponAuctionMinDollarAmountE.call(7)).to.be.bignumber.equal(new BN(900)); + expect(await this.regulator.getCouponAuctionMaxDollarAmountE.call(7)).to.be.bignumber.equal(new BN(2000)); + + expect(await this.regulator.getMinExpiryFilled(7)).to.be.bignumber.equal(new BN(5)); + expect(await this.regulator.getMaxExpiryFilled(7)).to.be.bignumber.equal(new BN(1000)); + expect(await this.regulator.getAvgExpiryFilled(7)).to.be.bignumber.equal(new BN(341)); + expect(await this.regulator.getMinYieldFilled(7)).to.be.bignumber.equal(new BN(25)); + expect(await this.regulator.getMaxYieldFilled(7)).to.be.bignumber.equal(new BN(55)); + expect(await this.regulator.getAvgYieldFilled(7)).to.be.bignumber.equal(new BN(43)); + expect(await this.regulator.getBidToCover(7)).to.be.bignumber.equal(new BN(100)); + expect(await this.regulator.getTotalFilled(7)).to.be.bignumber.equal(new BN(3)); + }); + }); + + describe('auction is finished', function () { + beforeEach(async function () { + //finish the auction + await this.regulator.finishCouponAuctionAtEpochE(7); + }); + + it('is able to not settle auction', async function () { + // add some bidders + this.result = await this.regulator.placeCouponAuctionBid(20, 1000, 50000, {from: userAddress}); + this.result1 = await this.regulator.placeCouponAuctionBid(5, 2000, 50000, {from: userAddress2}); + this.auction_settlement = await this.regulator.settleCouponAuctionE.call(7); + expect(this.auction_settlement).to.be.equal(false); + }); + + + }); + describe('auction is canceled', function () { + beforeEach(async function () { + //finish the auction + await this.regulator.cancelCouponAuctionAtEpochE(7); + }); + + it('is able to not settle auction', async function () { + // add some bidders + this.result = await this.regulator.placeCouponAuctionBid(20, 1000, 50000, {from: userAddress}); + this.result1 = await this.regulator.placeCouponAuctionBid(5, 2000, 50000, {from: userAddress2}); + this.auction_settlement = await this.regulator.settleCouponAuctionE.call(7); + expect(this.auction_settlement).to.be.equal(false); + }); + + }); + }); + + describe('when calling init again during auction', function () { + describe('auction is not finished and not canceled', function () { + beforeEach(async function () { + await this.regulator.mintToE(userAddress, 1000000); + await this.regulator.mintToE(userAddress2, 1000000); + await this.regulator.mintToE(userAddress3, 1000000); + await this.regulator.mintToE(userAddress4, 1000000); + await this.dollar.approve(this.regulator.address, 1000000, {from: userAddress}); + await this.dollar.approve(this.regulator.address, 1000000, {from: userAddress2}); + await this.dollar.approve(this.regulator.address, 1000000, {from: userAddress3}); + await this.dollar.approve(this.regulator.address, 1000000, {from: userAddress4}); + }); + + it('is able to settle auction and generated internals without resetting them', async function () { + // add some bidders + this.result = await this.regulator.placeCouponAuctionBid(20, 1000, 50000, {from: userAddress}); + this.result1 = await this.regulator.placeCouponAuctionBid(5, 2000, 50000, {from: userAddress2}); + //thise bidders will be rejected + this.result2 = await this.regulator.placeCouponAuctionBid(1000, 900, 50000, {from: userAddress3}); + this.result3 = await this.regulator.placeCouponAuctionBid(100990, 900, 50000, {from: userAddress4}); + this.auction_settlement = await this.regulator.settleCouponAuctionE(7); + + await this.regulator.initCouponAuctionE.call(); + + expect(await this.regulator.getCouponAuctionBidsE.call(7)).to.be.bignumber.equal(new BN(4)); + expect(await this.regulator.getCouponAuctionMinExpiryE.call(7)).to.be.bignumber.equal(new BN(12)); + expect(await this.regulator.getCouponAuctionMaxExpiryE.call(7)).to.be.bignumber.equal(new BN(100997)); + expect(await this.regulator.getCouponAuctionMinYieldE.call(7)).to.be.bignumber.equal(new BN(25)); + expect(await this.regulator.getCouponAuctionMaxYieldE.call(7)).to.be.bignumber.equal(new BN(55)); + expect(await this.regulator.getCouponAuctionMinDollarAmountE.call(7)).to.be.bignumber.equal(new BN(900)); + expect(await this.regulator.getCouponAuctionMaxDollarAmountE.call(7)).to.be.bignumber.equal(new BN(2000)); + + expect(await this.regulator.getMinExpiryFilled(7)).to.be.bignumber.equal(new BN(5)); + expect(await this.regulator.getMaxExpiryFilled(7)).to.be.bignumber.equal(new BN(100990)); + expect(await this.regulator.getAvgExpiryFilled(7)).to.be.bignumber.equal(new BN(25503)); + expect(await this.regulator.getMinYieldFilled(7)).to.be.bignumber.equal(new BN(25)); + expect(await this.regulator.getMaxYieldFilled(7)).to.be.bignumber.equal(new BN(55)); + expect(await this.regulator.getAvgYieldFilled(7)).to.be.bignumber.equal(new BN(46)); + expect(await this.regulator.getBidToCover(7)).to.be.bignumber.equal(new BN(100)); + expect(await this.regulator.getTotalFilled(7)).to.be.bignumber.equal(new BN(4)); + }); + }); + }); + it('doesnt mint new Dollar tokens', async function () { expect(await this.dollar.totalSupply()).to.be.bignumber.equal(new BN(1000000)); expect(await this.dollar.balanceOf(this.regulator.address)).to.be.bignumber.equal(new BN(1000000)); @@ -569,6 +753,13 @@ describe('Regulator', function () { }); }); + it('has created 1 auction in the past 7 epochs', async function () { + for(var a_idx = 1; a_idx<7; a_idx++){ + expect(await this.regulator.isCouponAuctionInitAtEpochE.call(a_idx)).equal(false); + } + expect(await this.regulator.isCouponAuctionInitAtEpochE.call(7)).equal(true); + }); + it('emits SupplyDecrease event', async function () { const event = await expectEvent.inTransaction(this.txHash, MockRegulator, 'SupplyDecrease', {});