-
Notifications
You must be signed in to change notification settings - Fork 95
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add in GhoDiscountRateStrategyService (#443)
* 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
Showing
11 changed files
with
906 additions
and
183 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
181 changes: 181 additions & 0 deletions
181
packages/contract-helpers/src/gho/GhoDiscountRateStrategyService.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
}); | ||
}); |
51 changes: 51 additions & 0 deletions
51
packages/contract-helpers/src/gho/GhoDiscountRateStrategyService.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
import { GhoDiscountRateStrategyService } from './GhoDiscountRateStrategyService'; | ||
|
||
export { GhoDiscountRateStrategyService }; |
Oops, something went wrong.