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

Add rebalanceDebt function #2351

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
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 @@ -11,7 +11,7 @@ interface IPerpsAccountModule {
error InvalidDistributor(uint128 collateralId);

/**
* @notice Gets fired when an account colateral is modified.
* @notice Gets fired when an account collateral is modified.
* @param accountId Id of the account.
* @param collateralId Id of the synth market used as collateral. Synth market id, 0 for snxUSD.
* @param amountDelta requested change in amount of collateral delegated to the account.
Expand Down Expand Up @@ -146,6 +146,13 @@ interface IPerpsAccountModule {
*/
function payDebt(uint128 accountId, uint256 amount) external;

/**
* @notice Allows the account owner to pay debt using it's margin
* @param accountId Id of the account.
* @param amount debt amount to pay off
*/
function rebalanceDebt(uint128 accountId, uint256 amount) external;

/**
* @notice Returns account's debt
* @param accountId Id of the account.
Expand Down
22 changes: 22 additions & 0 deletions markets/perps-market/contracts/modules/PerpsAccountModule.sol
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,28 @@ contract PerpsAccountModule is IPerpsAccountModule {
);
}

function rebalanceDebt(uint128 accountId, uint256 amount) external override {
FeatureFlag.ensureAccessToFeature(Flags.PERPS_SYSTEM);
Account.exists(accountId);
Account.loadAccountAndValidatePermission(
accountId,
AccountRBAC._PERPS_MODIFY_COLLATERAL_PERMISSION
);
AsyncOrder.checkPendingOrder(accountId);

PerpsAccount.Data storage account = PerpsAccount.load(accountId);
uint256 debtPaid = account.rebalanceDebt(amount);

emit DebtPaid(accountId, debtPaid, ERC2771Context._msgSender());

// update interest rate after debt paid since credit capacity for market has increased
(uint128 interestRate, ) = InterestRate.update(PerpsPrice.Tolerance.DEFAULT);
emit IGlobalPerpsMarketModule.InterestRateUpdated(
PerpsMarketFactory.load().perpsMarketId,
interestRate
);
}

/**
* @inheritdoc IPerpsAccountModule
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ library GlobalPerpsMarketConfiguration {
*/
uint128 maxCollateralsPerAccount;
/**
* @dev used together with minKeeperRewardUsd to get the minumum keeper reward for the sender who settles, or liquidates the account
* @dev used together with minKeeperRewardUsd to get the minimum keeper reward for the sender who settles, or liquidates the account
*/
uint256 minKeeperProfitRatioD18;
/**
Expand Down
36 changes: 36 additions & 0 deletions markets/perps-market/contracts/storage/PerpsAccount.sol
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ library PerpsAccount {
uint256 withdrawAmount
);

error InsufficientCreditForDebtRebalance(uint128 accountId, uint256 debt, uint256 credit);

error InsufficientAccountMargin(uint256 leftover);

error AccountLiquidatable(uint128 accountId);
Expand Down Expand Up @@ -283,6 +285,40 @@ library PerpsAccount {
);
}

/**
* @notice Allows the user to repay debt using his snxUSD margin if there is any
*/
function rebalanceDebt(Data storage self, uint256 amount) internal returns (uint256 debtPaid) {
if (self.debt == 0) {
revert NonexistentDebt(self.id);
}

/*
1. if the debt is less than the amount, set debt to 0 and only take from margin the debt amount
2. if the debt is more than the amount, subtract the amount from the debt
3. excess amount is ignored
*/

GlobalPerpsMarketConfiguration.Data storage config = GlobalPerpsMarketConfiguration.load();
PerpsMarketFactory.Data storage perpsMarketFactory = PerpsMarketFactory.load();

debtPaid = MathUtil.min(self.debt, amount);

uint256 creditAvailable = self.collateralAmounts[SNX_USD_MARKET_ID];

if (creditAvailable < debtPaid) {
revert InsufficientCreditForDebtRebalance(self.id, self.debt, creditAvailable);
}

updateAccountDebt(self, -debtPaid.toInt());

perpsMarketFactory.synthetix.withdrawMarketUsd(
perpsMarketFactory.perpsMarketId,
address(config.feeCollector),
debtPaid
);
}

/**
* @notice This function validates you have enough margin to withdraw without being liquidated.
* @dev This is done by checking your collateral value against your initial maintenance value.
Expand Down
117 changes: 117 additions & 0 deletions markets/perps-market/test/integration/Account/PayDebt.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const orderFees = {
};

const ethPerpsMarketId = bn(26);
const sUSDSynthId = 0;

describe('Account - payDebt()', () => {
const { systems, provider, perpsMarkets, trader1, synthMarkets, superMarketId } =
Expand Down Expand Up @@ -126,12 +127,23 @@ describe('Account - payDebt()', () => {
it('reverts', async () => {
await assertRevert(systems().PerpsMarket.payDebt(25, bn(200)), 'AccountNotFound');
});

it('reverts', async () => {
await assertRevert(systems().PerpsMarket.rebalanceDebt(25, bn(200)), 'AccountNotFound');
});
});

describe('when no debt exists', () => {
it('reverts', async () => {
await assertRevert(systems().PerpsMarket.payDebt(accountId, bn(200)), 'NonexistentDebt');
});

it('reverts', async () => {
await assertRevert(
systems().PerpsMarket.connect(trader1()).rebalanceDebt(accountId, bn(200)),
'NonexistentDebt'
);
});
});

describe('with debt', () => {
Expand Down Expand Up @@ -219,4 +231,109 @@ describe('Account - payDebt()', () => {
});
});
});

describe('using rebalanceDebt', () => {
let accruedDebt: Wei;
const { pnl: expectedPnl, totalFees } = openAndClosePosition(wei(50), wei(2000), wei(1500));

it('accrues correct amount of debt', async () => {
accruedDebt = expectedPnl.sub(totalFees).abs();
assertBn.equal(accruedDebt.abs().toBN(), await systems().PerpsMarket.debt(accountId));
});

it('revert with insufficient credit for rebalance', async () => {
await assertRevert(
systems().PerpsMarket.connect(trader1()).rebalanceDebt(accountId, bn(200)),
'InsufficientCreditForDebtRebalance'
);
});

describe('pay off some debt', () => {
let traderUSDBalance: Wei, reportedDebt: Wei;
const payoffAmount = wei(10_000);
let tx: ethers.providers.TransactionResponse;
before(async () => {
await systems()
.PerpsMarket.connect(trader1())
.modifyCollateral(accountId, sUSDSynthId, wei(20_000).toBN());

reportedDebt = wei(await systems().PerpsMarket.reportedDebt(superMarketId()));
traderUSDBalance = wei(await systems().USD.balanceOf(await trader1().getAddress()));

tx = await systems()
.PerpsMarket.connect(trader1())
.rebalanceDebt(accountId, payoffAmount.toBN());
});

it('has correct debt', async () => {
accruedDebt = accruedDebt.sub(payoffAmount);
assertBn.equal(await systems().PerpsMarket.debt(accountId), accruedDebt.toBN());
});

it('has correct USD balance', async () => {
assertBn.equal(
await systems().USD.balanceOf(await trader1().getAddress()),
traderUSDBalance.sub(payoffAmount).toBN()
);
});

it('has correct reported debt', async () => {
assertBn.equal(
await systems().PerpsMarket.reportedDebt(superMarketId()),
reportedDebt.add(payoffAmount).toBN()
);
});

it('emits event', async () => {
await assertEvent(
tx,
`DebtPaid(${accountId}, ${bn(10_000)}, "${await trader1().getAddress()}")`,
systems().PerpsMarket
);
});
});

describe('attempt to pay off more than debt', () => {
let traderUSDBalance: Wei, withdrawableMargin: Wei, reportedDebt: Wei;

before(async () => {
await systems()
.PerpsMarket.connect(trader1())
.modifyCollateral(accountId, sUSDSynthId, wei(10_000).toBN());

reportedDebt = wei(await systems().PerpsMarket.reportedDebt(superMarketId()));
traderUSDBalance = wei(await systems().USD.balanceOf(await trader1().getAddress()));
withdrawableMargin = wei(await systems().Core.getWithdrawableMarketUsd(superMarketId()));

await systems()
.PerpsMarket.connect(trader1())
.rebalanceDebt(accountId, accruedDebt.add(wei(10_000)).toBN());
});

it('zeroes out the debt', async () => {
assertBn.equal(await systems().PerpsMarket.debt(accountId), 0);
});

it('has correct USD balance', async () => {
assertBn.equal(
await systems().USD.balanceOf(await trader1().getAddress()),
traderUSDBalance.sub(accruedDebt).toBN()
);
});

it('has correct reported debt', async () => {
assertBn.equal(
await systems().PerpsMarket.reportedDebt(superMarketId()),
reportedDebt.add(accruedDebt).toBN()
);
});

it('has correct withdrawable margin', async () => {
assertBn.equal(
await systems().Core.getWithdrawableMarketUsd(superMarketId()),
withdrawableMargin.add(accruedDebt).toBN()
);
});
});
});
});