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

Liquidation test #731

Merged
merged 7 commits into from
Jul 9, 2024
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
15 changes: 13 additions & 2 deletions script/Common.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import '@script/Registry.s.sol';
import {Test} from 'forge-std/Test.sol';
import {VmSafe} from 'forge-std/Script.sol';
import {Params, ParamChecker, OD, ETH_A, ARB, JOB_REWARD} from '@script/Params.s.sol';
import {AuthorizableUpgradeable} from '@contracts/utils/AuthorizableUpgradeable.sol';
import 'forge-std/console.sol';

abstract contract Common is Contracts, Params, Test {
Expand Down Expand Up @@ -215,12 +216,22 @@ abstract contract Common is Contracts, Params, Test {
if (!isNetworkAnvil()) {
address systemCoinAddress = create2.create2deploy(_systemCoinSalt, _systemCoinInitCode);
systemCoin = ISystemCoin(systemCoinAddress);
systemCoin.initialize('Open Dollar', 'OD');
} else {
systemCoin = new OpenDollar();
if (_isTest && !isNetworkArbitrumSepolia()) {
Copy link
Member

Choose a reason for hiding this comment

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

this fix looks like it took some blood sweat and tears to figure out.

systemCoin = OpenDollar(0x221A0f68770658C15B525d0F89F5da2baAB5f321);
vm.stopPrank();
vm.startPrank(AuthorizableUpgradeable(address(systemCoin)).authorizedAccounts()[0]);
AuthorizableUpgradeable(address(systemCoin)).addAuthorization(deployer);
vm.stopPrank();
vm.startPrank(deployer);
} else {
systemCoin = new OpenDollar();
systemCoin.initialize('Open Dollar', 'OD');
}
protocolToken = new OpenDollarGovernance();
protocolToken.initialize('Open Dollar Governance', 'ODG');
}
systemCoin.initialize('Open Dollar', 'OD');
address[] memory members = new address[](0);

if (isNetworkAnvil()) {
Expand Down
294 changes: 294 additions & 0 deletions test/e2e/E2ELiquidation.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,294 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity 0.8.20;

import {IERC20} from '@openzeppelin/token/ERC20/IERC20.sol';
import {ODProxy} from '@contracts/proxies/ODProxy.sol';
import {IVault721} from '@interfaces/proxies/IVault721.sol';
import {ISAFEEngine} from '@interfaces/ISAFEEngine.sol';
import {ILiquidationEngine} from '@interfaces/ILiquidationEngine.sol';
import {IOracleRelayer} from '@interfaces/IOracleRelayer.sol';
import {Math, RAY} from '@libraries/Math.sol';
import {DelayedOracleForTest} from '@test/mocks/DelayedOracleForTest.sol';
import {Common, COLLAT, DEBT, RETH} from '@test/e2e/Common.t.sol';

uint256 constant MINUS_0_5_PERCENT_PER_HOUR = 999_998_607_628_240_588_157_433_861;
uint256 constant DEPOSIT = 0.5 ether; // $1000 worth of RETH (1 RETH = $2000)
uint256 constant MINT = 740 ether; // $740 worth of OD (135% over-collateralization)
uint256 constant USER_AMOUNT = 500 ether;
uint256 constant ELEVEN_PERCENT = 160 ether;
uint256 constant FIFTEEN_PERCENT = 218 ether;
uint256 constant TWENTY_PERCENT = 290 ether;
uint256 constant THIRTY_PERCENT = 436 ether;
uint256 constant FORTY_PERCENT = 582 ether;

contract E2ELiquidation is Common {
using Math for uint256;

uint256 public liquidationCRatio; // RAY
uint256 public safetyCRatio; // RAY
uint256 public accumulatedRate; // RAY
uint256 public liquidationPrice; // RAY

ISAFEEngine.SAFEEngineCollateralData public cTypeData;
IOracleRelayer.OracleRelayerCollateralParams public oracleParams;
ILiquidationEngine.LiquidationEngineCollateralParams public cTypeParams;

IVault721.NFVState public aliceNFV;
IVault721.NFVState public bobNFV;

address public aliceProxy;
address public bobProxy;
uint256 public initialSystemCoinSupply;

IERC20 public reth;

mapping(address proxy => uint256 safeId) public vaults;

function setUp() public virtual override {
super.setUp();
refreshCData(RETH);
reth = IERC20(address(collateral[RETH]));

cTypeParams = liquidationEngine.cParams(RETH);

aliceProxy = userVaultSetup(RETH, alice, USER_AMOUNT, 'AliceProxy');
aliceNFV = vault721.getNfvState(vaults[aliceProxy]);
depositCollateralAndGenDebt(RETH, vaults[aliceProxy], DEPOSIT, MINT, aliceProxy);

bobProxy = userVaultSetup(RETH, bob, USER_AMOUNT, 'BobProxy');
bobNFV = vault721.getNfvState(vaults[bobProxy]);
depositCollateralAndGenDebt(RETH, vaults[bobProxy], DEPOSIT * 3, MINT * 3, bobProxy);

initialSystemCoinSupply = systemCoin.totalSupply();

vm.prank(bob);
systemCoin.approve(bobProxy, type(uint256).max);
}

function testAssumptions() public {
assertEq(reth.balanceOf(alice), USER_AMOUNT - DEPOSIT);
assertEq(reth.balanceOf(bob), USER_AMOUNT - DEPOSIT * 3);
emitRatio(RETH, aliceNFV.safeHandler); // 135% over-collateralized
readDelayedPrice(RETH);
collateralDevaluation(RETH, ELEVEN_PERCENT); // 11% devaulation
readDelayedPrice(RETH);
emitRatio(RETH, aliceNFV.safeHandler); // 124% over-collateralized
liquidationEngine.liquidateSAFE(RETH, aliceNFV.safeHandler);
emit log_named_uint('Liquidation Penalty', cTypeParams.liquidationPenalty);
}

/**
* @dev initial setup
* RETH: $2000
* OD = $1
* Liquidation Penalty: 5%
* Liquidation Ratio: 125%
* Safety Ration: 135%
*
* @notice scenario
* User deposit $1000 worth of RETH (0.5 ether) and borrows $740 OD (135% ratio)
*
*/
function testLiquidation1() public {
_percentLiquidation(ELEVEN_PERCENT);
}

function testLiquidation2() public {
_percentLiquidation(FIFTEEN_PERCENT);
}

function testLiquidation3() public {
_percentLiquidation(TWENTY_PERCENT);
}

function testLiquidation4() public {
_percentLiquidation(THIRTY_PERCENT);
}

function testLiquidation5() public {
_percentLiquidation(FORTY_PERCENT);
}

function _percentLiquidation(uint256 _percent) public {
// read initial values
emitRatio(RETH, aliceNFV.safeHandler);
emitInternalAndExternalCollateralAndDebt();

// generate amoutToRaise in case of liquidation
uint256 _amountToRaise = _emitMathAndReturnAmountToRaise();

// devalue cType to below the liquidation ratio
collateralDevaluation(RETH, _percent);
emitRatio(RETH, aliceNFV.safeHandler);

// liquidate safe
auctionId = liquidationEngine.liquidateSAFE(RETH, aliceNFV.safeHandler);
emitInternalAndExternalCollateralAndDebt();

vm.startPrank(bob);
/// bob to buy half of auction @notice token kept in CAH
buyCollateral(RETH, auctionId, 0, _amountToRaise / 2, bobProxy);
emitInternalAndExternalCollateralAndDebt();

/// bob to buy remaining half of auction @notice token returned to Alice safe
buyCollateral(RETH, auctionId, 0, _amountToRaise / 2, bobProxy);
emitInternalAndExternalCollateralAndDebt();
}

function _emitMathAndReturnAmountToRaise() public returns (uint256) {
ILiquidationEngine.LiquidationEngineCollateralParams memory _rethParams = liquidationEngine.cParams(RETH);
ILiquidationEngine.LiquidationEngineParams memory _liqParams = liquidationEngine.params();

(uint256 _lc, uint256 _gd) = getSAFE(RETH, aliceNFV.safeHandler);
uint256 _lq = _rethParams.liquidationQuantity;
uint256 _ad =
(_liqParams.onAuctionSystemCoinLimit - liquidationEngine.currentOnAuctionSystemCoins()).wdiv(accumulatedRate);
uint256 _limitAdjustedDebt = Math.min(_gd, Math.min(_lq, _ad));
// emit log_named_uint('Generated Debt ', _gd);
// emit log_named_uint('Liquidation Quantity ', _lq);
// emit log_named_uint('Adjusted Debt ', _ad);
emit log_named_uint('* Limit Adjusted Debt ', _limitAdjustedDebt);

uint256 _lcXlad = _lc * _limitAdjustedDebt / _gd;
uint256 _collateralToSell = Math.min(_lc, _lcXlad);
// emit log_named_uint('Locked Collateral ', _lc);
// emit log_named_uint('Locked C X LimAdjDebt', _lcXlad);
emit log_named_uint('* Collateral To Sell ', _collateralToSell);

uint256 _amountToRaise = (_limitAdjustedDebt * accumulatedRate).wmul(_rethParams.liquidationPenalty) / 1e27;
emit log_named_uint('* Amount To Raise ', _amountToRaise);
return _amountToRaise;
}

// HELPER FUNCTIONS
function emitInternalAndExternalCollateralAndDebt() public {
emit log_named_uint('CAH System Coin Bal', systemCoin.balanceOf(address(collateralAuctionHouse[RETH])));
emit log_named_uint(
'CAH Internal cType Bal', safeEngine.tokenCollateral(RETH, address(collateralAuctionHouse[RETH]))
);
emit log_named_uint('Ali Internal CType Bal', safeEngine.tokenCollateral(RETH, aliceNFV.safeHandler));
(uint256 _a_c, uint256 _a_d) = getSAFE(RETH, aliceNFV.safeHandler);
emit log_named_uint('Ali Locked CType Bal', _a_c);
emit log_named_uint('Ali System Coin Bal', systemCoin.balanceOf(alice));
emit log_named_uint('Ali Generate Debt Bal', _a_d);
// emit log_named_uint('Bob Internal cType Bal', safeEngine.tokenCollateral(RETH, bobNFV.safeHandler));
// (uint256 _b_c, uint256 _b_d) = getSAFE(RETH, bobNFV.safeHandler);
// emit log_named_uint('Bob Locked cType Bal', _b_c);
// emit log_named_uint('Bob System Coin Bal', systemCoin.balanceOf(bob));
// emit log_named_uint('Bob Generate Debt Bal', _b_d);
emit log_named_bytes32('BREAK ----------------', bytes32(0x00));
}

function getSAFE(bytes32 _cType, address _safe) public view returns (uint256 _collateral, uint256 _debt) {
ISAFEEngine.SAFE memory _safeData = safeEngine.safes(_cType, _safe);
_collateral = _safeData.lockedCollateral;
_debt = _safeData.generatedDebt;
}

function getRatio(bytes32 _cType, uint256 _collateral, uint256 _debt) public view returns (uint256 _ratio) {
_ratio = _collateral.wmul(oracleRelayer.cParams(_cType).oracle.read()).wdiv(_debt.wmul(accumulatedRate));
}

function emitRatio(bytes32 _cType, address _safe) public returns (uint256 _ratio) {
(uint256 _collateral, uint256 _debt) = getSAFE(_cType, _safe);
_ratio = getRatio(_cType, _collateral, _debt);
emit log_named_uint('CType to Debt Ratio', _ratio / 1e7);
}

function readDelayedPrice(bytes32 _cType) public returns (uint256) {
uint256 _p = delayedOracle[_cType].read();
emit log_named_uint('CType Price Read', _p);
return _p;
}

function collateralDevaluation(bytes32 _cType, uint256 _devaluation) public returns (uint256) {
uint256 _p = delayedOracle[_cType].read();
DelayedOracleForTest(address(delayedOracle[_cType])).setPriceAndValidity(_p - _devaluation, true);
oracleRelayer.updateCollateralPrice(_cType);
refreshCData(_cType);
return delayedOracle[_cType].read();
}

function refreshCData(bytes32 _cType) public {
cTypeData = safeEngine.cData(_cType);
liquidationPrice = cTypeData.liquidationPrice;
accumulatedRate = cTypeData.accumulatedRate;

oracleParams = oracleRelayer.cParams(_cType);
liquidationCRatio = oracleParams.liquidationCRatio;
safetyCRatio = oracleParams.safetyCRatio;
}

function userVaultSetup(
bytes32 _cType,
address _user,
uint256 _amount,
string memory _name
) public returns (address _proxy) {
_proxy = deployOrFind(_user);
mintToken(_cType, _user, _amount, _proxy);
vm.label(_proxy, _name);
vm.prank(_proxy);
vaults[_proxy] = safeManager.openSAFE(_cType, _proxy);
}

function mintToken(bytes32 _cType, address _account, uint256 _amount, address _okAccount) public {
vm.startPrank(_account);
deal(address(collateral[_cType]), _account, _amount);
if (_okAccount != address(0)) {
IERC20(address(collateral[_cType])).approve(_okAccount, _amount);
}
vm.stopPrank();
}

function deployOrFind(address _owner) public returns (address) {
address proxy = vault721.getProxy(_owner);
if (proxy == address(0)) {
return address(vault721.build(_owner));
} else {
return proxy;
}
}

function depositCollateralAndGenDebt(
bytes32 _cType,
uint256 _safeId,
uint256 _collatAmount,
uint256 _deltaWad,
address _proxy
) public {
vm.startPrank(ODProxy(_proxy).OWNER());
bytes memory _payload = abi.encodeWithSelector(
basicActions.lockTokenCollateralAndGenerateDebt.selector,
address(safeManager),
address(collateralJoin[_cType]),
address(coinJoin),
_safeId,
_collatAmount,
_deltaWad
);
ODProxy(_proxy).execute(address(basicActions), _payload);
vm.stopPrank();
}

function buyCollateral(
bytes32 _cType,
uint256 _auctionId,
uint256 _minCollateral,
uint256 _bid,
address _proxy
) public {
vm.startPrank(ODProxy(_proxy).OWNER());
bytes memory _payload = abi.encodeWithSelector(
collateralBidActions.buyCollateral.selector,
address(coinJoin),
address(collateralJoin[_cType]),
address(collateralAuctionHouse[_cType]),
_auctionId,
_minCollateral,
_bid
);
ODProxy(_proxy).execute(address(collateralBidActions), _payload);
vm.stopPrank();
}
}
2 changes: 1 addition & 1 deletion test/e2e/TestParams.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ abstract contract TestParams is Contracts, Params {

_liquidationEngineCParams[_cType] = ILiquidationEngine.LiquidationEngineCollateralParams({
collateralAuctionHouse: address(collateralAuctionHouse[_cType]),
liquidationPenalty: 1.1e18, // WAD
liquidationPenalty: 1.05e18, // WAD
liquidationQuantity: 100_000e45 // RAD
});

Expand Down
2 changes: 1 addition & 1 deletion test/e2e/TestParams.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ abstract contract TestParams is Contracts, Params {

_liquidationEngineCParams[_cType] = ILiquidationEngine.LiquidationEngineCollateralParams({
collateralAuctionHouse: address(collateralAuctionHouse[_cType]),
liquidationPenalty: 1.1e18, // WAD
liquidationPenalty: 1.05e18, // WAD
liquidationQuantity: 100_000e45 // RAD
});

Expand Down
Loading