Skip to content

Commit

Permalink
feat: reapply ray pow improvements (#82)
Browse files Browse the repository at this point in the history
* Revert "revert: revert changes to pow (#81)"

This reverts commit 095a878.

* fix: corretly pass date
  • Loading branch information
sakulstra authored Feb 17, 2021
1 parent 74901f9 commit 14986a1
Show file tree
Hide file tree
Showing 10 changed files with 286 additions and 22 deletions.
7 changes: 7 additions & 0 deletions benchmarks/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Getting started

This is a micro benchmark suite we use to test if optimizations are paying off.
To not burden normal users with installing unnecessary deps we don't include them automatically.
If you want to run the benchmark install `benchmark` manually. `npm i -g benchmark`

Single micro-benchmarks can be executed by running `node ./benchmarks/<benchmarkName>`
59 changes: 59 additions & 0 deletions benchmarks/rayPow.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
const {
rayPow,
binomialApproximatedRayPow,
valueToZDBigNumber,
RAY,
SECONDS_PER_YEAR,
} = require('../dist');
const Benchmark = require('benchmark');

Benchmark.options.minSamples = 200;
const PRECISION = 2;
const results = [];
const suite = new Benchmark.Suite();

const timeDelta = valueToZDBigNumber(60 * 60 * 24);
const rayPowIn = valueToZDBigNumber('323788616402133497883602337')
.dividedBy(SECONDS_PER_YEAR)
.plus(RAY);
const binomialApproximatedRayPowIn = valueToZDBigNumber(
'323788616402133497883602337'
).dividedBy(SECONDS_PER_YEAR);

suite
// add tests
.add('rayPow', () => {
rayPow(rayPowIn, timeDelta);
})
.add('binomialApproximatedRayPow', () => {
binomialApproximatedRayPow(binomialApproximatedRayPowIn, timeDelta);
})

// add listeners
.on('cycle', event =>
results.push({
name: event.target.name,
hz: event.target.hz,
'margin of error': ${Number(event.target.stats.rme).toFixed(2)}%`,
'runs sampled': event.target.stats.sample.length,
})
)
.on('complete', function() {
const lowestHz = results.slice().sort((a, b) => a.hz - b.hz)[0].hz;

console.table(
results
.sort((a, b) => b.hz - a.hz)
.map(result => ({
...result,
hz: Math.round(result.hz).toLocaleString(),
numTimesFaster:
Math.round((10 ** PRECISION * result.hz) / lowestHz) /
10 ** PRECISION,
}))
.reduce((acc, { name, ...cur }) => ({ ...acc, [name]: cur }), {})
);
console.log('Fastest is ' + this.filter('fastest').map('name'));
})

.run({ async: false });
14 changes: 6 additions & 8 deletions src/helpers/pool-math.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,19 @@ export const LTV_PRECISION = 4;

export function calculateCompoundedInterest(
rate: BigNumberValue,
currentTimestamp: BigNumberValue,
lastUpdateTimestamp: BigNumberValue
currentTimestamp: number,
lastUpdateTimestamp: number
): BigNumber {
const timeDelta = valueToZDBigNumber(currentTimestamp).minus(
lastUpdateTimestamp
);
const timeDelta = valueToZDBigNumber(currentTimestamp - lastUpdateTimestamp);
const ratePerSecond = valueToZDBigNumber(rate).dividedBy(SECONDS_PER_YEAR);
return RayMath.rayPow(ratePerSecond.plus(RayMath.RAY), timeDelta);
return RayMath.binomialApproximatedRayPow(ratePerSecond, timeDelta);
}

export function getCompoundedBalance(
_principalBalance: BigNumberValue,
_reserveIndex: BigNumberValue,
_reserveRate: BigNumberValue,
_lastUpdateTimestamp: BigNumberValue,
_lastUpdateTimestamp: number,
currentTimestamp: number
): BigNumber {
const principalBalance = valueToZDBigNumber(_principalBalance);
Expand All @@ -51,7 +49,7 @@ export function getCompoundedBalance(
export function getCompoundedStableBalance(
_principalBalance: BigNumberValue,
_userStableRate: BigNumberValue,
_lastUpdateTimestamp: BigNumberValue,
_lastUpdateTimestamp: number,
currentTimestamp: number
): BigNumber {
const principalBalance = valueToZDBigNumber(_principalBalance);
Expand Down
31 changes: 31 additions & 0 deletions src/helpers/ray-math.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,37 @@ export function rayPow(a: BigNumberValue, p: BigNumberValue): BigNumber {
return z;
}

/**
* RayPow is slow and gas intensive therefore in v2 we switched to binomial approximation on the contract level.
* While the results ar not exact to the last decimal, they are close enough.
*/
export function binomialApproximatedRayPow(
a: BigNumberValue,
p: BigNumberValue
): BigNumber {
const base = valueToZDBigNumber(a);
const exp = valueToZDBigNumber(p);
if (exp.eq(0)) return RAY;
const expMinusOne = exp.minus(1);
const expMinusTwo = exp.gt(2) ? exp.minus(2) : 0;

const basePowerTwo = rayMul(base, base);
const basePowerThree = rayMul(basePowerTwo, base);

const firstTerm = exp.multipliedBy(base);
const secondTerm = exp
.multipliedBy(expMinusOne)
.multipliedBy(basePowerTwo)
.div(2);
const thirdTerm = exp
.multipliedBy(expMinusOne)
.multipliedBy(expMinusTwo)
.multipliedBy(basePowerThree)
.div(6);

return RAY.plus(firstTerm).plus(secondTerm).plus(thirdTerm);
}

export function rayToDecimal(a: BigNumberValue): BigNumber {
return valueToZDBigNumber(a).dividedBy(RAY);
}
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import * as v2 from './v2';

// export helpers
export * from './helpers/bignumber';
export * from './helpers/constants';
export * from './helpers/pool-math';
export * from './helpers/ray-math';

Expand Down
153 changes: 151 additions & 2 deletions src/test/ray-math.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,21 @@
import BigNumber from 'bignumber.js';
import { RAY, rayMul } from '../helpers/ray-math';
import { SECONDS_PER_YEAR } from '../helpers/constants';
import {
BigNumberValue,
valueToZDBigNumber,
normalize,
} from '../helpers/bignumber';
import {
RAY,
rayMul,
rayPow,
binomialApproximatedRayPow,
rayDiv,
} from '../helpers/ray-math';
import { calculateCompoundedInterest } from '../helpers/pool-math';

describe('wadMul should', () => {
it('works correct', () => {
it('work correctly', () => {
expect(rayMul(RAY, RAY).toString()).toEqual(RAY.toString());
});
it('not return decimal places', () => {
Expand All @@ -12,3 +25,139 @@ describe('wadMul should', () => {
expect(rayMul(new BigNumber(0.5).pow(27), RAY).toString()).toEqual('0');
});
});

const legacyCalculateCompoundedInterest = (
rate: BigNumberValue,
currentTimestamp: number,
lastUpdateTimestamp: number
): BigNumber => {
const timeDelta = valueToZDBigNumber(currentTimestamp - lastUpdateTimestamp);
const ratePerSecond = valueToZDBigNumber(rate).dividedBy(SECONDS_PER_YEAR);
return rayPow(ratePerSecond.plus(RAY), timeDelta);
};

describe('rayPow and binomialApproximatedRayPow', () => {
it('should be roughly equal', () => {
const result = rayPow(
valueToZDBigNumber('323788616402133497883602337')
.dividedBy(SECONDS_PER_YEAR)
.plus(RAY),
valueToZDBigNumber(60 * 60 * 24)
).toString();
const approx = binomialApproximatedRayPow(
valueToZDBigNumber('323788616402133497883602337').dividedBy(
SECONDS_PER_YEAR
),
valueToZDBigNumber(60 * 60 * 24)
).toString();
expect(result.substring(0, 8)).toEqual(approx.substring(0, 8));
});

it.each`
exponents | errorLte
${0} | ${0}
${60} | ${0.00001}
${60 * 60} | ${0.00001}
${60 * 60 * 24} | ${0.00001}
${60 * 60 * 24 * 31} | ${0.00001}
${60 * 60 * 24 * 365} | ${0.00001}
${60 * 60 * 24 * 365 * 2} | ${0.00002}
${60 * 60 * 24 * 365 * 5} | ${0.0003}
`(
'should have close results for varying exponents',
({ exponents, errorLte }) => {
const result = rayPow(
valueToZDBigNumber('10000000000000000000000000')
.dividedBy(SECONDS_PER_YEAR)
.plus(RAY),
valueToZDBigNumber(exponents)
);
const approx = binomialApproximatedRayPow(
valueToZDBigNumber('10000000000000000000000000').dividedBy(
SECONDS_PER_YEAR
),
valueToZDBigNumber(exponents)
);

const diff = result.gt(approx)
? result.minus(approx)
: approx.minus(result);
const diffPercentage = normalize(
rayDiv(diff, result.multipliedBy(100)),
24
).toString();
expect(Math.abs(Number.parseFloat(diffPercentage))).toBeLessThanOrEqual(
errorLte
);
}
);

it.each`
years | interest | errorLte
${1} | ${3} | ${0.00001}
${1} | ${5} | ${0.00001}
${1} | ${10} | ${0.00001}
${3} | ${3} | ${0.00001}
${3} | ${5} | ${0.00001}
${3} | ${10} | ${0.00005}
${5} | ${3} | ${0.00001}
${5} | ${5} | ${0.00003}
${5} | ${10} | ${0.0003}
`(
'should not be far off for big amounts and time spans',
({ years, interest, errorLte }) => {
/**
* We calculate the balance based on the last user interaction with that reserve.
* For most users this happens multiple times a year, but for long holders we have to ensure the abbreviation is somewhat close.
* This test showcases that it's:
* < 0.00005% error in one year
* < 0.0005% in 3 years
* < 0.005% in 5 years
*/
const timeSpan = 60 * 60 * 24 * 365 * Number.parseInt(years);
const rate = valueToZDBigNumber(
Number.parseFloat(interest) * 1000000000000000000000000
);
const balance = '100000000000000000000000000'; // 100M ETH
const accurateInterest = legacyCalculateCompoundedInterest(
rate,
timeSpan,
0
);
const approximatedInterest = calculateCompoundedInterest(
rate,
timeSpan,
0
);

const accurateBalanceI = accurateInterest
.multipliedBy(balance)
.minus(balance);
const approximatedBalanceI = approximatedInterest
.multipliedBy(balance)
.minus(balance);

const diff = accurateBalanceI.minus(approximatedBalanceI);
const diffPercentage = normalize(
rayDiv(diff, accurateBalanceI.multipliedBy(100)),
24
);
expect(Math.abs(Number.parseFloat(diffPercentage))).toBeLessThanOrEqual(
errorLte
);
}
);

it('should increase values over time', () => {
const rate = valueToZDBigNumber(109284371694014197840985614);
const ts1 = 2;
const ts2 = 3;
const accurateInterest1 = legacyCalculateCompoundedInterest(rate, ts1, 0);
const approximatedInterest1 = calculateCompoundedInterest(rate, ts1, 0);
const accurateInterest2 = legacyCalculateCompoundedInterest(rate, ts2, 0);
const approximatedInterest2 = calculateCompoundedInterest(rate, ts2, 0);

expect(accurateInterest1.lt(accurateInterest2)).toBe(true);
expect(approximatedInterest1.lt(approximatedInterest2)).toBe(true);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -27,21 +27,21 @@ Object {
"stableBorrows": "0",
"stableBorrowsETH": "0",
"stableBorrowsUSD": "0",
"totalBorrows": "137635.694716770184532442",
"totalBorrowsETH": "224.903606951938320035",
"totalBorrowsUSD": "2249036069519383200.35",
"totalBorrows": "137635.694716770188069538",
"totalBorrowsETH": "224.903606951938325815",
"totalBorrowsUSD": "375539.3154302718",
"underlyingBalance": "0",
"underlyingBalanceETH": "0",
"underlyingBalanceUSD": "0",
"usageAsCollateralEnabledOnUser": false,
"variableBorrowIndex": "0",
"variableBorrows": "137635.694716770184532442",
"variableBorrowsETH": "224.903606951938320035",
"variableBorrowsUSD": "2249036069519383200.35",
"variableBorrows": "137635.694716770188069538",
"variableBorrowsETH": "224.903606951938325815",
"variableBorrowsUSD": "375539.3154302718",
},
],
"totalBorrowsETH": "224.903606951938320035",
"totalBorrowsUSD": "2249036069519383200.35",
"totalBorrowsETH": "224.903606951938325815",
"totalBorrowsUSD": "375539.3154302718",
"totalCollateralETH": "0",
"totalCollateralUSD": "0",
"totalLiquidityETH": "0",
Expand Down
21 changes: 20 additions & 1 deletion src/test/v2/computation-and-formatting.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
formatReserves,
formatUserSummaryData,
} from '../../v2/computations-and-formatting';
import BigNumber from 'bignumber.js';

const mockReserve: ReserveData = {
underlyingAsset: '0xff795577d9ac8bd7d90ee22b6c1703490b6512fd',
Expand Down Expand Up @@ -75,7 +76,7 @@ describe('computations and formattings', () => {
[mockReserve],
[mockUserReserve],
'0cd96fb5ee9616f64d892644f53f35be4f90xff795577d9ac8bd7d90ee22b6c1703490b6512fd0x88757f2f99175387ab4c6a4b3067c77a695b0349',
'100',
'598881655557838',
mockUserReserve.reserve.lastUpdateTimestamp + 2000
);
expect(
Expand Down Expand Up @@ -126,5 +127,23 @@ describe('computations and formattings', () => {
)[0];
expect(formattedMockReserve.utilizationRate).toBe('0');
});

it('should increase over time', () => {
/**
* tests against a regression which switched two dates
*/
const first = formatReserves(
[mockReserve],
mockReserve.lastUpdateTimestamp + 1,
[mockReserve as any]
)[0];
const second = formatReserves(
[mockReserve],
mockReserve.lastUpdateTimestamp + 2,
[mockReserve as any]
)[0];

expect(new BigNumber(second.totalDebt).gte(first.totalDebt)).toBe(true);
});
});
});
2 changes: 1 addition & 1 deletion src/v1/computations-and-formatting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ export const calculateCompoundedInterest = (
): BigNumber => {
const timeDelta = valueToZDBigNumber(currentTimestamp - lastUpdateTimestamp);
const ratePerSecond = valueToZDBigNumber(rate).dividedBy(SECONDS_PER_YEAR);
return RayMath.rayPow(ratePerSecond.plus(RayMath.RAY), timeDelta);
return RayMath.binomialApproximatedRayPow(ratePerSecond, timeDelta);
};

export const calculateLinearInterest = (
Expand Down
Loading

0 comments on commit 14986a1

Please sign in to comment.