Merge pull request #2 from liquity/add-liquity-position-hints

add hint helper functions to Liquity resolver
This commit is contained in:
Edward Mulraney 2021-06-26 11:36:06 +01:00 committed by GitHub
commit 7508655435
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 287 additions and 122 deletions

View File

@ -30,13 +30,53 @@ interface PoolLike {
function getETH() external view returns (uint); function getETH() external view returns (uint);
} }
contract DSMath { interface HintHelpersLike {
function computeNominalCR(uint _coll, uint _debt) external pure returns (uint);
function computeCR(uint _coll, uint _debt, uint _price) external pure returns (uint);
function getApproxHint(uint _CR, uint _numTrials, uint _inputRandomSeed) external view returns (
address hintAddress,
uint diff,
uint latestRandomSeed
);
function getRedemptionHints(uint _LUSDamount, uint _price, uint _maxIterations) external view returns (
address firstHint,
uint partialRedemptionHintNICR,
uint truncatedLUSDamount
);
}
interface SortedTrovesLike {
function getSize() external view returns (uint256);
function findInsertPosition(uint256 _ICR, address _prevId, address _nextId) external view returns (address, address);
}
contract Math {
/* DSMath add */
function add(uint x, uint y) internal pure returns (uint z) { function add(uint x, uint y) internal pure returns (uint z) {
require((z = x + y) >= x, "math-not-safe"); require((z = x + y) >= x, "math-not-safe");
} }
/* DSMath mul */
function mul(uint x, uint y) internal pure returns (uint z) {
require(y == 0 || (z = x * y) / y == x, "math-not-safe");
} }
contract Helpers is DSMath { /* Uniswap V2 sqrt */
function sqrt(uint y) internal pure returns (uint z) {
if (y > 3) {
z = y;
uint x = y / 2 + 1;
while (x < z) {
z = x;
x = (y / x + x) / 2;
}
} else if (y != 0) {
z = 1;
}
}
}
contract Helpers is Math {
TroveManagerLike internal constant troveManager = TroveManagerLike internal constant troveManager =
TroveManagerLike(0xA39739EF8b0231DbFA0DcdA07d7e29faAbCf4bb2); TroveManagerLike(0xA39739EF8b0231DbFA0DcdA07d7e29faAbCf4bb2);
@ -52,6 +92,12 @@ contract Helpers is DSMath {
PoolLike internal constant defaultPool = PoolLike internal constant defaultPool =
PoolLike(0x896a3F03176f05CFbb4f006BfCd8723F2B0D741C); PoolLike(0x896a3F03176f05CFbb4f006BfCd8723F2B0D741C);
HintHelpersLike internal constant hintHelpers =
HintHelpersLike(0xE84251b93D9524E0d2e621Ba7dc7cb3579F997C0);
SortedTrovesLike internal constant sortedTroves =
SortedTrovesLike(0x8FdD3fbFEb32b28fb73555518f8b361bCeA741A6);
struct Trove { struct Trove {
uint collateral; uint collateral;
uint debt; uint debt;
@ -120,6 +166,32 @@ contract Resolver is Helpers {
bool isInRecoveryMode = troveManager.checkRecoveryMode(oracleEthPrice); bool isInRecoveryMode = troveManager.checkRecoveryMode(oracleEthPrice);
return System(borrowFee, ethTvl, tcr, isInRecoveryMode); return System(borrowFee, ethTvl, tcr, isInRecoveryMode);
} }
function getTrovePositionHints(uint collateral, uint debt, uint searchIterations, uint randomSeed) external view returns (
address upperHint,
address lowerHint
) {
// See: https://github.com/liquity/dev#supplying-hints-to-trove-operations
uint nominalCr = hintHelpers.computeNominalCR(collateral, debt);
searchIterations = searchIterations == 0 ? mul(10, sqrt(sortedTroves.getSize())) : searchIterations;
randomSeed = randomSeed == 0 ? block.number : randomSeed;
(address hintAddress, ,) = hintHelpers.getApproxHint(nominalCr, searchIterations, randomSeed);
return sortedTroves.findInsertPosition(nominalCr, hintAddress, hintAddress);
}
function getRedemptionPositionHints(uint amount, uint oracleEthPrice, uint searchIterations, uint randomSeed) external view returns (
uint partialHintNicr,
address firstHint,
address upperHint,
address lowerHint
) {
// See: https://github.com/liquity/dev#hints-for-redeemcollateral
(firstHint, partialHintNicr, ) = hintHelpers.getRedemptionHints(amount, oracleEthPrice, 0);
searchIterations = searchIterations == 0 ? mul(10, sqrt(sortedTroves.getSize())) : searchIterations;
randomSeed = randomSeed == 0 ? block.number : randomSeed;
(address hintAddress, ,) = hintHelpers.getApproxHint(partialHintNicr, searchIterations, randomSeed);
(upperHint, lowerHint) = sortedTroves.findInsertPosition(partialHintNicr, hintAddress, hintAddress);
}
} }
contract InstaLiquityResolver is Resolver { contract InstaLiquityResolver is Resolver {

View File

@ -1,5 +1,6 @@
const { expect } = require("chai"); const { expect } = require("chai");
const hardhatConfig = require("../hardhat.config"); const hardhatConfig = require("../hardhat.config");
const { BigNumber } = hre.ethers; const { BigNumber } = hre.ethers;
// Deterministic block number to run these tests from on forked mainnet. If you change this, tests will break. // Deterministic block number to run these tests from on forked mainnet. If you change this, tests will break.
@ -13,28 +14,37 @@ const PRICE_FEED_ADDRESS = "0x4c517D4e2C851CA76d7eC94B805269Df0f2201De";
const PRICE_FEED_ABI = ["function fetchPrice() external returns (uint)"]; const PRICE_FEED_ABI = ["function fetchPrice() external returns (uint)"];
/* Begin: Mock test data (based on specified BLOCK_NUMBER and JUSTIN_SUN_ADDRESS) */ /* Begin: Mock test data (based on specified BLOCK_NUMBER and JUSTIN_SUN_ADDRESS) */
const expectedTrovePosition = [ const expectedTrovePosition = {
/* collateral */ BigNumber.from("582880000000000000000000"), collateral: BigNumber.from("582880000000000000000000"),
/* debt */ BigNumber.from("372000200000000000000000000"), debt: BigNumber.from("372000200000000000000000000"),
/* icr */ BigNumber.from("3859882210893925325"), icr: BigNumber.from("3859882210893925325"),
]; };
const expectedStabilityPosition = [ const expectedStabilityPosition = {
/* deposit */ BigNumber.from("299979329615565997640451998"), deposit: BigNumber.from("299979329615565997640451998"),
/* ethGain */ BigNumber.from("8629038660000000000"), ethGain: BigNumber.from("8629038660000000000"),
/* lqtyGain */ BigNumber.from("53244322633874479119945"), lqtyGain: BigNumber.from("53244322633874479119945"),
]; };
const expectedStakePosition = [ const expectedStakePosition = {
/* amount */ BigNumber.from("981562996504090969804965"), amount: BigNumber.from("981562996504090969804965"),
/* ethGain */ BigNumber.from("18910541408996344243"), ethGain: BigNumber.from("18910541408996344243"),
/* lusdGain */ BigNumber.from("66201062534511228032281"), lusdGain: BigNumber.from("66201062534511228032281"),
]; };
const expectedSystemState = {
const expectedSystemState = [ borrowFee: BigNumber.from("6900285109012952"),
/* borrowFee */ BigNumber.from("6900285109012952"), ethTvl: BigNumber.from("852500462432421494350957"),
/* ethTvl */ BigNumber.from("852500462432421494350957"), tcr: BigNumber.from("3250195441371082828"),
/* tcr */ BigNumber.from("3250195441371082828"), isInRecoveryMode: false,
/* isInRecoveryMode */ false, };
]; const expectedTrovePositionHints = {
upperHint: "0xbf9a4eCC4151f28C03100bA2C0555a3D3e439e69",
lowerHint: "0xa4FC81A7AB93360543eb1e814D0127f466012CED",
};
const expectedRedemptionPositionHints = {
partialRedemptionHintNicr: "69529933762909647",
firstHint: "0xc16aDd8bA17ab81B27e930Da8a67848120565d8c",
upperHint: "0x66882C005188F0F4d95825ED7A7F78ed3055f167",
lowerHint: "0x0C22C11a8ed4C23ffD19629283548B1692b58e92",
};
/* End: Mock test data */ /* End: Mock test data */
describe("InstaLiquityResolver", () => { describe("InstaLiquityResolver", () => {
@ -55,6 +65,7 @@ describe("InstaLiquityResolver", () => {
); );
liquity = await liquityFactory.deploy(); liquity = await liquityFactory.deploy();
await liquity.deployed(); await liquity.deployed();
}); });
@ -69,7 +80,11 @@ describe("InstaLiquityResolver", () => {
JUSTIN_SUN_ADDRESS, JUSTIN_SUN_ADDRESS,
oracleEthPrice oracleEthPrice
); );
expect(trovePosition).to.eql(expectedTrovePosition); expect(trovePosition.collateral).to.equal(
expectedTrovePosition.collateral
);
expect(trovePosition.debt).to.equal(expectedTrovePosition.debt);
expect(trovePosition.icr).to.equal(expectedTrovePosition.icr);
}); });
}); });
@ -78,14 +93,24 @@ describe("InstaLiquityResolver", () => {
const stabilityPosition = await liquity.getStabilityDeposit( const stabilityPosition = await liquity.getStabilityDeposit(
JUSTIN_SUN_ADDRESS JUSTIN_SUN_ADDRESS
); );
expect(stabilityPosition).to.eql(expectedStabilityPosition); expect(stabilityPosition.deposit).to.equal(
expectedStabilityPosition.deposit
);
expect(stabilityPosition.ethGain).to.equal(
expectedStabilityPosition.ethGain
);
expect(stabilityPosition.lqtyGain).to.equal(
expectedStabilityPosition.lqtyGain
);
}); });
}); });
describe("getStake()", () => { describe("getStake()", () => {
it("returns a user's Stake position", async () => { it("returns a user's Stake position", async () => {
const stakePosition = await liquity.getStake(JUSTIN_SUN_ADDRESS); const stakePosition = await liquity.getStake(JUSTIN_SUN_ADDRESS);
expect(stakePosition).to.eql(expectedStakePosition); expect(stakePosition.amount).to.equal(expectedStakePosition.amount);
expect(stakePosition.ethGain).to.equal(expectedStakePosition.ethGain);
expect(stakePosition.lusdGain).to.equal(expectedStakePosition.lusdGain);
}); });
}); });
@ -96,12 +121,30 @@ describe("InstaLiquityResolver", () => {
JUSTIN_SUN_ADDRESS, JUSTIN_SUN_ADDRESS,
oracleEthPrice oracleEthPrice
); );
const expectedPosition = [ const expectedPosition = {
expectedTrovePosition, trove: expectedTrovePosition,
expectedStabilityPosition, stability: expectedStabilityPosition,
expectedStakePosition, stake: expectedStakePosition,
]; };
expect(position).to.eql(expectedPosition); expect(position.trove.collateral).to.equal(
expectedPosition.trove.collateral
);
expect(position.trove.debt).to.equal(expectedPosition.trove.debt);
expect(position.trove.icr).to.equal(expectedPosition.trove.icr);
expect(position.stability.deposit).to.equal(
expectedPosition.stability.deposit
);
expect(position.stability.ethGain).to.equal(
expectedPosition.stability.ethGain
);
expect(position.stability.lqtyGain).to.equal(
expectedPosition.stability.lqtyGain
);
expect(position.stake.amount).to.equal(expectedPosition.stake.amount);
expect(position.stake.ethGain).to.equal(expectedPosition.stake.ethGain);
expect(position.stake.lusdGain).to.equal(expectedPosition.stake.lusdGain);
}); });
}); });
@ -109,7 +152,57 @@ describe("InstaLiquityResolver", () => {
it("returns Liquity system state", async () => { it("returns Liquity system state", async () => {
const oracleEthPrice = await liquityPriceOracle.callStatic.fetchPrice(); const oracleEthPrice = await liquityPriceOracle.callStatic.fetchPrice();
const systemState = await liquity.getSystemState(oracleEthPrice); const systemState = await liquity.getSystemState(oracleEthPrice);
expect(systemState).to.eql(expectedSystemState); expect(systemState.borrowFee).to.equal(expectedSystemState.borrowFee);
expect(systemState.ethTvl).to.equal(expectedSystemState.ethTvl);
expect(systemState.tcr).to.equal(expectedSystemState.tcr);
expect(systemState.isInRecoveryMode).to.equal(
expectedSystemState.isInRecoveryMode
);
});
});
describe("getTrovePositionHints()", () => {
it("returns the upper and lower address of Troves nearest to the given Trove", async () => {
const collateral = hre.ethers.utils.parseEther("10");
const debt = hre.ethers.utils.parseUnits("5000", 18); // 5,000 LUSD
const searchIterations = 10;
const randomSeed = 3;
const [upperHint, lowerHint] = await liquity.getTrovePositionHints(
collateral,
debt,
searchIterations,
randomSeed
);
expect(upperHint).eq(expectedTrovePositionHints.upperHint);
expect(lowerHint).eq(expectedTrovePositionHints.lowerHint);
});
});
describe("getRedemptionPositionHints()", () => {
it("returns the upper and lower address of the range of Troves to be redeemed against the given amount", async () => {
const amount = hre.ethers.utils.parseUnits("10000", 18); // 10,000 LUSD
const oracleEthPrice = await liquityPriceOracle.callStatic.fetchPrice();
const searchIterations = 10;
const randomSeed = 3;
const [
partialRedemptionHintNicr,
firstHint,
upperHint,
lowerHint,
] = await liquity.getRedemptionPositionHints(
amount,
oracleEthPrice,
searchIterations,
randomSeed
);
expect(partialRedemptionHintNicr).eq(
expectedRedemptionPositionHints.partialRedemptionHintNicr
);
expect(firstHint).eq(expectedRedemptionPositionHints.firstHint);
expect(upperHint).eq(expectedRedemptionPositionHints.upperHint);
expect(lowerHint).eq(expectedRedemptionPositionHints.lowerHint);
}); });
}); });
}); });