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

[Fix] Add margin #92

Merged
merged 8 commits into from
Sep 5, 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
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,7 @@ export async function createMarket(
payoff: (payoff ?? instanceVars.payoff).address,
}
const riskParameter = {
margin: parse6decimal('0.3'),
maintenance: parse6decimal('0.3'),
takerFee: 0,
takerSkewFee: 0,
Expand All @@ -268,6 +269,7 @@ export async function createMarket(
k: parse6decimal('40000'),
max: parse6decimal('1.20'),
},
minMargin: parse6decimal('500'),
minMaintenance: parse6decimal('500'),
virtualTaker: 0,
staleAfter: 7200,
Expand Down Expand Up @@ -381,14 +383,14 @@ export async function createVault(
owner,
)
await instanceVars.oracleFactory.connect(owner).create(BTC_PRICE_FEE_ID, vaultOracleFactory.address)

const ethMarket = await deployProductOnMainnetFork({
factory: instanceVars.marketFactory,
token: instanceVars.dsu,
owner: owner,
oracle: ethOracle.address,
payoff: constants.AddressZero,
makerLimit: parse6decimal('1000'),
minMargin: parse6decimal('50'),
minMaintenance: parse6decimal('50'),
maxLiquidationFee: parse6decimal('25000'),
})
Expand All @@ -398,14 +400,15 @@ export async function createVault(
owner: owner,
oracle: btcOracle.address,
payoff: constants.AddressZero,
minMargin: parse6decimal('50'),
minMaintenance: parse6decimal('50'),
maxLiquidationFee: parse6decimal('25000'),
})

const vaultImpl = await new Vault__factory(owner).deploy()
const vaultFactoryImpl = await new VaultFactory__factory(owner).deploy(
instanceVars.marketFactory.address,
vaultImpl.address,
0,
)
await instanceVars.proxyAdmin.upgrade(vaultFactoryProxy.address, vaultFactoryImpl.address)
const vaultFactory = IVaultFactory__factory.connect(vaultFactoryProxy.address, owner)
Expand All @@ -422,7 +425,6 @@ export async function createVault(
await vault.updateParameter({
cap: maxCollateral ?? parse6decimal('500000'),
})

const usdc = IERC20Metadata__factory.connect(USDC, owner)
const asset = IERC20Metadata__factory.connect(await vault.asset(), owner)

Expand Down
32 changes: 16 additions & 16 deletions packages/perennial-vault/contracts/lib/StrategyLib.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import "../types/Registration.sol";

/// @title Strategy
/// @notice Logic for vault capital allocation
/// @dev - Deploys collateral first to satisfy the maintenance of each market, then deploys the rest by weight.
/// @dev - Deploys collateral first to satisfy the margin of each market, then deploys the rest by weight.
/// - Positions are then targeted based on the amount of collateral that ends up deployed to each market.
library StrategyLib {
/// @dev The maximum multiplier that is allowed for leverage
Expand All @@ -31,8 +31,8 @@ library StrategyLib {
/// @dev The latest valid price
UFixed6 latestPrice;

/// @dev The maintenance requirement of the vault
UFixed6 maintenance;
/// @dev The margin requirement of the vault
UFixed6 margin;
}

/// @dev The target allocation for a market
Expand Down Expand Up @@ -65,22 +65,22 @@ library StrategyLib {
for (uint256 marketId; marketId < registrations.length; marketId++)
contexts[marketId] = _loadContext(registrations[marketId]);

(uint256 totalWeight, UFixed6 totalMaintenance) = _aggregate(registrations, contexts);
(uint256 totalWeight, UFixed6 totalMargin) = _aggregate(registrations, contexts);

targets = new MarketTarget[](registrations.length);
for (uint256 marketId; marketId < registrations.length; marketId++) {
_AllocateLocals memory _locals;
_locals.marketCollateral = contexts[marketId].maintenance
.add(collateral.sub(totalMaintenance).muldiv(registrations[marketId].weight, totalWeight));
_locals.marketCollateral = contexts[marketId].margin
.add(collateral.sub(totalMargin).muldiv(registrations[marketId].weight, totalWeight));

_locals.marketAssets = assets
.muldiv(registrations[marketId].weight, totalWeight)
.min(_locals.marketCollateral.mul(LEVERAGE_BUFFER));

if (
contexts[marketId].marketParameter.closed ||
_locals.marketAssets.lt(contexts[marketId].riskParameter.minMaintenance)
) _locals.marketAssets = UFixed6Lib.ZERO;
UFixed6 minAssets = contexts[marketId].riskParameter.minMargin
.unsafeDiv(registrations[marketId].leverage.mul(contexts[marketId].riskParameter.maintenance));
if (contexts[marketId].marketParameter.closed || _locals.marketAssets.lt(minAssets))
_locals.marketAssets = UFixed6Lib.ZERO;

(_locals.minPosition, _locals.maxPosition) = _positionLimit(contexts[marketId]);

Expand Down Expand Up @@ -109,23 +109,23 @@ library StrategyLib {
context.currentPosition = registration.market.pendingPosition(global.currentId);

for (uint256 id = context.local.latestId; id < context.local.currentId; id++)
context.maintenance = registration.market.pendingPositions(address(this), id)
.maintenance(OracleVersion(0, global.latestPrice, true), context.riskParameter)
.max(context.maintenance);
context.margin = registration.market.pendingPositions(address(this), id)
.margin(OracleVersion(0, global.latestPrice, true), context.riskParameter)
.max(context.margin);
}

/// @notice Aggregate the context of all markets
/// @param registrations The registrations of the markets
/// @param contexts The contexts of the markets
/// @return totalWeight The total weight of all markets
/// @return totalMaintenance The total maintenance of all markets
/// @return totalMargin The total margin of all markets
function _aggregate(
Registration[] memory registrations,
MarketContext[] memory contexts
) private pure returns (uint256 totalWeight, UFixed6 totalMaintenance) {
) private pure returns (uint256 totalWeight, UFixed6 totalMargin) {
for (uint256 marketId; marketId < registrations.length; marketId++) {
totalWeight += registrations[marketId].weight;
totalMaintenance = totalMaintenance.add(contexts[marketId].maintenance);
totalMargin = totalMargin.add(contexts[marketId].margin);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export async function deployProductOnMainnetFork({
oracle,
payoff,
owner,
margin,
maintenance,
fundingFee,
interestFee,
Expand All @@ -33,13 +34,15 @@ export async function deployProductOnMainnetFork({
makerLimit,
efficiencyLimit,
utilizationCurve,
minMargin,
minMaintenance,
liquidationFee,
minLiquidationFee,
maxLiquidationFee,
staleAfter,
}: DeployProductParams): Promise<IMarket> {
const riskParameter: RiskParameterStruct = {
margin: margin ?? parse6decimal('0.10'),
maintenance: maintenance ?? parse6decimal('0.10'),
takerFee: takerFee ?? parse6decimal('0.0'),
takerSkewFee: takerSkewFee ?? parse6decimal('0.0'),
Expand All @@ -61,6 +64,7 @@ export async function deployProductOnMainnetFork({
k: parse6decimal('40000'),
max: parse6decimal('1.20'),
},
minMargin: minMargin ?? parse6decimal('100'),
minMaintenance: minMaintenance ?? parse6decimal('100'),
virtualTaker: 0,
staleAfter: staleAfter ?? 7200,
Expand Down Expand Up @@ -98,5 +102,6 @@ export async function deployProductOnMainnetFork({
const market = IMarket__factory.connect(productAddress, owner)
await market.connect(owner).updateRiskParameter(riskParameter)
await market.connect(owner).updateParameter(marketParameter)

return market
}
2 changes: 2 additions & 0 deletions packages/perennial-vault/test/integration/vault/Vault.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ describe('Vault', () => {
oracle: rootOracle.address,
payoff: constants.AddressZero,
makerLimit: parse6decimal('1000'),
minMargin: parse6decimal('50'),
minMaintenance: parse6decimal('50'),
maxLiquidationFee: parse6decimal('25000'),
})
Expand All @@ -214,6 +215,7 @@ describe('Vault', () => {
owner: owner,
oracle: btcRootOracle.address,
payoff: constants.AddressZero,
minMargin: parse6decimal('50'),
minMaintenance: parse6decimal('50'),
maxLiquidationFee: parse6decimal('25000'),
})
Expand Down
17 changes: 11 additions & 6 deletions packages/perennial/contracts/Market.sol
Original file line number Diff line number Diff line change
Expand Up @@ -476,7 +476,7 @@ contract Market is IMarket, Instance, ReentrancyGuard {

if (protected && (
!context.currentPosition.local.magnitude().isZero() ||
context.latestPosition.local.collateralized(
context.latestPosition.local.maintained(
context.latestVersion,
context.riskParameter,
collateralAfterFees.sub(collateral)
Expand Down Expand Up @@ -510,13 +510,18 @@ contract Market is IMarket, Instance, ReentrancyGuard {
) revert MarketExceedsPendingIdLimitError();

if (
!context.latestPosition.local.collateralized(context.latestVersion, context.riskParameter, collateralAfterFees)
) revert MarketInsufficientCollateralizationError();
!context.latestPosition.local.maintained(context.latestVersion, context.riskParameter, collateralAfterFees)
) revert MarketInsufficientMaintenanceError();

for (uint256 i; i < pendingLocalPositions.length; i++)
for (uint256 i; i < pendingLocalPositions.length - 1; i++)
if (
!pendingLocalPositions[i].collateralized(context.latestVersion, context.riskParameter, collateralAfterFees)
) revert MarketInsufficientCollateralizationError();
!pendingLocalPositions[i].maintained(context.latestVersion, context.riskParameter, collateralAfterFees)
) revert MarketInsufficientMaintenanceError();

if (
!pendingLocalPositions[pendingLocalPositions.length - 1]
.margined(context.latestVersion, context.riskParameter, collateralAfterFees)
) revert MarketInsufficientMarginError();

if (
(context.local.protection > context.latestPosition.local.timestamp) &&
Expand Down
3 changes: 2 additions & 1 deletion packages/perennial/contracts/interfaces/IMarket.sol
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ interface IMarket is IInstance {
event RewardUpdated(Token18 newReward);

error MarketInsufficientLiquidityError();
error MarketInsufficientCollateralizationError();
error MarketInsufficientMarginError();
error MarketInsufficientMaintenanceError();
error MarketInsufficientCollateralError();
error MarketProtectedError();
error MarketMakerOverLimitError();
Expand Down
19 changes: 17 additions & 2 deletions packages/perennial/contracts/test/PositionTester.sol
Original file line number Diff line number Diff line change
Expand Up @@ -129,12 +129,27 @@ abstract contract PositionTester {
return read().maintenance(latestVersion, riskParameter);
}

function collateralized(
function margin(
OracleVersion memory latestVersion,
RiskParameter memory riskParameter
) external view returns (UFixed6) {
return read().margin(latestVersion, riskParameter);
}

function maintained(
OracleVersion memory currentOracleVersion,
RiskParameter memory riskParameter,
Fixed6 collateral
) external view returns (bool) {
return read().maintained(currentOracleVersion, riskParameter, collateral);
}

function margined(
OracleVersion memory currentOracleVersion,
RiskParameter memory riskParameter,
Fixed6 collateral
) external view returns (bool) {
return read().collateralized(currentOracleVersion, riskParameter, collateral);
return read().margined(currentOracleVersion, riskParameter, collateral);
}

function liquidationFee(
Expand Down
49 changes: 42 additions & 7 deletions packages/perennial/contracts/types/Position.sol
Original file line number Diff line number Diff line change
Expand Up @@ -292,21 +292,40 @@ library PositionLib {
OracleVersion memory latestVersion,
RiskParameter memory riskParameter
) internal pure returns (UFixed6) {
return _collateralRequirement(self, latestVersion, riskParameter.maintenance, riskParameter.minMaintenance);
}

/// @notice Returns the margin requirement of the position
/// @param self The position object to check
/// @param latestVersion The latest oracle version
/// @param riskParameter The current risk parameter
/// @return The margin requirement of the position
function margin(
Position memory self,
OracleVersion memory latestVersion,
RiskParameter memory riskParameter
) internal pure returns (UFixed6) {
return _collateralRequirement(self, latestVersion, riskParameter.margin, riskParameter.minMargin);
}

function _collateralRequirement(
Position memory self,
OracleVersion memory latestVersion,
UFixed6 requirementRatio,
UFixed6 requirementFixed
) private pure returns (UFixed6) {
if (magnitude(self).isZero()) return UFixed6Lib.ZERO;
return magnitude(self)
.mul(latestVersion.price.abs())
.mul(riskParameter.maintenance)
.max(riskParameter.minMaintenance);
return magnitude(self).mul(latestVersion.price.abs()).mul(requirementRatio).max(requirementFixed);
}

/// @notice Returns the whether the position is collateralized
/// @notice Returns the whether the position is maintained
/// @dev shortfall is considered solvent for 0-position
/// @param self The position object to check
/// @param latestVersion The latest oracle version
/// @param riskParameter The current risk parameter
/// @param collateral The current account's collateral
/// @return Whether the position is collateralized
function collateralized(
/// @return Whether the position is maintained
function maintained(
Position memory self,
OracleVersion memory latestVersion,
RiskParameter memory riskParameter,
Expand All @@ -315,6 +334,22 @@ library PositionLib {
return collateral.max(Fixed6Lib.ZERO).gte(Fixed6Lib.from(maintenance(self, latestVersion, riskParameter)));
}

/// @notice Returns the whether the position is margined
/// @dev shortfall is considered solvent for 0-position
/// @param self The position object to check
/// @param latestVersion The latest oracle version
/// @param riskParameter The current risk parameter
/// @param collateral The current account's collateral
/// @return Whether the position is margined
function margined(
Position memory self,
OracleVersion memory latestVersion,
RiskParameter memory riskParameter,
Fixed6 collateral
) internal pure returns (bool) {
return collateral.max(Fixed6Lib.ZERO).gte(Fixed6Lib.from(margin(self, latestVersion, riskParameter)));
}

/// @notice Returns the liquidation fee of the position
/// @param self The position object to check
/// @param latestVersion The latest oracle version
Expand Down
Loading
Loading