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
+        );
     }
 
     /**