Manage Collateral
Partially collateralized positions can go underwater as spot price and time to expiry change. In this example, we build an on-chain collateral manager to reduce risk of liquidations of multiple large options positions. If a portfolio contains 1x short call and 1x short put and ETH price goes increases from $3,000 -> $3500 we have an opportunity to rebalance collateral from short put -> short call without requiring any extra funds.
In this guide, we will create an example collateral manager contract that interact with the Lyra markets via the
LyraAdapter.sol
:As in the Trading example, to greatly simplify integration, our manager contract can inherit the
LyraAdapter.sol
which contains all the standard Lyra functions in one place.pragma solidity 0.8.9;
import {LyraAdapter} from "@lyrafinance/protocol/contracts/periphery/LyraAdapter.sol";
contract CollateralManagerExample is LyraAdapter {
constructor() LyraAdapter();
function initAdapter(
address _lyraRegistry,
address _optionMarket,
address _curveSwap,
address _feeCounter
) external onlyOwner {
// set addresses for LyraAdapter
setLyraAddresses(_lyraRegistry, _optionMarket, _curveSwap, _feeCounter);
}
}
Assuming you already have open positions, we must first transfer ownership of these positions to the manager:
import { getMarketDeploys } from '@lyrafinance/protocol';
let lyraMarket = getMarketDeploys('kovan-ovm', 'sETH');
const optionToken = new Contract(lyraMarket.OptionToken.address, lyraMarket.OptionToken.abi, deployer);
// may need to add your own routine (try/catch, gas limit, etc)
await optionToken["transferFrom"](deployer.address, collateralManagerAddress, positionId);
After transfering ownership, we record the positions in the manager contract.
LyraAdapter._getPositions()
can be used to get all position details. As we are inheriting LyraAdapter
we can also use the built-in structs.uint[10] public trackedPositionIds; // setting hard 10x position limit
mapping(uint => OptionPosition) public trackedPositions;
uint public positionHead = 0;
function trackPositions(uint[] positionIds) external onlyOwner {
require(positionHead + positionIds.length <= trackedPositions.length,
"exceeded max # of tracked positions");
OptionPosition[positionIds.length] positions = _getPositions(positionIds);
for (uint i = 0; i < positionIds.length; i++) {
// screen out long positions
if (positions[i].state == PositionState.ACTIVE
&& positions[i].optionType != OptionType.LONG_CALL
&& positions[i].optionType != OptionType.LONG_PUT
) {
trackedPositionIds[positionHead] = positionIds[i];
trackedPositions[positionIds[i]] = positions[i];
positionHead++
}
}
}
For brevity, we skip over functions that allow manual removal of positions.
To decide whether we want to topOff or take excess collateral from a position, we calculate the
minCollateral
using LyraAdapter._getMinCollateralForPosition()
and add a 50% buffer. The direct alternative to this is OptionGreekCache.getMinCollateral()
but requires more cross-contract calls.function _getTargetCollateral(uint positionId)
internal returns (uint targetCollateral) {
targetCollateral = _getMinCollateralForPosition(positionId).multiplyDecimal(1e18 + 50 * 1e16);
}
If we wanted to estimate the
minCollatateral
if price jumped 50% we could use LyraAdapter._getMinCollateral()
which takes in manual params such as spotPrice
, expiry
.Now that we can calculate the
targetCollateral
for each position, we can gather collateral from excess positions and flag positions below our target collateral. We create flaggedPositionIds
and neededCollat
arrays to keep track of "risky" positions and gatheredCollat
to track total funds held by the manager.uint[] flaggedPositionIds;
mapping(uint => uint) neededCollat;
uint gatheredCollat;
function gatherAndFlag()
external {
delete flaggedPositionIds; // clear out outdated flags/collateral
TradeInputParameters tradeParams;
for (uint i; i < positionHead; i++) {
OptionPosition currentPosition = trackedPositions[trackedPositionIds[i]];
uint currentCollat = currentPosition.collateral;
uint targetCollat = _getTargetCollateral(trackedPositionIds[i]);
if (currentCollat > targetCollat) { // if there is excess collateral
tradeParams = TradeInputParameters({
strikeId: currentPosition.strikeId,
positionId: currentPosition.positionId,
iterations: 1, // no need to optimize iterations as amount does not change
optionType: currentPosition.optionType,
amount: 0,
setCollateralTo: targetCollat, // returns any excess collateral
minTotalCost: 0,
maxTotalCost: type(uint).max
});
_openPosition(tradeParams); // using _closePosition() would have the same effect
gatheredCollat += currentCollat - targetCollat;
// update position records
trackedPositions[trackedPositionIds[i]] = _getPositions([trackedPositionIds[i]])[0];
} else { // if collateral below target, flag position
neededCollat[trackedPositionIds[i]] = targetCollateral - currentCollat;
flaggedPositionIds.push(trackedPositionIds[i]);
}
}
}
To change collateral we can use
LyraAdapter._openPosition()
. Setting the setCollateralTo
param to targetCollat
returns any excess collateral to msg.sender
.To avoid dealing with ETH/USD conversions, we assume the portfolio only uses quote collateral.
We can topoff positions the same way we gathered excess collateral. If our manager runs out of funds, the contract reverts to simply closing the position.
function topoffOrClose() external {
OptionPosition currentPosition;
TradeInputParameters tradeParams;
for (uint i; i < flaggedPositionIds.length; i++) {
currentPosition = trackedPositions[flaggedPositionIds[i]];
tradeParams = TradeInputParameters({
strikeId: currentPosition.strikeId,
positionId: currentPosition.positionId,
iterations: 1,
optionType: currentPosition.optionType,
amount: 0,
setCollateralTo: _getTargetCollateral(currentPosition.positionId),
minTotalCost: 0,
maxTotalCost: type(uint).max
});
if (gatheredCollat >= neededCollat[i]) {
_openPosition(tradeParams);
gatheredCollat -= neededCollat[i];
} else { // fully close position if not enough collateral
tradeParams.setCollateral = 0;
tradeParams.amount = currentPosition.amount;
if (_needsForceClose(OptionPosition position)) {
_forceClosePosition(tradeParams);
} else {
_closePosition(tradeParams);
}
}
neededCollat[i] = 0;
}
delete flaggedPositionIds;
}
When closing positions in the above function, we had to determine whether we need to use
closePosition()
and forceClosePosition()
as sometimes positions may be outside of the delta cutoff range or too close to expiry. We can use the built-in LyraAdapter.closeOrForceClosePosition()
function to make this decision:function _closeOrForceClosePosition(TradeInputParameters memory params)
internal
returns (TradeResult memory tradeResult) {
if (!_isOutsideDeltaCutoff(params.strikeId) && !_isWithinTradingCutoff(params.strikeId)) {
return _closePosition(params);
} else {
// will pay less competitive price to close position but bypasses Lyra delta/trading cutoffs
return _forceClosePosition(params);
}
Last modified 8mo ago