Skip to content

👯‍♀️ Split SAFU's liquidate into 2 steps #1127

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

Merged
merged 4 commits into from
Nov 22, 2021
Merged
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
2 changes: 1 addition & 1 deletion contracts/truefi2/LoanFactory2.sol
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ contract LoanFactory2 is ILoanFactory2, Initializable {
emit FixedTermLoanAgencyChanged(_ftlAgency);
}

function debtTokens(address borrower) external override view returns (IDebtToken[] memory) {
function debtTokens(address borrower) external view override returns (IDebtToken[] memory) {
return _debtTokens[borrower];
}
}
12 changes: 10 additions & 2 deletions contracts/truefi2/SAFU.sol
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,6 @@ contract SAFU is ISAFU, UpgradeableClaimable {

/**
* @dev Liquidates defaulted debts, withdraws a portion of tru from staking pool
* then tries to cover the debts with own funds, to compensate TrueFiPool
* If SAFU does not have enough funds, deficit is saved to be redeemed later
* @param borrower Borrower whose loans are to be liquidated
*/
function liquidate(address borrower) external onlyOwner {
Expand All @@ -123,8 +121,18 @@ contract SAFU is ISAFU, UpgradeableClaimable {
}

liquidator.liquidate(debts);
}

/**
* Tries to cover liquidated debts with own funds, to compensate TrueFiPool
* If SAFU does not have enough funds, deficit is saved to be redeemed later
* @param borrower Borrower whose loans are to be liquidated
*/
function compensate(address borrower) external onlyOwner {
IDebtToken[] memory debts = loanFactory.debtTokens(borrower);

for (uint256 i = 0; i < debts.length; i++) {
require(debts[i].isLiquidated(), "SAFU: Debt not liquidated yet");
ITrueFiPool2 pool = ITrueFiPool2(debts[i].pool());
IERC20 token = IERC20(pool.token());

Expand Down
5 changes: 0 additions & 5 deletions contracts/truefi2/interface/IDebtToken.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,6 @@ import {ERC20} from "../../common/UpgradeableERC20.sol";
import {ITrueFiPool2} from "./ITrueFiPool2.sol";

interface IDebtToken is IERC20 {
enum Status {
Defaulted,
Liquidated
}

function borrower() external view returns (address);

function debt() external view returns (uint256);
Expand Down
83 changes: 56 additions & 27 deletions test/truefi2/SAFU.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,33 +151,44 @@ describe('SAFU', () => {
})
})

describe('Handles debt tokens', () => {
it('transfers DebtTokens to the SAFU', async () => {
describe('Slashes staked tru from StakingVault', () => {
it('works with no tru staked', async () => {
await safu.liquidate(borrower.address)
await expect(await debt.balanceOf(safu.address)).to.equal(defaultAmount)
expect(await tru.balanceOf(safu.address)).to.equal(0)
})

describe('Slashes staked tru from StakingVault', () => {
it('works with no tru staked', async () => {
await safu.liquidate(borrower.address)
expect(await tru.balanceOf(safu.address)).to.equal(0)
})
it('works with tru staked', async () => {
await tru.mint(borrower.address, parseTRU(100))
await tru.connect(borrower).approve(stakingVault.address, parseTRU(100))
await stakingVault.connect(borrower).stake(parseTRU(100))

it('works with tru staked', async () => {
await tru.mint(borrower.address, parseTRU(100))
await tru.connect(borrower).approve(stakingVault.address, parseTRU(100))
await stakingVault.connect(borrower).stake(parseTRU(100))

await borrowingMutex.allowLocker(owner.address, true)
await borrowingMutex.lock(borrower.address, owner.address)
await borrowingMutex.ban(borrower.address)
await borrowingMutex.allowLocker(owner.address, true)
await borrowingMutex.lock(borrower.address, owner.address)
await borrowingMutex.ban(borrower.address)

await safu.liquidate(borrower.address)
expect(await tru.balanceOf(safu.address)).to.eq(parseTRU(100))
})
await safu.liquidate(borrower.address)
expect(await tru.balanceOf(safu.address)).to.eq(parseTRU(100))
})
})

it('marks all debts as liquidated', async () => {
await safu.liquidate(borrower.address)
expect(await debt.isLiquidated()).to.be.true
})
})

describe('compensate', () => {
it('reverts if debt not liquidated', async () => {
await expect(safu.compensate(borrower.address))
.to.be.revertedWith('SAFU: Debt not liquidated yet')
})

it('transfers DebtTokens to the SAFU', async () => {
await safu.liquidate(borrower.address)
await safu.compensate(borrower.address)
await expect(await debt.balanceOf(safu.address)).to.equal(defaultAmount)
})

const createSupportedPool = async (poolFactory: PoolFactory): Promise<[TrueFiPool2, MockErc20Token]> => {
const poolImplementation = await new TrueFiPool2__factory(owner).deploy()
const token = await new MockErc20Token__factory(owner).deploy()
Expand All @@ -200,27 +211,32 @@ describe('SAFU', () => {
})

it('takes funds from safu', async () => {
await expect(() => safu.liquidate(borrower.address))
await safu.liquidate(borrower.address)
await expect(() => safu.compensate(borrower.address))
.to.changeTokenBalance(token, safu, defaultAmount.mul(-1))
})

it('transfers funds to the pool', async () => {
await expect(() => safu.liquidate(borrower.address))
await safu.liquidate(borrower.address)
await expect(() => safu.compensate(borrower.address))
.to.changeTokenBalance(token, pool, defaultAmount)
})

it('sets deficiencyToken', async () => {
await safu.liquidate(borrower.address)
await safu.compensate(borrower.address)
expect(await safu.deficiencyToken(debt.address)).to.eq(AddressZero)
})

it('increases pool deficit', async () => {
await safu.liquidate(borrower.address)
await safu.compensate(borrower.address)
expect(await safu.poolDeficit(pool.address)).to.eq(0)
})

it('emits event', async () => {
await expect(safu.liquidate(borrower.address))
await safu.liquidate(borrower.address)
await expect(safu.compensate(borrower.address))
.to.emit(safu, 'Liquidated')
.withArgs(debt.address, defaultAmount, AddressZero, 0)
})
Expand All @@ -243,6 +259,7 @@ describe('SAFU', () => {
await pool2.addDebt(debtToken2.address, defaultAmount.mul(1).div(4))

await safu.liquidate(borrower.address)
await safu.compensate(borrower.address)

expect(await safu.poolDeficit(pool.address)).to.eq(0)
expect(await safu.poolDeficit(pool2.address)).to.eq(0)
Expand All @@ -257,28 +274,33 @@ describe('SAFU', () => {
})

it('takes funds from safu', async () => {
await expect(() => safu.liquidate(borrower.address))
await safu.liquidate(borrower.address)
await expect(() => safu.compensate(borrower.address))
.to.changeTokenBalance(token, safu, defaultAmount.div(2).mul(-1))
})

it('transfers funds to the pool', async () => {
await expect(() => safu.liquidate(borrower.address))
await safu.liquidate(borrower.address)
await expect(() => safu.compensate(borrower.address))
.to.changeTokenBalance(token, pool, defaultAmount.div(2))
})

it('sets deficiencyToken', async () => {
const tx = await safu.liquidate(borrower.address)
const deficiencyToken = (await tx.wait()).events[8].args.deficiencyToken
await safu.liquidate(borrower.address)
const tx = await safu.compensate(borrower.address)
const deficiencyToken = (await tx.wait()).events[3].args.deficiencyToken
expect(await safu.deficiencyToken(debt.address)).to.eq(deficiencyToken)
})

it('increases pool deficit', async () => {
await safu.liquidate(borrower.address)
await safu.compensate(borrower.address)
expect(await safu.poolDeficit(pool.address)).to.eq(defaultAmount.div(2))
})

it('emits event', async () => {
const tx = await safu.liquidate(borrower.address)
await safu.liquidate(borrower.address)
const tx = await safu.compensate(borrower.address)
await expect(tx)
.to.emit(safu, 'Liquidated')
.withArgs(debt.address, defaultAmount.div(2), await safu.deficiencyToken(debt.address), defaultAmount.div(2))
Expand All @@ -302,6 +324,7 @@ describe('SAFU', () => {
await pool2.addDebt(debtToken2.address, defaultAmount)

await safu.liquidate(borrower.address)
await safu.compensate(borrower.address)
expect(await safu.poolDeficit(pool.address)).to.eq(defaultAmount.div(4))
expect(await safu.poolDeficit(pool2.address)).to.eq(defaultAmount.div(2))
expect(await token.balanceOf(safu.address)).to.eq(0)
Expand Down Expand Up @@ -405,6 +428,7 @@ describe('SAFU', () => {
beforeEach(async () => {
await token.mint(safu.address, defaultAmount.div(2))
await safu.liquidate(borrower.address)
await safu.compensate(borrower.address)
})

describe('reverts if', () => {
Expand Down Expand Up @@ -484,6 +508,7 @@ describe('SAFU', () => {
describe('reverts if', () => {
it('caller is not manager', async () => {
await safu.liquidate(borrower.address)
await safu.compensate(borrower.address)
await expect(safu.connect(borrower).redeem(debt.address))
.to.be.revertedWith('Ownable: caller is not the owner')
})
Expand All @@ -498,24 +523,28 @@ describe('SAFU', () => {

it('burns debt tokens', async () => {
await safu.liquidate(borrower.address)
await safu.compensate(borrower.address)
await token.mint(debt.address, defaultAmount)
await expect(() => safu.redeem(debt.address)).changeTokenBalance(debt, safu, defaultAmount.mul(-1))
})

it('redeems default value', async () => {
await safu.liquidate(borrower.address)
await safu.compensate(borrower.address)
await token.mint(debt.address, defaultAmount)
await expect(() => safu.redeem(debt.address)).changeTokenBalance(token, safu, defaultAmount)
})

it('redeems all available tokens', async () => {
await safu.liquidate(borrower.address)
await safu.compensate(borrower.address)
await token.mint(debt.address, defaultAmount.mul(2))
await expect(() => safu.redeem(debt.address)).changeTokenBalance(token, safu, defaultAmount.mul(2))
})

it('emits a proper event', async () => {
await safu.liquidate(borrower.address)
await safu.compensate(borrower.address)
await token.mint(debt.address, defaultAmount.mul(2))

await expect(safu.redeem(debt.address))
Expand Down
2 changes: 2 additions & 0 deletions test/truefi2/TrueFiPool2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,7 @@ describe('TrueFiPool2', () => {
await loan.enterDefault()
debt = await loan.debtToken()
await safu.liquidate(borrower.address)
await safu.compensate(borrower.address)
})

describe('deficitValue', () => {
Expand Down Expand Up @@ -476,6 +477,7 @@ describe('TrueFiPool2', () => {
await loan.enterDefault()
expect(await tusdPool.poolValue()).to.equal(joinAmount.add(136))
await safu.liquidate(borrower.address)
await safu.compensate(borrower.address)

expect(await tusdPool.deficitValue()).to.eq(500136)
expect(await tusdPool.poolValue()).to.equal(joinAmount.add(136))
Expand Down
1 change: 1 addition & 0 deletions test/truefi2/complete-flow/FixedTermLoans.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ describe('Fixed-term loans flow', () => {

const poolValueBefore = await pool.poolValue()
await safu.liquidate(borrower.address)
await safu.compensate(borrower.address)
expect(await pool.poolValue()).to.eq(poolValueBefore)

// borrower repays the debt
Expand Down
1 change: 1 addition & 0 deletions test/truefi2/complete-flow/LinesOfCredit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ describe('Lines Of Credit flow', () => {

const poolValueBefore = await pool.poolValue()
await safu.liquidate(borrower.address)
await safu.compensate(borrower.address)
expect(await pool.poolValue()).to.eq(poolValueBefore)

// borrower repays the debt
Expand Down