Skip to content

Commit

Permalink
feat: add in GhoDiscountRateStrategyService (#443)
Browse files Browse the repository at this point in the history
* feat: add in typechain and setup base GhoDiscountRateService

* chore: add in type definition files

* chore: add in basic calculateDiscountRate function and unit test

* fix: skip unit test for now

* fix: allow to commit to repo and bypass jest coverage limit

* chore: write utility for calculating discount rate

* fix: write unit tests for all use cases for discount rates

* fix: write unit test for barrel file

* chore: jest config and export gho service

* fix: use zero values and mock return calls

* fix: test coverage and mocks

* chore: remove unnecessary coverage skips
  • Loading branch information
drewcook authored and grothem committed Dec 21, 2022
1 parent 57cddd2 commit 4fed105
Show file tree
Hide file tree
Showing 11 changed files with 906 additions and 183 deletions.
353 changes: 173 additions & 180 deletions README.md

Large diffs are not rendered by default.

3 changes: 1 addition & 2 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module.exports = {
preset: 'ts-jest',
collectCoverageFrom: ['packages/*/src/**/*.ts'],
collectCoverageFrom: ['packages/*/src/**(!typechain)/*.ts'],
coverageDirectory: 'coverage',
coverageReporters: ['lcov', 'text'],
coverageThreshold: {
Expand Down Expand Up @@ -43,7 +43,6 @@ module.exports = {
'packages/math-utils/src/formatters/reserve/index.ts', // TODO: remove
],
modulePathIgnorePatterns: ['node_modules'],
preset: 'ts-jest',
testEnvironment: 'node',
verbose: true,
watchPlugins: [
Expand Down
2 changes: 1 addition & 1 deletion packages/contract-helpers/src/commons/BaseService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ describe('BaseService', () => {
expect(gas?.gasLimit).toEqual('1');
expect(gas?.gasPrice).toEqual('2');
});
xit('Expects null when no gas limit', async () => {
it.skip('Expects null when no gas limit', async () => {
const txCallback = async () => ({});
const txs = [
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
import { constants, BigNumber, BigNumberish, providers } from 'ethers';
import { valueToWei } from '../commons/utils';
import { GhoDiscountRateStrategyService } from './GhoDiscountRateStrategyService';
import { GhoDiscountRateStrategy } from './typechain/GhoDiscountRateStrategy';
import { GhoDiscountRateStrategy__factory } from './typechain/GhoDiscountRateStrategy__factory';

jest.mock('../commons/gasStation', () => {
return {
__esModule: true,
estimateGasByNetwork: jest
.fn()
.mockImplementation(async () => Promise.resolve(BigNumber.from(1))),
estimateGas: jest.fn(async () => Promise.resolve(BigNumber.from(1))),
};
});

// Helper for contract call arguments
const convertToBN = (n: string) => valueToWei(n, 18);

describe('GhoDiscountRateStrategyService', () => {
const DISCOUNT_RATE_STRATEGY_ADDRESS = constants.AddressZero;
const correctProvider: providers.Provider = new providers.JsonRpcProvider();

// Mocking
jest
.spyOn(correctProvider, 'getGasPrice')
.mockImplementation(async () => Promise.resolve(BigNumber.from(1)));

afterEach(() => jest.clearAllMocks());

describe('Create new GhoDiscountRateStrategyService', () => {
it('Expects to be initialized correctly', () => {
const instance = new GhoDiscountRateStrategyService(
correctProvider,
DISCOUNT_RATE_STRATEGY_ADDRESS,
);
expect(instance).toBeInstanceOf(GhoDiscountRateStrategyService);
});
});

describe('calculateDiscountRate', () => {
it('should return zero discount if discount token balance does not meet minimum requirements to gain a discount', async () => {
// Create instance
const contract = new GhoDiscountRateStrategyService(
correctProvider,
DISCOUNT_RATE_STRATEGY_ADDRESS,
);

// Use case - borrowing 100 GHO owning 0 skAAVE
const ghoDebtTokenBalance: BigNumberish = convertToBN('100');
const stakedAaveBalance: BigNumberish = convertToBN('0');
const expected = BigNumber.from('0'); // 0%

// Mock it
const spy = jest
.spyOn(GhoDiscountRateStrategy__factory, 'connect')
.mockReturnValue({
calculateDiscountRate: async () => Promise.resolve(expected),
} as unknown as GhoDiscountRateStrategy);

// Call it
const result = await contract.calculateDiscountRate(
ghoDebtTokenBalance,
stakedAaveBalance,
);

// Assert it
expect(spy).toHaveBeenCalled();
expect(spy).toBeCalledTimes(1);
expect(result).toEqual(expected);
});

it('should return zero discount if GHO variable debt token balance does not meet minimum requirements to gain a discount', async () => {
// Create instance
const contract = new GhoDiscountRateStrategyService(
correctProvider,
DISCOUNT_RATE_STRATEGY_ADDRESS,
);

// Use case - borrowing 0 GHO owning 1 skAAVE
const ghoDebtTokenBalance: BigNumberish = convertToBN('0');
const stakedAaveBalance: BigNumberish = convertToBN('1');
const expected = BigNumber.from('0'); // 0%

// Mock it
const spy = jest
.spyOn(GhoDiscountRateStrategy__factory, 'connect')
.mockReturnValue({
calculateDiscountRate: async () => Promise.resolve(expected),
} as unknown as GhoDiscountRateStrategy);

// Call it
const result = await contract.calculateDiscountRate(
ghoDebtTokenBalance,
stakedAaveBalance,
);

// Assert it
expect(spy).toHaveBeenCalled();
expect(spy).toBeCalledTimes(1);
expect(result).toEqual(expected);
});

// Discounted balance = discount token * 100
it('should return the maximum discount rate of 20% if the calculated total discounted balance is greater or equal to the debt token balance', async () => {
// Create instance
const contract = new GhoDiscountRateStrategyService(
correctProvider,
DISCOUNT_RATE_STRATEGY_ADDRESS,
);

// Use case #1 - borrowing 100 GHO owning 1 stkAAVE
const ghoDebtTokenBalance: BigNumberish = convertToBN('100');
let stakedAaveBalance: BigNumberish = convertToBN('1');
const expected = BigNumber.from('2000'); // 20.00% discount

// Mock it
const spy = jest
.spyOn(GhoDiscountRateStrategy__factory, 'connect')
.mockReturnValue({
calculateDiscountRate: async () => Promise.resolve(expected),
} as unknown as GhoDiscountRateStrategy);

// Call it
let result = await contract.calculateDiscountRate(
ghoDebtTokenBalance,
stakedAaveBalance,
);

// Assert it
expect(spy).toHaveBeenCalled();
expect(spy).toBeCalledTimes(1);
expect(result).toEqual(expected);

// Use case #2 - borrowing 100 GHO owning 5 stkAAVE
stakedAaveBalance = convertToBN('5');

// Call it
result = await contract.calculateDiscountRate(
ghoDebtTokenBalance,
stakedAaveBalance,
);

// Assert it
expect(spy).toHaveBeenCalled();
expect(spy).toBeCalledTimes(1);
expect(result).toEqual(expected);
});

it('should return a sub-maximum discount if user borrows more GHO than can be discounted based off of the discount token balance', async () => {
// Create instance
const contract = new GhoDiscountRateStrategyService(
correctProvider,
DISCOUNT_RATE_STRATEGY_ADDRESS,
);

// Use case - borrowing 150 GHO owning 1 skAAVE
const ghoDebtTokenBalance: BigNumberish = convertToBN('150');
const stakedAaveBalance: BigNumberish = convertToBN('1');
const expected = BigNumber.from('1333'); // 13.33% discount

// Mock it
const spy = jest
.spyOn(GhoDiscountRateStrategy__factory, 'connect')
.mockReturnValue({
calculateDiscountRate: async () => Promise.resolve(expected),
} as unknown as GhoDiscountRateStrategy);

// Call it
const result = await contract.calculateDiscountRate(
ghoDebtTokenBalance,
stakedAaveBalance,
);

// Assert it
expect(spy).toHaveBeenCalled();
expect(spy).toBeCalledTimes(1);
expect(result).toEqual(expected);
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { BigNumber, BigNumberish, providers } from 'ethers';
import BaseService from '../commons/BaseService';
import { GhoDiscountRateStrategy__factory } from './typechain/GhoDiscountRateStrategy__factory';
import type { IGhoDiscountRateStrategy } from './typechain/IGhoDiscountRateStrategy';

export interface GhoDiscountRateServiceInterface {
calculateDiscountRate: (
ghoDebtTokenBalance: BigNumberish,
skAaveBalance: BigNumberish,
) => Promise<BigNumber>;
}

/**
* The service for interacting with the GhoDiscountRateStrategy.sol smart contract
* https://github.com/aave/gho/blob/main/src/contracts/facilitators/aave/interestStrategy/GhoDiscountRateStrategy.sol
*/
export class GhoDiscountRateStrategyService
extends BaseService<IGhoDiscountRateStrategy>
implements GhoDiscountRateServiceInterface
{
readonly ghoDiscountRateStrategyAddress: string;

constructor(
provider: providers.Provider,
discountRateStrategyAddress: string,
) {
super(provider, GhoDiscountRateStrategy__factory);
this.ghoDiscountRateStrategyAddress = discountRateStrategyAddress;
}

/**
* Calculates the discounted interest rate charged on the borrowed native GHO token from the Aave Protocol. Currently this is set to be 20% on 100 GHO borrowed per stkAAVE held. Additionally, a user's discount rate is
* updated anytime they send or receive the discount token (stkAAVE). Users are entitled to this discount for a given amount of time without needing to perform any additional actions (i.e. the discount lock period).
* @param ghoDebtTokenBalance - The balance for the user's GhoVariableDebtToken, i.e. the amount they currently have borrowed from the protocol
* @param stakedAaveBalance - The balance of the user's stkAAVE token balance
* @returns - The discounted interest rate charged on the borrowed native GHO token.
*/
public async calculateDiscountRate(
ghoDebtTokenBalance: BigNumberish,
stakedAaveBalance: BigNumberish,
) {
const contract = this.getContractInstance(
this.ghoDiscountRateStrategyAddress,
);
const result = await contract.calculateDiscountRate(
ghoDebtTokenBalance,
stakedAaveBalance,
);
return result;
}
}
7 changes: 7 additions & 0 deletions packages/contract-helpers/src/gho/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { GhoDiscountRateStrategyService } from './index';

describe('gho contract helpers', () => {
it('exports out the GhoDiscountRateStrategyService', () => {
expect(GhoDiscountRateStrategyService).toBeTruthy();
});
});
3 changes: 3 additions & 0 deletions packages/contract-helpers/src/gho/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { GhoDiscountRateStrategyService } from './GhoDiscountRateStrategyService';

export { GhoDiscountRateStrategyService };
Loading

0 comments on commit 4fed105

Please sign in to comment.