diff --git a/contracts/flusher/balances.sol b/contracts/flusher/balances.sol index 58b111f..f9ec242 100644 --- a/contracts/flusher/balances.sol +++ b/contracts/flusher/balances.sol @@ -28,7 +28,7 @@ contract Resolver { tokensBal[i] = Balances({ flusher: flushers[i], balance: bals, - isDeployed: isContractDeployed(flushers[i]); + isDeployed: isContractDeployed(flushers[i]) }); } return tokensBal; @@ -46,4 +46,4 @@ contract Resolver { contract InstaFlusherERC20Resolver is Resolver { string public constant name = "ERC20-Flusher-Resolver-v1"; -} \ No newline at end of file +} diff --git a/contracts/protocols/mainnet/liquity.sol b/contracts/protocols/mainnet/liquity.sol new file mode 100644 index 0000000..c22f149 --- /dev/null +++ b/contracts/protocols/mainnet/liquity.sol @@ -0,0 +1,127 @@ +pragma solidity ^0.6.0; +pragma experimental ABIEncoderV2; + +interface TroveManagerLike { + function getBorrowingRateWithDecay() external view returns (uint); + function getTCR(uint _price) external view returns (uint); + function getCurrentICR(address _borrower, uint _price) external view returns (uint); + function checkRecoveryMode(uint _price) external view returns (bool); + function getEntireDebtAndColl(address _borrower) external view returns ( + uint debt, + uint coll, + uint pendingLUSDDebtReward, + uint pendingETHReward + ); +} + +interface StabilityPoolLike { + function getCompoundedLUSDDeposit(address _depositor) external view returns (uint); + function getDepositorETHGain(address _depositor) external view returns (uint); + function getDepositorLQTYGain(address _depositor) external view returns (uint); +} + +interface StakingLike { + function stakes(address owner) external view returns (uint); + function getPendingETHGain(address _user) external view returns (uint); + function getPendingLUSDGain(address _user) external view returns (uint); +} + +interface PoolLike { + function getETH() external view returns (uint); +} + +contract DSMath { + function add(uint x, uint y) internal pure returns (uint z) { + require((z = x + y) >= x, "math-not-safe"); + } +} + +contract Helpers is DSMath { + TroveManagerLike internal constant troveManager = + TroveManagerLike(0xA39739EF8b0231DbFA0DcdA07d7e29faAbCf4bb2); + + StabilityPoolLike internal constant stabilityPool = + StabilityPoolLike(0x66017D22b0f8556afDd19FC67041899Eb65a21bb); + + StakingLike internal constant staking = + StakingLike(0x4f9Fbb3f1E99B56e0Fe2892e623Ed36A76Fc605d); + + PoolLike internal constant activePool = + PoolLike(0xDf9Eb223bAFBE5c5271415C75aeCD68C21fE3D7F); + + PoolLike internal constant defaultPool = + PoolLike(0x896a3F03176f05CFbb4f006BfCd8723F2B0D741C); + + struct Trove { + uint collateral; + uint debt; + uint icr; + } + + struct StabilityDeposit { + uint deposit; + uint ethGain; + uint lqtyGain; + } + + struct Stake { + uint amount; + uint ethGain; + uint lusdGain; + } + + struct Position { + Trove trove; + StabilityDeposit stability; + Stake stake; + } + + struct System { + uint borrowFee; + uint ethTvl; + uint tcr; + bool isInRecoveryMode; + } +} + + +contract Resolver is Helpers { + function getTrove(address owner, uint oracleEthPrice) public view returns (Trove memory) { + (uint debt, uint collateral, , ) = troveManager.getEntireDebtAndColl(owner); + uint icr = troveManager.getCurrentICR(owner, oracleEthPrice); + return Trove(collateral, debt, icr); + } + + function getStabilityDeposit(address owner) public view returns (StabilityDeposit memory) { + uint deposit = stabilityPool.getCompoundedLUSDDeposit(owner); + uint ethGain = stabilityPool.getDepositorETHGain(owner); + uint lqtyGain = stabilityPool.getDepositorLQTYGain(owner); + return StabilityDeposit(deposit, ethGain, lqtyGain); + } + + function getStake(address owner) public view returns (Stake memory) { + uint amount = staking.stakes(owner); + uint ethGain = staking.getPendingETHGain(owner); + uint lusdGain = staking.getPendingLUSDGain(owner); + return Stake(amount, ethGain, lusdGain); + } + + function getPosition(address owner, uint oracleEthPrice) external view returns (Position memory) { + Trove memory trove = getTrove(owner, oracleEthPrice); + StabilityDeposit memory stability = getStabilityDeposit(owner); + Stake memory stake = getStake(owner); + return Position(trove, stability, stake); + } + + function getSystemState(uint oracleEthPrice) external view returns (System memory) { + uint borrowFee = troveManager.getBorrowingRateWithDecay(); + uint ethTvl = add(activePool.getETH(), defaultPool.getETH()); + uint tcr = troveManager.getTCR(oracleEthPrice); + bool isInRecoveryMode = troveManager.checkRecoveryMode(oracleEthPrice); + return System(borrowFee, ethTvl, tcr, isInRecoveryMode); + } +} + +contract InstaLiquityResolver is Resolver { + string public constant name = "Liquity-Resolver-v1"; +} diff --git a/package.json b/package.json index 6896ff6..c1459fd 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "The smart contracts which simplifies read operations.", "main": "index.js", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "test": "hardhat test" }, "repository": { "type": "git", diff --git a/test/liquity.js b/test/liquity.js new file mode 100644 index 0000000..b4a9170 --- /dev/null +++ b/test/liquity.js @@ -0,0 +1,129 @@ +const { expect } = require("chai"); +const hardhatConfig = require("../hardhat.config"); +const { BigNumber } = hre.ethers; + +// Deterministic block number to run these tests from on forked mainnet. If you change this, tests will break. +const BLOCK_NUMBER = 12478959; + +// Liquity user with a Trove, Stability deposit, and Stake +const JUSTIN_SUN_ADDRESS = "0x903d12bf2c57a29f32365917c706ce0e1a84cce3"; + +// Liquity price oracle +const PRICE_FEED_ADDRESS = "0x4c517D4e2C851CA76d7eC94B805269Df0f2201De"; +const PRICE_FEED_ABI = ["function fetchPrice() external returns (uint)"]; + +/* Begin: Mock test data (based on specified BLOCK_NUMBER and JUSTIN_SUN_ADDRESS) */ +const expectedTrovePosition = [ + /* collateral */ BigNumber.from("582880000000000000000000"), + /* debt */ BigNumber.from("372000200000000000000000000"), + /* icr */ BigNumber.from("3859882210893925325"), +]; +const expectedStabilityPosition = [ + /* deposit */ BigNumber.from("299979329615565997640451998"), + /* ethGain */ BigNumber.from("8629038660000000000"), + /* lqtyGain */ BigNumber.from("53244322633874479119945"), +]; +const expectedStakePosition = [ + /* amount */ BigNumber.from("981562996504090969804965"), + /* ethGain */ BigNumber.from("18910541408996344243"), + /* lusdGain */ BigNumber.from("66201062534511228032281"), +]; + +const expectedSystemState = [ + /* borrowFee */ BigNumber.from("6900285109012952"), + /* ethTvl */ BigNumber.from("852500462432421494350957"), + /* tcr */ BigNumber.from("3250195441371082828"), + /* isInRecoveryMode */ false, +]; +/* End: Mock test data */ + +describe("InstaLiquityResolver", () => { + let liquity; + let liquityPriceOracle; + + before(async () => { + await resetHardhatBlockNumber(BLOCK_NUMBER); // Start tests from clean mainnet fork at BLOCK_NUMBER + + const liquityFactory = await hre.ethers.getContractFactory( + "InstaLiquityResolver" + ); + + liquityPriceOracle = new hre.ethers.Contract( + PRICE_FEED_ADDRESS, + PRICE_FEED_ABI, + hre.ethers.provider + ); + + liquity = await liquityFactory.deploy(); + await liquity.deployed(); + }); + + it("deploys the resolver", () => { + expect(liquity.address).to.exist; + }); + + describe("getTrove()", () => { + it("returns a user's Trove position", async () => { + const oracleEthPrice = await liquityPriceOracle.callStatic.fetchPrice(); + const trovePosition = await liquity.getTrove( + JUSTIN_SUN_ADDRESS, + oracleEthPrice + ); + expect(trovePosition).to.eql(expectedTrovePosition); + }); + }); + + describe("getStabilityDeposit()", () => { + it("returns a user's Stability Pool position", async () => { + const stabilityPosition = await liquity.getStabilityDeposit( + JUSTIN_SUN_ADDRESS + ); + expect(stabilityPosition).to.eql(expectedStabilityPosition); + }); + }); + + describe("getStake()", () => { + it("returns a user's Stake position", async () => { + const stakePosition = await liquity.getStake(JUSTIN_SUN_ADDRESS); + expect(stakePosition).to.eql(expectedStakePosition); + }); + }); + + describe("getPosition()", () => { + it("returns a user's Liquity position", async () => { + const oracleEthPrice = await liquityPriceOracle.callStatic.fetchPrice(); + const position = await liquity.getPosition( + JUSTIN_SUN_ADDRESS, + oracleEthPrice + ); + const expectedPosition = [ + expectedTrovePosition, + expectedStabilityPosition, + expectedStakePosition, + ]; + expect(position).to.eql(expectedPosition); + }); + }); + + describe("getSystemState()", () => { + it("returns Liquity system state", async () => { + const oracleEthPrice = await liquityPriceOracle.callStatic.fetchPrice(); + const systemState = await liquity.getSystemState(oracleEthPrice); + expect(systemState).to.eql(expectedSystemState); + }); + }); +}); + +const resetHardhatBlockNumber = async (blockNumber) => { + return await hre.network.provider.request({ + method: "hardhat_reset", + params: [ + { + forking: { + jsonRpcUrl: hardhatConfig.networks.hardhat.forking.url, + blockNumber, + }, + }, + ], + }); +};