diff --git a/contracts/feeds/CustomAggregatorV3CompatibleFeedDiscounted.sol b/contracts/feeds/CustomAggregatorV3CompatibleFeedAdjusted.sol similarity index 53% rename from contracts/feeds/CustomAggregatorV3CompatibleFeedDiscounted.sol rename to contracts/feeds/CustomAggregatorV3CompatibleFeedAdjusted.sol index 8f71ad1a..604dbe26 100644 --- a/contracts/feeds/CustomAggregatorV3CompatibleFeedDiscounted.sol +++ b/contracts/feeds/CustomAggregatorV3CompatibleFeedAdjusted.sol @@ -4,38 +4,44 @@ pragma solidity 0.8.9; import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol"; /** - * @title CustomAggregatorV3CompatibleFeedDiscounted - * @notice AggregatorV3 compatible proxy-feed that discounts the price - * of an underlying chainlink compatible feed by a given percentage + * @title CustomAggregatorV3CompatibleFeedAdjusted + * @notice AggregatorV3 compatible proxy-feed that adjusts the price + * of an underlying chainlink compatible feed by a given signed percentage. + * Positive adjustmentPercentage raises the reported price. + * Negative adjustmentPercentage lowers the reported price. * @author RedDuck Software */ -contract CustomAggregatorV3CompatibleFeedDiscounted is AggregatorV3Interface { +contract CustomAggregatorV3CompatibleFeedAdjusted is AggregatorV3Interface { /** * @notice the underlying chainlink compatible feed */ AggregatorV3Interface public immutable underlyingFeed; /** - * @notice the discount percentage. Expressed in 10 ** decimals() precision - * Example: 10 ** decimals() = 1% + * @notice the adjustment percentage (signed). + * Expressed in 10 ** decimals() precision. + * Example: 10 ** decimals() = 1%, -(10 ** decimals()) = -1% + * Positive values raise the reported price. + * Negative values lower the reported price. */ - uint256 public immutable discountPercentage; + int256 public immutable adjustmentPercentage; /** * @notice constructor * @param _underlyingFeed the underlying chainlink compatible feed - * @param _discountPercentage the discount percentage. Expressed in 10 ** decimals() precision + * @param _adjustmentPercentage signed adjustment percentage in 10 ** decimals() precision */ - constructor(address _underlyingFeed, uint256 _discountPercentage) { - require(_underlyingFeed != address(0), "CAD: !underlying feed"); + constructor(address _underlyingFeed, int256 _adjustmentPercentage) { + require(_underlyingFeed != address(0), "CAA: !underlying feed"); underlyingFeed = AggregatorV3Interface(_underlyingFeed); + int256 maxPct = int256(100 * (10**decimals())); require( - _discountPercentage <= 100 * (10**decimals()), - "CAD: !discount percentage" + _adjustmentPercentage >= -maxPct && _adjustmentPercentage <= maxPct, + "CAA: invalid adjustment" ); - discountPercentage = _discountPercentage; + adjustmentPercentage = _adjustmentPercentage; } /** @@ -60,7 +66,7 @@ contract CustomAggregatorV3CompatibleFeedDiscounted is AggregatorV3Interface { answeredInRound ) = underlyingFeed.latestRoundData(); - answer = _calculateDiscountedAnswer(answer); + answer = _calculateAdjustedAnswer(answer); } /** @@ -91,7 +97,7 @@ contract CustomAggregatorV3CompatibleFeedDiscounted is AggregatorV3Interface { updatedAt, answeredInRound ) = underlyingFeed.getRoundData(_roundId); - answer = _calculateDiscountedAnswer(answer); + answer = _calculateAdjustedAnswer(answer); } /** @@ -105,27 +111,28 @@ contract CustomAggregatorV3CompatibleFeedDiscounted is AggregatorV3Interface { * @inheritdoc AggregatorV3Interface */ function description() public view returns (string memory) { - return - string( - abi.encodePacked(underlyingFeed.description(), " Discounted") - ); + if (adjustmentPercentage == 0) return underlyingFeed.description(); + string memory suffix = adjustmentPercentage > 0 + ? " PriceRaised" + : " PriceLowered"; + return string(abi.encodePacked(underlyingFeed.description(), suffix)); } /** - * @dev calculates the discounted answer - * @param _answer the answer to discount - * @return the discounted answer + * @dev calculates the adjusted answer + * @param _answer the answer to adjust + * @return the adjusted answer */ - function _calculateDiscountedAnswer(int256 _answer) + function _calculateAdjustedAnswer(int256 _answer) internal view returns (int256) { - require(_answer >= 0, "CAD: !_answer"); + require(_answer >= 0, "CAA: !_answer"); - int256 discount = (_answer * int256(discountPercentage)) / + int256 adjustment = (_answer * adjustmentPercentage) / int256(100 * 10**decimals()); - return _answer - discount; + return _answer + adjustment; } } diff --git a/contracts/testers/CustomAggregatorV3CompatibleFeedAdjustedTester.sol b/contracts/testers/CustomAggregatorV3CompatibleFeedAdjustedTester.sol new file mode 100644 index 00000000..c58fda64 --- /dev/null +++ b/contracts/testers/CustomAggregatorV3CompatibleFeedAdjustedTester.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.9; + +import "../feeds/CustomAggregatorV3CompatibleFeedAdjusted.sol"; + +contract CustomAggregatorV3CompatibleFeedAdjustedTester is + CustomAggregatorV3CompatibleFeedAdjusted +{ + constructor(address _underlyingFeed, int256 _adjustmentPercentage) + CustomAggregatorV3CompatibleFeedAdjusted( + _underlyingFeed, + _adjustmentPercentage + ) + {} + + function getAdjustedAnswer(int256 _answer) public view returns (int256) { + return _calculateAdjustedAnswer(_answer); + } +} diff --git a/contracts/testers/CustomAggregatorV3CompatibleFeedDiscountedTester.sol b/contracts/testers/CustomAggregatorV3CompatibleFeedDiscountedTester.sol deleted file mode 100644 index c5265f6f..00000000 --- a/contracts/testers/CustomAggregatorV3CompatibleFeedDiscountedTester.sol +++ /dev/null @@ -1,19 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.9; - -import "../feeds/CustomAggregatorV3CompatibleFeedDiscounted.sol"; - -contract CustomAggregatorV3CompatibleFeedDiscountedTester is - CustomAggregatorV3CompatibleFeedDiscounted -{ - constructor(address _underlyingFeed, uint256 _discountPercentage) - CustomAggregatorV3CompatibleFeedDiscounted( - _underlyingFeed, - _discountPercentage - ) - {} - - function getDiscountedAnswer(int256 _answer) public view returns (int256) { - return _calculateDiscountedAnswer(_answer); - } -} diff --git a/helpers/contracts.ts b/helpers/contracts.ts index 13f557cd..a1eaa303 100644 --- a/helpers/contracts.ts +++ b/helpers/contracts.ts @@ -26,7 +26,7 @@ export type TokenContractNames = { type CommonContractNames = Omit & { ac: string; customAggregator: string; - customAggregatorDiscounted: string; + customAggregatorAdjusted: string; layerZero: { oftAdapter: string; vaultComposer: string; @@ -149,7 +149,7 @@ export const getCommonContractNames = (): CommonContractNames => { dataFeed: 'DataFeed', customAggregator: 'CustomAggregatorV3CompatibleFeed', customAggregatorGrowth: 'CustomAggregatorV3CompatibleFeedGrowth', - customAggregatorDiscounted: 'CustomAggregatorV3CompatibleFeedDiscounted', + customAggregatorAdjusted: 'CustomAggregatorV3CompatibleFeedAdjusted', roles: 'MidasAccessControlRoles', dataFeedComposite: 'CompositeDataFeed', dataFeedMultiply: 'CompositeDataFeedMultiply', diff --git a/scripts/deploy/common/data-feed.ts b/scripts/deploy/common/data-feed.ts index 869cb5f4..563da3c7 100644 --- a/scripts/deploy/common/data-feed.ts +++ b/scripts/deploy/common/data-feed.ts @@ -72,8 +72,8 @@ export type DeployCustomAggregatorRegularConfig = type?: 'REGULAR'; }; -export type DeployCustomAggregatorDiscountedConfig = { - discountPercentage: BigNumberish; +export type DeployCustomAggregatorAdjustedConfig = { + adjustmentPercentage: BigNumberish; underlyingFeed: `0x${string}` | 'customFeed'; }; @@ -410,14 +410,14 @@ export const deployMTokenDataFeed = async ( ); }; -export const deployMTokenCustomAggregatorDiscounted = async ( +export const deployMTokenCustomAggregatorAdjusted = async ( hre: HardhatRuntimeEnvironment, token: MTokenName, ) => { - await deployCustomAggregatorDiscounted( + await deployCustomAggregatorAdjusted( hre, token, - getDeploymentGenericConfig(hre, token, 'customAggregatorDiscounted'), + getDeploymentGenericConfig(hre, token, 'customAggregatorAdjusted'), ); }; @@ -536,10 +536,10 @@ const deployCustomAggregator = async ( ); }; -const deployCustomAggregatorDiscounted = async ( +const deployCustomAggregatorAdjusted = async ( hre: HardhatRuntimeEnvironment, token: MTokenName, - networkConfig?: DeployCustomAggregatorDiscountedConfig, + networkConfig?: DeployCustomAggregatorAdjustedConfig, ) => { const addresses = getCurrentAddresses(hre); @@ -558,7 +558,7 @@ const deployCustomAggregatorDiscounted = async ( await deployAndVerify( hre, - getCommonContractNames().customAggregatorDiscounted, - [underlyingFeed, networkConfig.discountPercentage], + getCommonContractNames().customAggregatorAdjusted, + [underlyingFeed, networkConfig.adjustmentPercentage], ); }; diff --git a/scripts/deploy/common/types.ts b/scripts/deploy/common/types.ts index 6424b0f4..f5f6b500 100644 --- a/scripts/deploy/common/types.ts +++ b/scripts/deploy/common/types.ts @@ -3,8 +3,8 @@ import { HardhatRuntimeEnvironment } from 'hardhat/types'; import { AddFeeWaivedConfig, AddPaymentTokensConfig } from './common-vault'; import { + DeployCustomAggregatorAdjustedConfig, DeployCustomAggregatorConfig, - DeployCustomAggregatorDiscountedConfig, DeployDataFeedConfig, SetRoundDataConfig, } from './data-feed'; @@ -94,7 +94,7 @@ export type PostDeployConfig = { export type DeploymentConfig = { genericConfigs: { customAggregator?: DeployCustomAggregatorConfig; - customAggregatorDiscounted?: DeployCustomAggregatorDiscountedConfig; + customAggregatorAdjusted?: DeployCustomAggregatorAdjustedConfig; dataFeed?: DeployDataFeedConfig; }; networkConfigs: Record< diff --git a/scripts/deploy/misc/deploy_CustomAggregatorDiscounted.ts b/scripts/deploy/misc/deploy_CustomAggregatorAdjusted.ts similarity index 68% rename from scripts/deploy/misc/deploy_CustomAggregatorDiscounted.ts rename to scripts/deploy/misc/deploy_CustomAggregatorAdjusted.ts index 4a188489..cfc60da4 100644 --- a/scripts/deploy/misc/deploy_CustomAggregatorDiscounted.ts +++ b/scripts/deploy/misc/deploy_CustomAggregatorAdjusted.ts @@ -1,12 +1,12 @@ import { HardhatRuntimeEnvironment } from 'hardhat/types'; import { getMTokenOrThrow } from '../../../helpers/utils'; -import { deployMTokenCustomAggregatorDiscounted } from '../common/data-feed'; +import { deployMTokenCustomAggregatorAdjusted } from '../common/data-feed'; import { DeployFunction } from '../common/types'; const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { const mToken = getMTokenOrThrow(hre); - await deployMTokenCustomAggregatorDiscounted(hre, mToken); + await deployMTokenCustomAggregatorAdjusted(hre, mToken); }; export default func; diff --git a/test/common/fixtures.ts b/test/common/fixtures.ts index 752c2b41..70e0ab6e 100644 --- a/test/common/fixtures.ts +++ b/test/common/fixtures.ts @@ -43,7 +43,7 @@ import { RedemptionVaultWithMTokenTest__factory, AaveV3PoolMock__factory, MorphoVaultMock__factory, - CustomAggregatorV3CompatibleFeedDiscountedTester__factory, + CustomAggregatorV3CompatibleFeedAdjustedTester__factory, DepositVaultWithAaveTest__factory, DepositVaultWithMorphoTest__factory, DepositVaultWithMTokenTest__factory, @@ -714,8 +714,8 @@ export const defaultDeploy = async () => { parseUnits('10000', mockedAggregatorDecimals), ); - const customFeedDiscounted = - await new CustomAggregatorV3CompatibleFeedDiscountedTester__factory( + const customFeedAdjusted = + await new CustomAggregatorV3CompatibleFeedAdjustedTester__factory( owner, ).deploy(customFeed.address, parseUnits('10', 8)); @@ -820,7 +820,7 @@ export const defaultDeploy = async () => { return { customFeed, - customFeedDiscounted, + customFeedAdjusted, customFeedGrowth, mTBILL, mBASIS, diff --git a/test/unit/CustomFeedAdjusted.test.ts b/test/unit/CustomFeedAdjusted.test.ts new file mode 100644 index 00000000..5756b6ce --- /dev/null +++ b/test/unit/CustomFeedAdjusted.test.ts @@ -0,0 +1,362 @@ +import { loadFixture } from '@nomicfoundation/hardhat-network-helpers'; +import { expect } from 'chai'; +import { parseUnits } from 'ethers/lib/utils'; +import { ethers } from 'hardhat'; + +import { CustomAggregatorV3CompatibleFeedAdjustedTester__factory } from '../../typechain-types'; +import { setRoundData } from '../common/custom-feed.helpers'; +import { defaultDeploy } from '../common/fixtures'; + +describe('CustomAggregatorV3CompatibleFeedAdjusted', function () { + it('deployment with positive adjustment (price raised)', async () => { + const { customFeedAdjusted, customFeed } = await loadFixture(defaultDeploy); + + expect(await customFeedAdjusted.underlyingFeed()).eq(customFeed.address); + expect(await customFeedAdjusted.adjustmentPercentage()).eq( + parseUnits('10', 8), + ); + + expect(await customFeedAdjusted.decimals()).eq(await customFeed.decimals()); + expect(await customFeedAdjusted.version()).eq(await customFeed.version()); + expect(await customFeedAdjusted.description()).eq( + (await customFeed.description()) + ' PriceRaised', + ); + }); + + it('deployment with negative adjustment (price lowered)', async () => { + const { customFeed, owner } = await loadFixture(defaultDeploy); + + const loweredFeed = + await new CustomAggregatorV3CompatibleFeedAdjustedTester__factory( + owner, + ).deploy(customFeed.address, parseUnits('-10', 8)); + + expect(await loweredFeed.adjustmentPercentage()).eq(parseUnits('-10', 8)); + expect(await loweredFeed.description()).eq( + (await customFeed.description()) + ' PriceLowered', + ); + }); + + it('deployment with zero adjustment', async () => { + const { customFeed, owner } = await loadFixture(defaultDeploy); + + const zeroFeed = + await new CustomAggregatorV3CompatibleFeedAdjustedTester__factory( + owner, + ).deploy(customFeed.address, 0); + + expect(await zeroFeed.adjustmentPercentage()).eq(0); + expect(await zeroFeed.description()).eq(await customFeed.description()); + }); + + it('initialize', async () => { + const fixture = await loadFixture(defaultDeploy); + + await expect( + new CustomAggregatorV3CompatibleFeedAdjustedTester__factory( + fixture.owner, + ).deploy(ethers.constants.AddressZero, 0), + ).revertedWith('CAA: !underlying feed'); + + await expect( + new CustomAggregatorV3CompatibleFeedAdjustedTester__factory( + fixture.owner, + ).deploy(fixture.customFeed.address, parseUnits('100.1', 8)), + ).revertedWith('CAA: invalid adjustment'); + + await expect( + new CustomAggregatorV3CompatibleFeedAdjustedTester__factory( + fixture.owner, + ).deploy(fixture.customFeed.address, parseUnits('-100.1', 8)), + ).revertedWith('CAA: invalid adjustment'); + }); + + describe('latestRoundData (positive adjustment)', () => { + it('+10%: price 100 -> 110', async () => { + const { customFeed, customFeedAdjusted, owner } = await loadFixture( + defaultDeploy, + ); + + await setRoundData({ customFeed, owner }, 100); + + const adjusted = await customFeedAdjusted.latestRoundData(); + const original = await customFeed.latestRoundData(); + + expect(adjusted.answer).eq(parseUnits('110', 8)); + expect(adjusted.roundId).eq(original.roundId); + expect(adjusted.startedAt).eq(original.startedAt); + expect(adjusted.updatedAt).eq(original.updatedAt); + expect(adjusted.answeredInRound).eq(original.answeredInRound); + }); + + it('+100%: price 1 -> 2', async () => { + const { customFeed, owner } = await loadFixture(defaultDeploy); + + await setRoundData({ customFeed, owner }, 1); + + const fullRaise = + await new CustomAggregatorV3CompatibleFeedAdjustedTester__factory( + owner, + ).deploy(customFeed.address, parseUnits('100', 8)); + + const adjusted = await fullRaise.latestRoundData(); + expect(adjusted.answer).eq(parseUnits('2', 8)); + }); + + it('+7.7%: 1.01234568 -> 1.09029629', async () => { + const fixture = await loadFixture(defaultDeploy); + + const feed = + await new CustomAggregatorV3CompatibleFeedAdjustedTester__factory( + fixture.owner, + ).deploy(fixture.customFeed.address, parseUnits('7.7', 8)); + + await setRoundData( + { customFeed: fixture.customFeed, owner: fixture.owner }, + 1.01234568, + ); + const roundData = await feed.latestRoundData(); + + expect(roundData.answer).eq(parseUnits('1.09029629', 8)); + }); + }); + + describe('latestRoundData (negative adjustment)', () => { + it('-10%: price 100 -> 90', async () => { + const { customFeed, owner } = await loadFixture(defaultDeploy); + + await setRoundData({ customFeed, owner }, 100); + + const loweredFeed = + await new CustomAggregatorV3CompatibleFeedAdjustedTester__factory( + owner, + ).deploy(customFeed.address, parseUnits('-10', 8)); + + const adjusted = await loweredFeed.latestRoundData(); + const original = await customFeed.latestRoundData(); + + expect(adjusted.answer).eq(parseUnits('90', 8)); + expect(adjusted.roundId).eq(original.roundId); + expect(adjusted.startedAt).eq(original.startedAt); + expect(adjusted.updatedAt).eq(original.updatedAt); + expect(adjusted.answeredInRound).eq(original.answeredInRound); + }); + + it('-100%: price 1 -> 0', async () => { + const { customFeed, owner } = await loadFixture(defaultDeploy); + + await setRoundData({ customFeed, owner }, 1); + + const fullLower = + await new CustomAggregatorV3CompatibleFeedAdjustedTester__factory( + owner, + ).deploy(customFeed.address, parseUnits('-100', 8)); + + const adjusted = await fullLower.latestRoundData(); + expect(adjusted.answer).eq(parseUnits('0', 8)); + }); + + it('-7.7%: 1.01234568 -> 0.93439507', async () => { + const fixture = await loadFixture(defaultDeploy); + + const feed = + await new CustomAggregatorV3CompatibleFeedAdjustedTester__factory( + fixture.owner, + ).deploy(fixture.customFeed.address, parseUnits('-7.7', 8)); + + await setRoundData( + { customFeed: fixture.customFeed, owner: fixture.owner }, + 1.01234568, + ); + const roundData = await feed.latestRoundData(); + + expect(roundData.answer).eq(parseUnits('0.93439507', 8)); + }); + }); + + describe('getRoundData (positive adjustment)', () => { + it('+10%: price 100 -> 110', async () => { + const { customFeed, customFeedAdjusted, owner } = await loadFixture( + defaultDeploy, + ); + + await setRoundData({ customFeed, owner }, 100); + const roundId = await customFeed.latestRound(); + await setRoundData({ customFeed, owner }, 200); + + const adjusted = await customFeedAdjusted.getRoundData(roundId); + const original = await customFeed.getRoundData(roundId); + + expect(adjusted.answer).eq(parseUnits('110', 8)); + expect(adjusted.roundId).eq(original.roundId); + expect(adjusted.startedAt).eq(original.startedAt); + expect(adjusted.updatedAt).eq(original.updatedAt); + expect(adjusted.answeredInRound).eq(original.answeredInRound); + }); + + it('+7.7%: 1.01234568 -> 1.09029629', async () => { + const fixture = await loadFixture(defaultDeploy); + + const feed = + await new CustomAggregatorV3CompatibleFeedAdjustedTester__factory( + fixture.owner, + ).deploy(fixture.customFeed.address, parseUnits('7.7', 8)); + + await setRoundData( + { customFeed: fixture.customFeed, owner: fixture.owner }, + 1.01234568, + ); + const roundId = await fixture.customFeed.latestRound(); + + const roundData = await feed.getRoundData(roundId); + + expect(roundData.answer).eq(parseUnits('1.09029629', 8)); + }); + }); + + describe('getRoundData (negative adjustment)', () => { + it('-10%: price 100 -> 90', async () => { + const { customFeed, owner } = await loadFixture(defaultDeploy); + + await setRoundData({ customFeed, owner }, 100); + const roundId = await customFeed.latestRound(); + await setRoundData({ customFeed, owner }, 200); + + const loweredFeed = + await new CustomAggregatorV3CompatibleFeedAdjustedTester__factory( + owner, + ).deploy(customFeed.address, parseUnits('-10', 8)); + + const adjusted = await loweredFeed.getRoundData(roundId); + const original = await customFeed.getRoundData(roundId); + + expect(adjusted.answer).eq(parseUnits('90', 8)); + expect(adjusted.roundId).eq(original.roundId); + expect(adjusted.startedAt).eq(original.startedAt); + expect(adjusted.updatedAt).eq(original.updatedAt); + expect(adjusted.answeredInRound).eq(original.answeredInRound); + }); + + it('-7.7%: 1.01234568 -> 0.93439507', async () => { + const fixture = await loadFixture(defaultDeploy); + + const feed = + await new CustomAggregatorV3CompatibleFeedAdjustedTester__factory( + fixture.owner, + ).deploy(fixture.customFeed.address, parseUnits('-7.7', 8)); + + await setRoundData( + { customFeed: fixture.customFeed, owner: fixture.owner }, + 1.01234568, + ); + const roundId = await fixture.customFeed.latestRound(); + + const roundData = await feed.getRoundData(roundId); + + expect(roundData.answer).eq(parseUnits('0.93439507', 8)); + }); + }); + + describe('_calculateAdjustedAnswer', () => { + it('should fail: when answer is negative', async () => { + const fixture = await loadFixture(defaultDeploy); + + await expect( + fixture.customFeedAdjusted.getAdjustedAnswer(-1), + ).revertedWith('CAA: !_answer'); + }); + + it('when answer is 0 should return 0 (positive adjustment)', async () => { + const fixture = await loadFixture(defaultDeploy); + + expect( + await fixture.customFeedAdjusted.getAdjustedAnswer(parseUnits('0', 8)), + ).eq(parseUnits('0', 8)); + }); + + it('when answer is 0 should return 0 (negative adjustment)', async () => { + const { customFeed, owner } = await loadFixture(defaultDeploy); + + const loweredFeed = + await new CustomAggregatorV3CompatibleFeedAdjustedTester__factory( + owner, + ).deploy(customFeed.address, parseUnits('-10', 8)); + + expect(await loweredFeed.getAdjustedAnswer(parseUnits('0', 8))).eq( + parseUnits('0', 8), + ); + }); + + it('+10%: answer 110 -> 121 (positive adjustment)', async () => { + const fixture = await loadFixture(defaultDeploy); + + expect( + await fixture.customFeedAdjusted.getAdjustedAnswer( + parseUnits('110', 8), + ), + ).eq(parseUnits('121', 8)); + }); + + it('+10%: answer 100 -> 110 (positive adjustment)', async () => { + const fixture = await loadFixture(defaultDeploy); + + expect( + await fixture.customFeedAdjusted.getAdjustedAnswer( + parseUnits('100', 8), + ), + ).eq(parseUnits('110', 8)); + }); + + it('-10%: answer 100 -> 90 (negative adjustment)', async () => { + const { customFeed, owner } = await loadFixture(defaultDeploy); + + const loweredFeed = + await new CustomAggregatorV3CompatibleFeedAdjustedTester__factory( + owner, + ).deploy(customFeed.address, parseUnits('-10', 8)); + + expect(await loweredFeed.getAdjustedAnswer(parseUnits('100', 8))).eq( + parseUnits('90', 8), + ); + }); + + it('-10%: answer 110 -> 99 (negative adjustment)', async () => { + const { customFeed, owner } = await loadFixture(defaultDeploy); + + const loweredFeed = + await new CustomAggregatorV3CompatibleFeedAdjustedTester__factory( + owner, + ).deploy(customFeed.address, parseUnits('-10', 8)); + + expect(await loweredFeed.getAdjustedAnswer(parseUnits('110', 8))).eq( + parseUnits('99', 8), + ); + }); + + it('+7.7%: 1.01234568 -> 1.09029629 (positive adjustment)', async () => { + const fixture = await loadFixture(defaultDeploy); + + const feed = + await new CustomAggregatorV3CompatibleFeedAdjustedTester__factory( + fixture.owner, + ).deploy(fixture.customFeed.address, parseUnits('7.7', 8)); + + expect(await feed.getAdjustedAnswer(parseUnits('1.01234568', 8))).eq( + parseUnits('1.09029629', 8), + ); + }); + + it('-7.7%: 1.01234568 -> 0.93439507 (negative adjustment)', async () => { + const fixture = await loadFixture(defaultDeploy); + + const feed = + await new CustomAggregatorV3CompatibleFeedAdjustedTester__factory( + fixture.owner, + ).deploy(fixture.customFeed.address, parseUnits('-7.7', 8)); + + expect(await feed.getAdjustedAnswer(parseUnits('1.01234568', 8))).eq( + parseUnits('0.93439507', 8), + ); + }); + }); +}); diff --git a/test/unit/CustomFeedDiscounted.test.ts b/test/unit/CustomFeedDiscounted.test.ts deleted file mode 100644 index b827775b..00000000 --- a/test/unit/CustomFeedDiscounted.test.ts +++ /dev/null @@ -1,239 +0,0 @@ -import { loadFixture } from '@nomicfoundation/hardhat-network-helpers'; -import { expect } from 'chai'; -import { parseUnits } from 'ethers/lib/utils'; -import { ethers } from 'hardhat'; - -import { CustomAggregatorV3CompatibleFeedDiscountedTester__factory } from '../../typechain-types'; -import { setRoundData } from '../common/custom-feed.helpers'; -import { defaultDeploy } from '../common/fixtures'; - -describe('CustomAggregatorV3CompatibleFeedDiscounted', function () { - it('deployment', async () => { - const { customFeedDiscounted, customFeed } = await loadFixture( - defaultDeploy, - ); - - expect(await customFeedDiscounted.underlyingFeed()).eq(customFeed.address); - expect(await customFeedDiscounted.discountPercentage()).eq( - parseUnits('10', 8), - ); - - expect(await customFeedDiscounted.decimals()).eq( - await customFeed.decimals(), - ); - expect(await customFeedDiscounted.version()).eq(await customFeed.version()); - expect(await customFeedDiscounted.description()).eq( - (await customFeed.description()) + ' Discounted', - ); - }); - - it('initialize', async () => { - const fixture = await loadFixture(defaultDeploy); - - await expect( - new CustomAggregatorV3CompatibleFeedDiscountedTester__factory( - fixture.owner, - ).deploy(ethers.constants.AddressZero, 0), - ).revertedWith('CAD: !underlying feed'); - - await expect( - new CustomAggregatorV3CompatibleFeedDiscountedTester__factory( - fixture.owner, - ).deploy(fixture.customFeed.address, parseUnits('100.1', 8)), - ).revertedWith('CAD: !discount percentage'); - }); - - describe('latestRoundData', () => { - it('when answer is 100 should return 90', async () => { - const { customFeed, customFeedDiscounted, owner } = await loadFixture( - defaultDeploy, - ); - - await setRoundData({ customFeed, owner }, 100); - - const latestRoundDataDiscounted = - await customFeedDiscounted.latestRoundData(); - - const latestRoundData = await customFeed.latestRoundData(); - - expect(latestRoundDataDiscounted.answer).eq(parseUnits('90', 8)); - expect(latestRoundDataDiscounted.roundId).eq(latestRoundData.roundId); - expect(latestRoundDataDiscounted.startedAt).eq(latestRoundData.startedAt); - expect(latestRoundDataDiscounted.updatedAt).eq(latestRoundData.updatedAt); - expect(latestRoundDataDiscounted.answeredInRound).eq( - latestRoundData.answeredInRound, - ); - }); - - it('when answer is 1, discount is 100%, should return 0', async () => { - let { customFeed, customFeedDiscounted, owner } = await loadFixture( - defaultDeploy, - ); - - await setRoundData({ customFeed, owner }, 1); - - customFeedDiscounted = - await new CustomAggregatorV3CompatibleFeedDiscountedTester__factory( - owner, - ).deploy(customFeed.address, parseUnits('100', 8)); - - const latestRoundDataDiscounted = - await customFeedDiscounted.latestRoundData(); - - const latestRoundData = await customFeed.latestRoundData(); - - expect(latestRoundDataDiscounted.answer).eq(parseUnits('0', 8)); - expect(latestRoundDataDiscounted.roundId).eq(latestRoundData.roundId); - expect(latestRoundDataDiscounted.startedAt).eq(latestRoundData.startedAt); - expect(latestRoundDataDiscounted.updatedAt).eq(latestRoundData.updatedAt); - expect(latestRoundDataDiscounted.answeredInRound).eq( - latestRoundData.answeredInRound, - ); - }); - - it('when answer is 1.01234568, discount 7.7% should return 0.93439507', async () => { - const fixture = await loadFixture(defaultDeploy); - - const customFeedDiscounted = - await new CustomAggregatorV3CompatibleFeedDiscountedTester__factory( - fixture.owner, - ).deploy(fixture.customFeed.address, parseUnits('7.7', 8)); - - await setRoundData( - { customFeed: fixture.customFeed, owner: fixture.owner }, - 1.01234568, - ); - const roundData = await customFeedDiscounted.latestRoundData(); - - expect(roundData.answer).eq(parseUnits('0.93439507', 8)); - }); - }); - - describe('getRoundData', () => { - it('when answer is 100, discount is 10%, should return 90', async () => { - const { customFeed, customFeedDiscounted, owner } = await loadFixture( - defaultDeploy, - ); - - await setRoundData({ customFeed, owner }, 100); - const roundId = await customFeed.latestRound(); - await setRoundData({ customFeed, owner }, 200); - - const latestRoundDataDiscounted = await customFeedDiscounted.getRoundData( - roundId, - ); - - const latestRoundData = await customFeed.getRoundData(roundId); - - expect(latestRoundDataDiscounted.answer).eq(parseUnits('90', 8)); - expect(latestRoundDataDiscounted.roundId).eq(latestRoundData.roundId); - expect(latestRoundDataDiscounted.startedAt).eq(latestRoundData.startedAt); - expect(latestRoundDataDiscounted.updatedAt).eq(latestRoundData.updatedAt); - expect(latestRoundDataDiscounted.answeredInRound).eq( - latestRoundData.answeredInRound, - ); - }); - - it('when answer is 1, discount is 100%, should return 0', async () => { - let { customFeed, customFeedDiscounted, owner } = await loadFixture( - defaultDeploy, - ); - - await setRoundData({ customFeed, owner }, 1); - const roundId = await customFeed.latestRound(); - await setRoundData({ customFeed, owner }, 1); - - customFeedDiscounted = - await new CustomAggregatorV3CompatibleFeedDiscountedTester__factory( - owner, - ).deploy(customFeed.address, parseUnits('100', 8)); - const latestRoundDataDiscounted = await customFeedDiscounted.getRoundData( - roundId, - ); - - const latestRoundData = await customFeed.getRoundData(roundId); - - expect(latestRoundDataDiscounted.answer).eq(parseUnits('0', 8)); - expect(latestRoundDataDiscounted.roundId).eq(latestRoundData.roundId); - expect(latestRoundDataDiscounted.startedAt).eq(latestRoundData.startedAt); - expect(latestRoundDataDiscounted.updatedAt).eq(latestRoundData.updatedAt); - expect(latestRoundDataDiscounted.answeredInRound).eq( - latestRoundData.answeredInRound, - ); - }); - - it('when answer is 1.01234568, discount 7.7% should return 0.93439507', async () => { - const fixture = await loadFixture(defaultDeploy); - - const customFeedDiscounted = - await new CustomAggregatorV3CompatibleFeedDiscountedTester__factory( - fixture.owner, - ).deploy(fixture.customFeed.address, parseUnits('7.7', 8)); - - await setRoundData( - { customFeed: fixture.customFeed, owner: fixture.owner }, - 1.01234568, - ); - const roundId = await fixture.customFeed.latestRound(); - - const roundData = await customFeedDiscounted.getRoundData(roundId); - - expect(roundData.answer).eq(parseUnits('0.93439507', 8)); - }); - }); - - describe('_calculateDiscountedAnswer', async () => { - it('should fail: when answer is negative', async () => { - const fixture = await loadFixture(defaultDeploy); - - await expect( - fixture.customFeedDiscounted.getDiscountedAnswer(-1), - ).revertedWith('CAD: !_answer'); - }); - - it('when answer is 110 should return 99', async () => { - const fixture = await loadFixture(defaultDeploy); - - expect( - await fixture.customFeedDiscounted.getDiscountedAnswer( - parseUnits('110', 8), - ), - ).eq(parseUnits('99', 8)); - }); - - it('when answer is 100 should return 90', async () => { - const fixture = await loadFixture(defaultDeploy); - - expect( - await fixture.customFeedDiscounted.getDiscountedAnswer( - parseUnits('100', 8), - ), - ).eq(parseUnits('90', 8)); - }); - - it('when answer is 1.01234568, discount 7.7% should return 0.93439507', async () => { - const fixture = await loadFixture(defaultDeploy); - - const customFeedDiscounted = - await new CustomAggregatorV3CompatibleFeedDiscountedTester__factory( - fixture.owner, - ).deploy(fixture.customFeed.address, parseUnits('7.7', 8)); - - expect( - await customFeedDiscounted.getDiscountedAnswer( - parseUnits('1.01234568', 8), - ), - ).eq(parseUnits('0.93439507', 8)); - }); - - it('when answer is 0 should return 0', async () => { - const fixture = await loadFixture(defaultDeploy); - - expect( - await fixture.customFeedDiscounted.getDiscountedAnswer( - parseUnits('0', 8), - ), - ).eq(parseUnits('0', 8)); - }); - }); -});