From a67cea2fcfeb4d3782fd0cf46f9596853f36c43d Mon Sep 17 00:00:00 2001 From: Edward Mulraney Date: Thu, 10 Jun 2021 17:16:24 +0100 Subject: [PATCH 1/3] add liquity resolver. fix flusher syntax error --- contracts/flusher/balances.sol | 4 +- contracts/protocols/mainnet/liquity.sol | 101 ++++++++++++++++++++++++ package.json | 2 +- test/liquity.js | 95 ++++++++++++++++++++++ 4 files changed, 199 insertions(+), 3 deletions(-) create mode 100644 contracts/protocols/mainnet/liquity.sol create mode 100644 test/liquity.js 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..3dfec37 --- /dev/null +++ b/contracts/protocols/mainnet/liquity.sol @@ -0,0 +1,101 @@ +pragma solidity ^0.6.0; +pragma experimental ABIEncoderV2; + +interface TroveManagerLike { + function getCurrentICR(address _borrower, uint _price) external view returns (uint); + 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); +} + +abstract contract StakingLike { + mapping(address => uint) public stakes; + function getPendingETHGain(address _user) external virtual view returns (uint); + function getPendingLUSDGain(address _user) external virtual view returns (uint); +} + +abstract contract PriceFeedLike { + uint public lastGoodPrice; +} + +contract Helpers { + TroveManagerLike internal constant troveManager = + TroveManagerLike(0xA39739EF8b0231DbFA0DcdA07d7e29faAbCf4bb2); + + StabilityPoolLike internal constant stabilityPool = + StabilityPoolLike(0x66017D22b0f8556afDd19FC67041899Eb65a21bb); + + StakingLike internal constant staking = + StakingLike(0x4f9Fbb3f1E99B56e0Fe2892e623Ed36A76Fc605d); + + PriceFeedLike internal constant priceFeed = + PriceFeedLike(0x4c517D4e2C851CA76d7eC94B805269Df0f2201De); + + 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; + } +} + + +contract Resolver is Helpers { + function getTrove(address owner) public view returns (Trove memory) { + (uint debt, uint collateral, uint _, uint __) = troveManager.getEntireDebtAndColl(owner); + uint price = priceFeed.lastGoodPrice(); + uint icr = troveManager.getCurrentICR(owner, price); + 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) external view returns (Position memory) { + Trove memory trove = getTrove(owner); + StabilityDeposit memory stability = getStabilityDeposit(owner); + Stake memory stake = getStake(owner); + return Position(trove, stability, stake); + } +} + +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..5f76158 --- /dev/null +++ b/test/liquity.js @@ -0,0 +1,95 @@ +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"; + +/* 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("3839454035181671407"), +]; +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"), +]; +/* End: Mock test data */ + +describe("InstaLiquityResolver", () => { + let liquity; + + before(async () => { + await resetHardhatBlockNumber(BLOCK_NUMBER); // Start tests from clean mainnet fork at BLOCK_NUMBER + + const LiquityFactory = await hre.ethers.getContractFactory( + "InstaLiquityResolver" + ); + + 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 trovePosition = await liquity.getTrove(JUSTIN_SUN_ADDRESS); + 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 position = await liquity.getPosition(JUSTIN_SUN_ADDRESS); + const expectedPosition = [ + expectedTrovePosition, + expectedStabilityPosition, + expectedStakePosition, + ]; + expect(position).to.eql(expectedPosition); + }); + }); +}); + +const resetHardhatBlockNumber = async (blockNumber) => { + return await hre.network.provider.request({ + method: "hardhat_reset", + params: [ + { + forking: { + jsonRpcUrl: hardhatConfig.networks.hardhat.forking.url, + blockNumber, + }, + }, + ], + }); +}; From dc108bf05245a4fdb4f5b4e62d38e3c454c5feed Mon Sep 17 00:00:00 2001 From: Edward Mulraney Date: Fri, 11 Jun 2021 13:10:29 +0100 Subject: [PATCH 2/3] add system state getter to make it easier for instadapp to display required information --- contracts/protocols/mainnet/liquity.sol | 35 ++++++++++++++++++++++--- test/liquity.js | 14 ++++++++++ 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/contracts/protocols/mainnet/liquity.sol b/contracts/protocols/mainnet/liquity.sol index 3dfec37..fef0dc0 100644 --- a/contracts/protocols/mainnet/liquity.sol +++ b/contracts/protocols/mainnet/liquity.sol @@ -2,7 +2,10 @@ 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, @@ -27,7 +30,13 @@ abstract contract PriceFeedLike { uint public lastGoodPrice; } -contract Helpers { +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); @@ -40,6 +49,10 @@ contract Helpers { PriceFeedLike internal constant priceFeed = PriceFeedLike(0x4c517D4e2C851CA76d7eC94B805269Df0f2201De); + address constant activePoolAddress = 0xDf9Eb223bAFBE5c5271415C75aeCD68C21fE3D7F; + + address constant defaultPoolAddress = 0x896a3F03176f05CFbb4f006BfCd8723F2B0D741C; + struct Trove { uint collateral; uint debt; @@ -63,14 +76,21 @@ contract Helpers { StabilityDeposit stability; Stake stake; } + + struct System { + uint borrowFee; + uint ethTvl; + uint tcr; + bool isInRecoveryMode; + } } contract Resolver is Helpers { function getTrove(address owner) public view returns (Trove memory) { (uint debt, uint collateral, uint _, uint __) = troveManager.getEntireDebtAndColl(owner); - uint price = priceFeed.lastGoodPrice(); - uint icr = troveManager.getCurrentICR(owner, price); + uint ethPrice = priceFeed.lastGoodPrice(); + uint icr = troveManager.getCurrentICR(owner, ethPrice); return Trove(collateral, debt, icr); } @@ -94,6 +114,15 @@ contract Resolver is Helpers { Stake memory stake = getStake(owner); return Position(trove, stability, stake); } + + function getSystemState() external view returns (System memory) { + uint borrowFee = troveManager.getBorrowingRateWithDecay(); + uint ethTvl = add(activePoolAddress.balance, defaultPoolAddress.balance); + uint ethPrice = priceFeed.lastGoodPrice(); + uint tcr = troveManager.getTCR(ethPrice); + bool isInRecoveryMode = troveManager.checkRecoveryMode(ethPrice); + return System(borrowFee, ethTvl, tcr, isInRecoveryMode); + } } contract InstaLiquityResolver is Resolver { diff --git a/test/liquity.js b/test/liquity.js index 5f76158..f0f85f0 100644 --- a/test/liquity.js +++ b/test/liquity.js @@ -24,6 +24,13 @@ const expectedStakePosition = [ /* ethGain */ BigNumber.from("18910541408996344243"), /* lusdGain */ BigNumber.from("66201062534511228032281"), ]; + +const expectedSystemState = [ + /* borrowFee */ BigNumber.from("6900285109012952"), + /* ethTvl */ BigNumber.from("852500462432421494350957"), + /* tcr */ BigNumber.from("3232993993257432140"), + /* isInRecoveryMode */ false, +]; /* End: Mock test data */ describe("InstaLiquityResolver", () => { @@ -78,6 +85,13 @@ describe("InstaLiquityResolver", () => { expect(position).to.eql(expectedPosition); }); }); + + describe("getSystemState()", () => { + it("returns Liquity system state", async () => { + const systemState = await liquity.getSystemState(); + expect(systemState).to.eql(expectedSystemState); + }); + }); }); const resetHardhatBlockNumber = async (blockNumber) => { From 34b7c390251164a64db77d24589af1620c528c41 Mon Sep 17 00:00:00 2001 From: Edward Mulraney Date: Mon, 14 Jun 2021 10:01:18 +0100 Subject: [PATCH 3/3] pass oracle price into resolver as a parameter since it can't be queried on chain --- contracts/protocols/mainnet/liquity.sol | 41 ++++++++++++------------- test/liquity.js | 34 +++++++++++++++----- 2 files changed, 46 insertions(+), 29 deletions(-) diff --git a/contracts/protocols/mainnet/liquity.sol b/contracts/protocols/mainnet/liquity.sol index fef0dc0..c22f149 100644 --- a/contracts/protocols/mainnet/liquity.sol +++ b/contracts/protocols/mainnet/liquity.sol @@ -20,14 +20,14 @@ interface StabilityPoolLike { function getDepositorLQTYGain(address _depositor) external view returns (uint); } -abstract contract StakingLike { - mapping(address => uint) public stakes; - function getPendingETHGain(address _user) external virtual view returns (uint); - function getPendingLUSDGain(address _user) external virtual 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); } -abstract contract PriceFeedLike { - uint public lastGoodPrice; +interface PoolLike { + function getETH() external view returns (uint); } contract DSMath { @@ -46,12 +46,11 @@ contract Helpers is DSMath { StakingLike internal constant staking = StakingLike(0x4f9Fbb3f1E99B56e0Fe2892e623Ed36A76Fc605d); - PriceFeedLike internal constant priceFeed = - PriceFeedLike(0x4c517D4e2C851CA76d7eC94B805269Df0f2201De); + PoolLike internal constant activePool = + PoolLike(0xDf9Eb223bAFBE5c5271415C75aeCD68C21fE3D7F); - address constant activePoolAddress = 0xDf9Eb223bAFBE5c5271415C75aeCD68C21fE3D7F; - - address constant defaultPoolAddress = 0x896a3F03176f05CFbb4f006BfCd8723F2B0D741C; + PoolLike internal constant defaultPool = + PoolLike(0x896a3F03176f05CFbb4f006BfCd8723F2B0D741C); struct Trove { uint collateral; @@ -87,10 +86,9 @@ contract Helpers is DSMath { contract Resolver is Helpers { - function getTrove(address owner) public view returns (Trove memory) { - (uint debt, uint collateral, uint _, uint __) = troveManager.getEntireDebtAndColl(owner); - uint ethPrice = priceFeed.lastGoodPrice(); - uint icr = troveManager.getCurrentICR(owner, ethPrice); + 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); } @@ -108,19 +106,18 @@ contract Resolver is Helpers { return Stake(amount, ethGain, lusdGain); } - function getPosition(address owner) external view returns (Position memory) { - Trove memory trove = getTrove(owner); + 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() external view returns (System memory) { + function getSystemState(uint oracleEthPrice) external view returns (System memory) { uint borrowFee = troveManager.getBorrowingRateWithDecay(); - uint ethTvl = add(activePoolAddress.balance, defaultPoolAddress.balance); - uint ethPrice = priceFeed.lastGoodPrice(); - uint tcr = troveManager.getTCR(ethPrice); - bool isInRecoveryMode = troveManager.checkRecoveryMode(ethPrice); + uint ethTvl = add(activePool.getETH(), defaultPool.getETH()); + uint tcr = troveManager.getTCR(oracleEthPrice); + bool isInRecoveryMode = troveManager.checkRecoveryMode(oracleEthPrice); return System(borrowFee, ethTvl, tcr, isInRecoveryMode); } } diff --git a/test/liquity.js b/test/liquity.js index f0f85f0..b4a9170 100644 --- a/test/liquity.js +++ b/test/liquity.js @@ -8,11 +8,15 @@ 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("3839454035181671407"), + /* icr */ BigNumber.from("3859882210893925325"), ]; const expectedStabilityPosition = [ /* deposit */ BigNumber.from("299979329615565997640451998"), @@ -28,22 +32,29 @@ const expectedStakePosition = [ const expectedSystemState = [ /* borrowFee */ BigNumber.from("6900285109012952"), /* ethTvl */ BigNumber.from("852500462432421494350957"), - /* tcr */ BigNumber.from("3232993993257432140"), + /* 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( + const liquityFactory = await hre.ethers.getContractFactory( "InstaLiquityResolver" ); - liquity = await LiquityFactory.deploy(); + liquityPriceOracle = new hre.ethers.Contract( + PRICE_FEED_ADDRESS, + PRICE_FEED_ABI, + hre.ethers.provider + ); + + liquity = await liquityFactory.deploy(); await liquity.deployed(); }); @@ -53,7 +64,11 @@ describe("InstaLiquityResolver", () => { describe("getTrove()", () => { it("returns a user's Trove position", async () => { - const trovePosition = await liquity.getTrove(JUSTIN_SUN_ADDRESS); + const oracleEthPrice = await liquityPriceOracle.callStatic.fetchPrice(); + const trovePosition = await liquity.getTrove( + JUSTIN_SUN_ADDRESS, + oracleEthPrice + ); expect(trovePosition).to.eql(expectedTrovePosition); }); }); @@ -76,7 +91,11 @@ describe("InstaLiquityResolver", () => { describe("getPosition()", () => { it("returns a user's Liquity position", async () => { - const position = await liquity.getPosition(JUSTIN_SUN_ADDRESS); + const oracleEthPrice = await liquityPriceOracle.callStatic.fetchPrice(); + const position = await liquity.getPosition( + JUSTIN_SUN_ADDRESS, + oracleEthPrice + ); const expectedPosition = [ expectedTrovePosition, expectedStabilityPosition, @@ -88,7 +107,8 @@ describe("InstaLiquityResolver", () => { describe("getSystemState()", () => { it("returns Liquity system state", async () => { - const systemState = await liquity.getSystemState(); + const oracleEthPrice = await liquityPriceOracle.callStatic.fetchPrice(); + const systemState = await liquity.getSystemState(oracleEthPrice); expect(systemState).to.eql(expectedSystemState); }); });