Skip to content

Commit

Permalink
UNI-1017 Loan can be written off by anybody before overdue delay expi…
Browse files Browse the repository at this point in the history
…res (#64)

* fix: loan can be written off by anybody

* new: add github script to run fix tests

* chg: update github script

* chg: remove obsoleted code

* chg: rename audit-fix folder to findings

* chg: update github script

* chg: do not update borrower.lastRepay when the bad debt is not fully written-off
  • Loading branch information
maxweng authored Dec 1, 2022
1 parent f507714 commit f529d46
Show file tree
Hide file tree
Showing 4 changed files with 174 additions and 0 deletions.
8 changes: 8 additions & 0 deletions contracts/market/UToken.sol
Original file line number Diff line number Diff line change
Expand Up @@ -664,11 +664,19 @@ contract UToken is IUToken, Controller, ERC20PermitUpgradeable, ReentrancyGuardU
}

function debtWriteOff(address borrower, uint256 amount) external override whenNotPaused onlyUserManager {
if (amount == 0) revert AmountZero();

uint256 oldPrincipal = getBorrowed(borrower);
uint256 repayAmount = amount > oldPrincipal ? oldPrincipal : amount;

accountBorrows[borrower].principal = oldPrincipal - repayAmount;
totalBorrows -= repayAmount;

if (repayAmount == oldPrincipal) {
// If all principal is written off, we can reset the last repaid block to 0.
// which indicates that the borrower has no outstanding loans.
accountBorrows[borrower].lastRepay = 0;
}
}

/* -------------------------------------------------------------------
Expand Down
5 changes: 5 additions & 0 deletions deploy/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ import goerliConfig from "./goerli";
// Fork Configs
import goerliForkConfig from "./goerli-fork";

// local test configs
import localConfig from "./local";

export const baseConfig = {
addresses: {
aave: {
Expand Down Expand Up @@ -49,6 +52,8 @@ export const getConfig = () => {
return {...baseConfig, ...goerliConfig};
case "goerli-fork":
return {...baseConfig, ...goerliForkConfig};
case "local":
return {...baseConfig, ...localConfig};
default:
return baseConfig;
}
Expand Down
22 changes: 22 additions & 0 deletions deploy/config/local.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import {parseUnits} from "ethers/lib/utils";

import {DeployConfig} from "../index";

export default {
userManager: {
maxOverdue: "120", // 12 x overdueBlocks
effectiveCount: "1",
maxVouchers: "1000"
},
uToken: {
name: "uDAI",
symbol: "uDAI",
initialExchangeRateMantissa: parseUnits("1"),
reserveFactorMantissa: parseUnits("1"),
originationFee: parseUnits("0.005"),
debtCeiling: parseUnits("250000"),
maxBorrow: parseUnits("25000"),
minBorrow: parseUnits("100"),
overdueBlocks: "10" // in blocks
}
} as DeployConfig;
139 changes: 139 additions & 0 deletions test/findings/uni-1017-115-loan-can-be-written-off-by-anybody.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import {expect} from "chai";
import {ethers} from "hardhat";
import {Signer} from "ethers";
import {commify, formatUnits, parseUnits} from "ethers/lib/utils";

import deploy, {Contracts} from "../../deploy";
import {getConfig} from "../../deploy/config";
import {getDai, roll} from "../../test/utils";

const PAD = 22;

describe("Debt write off", () => {
let staker: Signer;
let stakerAddress: string;

let borrower: Signer;
let borrowerAddress: string;

let deployer: Signer;
let deployerAddress: string;

let contracts: Contracts;

const borrowAmount = parseUnits("1000");

before(async function () {
const signers = await ethers.getSigners();
deployer = signers[0];
deployerAddress = await deployer.getAddress();
contracts = await deploy({...getConfig(), admin: deployerAddress}, deployer);

staker = signers[1];
stakerAddress = await staker.getAddress();

borrower = signers[2];
borrowerAddress = await borrower.getAddress();
});

const logInfo = async () => {
const principal = await contracts.uToken.getBorrowed(borrowerAddress);
const interest = await contracts.uToken.calculatingInterest(borrowerAddress);
const owed = await contracts.uToken.borrowBalanceView(borrowerAddress);
const totalBorrows = await contracts.uToken.totalBorrows();
const totalRedeemable = await contracts.uToken.totalRedeemable();
const totalReserves = await contracts.uToken.totalReserves();

console.log("");
console.log("Principal:".padEnd(PAD), commify(formatUnits(principal)));
console.log("Interest:".padEnd(PAD), commify(formatUnits(interest)));
console.log("Total Owed:".padEnd(PAD), commify(formatUnits(owed)));
console.log("Total Borrows:".padEnd(PAD), commify(formatUnits(totalBorrows)));
console.log("Total Redeemable:".padEnd(PAD), commify(formatUnits(totalRedeemable)));
console.log("Total reserves:".padEnd(PAD), commify(formatUnits(totalReserves)));
console.log("");

const balance = await contracts.userManager.getStakerBalance(stakerAddress);

console.log("Staker balance:".padEnd(PAD), commify(formatUnits(balance)));
console.log("");

const lastRepay = await contracts.uToken.getLastRepay(borrowerAddress);
console.log("Borrower's last repay:".padEnd(PAD), lastRepay.toString());
console.log("");
};

it("set up credit line", async () => {
const AMOUNT = parseUnits("10000");

await contracts.userManager.addMember(stakerAddress);
await contracts.userManager.addMember(borrowerAddress);

await getDai(contracts.dai, deployer, AMOUNT);
await getDai(contracts.dai, staker, AMOUNT);

await contracts.dai.connect(staker).approve(contracts.userManager.address, AMOUNT);
await contracts.userManager.connect(staker).stake(AMOUNT);

await contracts.dai.approve(contracts.uToken.address, AMOUNT);
await contracts.uToken.mint(AMOUNT);

await contracts.userManager.connect(staker).updateTrust(borrowerAddress, AMOUNT);

const creditLine = await contracts.userManager.getCreditLimit(borrowerAddress);
console.log("Credit line:".padEnd(PAD), commify(formatUnits(creditLine)));

expect(creditLine).gt(0);

await logInfo();
});

it("borrow", async () => {
await contracts.uToken.connect(borrower).borrow(borrowerAddress, borrowAmount);
const principal = await contracts.uToken.getBorrowed(borrowerAddress);
expect(principal).gt(borrowAmount);

await logInfo();
});

it("staker writes off debt", async () => {
const stakeBalBefore = await contracts.userManager.getStakerBalance(stakerAddress);
const principal = await contracts.uToken.getBorrowed(borrowerAddress);
await contracts.userManager.connect(staker).debtWriteOff(stakerAddress, borrowerAddress, principal);
const stakeBalAfter = await contracts.userManager.getStakerBalance(stakerAddress);
expect(stakeBalBefore.sub(stakeBalAfter)).eq(principal);

await logInfo();
});

it("wait until max overdue time passes", async () => {
console.log("\nRoll 200 blocks into the future and borrow again\n");
await roll(200); // wait until the max overdue blocks passes
});

it("borrow again", async () => {
await contracts.uToken.connect(borrower).borrow(borrowerAddress, borrowAmount);
const principal = await contracts.uToken.getBorrowed(borrowerAddress);
expect(principal).gt(borrowAmount);

await logInfo();
});

it("Cannot write off debt by others", async () => {
const principal = await contracts.uToken.getBorrowed(borrowerAddress);
const resp = contracts.userManager.debtWriteOff(stakerAddress, borrowerAddress, principal);
await expect(resp).to.be.revertedWithCustomError(contracts.userManager, "AuthFailed");

await logInfo();
});

it("Can write off debt by the staker", async () => {
const stakeBalBefore = await contracts.userManager.getStakerBalance(stakerAddress);
const principal = await contracts.uToken.getBorrowed(borrowerAddress);
await contracts.userManager.connect(staker).debtWriteOff(stakerAddress, borrowerAddress, principal);
const stakeBalAfter = await contracts.userManager.getStakerBalance(stakerAddress);
expect(stakeBalBefore.sub(stakeBalAfter)).eq(principal);

await logInfo();
});
});

0 comments on commit f529d46

Please sign in to comment.