Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Further fixes #316

Merged
merged 4 commits into from
Nov 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
697 changes: 349 additions & 348 deletions .gas-snapshot

Large diffs are not rendered by default.

11 changes: 11 additions & 0 deletions src/feeds/LyraForwardFeed.sol
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ contract LyraForwardFeed is BaseLyraFeed, ILyraForwardFeed, IForwardFeed, ISettl

ISpotFeed public spotFeed;

uint64 public maxExpiry = 365 days;

/// @dev secondary heartbeat for when the forward price is close to expiry
uint64 public settlementHeartbeat = 5 minutes;

Expand Down Expand Up @@ -65,6 +67,11 @@ contract LyraForwardFeed is BaseLyraFeed, ILyraForwardFeed, IForwardFeed, ISettl
emit SettlementHeartbeatUpdated(_settlementHeartbeat);
}

function setMaxExpiry(uint64 _maxExpiry) external onlyOwner {
maxExpiry = _maxExpiry;
emit MaxExpiryUpdated(_maxExpiry);
}

/**
* @dev update the spot feed address
*/
Expand Down Expand Up @@ -217,6 +224,10 @@ contract LyraForwardFeed is BaseLyraFeed, ILyraForwardFeed, IForwardFeed, ISettl
revert LFF_InvalidFwdDataTimestamp();
}

if (expiry > block.timestamp + maxExpiry) {
revert LFF_ExpiryTooFarInFuture();
}

SettlementDetails memory settlementData;
if (feedData.timestamp >= expiry - SETTLEMENT_TWAP_DURATION) {
// Settlement data, must include spot aggregate values
Expand Down
16 changes: 14 additions & 2 deletions src/feeds/LyraSpotDiffFeed.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ pragma solidity ^0.8.18;
// libraries
import "openzeppelin/utils/math/Math.sol";
import "openzeppelin/utils/math/SafeCast.sol";
import "lyra-utils/decimals/SignedDecimalMath.sol";

// inherited
import "./BaseLyraFeed.sol";
Expand All @@ -19,11 +20,14 @@ import {ISpotFeed} from "../interfaces/ISpotFeed.sol";
* @notice Feed that returns the total of a spot feed and the updated feed value
*/
contract LyraSpotDiffFeed is BaseLyraFeed, ILyraSpotDiffFeed, ISpotDiffFeed {
using SignedDecimalMath for int;
////////////////////////
// Variables //
////////////////////////

ISpotFeed public spotFeed;
/// @dev spotDiffCap Cap the value returned based on a percentage of the spot price
int public constant SPOT_DIFF_CAP = 1.1e18;

SpotDiffDetail public spotDiffDetails;

Expand Down Expand Up @@ -54,13 +58,21 @@ contract LyraSpotDiffFeed is BaseLyraFeed, ILyraSpotDiffFeed, ISpotDiffFeed {
*/
function getResult() public view returns (uint, uint) {
(uint spot, uint spotConfidence) = spotFeed.getSpot();
int spotInt = SafeCast.toInt256(spot);

SpotDiffDetail memory diffDetails = spotDiffDetails;
_checkNotStale(diffDetails.timestamp);

uint res = SafeCast.toUint256(SafeCast.toInt256(spot) + int(diffDetails.spotDiff));
int spotDiff = int(diffDetails.spotDiff);
int res = spotInt + spotDiff;

return (res, Math.min(spotConfidence, diffDetails.confidence));
if (spotDiff > 0) {
res = SignedMath.min(res, spotInt.multiplyDecimal(SPOT_DIFF_CAP));
} else {
res = SignedMath.max(res, spotInt.divideDecimal(SPOT_DIFF_CAP));
}

return (SafeCast.toUint256(res), Math.min(spotConfidence, diffDetails.confidence));
}

/**
Expand Down
2 changes: 0 additions & 2 deletions src/interfaces/IDutchAuction.sol
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@ interface IDutchAuction {
bool ongoing;
/// For insolvent auctions, snapshot MM at the time the auction starts
uint cachedMM;
/// The percentage of the portfolio that is left to be auctioned
uint percentageLeft;
/// The startTime of the auction
uint startTime;
/// The total amount of cash paid into the account during the auction
Expand Down
2 changes: 2 additions & 0 deletions src/interfaces/ILyraForwardFeed.sol
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ interface ILyraForwardFeed is IBaseLyraFeed {
////////////////////////
event SpotFeedUpdated(ISpotFeed spotFeed);
event SettlementHeartbeatUpdated(uint64 settlementHeartbeat);
event MaxExpiryUpdated(uint64 maxExpiry);
event ForwardDataUpdated(uint64 indexed expiry, ForwardDetails fwdDetails, SettlementDetails settlementDetails);

////////////////////////
Expand All @@ -33,4 +34,5 @@ interface ILyraForwardFeed is IBaseLyraFeed {
error LFF_InvalidFwdDataTimestamp();
error LFF_InvalidDataTimestampForSettlement();
error LFF_SettlementDataTooOld();
error LFF_ExpiryTooFarInFuture();
}
45 changes: 14 additions & 31 deletions src/liquidation/DutchAuction.sol
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,6 @@ contract DutchAuction is IDutchAuction, Ownable2Step, ReentrancyGuard {
ongoing: true,
cachedMM: 0,
startTime: block.timestamp,
percentageLeft: 1e18,
reservedCash: 0
});

Expand All @@ -188,7 +187,6 @@ contract DutchAuction is IDutchAuction, Ownable2Step, ReentrancyGuard {
ongoing: true,
cachedMM: insolventMM,
startTime: block.timestamp,
percentageLeft: 1e18,
reservedCash: 0
});
emit InsolventAuctionStarted(accountId, scenarioId, maintenanceMargin);
Expand Down Expand Up @@ -260,7 +258,7 @@ contract DutchAuction is IDutchAuction, Ownable2Step, ReentrancyGuard {
* @dev Takes in the auction and returns the account id
* @param accountId Account ID of the liquidated account
* @param bidderId Account ID of bidder, must be owned by msg.sender
* @param percentOfAccount Percentage of account to liquidate, in 18 decimals
* @param percentOfAccount Percentage of current account to liquidate, in 18 decimals
* @param priceLimit Maximum amount of cash to be paid from bidder to liquidated account (including negative amounts for insolvent auctions). This param is ignored if set to 0
* @param expectedLastTradeId The last trade id that the bidder expects the account to be on. Can be used to prevent frontrun
* @return finalPercentage percentage of portfolio being liquidated
Expand Down Expand Up @@ -327,8 +325,6 @@ contract DutchAuction is IDutchAuction, Ownable2Step, ReentrancyGuard {
int bufferMargin,
int markToMarket
) internal returns (bool canTerminate, uint percentLiquidated, uint cashFromBidder) {
percentLiquidated = percentOfAccount;

// calculate the max percentage of "current portfolio" that can be liquidated. Priced using original portfolio.
int bidPrice = _getSolventAuctionBidPrice(accountId, markToMarket);
if (bidPrice <= 0) revert DA_SolventAuctionEnded();
Expand All @@ -340,32 +336,28 @@ contract DutchAuction is IDutchAuction, Ownable2Step, ReentrancyGuard {
// max percentage of the "current" portfolio that can be liquidated
uint maxOfCurrent = _getMaxProportion(markToMarket, bufferMargin, discount, currentAuction.reservedCash);

// calculate percentage of the original portfolio, to percentage of current portfolio
uint convertedPercentage = percentOfAccount.divideDecimal(currentAuction.percentageLeft);
if (convertedPercentage >= maxOfCurrent) {
convertedPercentage = maxOfCurrent;
percentLiquidated = convertedPercentage.multiplyDecimal(currentAuction.percentageLeft);
if (percentOfAccount >= maxOfCurrent) {
percentOfAccount = maxOfCurrent;
canTerminate = true;
}

cashFromBidder = bidPrice.toUint256().multiplyDecimal(percentLiquidated);
cashFromBidder = bidPrice.toUint256().multiplyDecimal(percentOfAccount);

// Bidder must have enough cash to pay for the bid, and enough cash to cover the buffer margin
_ensureBidderCashBalance(
bidderId,
cashFromBidder
+ (SignedMath.abs(bufferMargin - currentAuction.reservedCash.toInt256())).multiplyDecimal(convertedPercentage)
+ (SignedMath.abs(bufferMargin - currentAuction.reservedCash.toInt256())).multiplyDecimal(percentOfAccount)
);

// risk manager transfers portion of the account to the bidder, liquidator pays cash to accountId
ILiquidatableManager(address(subAccounts.manager(accountId))).executeBid(
accountId, bidderId, convertedPercentage, cashFromBidder, currentAuction.reservedCash
accountId, bidderId, percentOfAccount, cashFromBidder, currentAuction.reservedCash
);

currentAuction.reservedCash += cashFromBidder;
currentAuction.percentageLeft -= percentLiquidated;

return (canTerminate, percentLiquidated, cashFromBidder);
return (canTerminate, percentOfAccount, cashFromBidder);
}

/**
Expand All @@ -383,16 +375,13 @@ contract DutchAuction is IDutchAuction, Ownable2Step, ReentrancyGuard {
) internal returns (bool canTerminate, uint percentLiquidated, uint cashToBidder) {
Auction storage currentAuction = auctions[accountId];

uint percentageOfOriginalLeft = currentAuction.percentageLeft;
percentLiquidated = percentOfAccount > percentageOfOriginalLeft ? percentageOfOriginalLeft : percentOfAccount;

// the account is insolvent when the bid price for the account falls below zero
// someone get paid from security module to take on the risk
cashToBidder = (-_getInsolventAuctionBidPrice(accountId, maintenanceMargin, markToMarket)).toUint256()
.multiplyDecimal(percentLiquidated);
.multiplyDecimal(percentOfAccount);

_ensureBidderCashBalance(
bidderId, SignedMath.abs(maintenanceMargin).multiplyDecimal(percentLiquidated) - cashToBidder
bidderId, SignedMath.abs(maintenanceMargin).multiplyDecimal(percentOfAccount) - cashToBidder
);

// we first ask the security module to compensate the bidder
Expand All @@ -403,16 +392,12 @@ contract DutchAuction is IDutchAuction, Ownable2Step, ReentrancyGuard {
cash.socializeLoss(loss, bidderId);
}

// risk manager transfers portion of the account to the bidder, liquidator pays 0
uint percentageOfCurrent = percentLiquidated.divideDecimal(percentageOfOriginalLeft);

currentAuction.percentageLeft -= percentLiquidated;

ILiquidatableManager(address(subAccounts.manager(accountId))).executeBid(
accountId, bidderId, percentageOfCurrent, 0, currentAuction.reservedCash
accountId, bidderId, percentOfAccount, 0, currentAuction.reservedCash
);

canTerminate = currentAuction.percentageLeft == 0;
// can terminate as soon as someone takes 100% of the account
return (percentOfAccount == 1e18, percentOfAccount, cashToBidder);
}

////////////////////////
Expand Down Expand Up @@ -655,10 +640,8 @@ contract DutchAuction is IDutchAuction, Ownable2Step, ReentrancyGuard {
// calculate Bid price using discount and MTM
uint discount = _getDiscountPercentage(auction.startTime, block.timestamp);

// divide by percentage left to scale the MtM to the original portfolio size
int scaledMtM = (markToMarket - int(auction.reservedCash)).divideDecimal(int(auction.percentageLeft));

return scaledMtM.multiplyDecimal(int(discount));
// Discount the portfolio excluding reserved cash
return (markToMarket - int(auction.reservedCash)).multiplyDecimal(int(discount));
}

/**
Expand Down
12 changes: 3 additions & 9 deletions src/risk-managers/PMRM.sol
Original file line number Diff line number Diff line change
Expand Up @@ -179,13 +179,11 @@ contract PMRM is IPMRM, ILiquidatableManager, BaseManager, ReentrancyGuard {
) external onlyAccounts nonReentrant {
_preAdjustmentHooks(accountId, tradeId, caller, assetDeltas, managerData);

// Block any transfers where an account is under liquidation
_checkIfLiveAuction(accountId);

bool riskAdding = false;
bool cashOnly = true;
for (uint i = 0; i < assetDeltas.length; i++) {
if (assetDeltas[i].asset != cashAsset) {
cashOnly = false;
}

if (assetDeltas[i].asset == perp) {
// Settle perp PNL into cash if the user traded perp in this tx.
_settlePerpRealizedPNL(perp, accountId);
Expand All @@ -201,10 +199,6 @@ contract PMRM is IPMRM, ILiquidatableManager, BaseManager, ReentrancyGuard {
}
}

if (riskAdding || !cashOnly) {
_checkIfLiveAuction(accountId);
}

ISubAccounts.AssetBalance[] memory assetBalances = subAccounts.getAccountBalances(accountId);

if (
Expand Down
10 changes: 3 additions & 7 deletions src/risk-managers/StandardManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -272,10 +272,12 @@ contract StandardManager is IStandardManager, ILiquidatableManager, BaseManager,
) external override onlyAccounts nonReentrant {
_preAdjustmentHooks(accountId, tradeId, caller, assetDeltas, managerData);

// Block any transfers where an account is under liquidation
_checkIfLiveAuction(accountId);

// if account is only reduce perp position, increasing cash, or increasing option position, bypass check
bool riskAdding = false;
bool isPositiveCashDelta = true;
bool cashOnly = true;

// check assets are only cash or whitelisted perp and options
for (uint i = 0; i < assetDeltas.length; i++) {
Expand All @@ -287,8 +289,6 @@ contract StandardManager is IStandardManager, ILiquidatableManager, BaseManager,
continue;
}

cashOnly = false;

AssetDetail memory detail = _assetDetails[assetDeltas[i].asset];

if (!detail.isWhitelisted) revert SRM_UnsupportedAsset();
Expand All @@ -314,10 +314,6 @@ contract StandardManager is IStandardManager, ILiquidatableManager, BaseManager,
}
}

if (riskAdding || !cashOnly) {
_checkIfLiveAuction(accountId);
}

ISubAccounts.AssetBalance[] memory assetBalances = subAccounts.getAccountBalances(accountId);

if (
Expand Down
2 changes: 1 addition & 1 deletion test/auction/unit-tests/InsolventAuction.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ contract UNIT_TestInsolventAuction is DutchAuctionBase {

vm.warp(block.timestamp + 2 minutes);
vm.prank(bob);
dutchAuction.bid(aliceAcc, bobAcc, 0.5e18, 0, 0);
dutchAuction.bid(aliceAcc, bobAcc, 1e18, 0, 0);

assertEq(dutchAuction.getIsWithdrawBlocked(), false);
}
Expand Down
17 changes: 7 additions & 10 deletions test/auction/unit-tests/SolventAuction.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -172,13 +172,10 @@ contract UNIT_TestSolventAuction is DutchAuctionBase {
manager.setMarkToMarket(aliceAcc, 270e18 + int(cashFromBob));

vm.prank(charlie);
(uint charliePercentage, uint cashFromCharlie,) = dutchAuction.bid(aliceAcc, charlieAcc, percentage, 0, 0);
(uint charliePercentage, uint cashFromCharlie,) = dutchAuction.bid(aliceAcc, charlieAcc, percentage * 10 / 9, 0, 0);

assertEq(cashFromCharlie, cashFromBob, "charlie and bob should pay the same amount");
assertEq(charliePercentage, bobPercentage, "charlie should receive the same percentage as bob");

DutchAuction.Auction memory auction = dutchAuction.getAuction(aliceAcc);
assertEq(auction.percentageLeft, 0.8e18, "percentageLeft should be 0.8");
assertApproxEqAbs(cashFromCharlie, cashFromBob, 0.0001e18, "charlie and bob should pay the same amount");
assertEq(charliePercentage, bobPercentage * 10 / 9, "charlie should receive the same percentage as bob");
}

function testBidMarkToMarketChange() public {
Expand Down Expand Up @@ -456,9 +453,9 @@ contract UNIT_TestSolventAuction is DutchAuctionBase {
// check that Bob's max liquidatable percentage is capped
vm.prank(bob);
{
(uint finalPercentage, uint cashFromBob,) = dutchAuction.bid(seanAcc, bobAcc, 0.5e18, 0, 0);
assertEq(finalPercentage / 1e14, 3756); // capped at 37.5675 of original
assertEq(cashFromBob / 1e18, 1803); // 37.5675% of portfolio, price at 4800
(uint finalPercentage, uint cashFromBob,) = dutchAuction.bid(seanAcc, bobAcc, 1e18, 0, 0);
assertEq(finalPercentage / 1e14, 5366); // capped at 53.66 of current
assertEq(cashFromBob / 1e18, 1803); // 37.5675% of original portfolio, price at 4800
}
}

Expand Down Expand Up @@ -496,7 +493,7 @@ contract UNIT_TestSolventAuction is DutchAuctionBase {
manager.setMockMargin(seanAcc, false, scenario, -10000e18);
manager.setMarkToMarket(seanAcc, 9600e18);

assertEq(dutchAuction.getCurrentBidPrice(seanAcc), 7000e18);
assertEq(dutchAuction.getCurrentBidPrice(seanAcc), 5600e18); // 10000 * 0.7 *discount) * 0.8 (remaining)
manager.setMarkToMarket(seanAcc, 1000e18);

// Can restart auction
Expand Down
2 changes: 1 addition & 1 deletion test/feed/unit-tests/LyraForwardFeed.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ contract UNIT_LyraForwardFeed is LyraFeedTestUtils {
assertEq(confidence, 1e18);
}

function testCannotGetInvalidForwardDiff() public {
function testCannotGetInvalidForwardDiff2() public {
IBaseLyraFeed.FeedData memory feedData = _getDefaultForwardData();

int newDiff = -1000e18;
Expand Down
31 changes: 23 additions & 8 deletions test/feed/unit-tests/LyraSpotDiffFeed.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -54,22 +54,37 @@ contract UNIT_LyraSpotDiffFeed is LyraFeedTestUtils {
assertEq(confidence, 1e18);
}

function testCannotGetInvalidForwardDiff() public {
function testCanGetSpotDiffOutsideBounds() public {
IBaseLyraFeed.FeedData memory feedData = _getDefaultSpotDiffData();

// if 0 is expected, return the capped value
feedData.data = abi.encode(-990e18, 1e18);
feedData.timestamp = uint64(block.timestamp);
feed.acceptData(_signFeedData(feed, pk, feedData));
(uint res,) = feed.getResult();
// res is capped at spot - 10%
assertEq(res, 990e18 * 1e18 / 1.1e18);

vm.warp(block.timestamp + 1);

// even if the data would make the result negative, return the capped value
feedData.data = abi.encode(-1000e18, 1e18);
feedData.timestamp = uint64(block.timestamp);
feed.acceptData(_signFeedData(feed, pk, feedData));

vm.expectRevert("SafeCast: value must be positive");
feed.getResult();
(res,) = feed.getResult();
// res is capped at spot - 10%
assertEq(res, 990e18 * 1e18 / 1.1e18);

vm.warp(block.timestamp + 1);

// but can return 0
feedData.data = abi.encode(-990e18, 1e18);
feedData.timestamp += 1;
// also works for a positive result
feedData.data = abi.encode(1000e18, 1e18);
feedData.timestamp = uint64(block.timestamp);
feed.acceptData(_signFeedData(feed, pk, feedData));
(uint res,) = feed.getResult();
assertEq(res, 0);
(res,) = feed.getResult();
// res is capped at spot + 10%
assertEq(res, 990e18 * 1.1e18 / 1e18);
}

function testCannotUpdateSpotDiffFeedFromInvalidSigner() public {
Expand Down
Loading