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

🥔 SAFU hotfix: set tfTOKEN.deficitValue() to zero + don't let SAFU.liquidate() repay TOKEN #1198

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
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