Skip to content
39 changes: 19 additions & 20 deletions contracts/truefi2/SAFU.sol
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ contract SAFU is ISAFU, UpgradeableClaimable {
I1Inch3 public _1Inch;

mapping(ILoanToken2 => IDeficiencyToken) public override deficiencyToken;
mapping(address => uint256) public override poolDeficit;
mapping(address => uint256) public internalPoolDeficit;

// ======= STORAGE DECLARATION END ============

Expand All @@ -51,7 +51,7 @@ contract SAFU is ISAFU, UpgradeableClaimable {
/**
* @dev Emitted when a loan gets liquidated
* @param loan Loan that has been liquidated
* @param repaid Amount repaid to the pool
* @param repaid DEPRECATED Amount repaid to the pool
* @param deficiencyToken Deficiency token representing a deficit that is owed to the pool by SAFU
* @param deficit Deficit amount that SAFU still owes the pool
*/
Expand Down Expand Up @@ -81,33 +81,32 @@ contract SAFU is ISAFU, UpgradeableClaimable {
}

/**
* @dev Liquidates a defaulted Loan, withdraws a portion of tru from staking pool
* then tries to cover the loan with own funds, to compensate TrueFiPool
* If SAFU does not have enough funds, deficit is saved to be redeemed later
* @dev Dummy view so that tfTOKEN.deficitValue() discounts deficiency tokens to zero value.
* Does not affect SAFU internal deficiency token tracking.
*/
function poolDeficit(address) external override view returns (uint256) {
return 0;
}

/**
* @dev Liquidates a defaulted Loan and withdraws a portion of tru from staking pool
* to compensate TrueFiPool. Deficit is saved to be redeemed later
* @param loan Loan to be liquidated
*/
function liquidate(ILoanToken2 loan) external {
require(loanFactory.isLoanToken(address(loan)), "SAFU: Unknown loan");
require(loan.status() == ILoanToken2.Status.Defaulted, "SAFU: Loan is not defaulted");

ITrueFiPool2 pool = ITrueFiPool2(loan.pool());
IERC20 token = IERC20(pool.token());

liquidator.liquidate(loan);
pool.liquidate(loan);
uint256 owedToPool = loan.debt().mul(tokenBalance(loan)).div(loan.totalSupply());
uint256 safuTokenBalance = tokenBalance(token);

uint256 deficit = 0;
uint256 toTransfer = owedToPool;
if (owedToPool > safuTokenBalance) {
deficit = owedToPool.sub(safuTokenBalance);
toTransfer = safuTokenBalance;
deficiencyToken[loan] = new DeficiencyToken(loan, deficit);
poolDeficit[address(loan.pool())] = poolDeficit[address(loan.pool())].add(deficit);
}
token.safeTransfer(address(pool), toTransfer);
emit Liquidated(loan, toTransfer, deficiencyToken[loan], deficit);

uint256 deficit = loan.debt().mul(tokenBalance(loan)).div(loan.totalSupply());
deficiencyToken[loan] = new DeficiencyToken(loan, deficit);
internalPoolDeficit[address(pool)] = internalPoolDeficit[address(pool)].add(deficit);

emit Liquidated(loan, 0, deficiencyToken[loan], deficit);
}

/**
Expand Down Expand Up @@ -145,7 +144,7 @@ contract SAFU is ISAFU, UpgradeableClaimable {
require(address(dToken) != address(0), "SAFU: No deficiency token found for loan");
require(dToken.balanceOf(poolAddress) > 0, "SAFU: Pool does not have deficiency tokens to be reclaimed");

poolDeficit[poolAddress] = poolDeficit[poolAddress].sub(amount);
internalPoolDeficit[poolAddress] = internalPoolDeficit[poolAddress].sub(amount);
dToken.burnFrom(msg.sender, amount);
loan.token().safeTransfer(poolAddress, amount);

Expand Down
4 changes: 2 additions & 2 deletions deployments.json
Original file line number Diff line number Diff line change
Expand Up @@ -557,8 +557,8 @@
"address": "0xCB829B1Aa77B8B57D320AF05a780757c8c2B88C1"
},
"sAFU": {
"txHash": "0x069ca631faace9d865bf47ad09df5326633eb4d1d34aafafd0df3bcd7c37cd14",
"address": "0xC83E731e0cab21ce5B0Bbbe3252bAefD0e11fc03"
"txHash": "0x5856ef56a67575ed46d74f86e7c5c6a4eb344a516ddfdc83fac445b324ac96ac",
"address": "0xc7B4BB7c8e3620A6c4F9E96524ccB8a81D52A1b1"
},
"sAFU_proxy": {
"txHash": "0xaa5bbaa6ca71899793cea116bfa42e4b06791c7ab85523e83bf911a4b9e12c42",
Expand Down
118 changes: 118 additions & 0 deletions test/integration/safuHotfix20221013.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { forkChain } from './suite20221013'
import { setupDeploy } from 'scripts/utils'
import { Erc20, Erc20__factory, LoanToken2, LoanToken2__factory, OwnedUpgradeabilityProxy__factory, Safu, Safu__factory, TrueFiPool2, TrueFiPool2__factory } from 'contracts'
import { expect, use } from 'chai'
import { solidity } from 'ethereum-waffle'
import { parseEth } from 'utils'
import { JsonRpcSigner } from '@ethersproject/providers'

use(solidity)

describe('SAFU hotfix 2022-10-13', () => {
const SAFU_OWNER = '0x16cEa306506c387713C70b9C1205fd5aC997E78E'
const SAFU_ADDRESS = '0x1eA63189eB1F4c109B10Cf6567f328C826AA6151'
const TFBUSD_ADDRESS = '0x1Ed460D149D48FA7d91703bf4890F97220C09437'
const LOAN_ADDRESS = '0x4A66a867f52DF4Ed1D8580A1C383B2dD036a3C47'
const ETH_HOLDER = '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2'
const BUSD_HOLDER = '0xF977814e90dA44bFA03b6295A0616a897441aceC'
const BUSD_ADDRESS = '0x4Fabb145d64652a948d72533023f6E7A623C7C53'
const BLOCK_NUMBER = 15734123 // 2022-10-12

let safuOwner: JsonRpcSigner
let safu: Safu
let tfBUSD: TrueFiPool2
let loan: LoanToken2
let busd: Erc20

beforeEach(async () => {
const provider = forkChain([SAFU_OWNER, ETH_HOLDER, BUSD_HOLDER], BLOCK_NUMBER - 1)

safuOwner = provider.getSigner(SAFU_OWNER)
safu = Safu__factory.connect(SAFU_ADDRESS, safuOwner)
loan = LoanToken2__factory.connect(LOAN_ADDRESS, safuOwner)
tfBUSD = TrueFiPool2__factory.connect(TFBUSD_ADDRESS, safuOwner)

const ethHolder = provider.getSigner(ETH_HOLDER)
await ethHolder.sendTransaction({ value: parseEth(100), to: SAFU_OWNER })

const busdHolder = provider.getSigner(BUSD_HOLDER)
busd = Erc20__factory.connect(BUSD_ADDRESS, busdHolder)
})

describe('before SAFU upgrade', () => {
it('tfBUSD pool value includes 100% of deficiency token value', async () => {
await safu.liquidate(loan.address)

const poolValue = await tfBUSD.poolValue()
const totalSupply = await tfBUSD.totalSupply()
const deficitValue = await tfBUSD.deficitValue()
const liquidValue = await tfBUSD.liquidValue()
const loansValue = await tfBUSD.loansValue()

expect(totalSupply).to.be.lt(poolValue)
expect(deficitValue).to.equal(await loan.debt())
expect(poolValue).to.equal(liquidValue.add(loansValue).add(deficitValue))
})

it('SAFU transfers BUSD tokens to pool during liquidation', async () => {
await busd.transfer(safu.address, parseEth(1_000_000_000))

const liquidValueBefore = await tfBUSD.liquidValue()

await safu.liquidate(loan.address)

const poolValue = await tfBUSD.poolValue()
const totalSupply = await tfBUSD.totalSupply()
const deficitValue = await tfBUSD.deficitValue()
const liquidValue = await tfBUSD.liquidValue()
const loansValue = await tfBUSD.loansValue()

expect(totalSupply).to.be.lt(poolValue)
expect(deficitValue).to.equal(0)
expect(poolValue).to.equal(liquidValue.add(loansValue))
expect(liquidValue).to.equal(liquidValueBefore.add(await loan.debt()))
})
})

describe('after SAFU upgrade', () => {
beforeEach(async () => {
const deployContract = setupDeploy(safuOwner)
const newSAFU = await deployContract(Safu__factory)
const safuProxy = OwnedUpgradeabilityProxy__factory.connect(SAFU_ADDRESS, safuOwner)
await safuProxy.upgradeTo(newSAFU.address)
})

it('tfBUSD pool value includes 0% of deficiency token value', async () => {
await safu.liquidate(loan.address)

const poolValue = await tfBUSD.poolValue()
const totalSupply = await tfBUSD.totalSupply()
const deficitValue = await tfBUSD.deficitValue()
const liquidValue = await tfBUSD.liquidValue()
const loansValue = await tfBUSD.loansValue()

expect(totalSupply).to.be.gt(poolValue)
expect(deficitValue).to.equal(0)
expect(poolValue).to.equal(liquidValue.add(loansValue))
})

it('SAFU does not transfer BUSD tokens to pool during liquidation', async () => {
await busd.transfer(safu.address, parseEth(1_000_000_000))

const liquidValueBefore = await tfBUSD.liquidValue()

await safu.liquidate(loan.address)

const poolValue = await tfBUSD.poolValue()
const totalSupply = await tfBUSD.totalSupply()
const deficitValue = await tfBUSD.deficitValue()
const liquidValue = await tfBUSD.liquidValue()
const loansValue = await tfBUSD.loansValue()

expect(totalSupply).to.be.gt(poolValue)
expect(deficitValue).to.equal(0)
expect(poolValue).to.equal(liquidValue.add(loansValue))
expect(liquidValue).to.equal(liquidValueBefore)
})
})
})
76 changes: 76 additions & 0 deletions test/integration/suite20221013.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/* eslint-disable no-redeclare */
import { BigNumberish, Contract, providers } from 'ethers'
import { ContractFactoryConstructor, deployContract } from 'scripts/utils/deployContract'
import ganache from 'ganache-core'
import { OwnedUpgradeabilityProxy__factory } from 'contracts'
import { expect } from 'chai'
import { parseEth } from 'utils'

export const CONTRACTS_OWNER = '0x16cEa306506c387713C70b9C1205fd5aC997E78E'
export const ETHER_HOLDER = '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2'

function _forkChain (rpc: string, unlockedAccounts: string[] = [], blockNumber?: BigNumberish,
) {
return new providers.Web3Provider(ganache.provider({
fork: blockNumber ? `${rpc}@${blockNumber.toString()}` : rpc,
unlocked_accounts: unlockedAccounts,
}))
}

export function forkChain (unlockedAccounts: string[] = [], blockNumber?: BigNumberish) {
const infura_key = process.env.INFURA_PROJECT_ID
const infura_secret = process.env.INFURA_PROJECT_SECRET

const rpc = infura_key ? `https://:${infura_secret}@mainnet.infura.io/v3/${infura_key}` : 'https://eth-mainnet.alchemyapi.io/v2/Vc3xNXIWdxEbDOToa69DhWeyhgFVBDWl'
return _forkChain(rpc, unlockedAccounts, blockNumber)
}

type Getter<T extends Contract> = keyof T['callStatic'] | ((contract: T) => any)

const execGetter = <T extends Contract>(contract: T) => async (getter: Getter<T>) => {
if (typeof getter === 'function') {
return getter(contract)
}
return contract[getter]()
}

export const TEST_STATE_BLOCK_NUMBER = 12010725

export function upgradeSuite<T extends Contract>(blockNumber: number, Factory: ContractFactoryConstructor<T>, currentAddress: string,
getters: Getter<T>[], contractsOwner?: string): Promise<T>
export function upgradeSuite<T extends Contract>(Factory: ContractFactoryConstructor<T>, currentAddress: string,
getters: Getter<T>[], contractsOwner?: string): Promise<T>
export function upgradeSuite (...args: any[]): any {
if (typeof args[0] === 'number') {
const [bn, factory, address, getters, owner] = args
return _upgradeSuite(factory, address, getters, owner, bn)
}
const [factory, address, getters, owner] = args
return _upgradeSuite(factory, address, getters, owner)
}

async function _upgradeSuite<T extends Contract> (
Factory: ContractFactoryConstructor<T>,
currentAddress: string,
getters: Getter<T>[],
contractsOwner: string = CONTRACTS_OWNER,
blockNumber?: number | undefined,
) {
const provider = forkChain([contractsOwner, ETHER_HOLDER], blockNumber)
const owner = provider.getSigner(contractsOwner)
const holder = provider.getSigner(ETHER_HOLDER)
await holder.sendTransaction({ value: parseEth(100), to: contractsOwner })
const newContract = await deployContract(owner, Factory)
const existingContract = new Factory(owner).attach(currentAddress)
const oldValues = await Promise.all(getters.map(execGetter(existingContract)))
const proxy = new OwnedUpgradeabilityProxy__factory(owner).attach(currentAddress)
await (await proxy.upgradeTo(newContract.address)).wait()
const newValues = await Promise.all(getters.map(execGetter(existingContract)))
for (let i = 0; i < oldValues.length; i++) {
expect(oldValues[i], `Possible corrupted storage:
Getter: ${getters[i]}
Current: ${oldValues[i].toString()}
Post upgrade: ${newValues[i].toString()} \n`).to.deep.equal(newValues[i])
}
return existingContract
}
Loading