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

feat: add BatchBadDebtRepay steward contract #4

Open
wants to merge 24 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
34929ef
Added submodule aave-address-book
TepNik Jan 13, 2025
1e92a0f
Added submodule solidity-utils
TepNik Jan 13, 2025
e6e3697
Added Prettier
TepNik Jan 14, 2025
65f4bbb
Added node_modules and VS Code settings into the gitignore file
TepNik Jan 14, 2025
5aa08c3
Added BatchRepayBadDebtSteward contract
TepNik Jan 14, 2025
12693d1
Added tests for the BatchRepayBadDebtSteward contract
TepNik Jan 14, 2025
a186712
Deleted prettier
TepNik Jan 14, 2025
4160fa4
Used Foundry prettier
TepNik Jan 14, 2025
548af0f
Removed aave-address-book and solidity-utils submodules
TepNik Jan 14, 2025
56bbdd2
Made gas improvements in the BatchRepayBadDebtSteward contract
TepNik Jan 14, 2025
c7ad378
Updated aave-helpers
TepNik Jan 14, 2025
b3bd46f
Moved tests to the Avalanche network fork
TepNik Jan 14, 2025
00f66b7
Deleted the UserBadDebtRepaid event
TepNik Jan 14, 2025
1890937
Added gas snapshots
TepNik Jan 15, 2025
6402a01
Removed the UserHasNoDebt error
TepNik Jan 15, 2025
b708e61
Added the batchLiquidate function to the BatchRepayBadDebtSteward con…
TepNik Jan 20, 2025
8f1b538
Added tests for the batchLiquidate and for the getDebtAmount function…
TepNik Jan 20, 2025
cbb9ce1
Added rescue functions into the BatchRepayBadDebtSteward contract
TepNik Jan 20, 2025
3a5bdf0
Added the AccessControl contract and the CLEANUP role into the BatchR…
TepNik Jan 20, 2025
adbe47f
Changed immutable variables to an uppercase
TepNik Jan 21, 2025
17e27ff
Removed to argument from rescue* functions
TepNik Jan 21, 2025
cfba539
Added the batchLiquidateWithMaxCap function to the BatchRepayBadDebtS…
TepNik Jan 21, 2025
8e81a30
Fixed gas snapshots
TepNik Jan 21, 2025
498031e
fix: add working ci
sakulstra Jan 27, 2025
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
13 changes: 13 additions & 0 deletions .github/workflows/comment.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
name: PR Comment

on:
workflow_run:
workflows: [Test]
types:
- completed

jobs:
test:
uses: bgd-labs/github-workflows/.github/workflows/comment.yml@main
secrets:
READ_ONLY_PAT: ${{ secrets.READ_ONLY_PAT }}
44 changes: 30 additions & 14 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
name: CI
name: Test

concurrency:
group: ${{ github.head_ref || github.run_id }}
cancel-in-progress: true

on:
push:
Expand All @@ -9,21 +13,22 @@ env:
FOUNDRY_PROFILE: ci

jobs:
check:
strategy:
fail-fast: true

test:
name: Foundry project
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: recursive

- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1
- uses: bgd-labs/action-rpc-env@main
with:
ALCHEMY_API_KEY: ${{ secrets.ALCHEMY_API_KEY }}

- name: Run Foundry setup
uses: bgd-labs/github-workflows/.github/actions/foundry-setup@main
with:
version: nightly
ZKSYNC: "true"

- name: Show Forge version
run: |
Expand All @@ -34,12 +39,23 @@ jobs:
forge fmt --check
id: fmt

- name: Run Forge build
run: |
forge build --sizes
id: build
- name: Run Forge tests
id: test
uses: bgd-labs/github-workflows/.github/actions/foundry-test@main

- name: Run ZK tests
id: zktest
uses: bgd-labs/github-workflows/.github/actions/foundry-test@main
with:
ZKSYNC: true
ROOT_DIR: "zksync"

- name: Run Forge tests
uses: bgd-labs/github-workflows/.github/actions/comment-artifact@main

# we let failing tests pass so we can log them in the comment, still we want the ci to fail
- name: Post test
if: ${{ steps.test.outputs.testStatus != 0 || steps.zktest.outputs.testStatus != 0 }}
run: |
forge test -vvv
id: test
echo "tests failed"
exit 1
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,5 @@ docs/

# Dotenv file
.env

.vscode
2 changes: 1 addition & 1 deletion .gitmodules
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
[submodule "lib/aave-helpers"]
path = lib/aave-helpers
url = https://github.com/bgd-labs/aave-helpers
url = https://github.com/bgd-labs/aave-helpers
1 change: 1 addition & 0 deletions foundry.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
[profile.default]
src = "src"
out = "out"
test = "tests"
libs = ["lib"]

# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options
Expand Down
2 changes: 1 addition & 1 deletion lib/aave-helpers
Submodule aave-helpers updated 120 files
35 changes: 35 additions & 0 deletions snapshots/BatchRepayBadDebtSteward.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"function batchLiquidate: with 0 users": "95912",
"function batchLiquidate: with 1 user": "624333",
"function batchLiquidate: with 2 users": "912060",
"function batchLiquidate: with 3 users": "1193557",
"function batchLiquidate: with 4 users": "1554574",
"function batchLiquidate: with 5 users": "1802139",
"function batchLiquidate: with 6 users": "2023707",
"function batchLiquidateWithMaxCap: with 0 users": "84855",
"function batchLiquidateWithMaxCap: with 1 user": "628028",
"function batchLiquidateWithMaxCap: with 2 users": "907219",
"function batchLiquidateWithMaxCap: with 3 users": "1180039",
"function batchLiquidateWithMaxCap: with 4 users": "1532251",
"function batchLiquidateWithMaxCap: with 5 users": "1744735",
"function batchLiquidateWithMaxCap: with 6 users": "1957206",
"function batchRepayBadDebt: with 0 users": "91680",
"function batchRepayBadDebt: with 1 user": "230976",
"function batchRepayBadDebt: with 2 users": "293874",
"function batchRepayBadDebt: with 3 users": "356913",
"function batchRepayBadDebt: with 4 users": "420091",
"function batchRepayBadDebt: with 5 users": "483410",
"function batchRepayBadDebt: with 6 users": "546869",
"function getBadDebtAmount: with 0 users": "43260",
"function getBadDebtAmount: with 1 user": "65315",
"function getBadDebtAmount: with 2 users": "78458",
"function getBadDebtAmount: with 4 users": "105165",
"function getBadDebtAmount: with 5 users": "118728",
"function getBadDebtAmount: with 6 users": "132432",
"function getDebtAmount: with 0 users": "43327",
"function getDebtAmount: with 1 user": "65313",
"function getDebtAmount: with 2 users": "78458",
"function getDebtAmount: with 4 users": "104956",
"function getDebtAmount: with 5 users": "118450",
"function getDebtAmount: with 6 users": "132085"
}
200 changes: 200 additions & 0 deletions src/maintenance/BatchRepayBadDebtSteward.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import {IPool, DataTypes} from "aave-address-book/AaveV3.sol";

import {UserConfiguration} from "aave-v3-origin/contracts/protocol/libraries/configuration/UserConfiguration.sol";

import {IERC20} from "solidity-utils/contracts/oz-common/interfaces/IERC20.sol";
import {SafeERC20} from "solidity-utils/contracts/oz-common/SafeERC20.sol";

import {IRescuableBase} from "solidity-utils/contracts/utils/interfaces/IRescuableBase.sol";
import {RescuableBase} from "solidity-utils/contracts/utils/RescuableBase.sol";

import {IWithGuardian} from "solidity-utils/contracts/access-control/interfaces/IWithGuardian.sol";
import {OwnableWithGuardian} from "solidity-utils/contracts/access-control/OwnableWithGuardian.sol";
import {Context as OzCommonContext} from "solidity-utils/contracts/oz-common/Context.sol";

import {AccessControl} from "openzeppelin-contracts/contracts/access/AccessControl.sol";
import {Context as OzContext} from "openzeppelin-contracts/contracts/utils/Context.sol";

import {IBatchRepayBadDebtSteward} from "./interfaces/IBatchRepayBadDebtSteward.sol";

/// @title BatchRepayBadDebtSteward
/// @author BGD Labs
/// @notice This contract allows to repay all the bad debt of a list of users
/// @dev Only allowed those users that have some debt and doesn't have any collateral
contract BatchRepayBadDebtSteward is IBatchRepayBadDebtSteward, RescuableBase, OwnableWithGuardian, AccessControl {
using SafeERC20 for IERC20;
using UserConfiguration for DataTypes.UserConfigurationMap;

/* PUBLIC GLOBAL VARIABLES */

/// @inheritdoc IBatchRepayBadDebtSteward
bytes32 public constant CLEANUP = keccak256("CLEANUP");

/// @inheritdoc IBatchRepayBadDebtSteward
IPool public immutable override POOL;

/// @inheritdoc IBatchRepayBadDebtSteward
address public immutable override COLLECTOR;

/* CONSTRUCTOR */

constructor(address _pool, address _guardian, address _owner, address _collector) {
if (_pool == address(0) || _guardian == address(0) || _owner == address(0) || _collector == address(0)) {
revert ZeroAddress();
}

POOL = IPool(_pool);
COLLECTOR = _collector;

if (msg.sender != _guardian) {
_updateGuardian(_guardian);
}

if (msg.sender != _owner) {
_transferOwnership(_owner);
}

_grantRole(DEFAULT_ADMIN_ROLE, _owner);
_grantRole(CLEANUP, _guardian);
}

/* EXTERNAL FUNCTIONS */

/// @inheritdoc IBatchRepayBadDebtSteward
function batchLiquidate(address debtAsset, address[] memory collateralAssets, address[] memory users)
external
override
{
(uint256 totalDebtAmount,) = getDebtAmount(debtAsset, users);
TepNik marked this conversation as resolved.
Show resolved Hide resolved

batchLiquidateWithMaxCap(debtAsset, totalDebtAmount, collateralAssets, users);
}

/// @inheritdoc IBatchRepayBadDebtSteward
function batchLiquidateWithMaxCap(
address debtAsset,
uint256 debtTokenAmount,
address[] memory collateralAssets,
address[] memory users
) public override onlyRole(CLEANUP) {
uint256 balanceBefore = IERC20(debtAsset).balanceOf(address(this));

IERC20(debtAsset).safeTransferFrom(COLLECTOR, address(this), debtTokenAmount);
IERC20(debtAsset).forceApprove(address(POOL), debtTokenAmount);

uint256 length = users.length;
for (uint256 i = 0; i < length; i++) {
POOL.liquidationCall({
collateralAsset: collateralAssets[i],
debtAsset: debtAsset,
user: users[i],
debtToCover: type(uint256).max,
receiveAToken: true
});
}

uint256 balanceAfter = IERC20(debtAsset).balanceOf(address(this));

if (balanceAfter > balanceBefore) {
IERC20(debtAsset).safeTransfer(COLLECTOR, balanceAfter - balanceBefore);
}
}

/// @inheritdoc IBatchRepayBadDebtSteward
function batchRepayBadDebt(address asset, address[] memory users) external override onlyRole(CLEANUP) {
(uint256 totalDebtAmount, uint256[] memory debtAmounts) = getBadDebtAmount(asset, users);

IERC20(asset).safeTransferFrom(COLLECTOR, address(this), totalDebtAmount);
IERC20(asset).forceApprove(address(POOL), totalDebtAmount);

uint256 length = users.length;
for (uint256 i = 0; i < length; i++) {
POOL.repay({asset: asset, amount: debtAmounts[i], interestRateMode: 2, onBehalfOf: users[i]});
}
}

/// @inheritdoc IBatchRepayBadDebtSteward
function rescueToken(address token) external override onlyOwnerOrGuardian {
_emergencyTokenTransfer(token, COLLECTOR, type(uint256).max);
}

/// @inheritdoc IBatchRepayBadDebtSteward
function rescueEth() external override onlyOwnerOrGuardian {
_emergencyEtherTransfer(COLLECTOR, address(this).balance);
}

/* PUBLIC VIEW FUNCTIONS */

/// @inheritdoc IBatchRepayBadDebtSteward
function getDebtAmount(address asset, address[] memory users)
public
view
override
returns (uint256, uint256[] memory)
{
return _getUsersDebtAmounts({asset: asset, users: users, usersCanHaveCollateral: true});
}

/// @inheritdoc IBatchRepayBadDebtSteward
function getBadDebtAmount(address asset, address[] memory users)
public
view
override
returns (uint256, uint256[] memory)
{
return _getUsersDebtAmounts({asset: asset, users: users, usersCanHaveCollateral: false});
}

/// @inheritdoc IRescuableBase
function maxRescue(address erc20Token) public view virtual override(IRescuableBase, RescuableBase) returns (uint256) {
return IERC20(erc20Token).balanceOf(address(this));
}

/* INTERNAL FUNCTIONS */

function _msgSender() internal view override(OzCommonContext, OzContext) returns (address) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is a bit ugly, let me see if we can get rid of the two contexts

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem here is that the OwnableWithGuardian and the AccessControl contracts have a different Context contract.

return msg.sender;
}

function _msgData() internal pure override(OzCommonContext, OzContext) returns (bytes calldata) {
return msg.data;
}

/* PRIVATE VIEW FUNCTIONS */

function _getUsersDebtAmounts(address asset, address[] memory users, bool usersCanHaveCollateral)
private
view
returns (uint256, uint256[] memory)
{
uint256 length = users.length;

uint256 totalDebtAmount;
uint256[] memory debtAmounts = new uint256[](length);

DataTypes.ReserveDataLegacy memory reserveData = POOL.getReserveData(asset);

for (uint256 i = 0; i < length; i++) {
address user = users[i];

for (uint256 j = i + 1; j < length; j++) {
TepNik marked this conversation as resolved.
Show resolved Hide resolved
if (user == users[j]) {
revert UsersShouldBeDifferent(user);
}
}

DataTypes.UserConfigurationMap memory userConfiguration = POOL.getUserConfiguration(user);

if (!usersCanHaveCollateral && userConfiguration.isUsingAsCollateralAny()) {
revert UserHasSomeCollateral(user);
}

totalDebtAmount += debtAmounts[i] = IERC20(reserveData.variableDebtTokenAddress).balanceOf(user);
}

return (totalDebtAmount, debtAmounts);
}
}
Loading
Loading