From 2e45687cc77808a00d973b46d6dfee4795cc3057 Mon Sep 17 00:00:00 2001 From: 0xMithrandir <173684735+0xMithrandir@users.noreply.github.com> Date: Wed, 18 Dec 2024 15:19:09 +0100 Subject: [PATCH 1/4] Add rebalanceDebt function --- .../interfaces/IPerpsAccountModule.sol | 9 +++++- .../contracts/modules/PerpsAccountModule.sol | 22 +++++++++++++ .../contracts/storage/PerpsAccount.sol | 31 +++++++++++++++++++ 3 files changed, 61 insertions(+), 1 deletion(-) diff --git a/markets/perps-market/contracts/interfaces/IPerpsAccountModule.sol b/markets/perps-market/contracts/interfaces/IPerpsAccountModule.sol index 43e9521f2f..90e06ebdc7 100644 --- a/markets/perps-market/contracts/interfaces/IPerpsAccountModule.sol +++ b/markets/perps-market/contracts/interfaces/IPerpsAccountModule.sol @@ -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. @@ -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. diff --git a/markets/perps-market/contracts/modules/PerpsAccountModule.sol b/markets/perps-market/contracts/modules/PerpsAccountModule.sol index 2d906b1a7c..fa795835a8 100644 --- a/markets/perps-market/contracts/modules/PerpsAccountModule.sol +++ b/markets/perps-market/contracts/modules/PerpsAccountModule.sol @@ -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 */ diff --git a/markets/perps-market/contracts/storage/PerpsAccount.sol b/markets/perps-market/contracts/storage/PerpsAccount.sol index 9948ef6a53..5cb61f9832 100644 --- a/markets/perps-market/contracts/storage/PerpsAccount.sol +++ b/markets/perps-market/contracts/storage/PerpsAccount.sol @@ -67,6 +67,8 @@ library PerpsAccount { uint256 withdrawAmount ); + error InsufficientCreditForDebtRebalance(uint128 accountId, uint256 debt, uint256 credit); + error InsufficientAccountMargin(uint256 leftover); error AccountLiquidatable(uint128 accountId); @@ -283,6 +285,35 @@ 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 + */ + + 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()); + // charge user account the repaid debt amount + charge(self, -debtPaid.toInt()); + } + /** * @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. From f4ab7f40ff09fc7aa22ee579d847e7201e180eee Mon Sep 17 00:00:00 2001 From: 0xMithrandir <173684735+0xMithrandir@users.noreply.github.com> Date: Thu, 19 Dec 2024 14:18:33 +0100 Subject: [PATCH 2/4] Add tests --- .../test/integration/Account/PayDebt.test.ts | 117 ++++++++++++++++++ 1 file changed, 117 insertions(+) diff --git a/markets/perps-market/test/integration/Account/PayDebt.test.ts b/markets/perps-market/test/integration/Account/PayDebt.test.ts index 30b93390bb..e09b4bfa4d 100644 --- a/markets/perps-market/test/integration/Account/PayDebt.test.ts +++ b/markets/perps-market/test/integration/Account/PayDebt.test.ts @@ -23,6 +23,7 @@ const orderFees = { }; const ethPerpsMarketId = bn(26); +const sUSDSynthId = 0; describe('Account - payDebt()', () => { const { systems, provider, perpsMarkets, trader1, synthMarkets, superMarketId } = @@ -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', () => { @@ -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 () => { + reportedDebt = wei(await systems().PerpsMarket.reportedDebt(superMarketId())); + traderUSDBalance = wei(await systems().USD.balanceOf(await trader1().getAddress())); + + await systems() + .PerpsMarket.connect(trader1()) + .modifyCollateral(accountId, sUSDSynthId, wei(20_000).toBN()); + + 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 () => { + 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()) + .modifyCollateral(accountId, sUSDSynthId, wei(10_000).toBN()); + + 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() + ); + }); + }); + }); }); From 9d8f69bf6721ba7119a00ec6287e590b06919d75 Mon Sep 17 00:00:00 2001 From: 0xMithrandir <173684735+0xMithrandir@users.noreply.github.com> Date: Fri, 20 Dec 2024 13:22:53 +0100 Subject: [PATCH 3/4] Update tests --- .../contracts/storage/PerpsAccount.sol | 2 -- .../test/integration/Account/PayDebt.test.ts | 14 +++++++------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/markets/perps-market/contracts/storage/PerpsAccount.sol b/markets/perps-market/contracts/storage/PerpsAccount.sol index 5cb61f9832..9135a1c7b4 100644 --- a/markets/perps-market/contracts/storage/PerpsAccount.sol +++ b/markets/perps-market/contracts/storage/PerpsAccount.sol @@ -310,8 +310,6 @@ library PerpsAccount { } updateAccountDebt(self, -debtPaid.toInt()); - // charge user account the repaid debt amount - charge(self, -debtPaid.toInt()); } /** diff --git a/markets/perps-market/test/integration/Account/PayDebt.test.ts b/markets/perps-market/test/integration/Account/PayDebt.test.ts index e09b4bfa4d..2d3b0fc4a7 100644 --- a/markets/perps-market/test/integration/Account/PayDebt.test.ts +++ b/markets/perps-market/test/integration/Account/PayDebt.test.ts @@ -253,13 +253,13 @@ describe('Account - payDebt()', () => { const payoffAmount = wei(10_000); let tx: ethers.providers.TransactionResponse; before(async () => { - reportedDebt = wei(await systems().PerpsMarket.reportedDebt(superMarketId())); - traderUSDBalance = wei(await systems().USD.balanceOf(await trader1().getAddress())); - 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()); @@ -297,14 +297,14 @@ describe('Account - payDebt()', () => { let traderUSDBalance: Wei, withdrawableMargin: Wei, reportedDebt: Wei; before(async () => { - 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()) .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()); From 982423e36c401c64beb84a895e20a66b49be04ce Mon Sep 17 00:00:00 2001 From: 0xMithrandir <173684735+0xMithrandir@users.noreply.github.com> Date: Fri, 20 Dec 2024 15:12:37 +0100 Subject: [PATCH 4/4] Update rebalanceDebt function --- .../contracts/storage/GlobalPerpsMarketConfiguration.sol | 2 +- markets/perps-market/contracts/storage/PerpsAccount.sol | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/markets/perps-market/contracts/storage/GlobalPerpsMarketConfiguration.sol b/markets/perps-market/contracts/storage/GlobalPerpsMarketConfiguration.sol index 7e14ddb4aa..9911646cd6 100644 --- a/markets/perps-market/contracts/storage/GlobalPerpsMarketConfiguration.sol +++ b/markets/perps-market/contracts/storage/GlobalPerpsMarketConfiguration.sol @@ -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; /** diff --git a/markets/perps-market/contracts/storage/PerpsAccount.sol b/markets/perps-market/contracts/storage/PerpsAccount.sol index 9135a1c7b4..91fb4090bf 100644 --- a/markets/perps-market/contracts/storage/PerpsAccount.sol +++ b/markets/perps-market/contracts/storage/PerpsAccount.sol @@ -299,6 +299,7 @@ library PerpsAccount { 3. excess amount is ignored */ + GlobalPerpsMarketConfiguration.Data storage config = GlobalPerpsMarketConfiguration.load(); PerpsMarketFactory.Data storage perpsMarketFactory = PerpsMarketFactory.load(); debtPaid = MathUtil.min(self.debt, amount); @@ -310,6 +311,12 @@ library PerpsAccount { } updateAccountDebt(self, -debtPaid.toInt()); + + perpsMarketFactory.synthetix.withdrawMarketUsd( + perpsMarketFactory.perpsMarketId, + address(config.feeCollector), + debtPaid + ); } /**