diff --git a/contracts/YieldStreamer.sol b/contracts/YieldStreamer.sol index 7cf44f7..1de2d2f 100644 --- a/contracts/YieldStreamer.sol +++ b/contracts/YieldStreamer.sol @@ -29,7 +29,7 @@ contract YieldStreamer is uint240 public constant RATE_FACTOR = 1000000000000; /// @notice The fee rate that is used to calculate the fee amount - uint240 public constant FEE_RATE = 225000000000; + uint240 public constant FEE_RATE = 0; /// @notice The coefficient used to round the yield, fee and other related values /// @dev e.g. value `12345678` will be rounded upward to `12350000` and down to `12340000` @@ -38,6 +38,9 @@ contract YieldStreamer is /// @notice The minimum amount that is allowed to be claimed uint256 public constant MIN_CLAIM_AMOUNT = 1000000; + /// @notice The maximum daily balance cap allowed for calculation claim + uint256 public constant MAX_DAILY_BALANCE_LIMIT = 200000000000; + /// @notice The initial state of the next claim for an account struct ClaimState { uint16 day; // The index of the day from which the yield will be calculated next time @@ -792,7 +795,7 @@ contract YieldStreamer is // Define first day yield and initial sum yield uint256 sumYield = 0; - uint256 dayYield = (_getMinimumInRange(possibleBalanceByDays, 0, periodLength) * rateValue) / RATE_FACTOR; + uint256 dayYield = (_defineDailyBalance(possibleBalanceByDays, 0, periodLength) * rateValue) / RATE_FACTOR; if (dayYield > nextClaimDebit) { sumYield = dayYield - nextClaimDebit; } @@ -807,7 +810,7 @@ contract YieldStreamer is nextRateDay = yieldRates[++rateIndex].effectiveDay; } } - uint256 minBalance = _getMinimumInRange(possibleBalanceByDays, i, i + periodLength); + uint256 minBalance = _defineDailyBalance(possibleBalanceByDays, i, i + periodLength); dayYield = (minBalance * rateValue) / RATE_FACTOR; sumYield += dayYield; possibleBalanceByDays[i + periodLength] += sumYield; @@ -816,13 +819,16 @@ contract YieldStreamer is } /** - * @notice Searches a minimum value in an array for a specified range of indexes + * @notice Defines a value that will be used to calculate interest. + * + * Function searches a minimum value in an array for a specified range of indexes + * that should not be greater than the limit. * * @param array The array to search in * @param begIndex The index of the array from which the search begins, including that index * @param endIndex The index of the array at which the search ends, excluding that index */ - function _getMinimumInRange( + function _defineDailyBalance( uint256[] memory array, uint256 begIndex, uint256 endIndex @@ -834,6 +840,11 @@ contract YieldStreamer is min = value; } } + + if (min > MAX_DAILY_BALANCE_LIMIT) { + min = MAX_DAILY_BALANCE_LIMIT; + } + return min; } diff --git a/test/BalanceTracker.test.ts b/test/BalanceTracker.test.ts index 3bebd80..34a17ca 100644 --- a/test/BalanceTracker.test.ts +++ b/test/BalanceTracker.test.ts @@ -46,7 +46,7 @@ interface DailyBalancesRequest { dayTo: number; } -async function setUpFixture(func: any) { +async function setUpFixture(func: () => Promise): Promise { if (network.name === "hardhat") { return loadFixture(func); } else { @@ -168,7 +168,7 @@ function applyBalanceChange(context: TestContext, balanceChange: BalanceChange): if (lastRecord.day == newBalanceRecord.day) { newBalanceRecord = undefined; } else { - newBalanceRecord.index = lastRecord + 1; + newBalanceRecord.index = lastRecord.index + 1; balanceRecords.push(newBalanceRecord); } } @@ -314,7 +314,7 @@ describe("Contract 'BalanceTracker'", async () => { if (!newBalanceRecord1 && !newBalanceRecord2) { await expect(tx).not.to.emit(balanceTracker, "BalanceRecordCreated"); } else { - if (!!newBalanceRecord1) { + if (newBalanceRecord1) { await expect(tx) .to.emit(balanceTracker, "BalanceRecordCreated") .withArgs( @@ -323,7 +323,7 @@ describe("Contract 'BalanceTracker'", async () => { newBalanceRecord1.value ); } - if (!!newBalanceRecord2) { + if (newBalanceRecord2) { await expect(tx) .to.emit(balanceTracker, "BalanceRecordCreated") .withArgs( @@ -368,7 +368,7 @@ describe("Contract 'BalanceTracker'", async () => { describe("Function 'afterTokenTransfer()'", async () => { async function checkTokenTransfers(context: TestContext, transfers: TokenTransfer[]) { await executeTokenTransfers(context, transfers); - for (let address: string of context.balanceRecordsByAddressMap.keys()) { + for (const address of context.balanceRecordsByAddressMap.keys()) { const expectedBalanceRecords: BalanceRecord[] = context.balanceRecordsByAddressMap.get(address) ?? []; await checkBalanceRecordsForAccount(context.balanceTracker, address, expectedBalanceRecords); } diff --git a/test/YieldStreamer.test.ts b/test/YieldStreamer.test.ts index 68f3e3a..cfba1ca 100644 --- a/test/YieldStreamer.test.ts +++ b/test/YieldStreamer.test.ts @@ -10,16 +10,17 @@ const ZERO_ADDRESS = ethers.constants.AddressZero; const BIG_NUMBER_ZERO = ethers.constants.Zero; const BIG_NUMBER_MAX_UINT256 = ethers.constants.MaxUint256; const YIELD_STREAMER_INIT_TOKEN_BALANCE: BigNumber = BigNumber.from(1000_000_000_000); -const USER_CURRENT_TOKEN_BALANCE: BigNumber = BigNumber.from(1000_000_000_000); +const USER_CURRENT_TOKEN_BALANCE: BigNumber = BigNumber.from(100_000_000_000); const LOOK_BACK_PERIOD_LENGTH: number = 2; const LOOK_BACK_PERIOD_INDEX_ZERO = 0; const INITIAL_YIELD_RATE = 10000000000; // 1% const BALANCE_TRACKER_INIT_DAY = 100; const YIELD_STREAMER_INIT_DAY = BALANCE_TRACKER_INIT_DAY + LOOK_BACK_PERIOD_LENGTH - 1; const YIELD_RATE_INDEX_ZERO = 0; -const FEE_RATE: BigNumber = BigNumber.from(225000000000); +const FEE_RATE: BigNumber = BigNumber.from(0); const RATE_FACTOR: BigNumber = BigNumber.from(1000000000000); const MIN_CLAIM_AMOUNT: BigNumber = BigNumber.from(1000000); +const MAX_DAILY_BALANCE_LIMIT: BigNumber = BigNumber.from(200_000_000_000); const ROUNDING_COEF: BigNumber = BigNumber.from(10000); const BALANCE_TRACKER_ADDRESS_STUB = "0x0000000000000000000000000000000000000001"; const ZERO_GROUP_ID = ethers.utils.formatBytes32String(""); @@ -91,14 +92,14 @@ interface ClaimState { const balanceRecordsCase1: BalanceRecord[] = [ { day: BALANCE_TRACKER_INIT_DAY, value: BigNumber.from(0) }, - { day: BALANCE_TRACKER_INIT_DAY + 1, value: BigNumber.from(8000_000_000_000) }, - { day: BALANCE_TRACKER_INIT_DAY + 2, value: BigNumber.from(7000_000_000_000) }, - { day: BALANCE_TRACKER_INIT_DAY + 3, value: BigNumber.from(6000_000_000_000) }, - { day: BALANCE_TRACKER_INIT_DAY + 4, value: BigNumber.from(5000_000_000_000) }, - { day: BALANCE_TRACKER_INIT_DAY + 5, value: BigNumber.from(1000_000_000_000) }, - { day: BALANCE_TRACKER_INIT_DAY + 6, value: BigNumber.from(3000_000_000_000) }, - { day: BALANCE_TRACKER_INIT_DAY + 7, value: BigNumber.from(2000_000_000_000) }, - { day: BALANCE_TRACKER_INIT_DAY + 8, value: BigNumber.from(1000_000_000_000) } + { day: BALANCE_TRACKER_INIT_DAY + 1, value: BigNumber.from(80_000_000_000) }, + { day: BALANCE_TRACKER_INIT_DAY + 2, value: BigNumber.from(70_000_000_000) }, + { day: BALANCE_TRACKER_INIT_DAY + 3, value: BigNumber.from(60_000_000_000) }, + { day: BALANCE_TRACKER_INIT_DAY + 4, value: BigNumber.from(50_000_000_000) }, + { day: BALANCE_TRACKER_INIT_DAY + 5, value: BigNumber.from(10_000_000_000) }, + { day: BALANCE_TRACKER_INIT_DAY + 6, value: BigNumber.from(30_000_000_000) }, + { day: BALANCE_TRACKER_INIT_DAY + 7, value: BigNumber.from(20_000_000_000) }, + { day: BALANCE_TRACKER_INIT_DAY + 8, value: BigNumber.from(10_000_000_000) } ]; const yieldRateRecordCase1: YieldRateRecord = { @@ -146,12 +147,19 @@ function defineExpectedDailyBalances(balanceRecords: BalanceRecord[], dayFrom: n return dailyBalances; } -function min(bigNumber1: BigNumber, bigNumber2: BigNumber): BigNumber { +function defineDailyBalance(bigNumber1: BigNumber, bigNumber2: BigNumber): BigNumber { + let res: BigNumber; if (bigNumber1.lt(bigNumber2)) { - return bigNumber1; + res = bigNumber1; } else { - return bigNumber2; + res = bigNumber2; } + + if (res.gt(MAX_DAILY_BALANCE_LIMIT)) { + res = MAX_DAILY_BALANCE_LIMIT; + } + + return res; } function roundDown(value: BigNumber): BigNumber { @@ -199,7 +207,7 @@ function defineExpectedYieldByDays(yieldByDaysRequest: YieldByDaysRequest): BigN let sumYield: BigNumber = BIG_NUMBER_ZERO; for (let i = 0; i < len; ++i) { const yieldRate: BigNumber = defineYieldRate(yieldRateRecords, dayFrom + i); - const minBalance: BigNumber = balances.slice(i, lookBackPeriodLength + i).reduce(min); + const minBalance: BigNumber = balances.slice(i, lookBackPeriodLength + i).reduce(defineDailyBalance); const yieldValue: BigNumber = minBalance.mul(yieldRate).div(RATE_FACTOR); if (i == 0) { if (yieldValue.gt(claimDebit)) { @@ -360,7 +368,7 @@ function defineExpectedClaimAllResult(claimRequest: ClaimRequest): ClaimResult { return claimResult; } -function compareClaimPreviews(actualClaimPreviewResult: any, expectedClaimPreviewResult: ClaimResult) { +function compareClaimPreviews(actualClaimPreviewResult: ClaimResult, expectedClaimPreviewResult: ClaimResult) { expect(actualClaimPreviewResult.nextClaimDay.toString()).to.equal( expectedClaimPreviewResult.nextClaimDay.toString(), "The 'nextClaimDay' field is wrong" @@ -468,7 +476,7 @@ async function checkYieldRates( } } -async function setUpFixture(func: any) { +async function setUpFixture(func: () => Promise): Promise { if (network.name === "hardhat") { return loadFixture(func); } else { @@ -581,6 +589,14 @@ describe("Contract 'YieldStreamer'", async () => { }; } + describe("Test settings", async () => { + it("All daily balances in test balance records are less than the yield-generating limit", async () => { + for (const balanceRecord of balanceRecordsCase1) { + expect(balanceRecord.value).to.be.lessThan(MAX_DAILY_BALANCE_LIMIT); + } + }); + }); + describe("Function 'initialize()'", async () => { it("Configures the contract as expected", async () => { const context: TestContext = await setUpFixture(deployContracts); @@ -592,6 +608,7 @@ describe("Contract 'YieldStreamer'", async () => { expect(await yieldStreamer.FEE_RATE()).to.equal(FEE_RATE); expect(await yieldStreamer.MIN_CLAIM_AMOUNT()).to.equal(MIN_CLAIM_AMOUNT); expect(await yieldStreamer.ROUNDING_COEF()).to.equal(ROUNDING_COEF); + expect(await yieldStreamer.MAX_DAILY_BALANCE_LIMIT()).to.equal(MAX_DAILY_BALANCE_LIMIT); await checkLookBackPeriods(yieldStreamer, []); await checkYieldRates(yieldStreamer, [], ZERO_GROUP_ID); }); @@ -693,7 +710,7 @@ describe("Contract 'YieldStreamer'", async () => { }); describe("Function 'assignAccountGroup()'", async () => { - let users: any; + let users: string[]; before(async () => { users = [user.address, user2.address, user3.address]; }); @@ -1017,10 +1034,10 @@ describe("Contract 'YieldStreamer'", async () => { const recordIndex = Math.floor(oldExpectedYieldRateRecords.length / 2); newExpectedYieldRateRecord[recordIndex] = { effectiveDay: oldExpectedYieldRateRecords[recordIndex].effectiveDay + 1, - value: BigNumber.from(oldExpectedYieldRateRecords[recordIndex].value + 1) + value: BigNumber.from(oldExpectedYieldRateRecords[recordIndex].value.add(1)) }; - for (let expectedYieldRateRecord of oldExpectedYieldRateRecords) { + for (const expectedYieldRateRecord of oldExpectedYieldRateRecords) { await proveTx( context.yieldStreamer.configureYieldRate( ZERO_GROUP_ID, @@ -1057,7 +1074,7 @@ describe("Contract 'YieldStreamer'", async () => { const [oldExpectedYieldRateRecord] = defineExpectedYieldRateRecords(); const newExpectedYieldRateRecord: YieldRateRecord = { effectiveDay: oldExpectedYieldRateRecord.effectiveDay + 1, - value: BigNumber.from(oldExpectedYieldRateRecord.value + 1) + value: BigNumber.from(oldExpectedYieldRateRecord.value.add(1)) }; await proveTx( @@ -1137,7 +1154,7 @@ describe("Contract 'YieldStreamer'", async () => { const context: TestContext = await setUpFixture(deployContracts); const expectedYieldRateRecords: YieldRateRecord[] = defineExpectedYieldRateRecords(); - for (const expectedYieldRateRecord: YieldRateRecord of expectedYieldRateRecords) { + for (const expectedYieldRateRecord of expectedYieldRateRecords) { await proveTx( context.yieldStreamer.configureYieldRate( ZERO_GROUP_ID, @@ -1317,13 +1334,50 @@ describe("Contract 'YieldStreamer'", async () => { yieldRateRecords: [yieldRateRecordCase1], balanceRecords: balanceRecordsCase1 }; + + async function executeAndCheckClaimAll( + context: TestContext, + balanceRecords: BalanceRecord[] + ): Promise { + await proveTx(context.balanceTrackerMock.setBalanceRecords(user.address, balanceRecords)); + const actualClaimResult = await context.yieldStreamer.claimAllPreview(user.address); + claimRequest.balanceRecords = balanceRecords; + const expectedClaimResult = defineExpectedClaimResult(claimRequest); + compareClaimPreviews(actualClaimResult, expectedClaimResult); + return actualClaimResult; + } + it("Token balances are according to case 1", async () => { const context: TestContext = await setUpFixture(deployAndConfigureContracts); - await proveTx(context.balanceTrackerMock.setBalanceRecords(user.address, claimRequest.balanceRecords)); await proveTx(context.balanceTrackerMock.setDayAndTime(claimRequest.claimDay, claimRequest.claimTime)); - const expectedClaimResult: ClaimResult = defineExpectedClaimResult(claimRequest); - const actualClaimResult = await context.yieldStreamer.claimAllPreview(user.address); - compareClaimPreviews(actualClaimResult, expectedClaimResult); + await executeAndCheckClaimAll(context, claimRequest.balanceRecords); + }); + + it("Token min daily balance becomes larger than yield-generating daily balance limit", async () => { + claimRequest.claimDay = YIELD_STREAMER_INIT_DAY + 3; + const context: TestContext = await setUpFixture(deployAndConfigureContracts); + const balanceRecords: BalanceRecord[] = [ + { day: BALANCE_TRACKER_INIT_DAY, value: BigNumber.from(500_000_000_000) }, + { day: BALANCE_TRACKER_INIT_DAY + 1, value: BigNumber.from(150_000_000_000) }, + { day: BALANCE_TRACKER_INIT_DAY + 2, value: BigNumber.from(300_000_000_000) } + ]; + expect(balanceRecords[0].value).to.be.greaterThan(MAX_DAILY_BALANCE_LIMIT); + expect(balanceRecords[1].value).to.be.lessThan(MAX_DAILY_BALANCE_LIMIT); + expect(balanceRecords[2].value).to.be.greaterThan(MAX_DAILY_BALANCE_LIMIT); + + await proveTx(context.balanceTrackerMock.setDayAndTime(claimRequest.claimDay, claimRequest.claimTime)); + const actualClaimResult1 = await executeAndCheckClaimAll(context, balanceRecords); + + balanceRecords[1].value = balanceRecords[1].value.add(BigNumber.from(50_000_000_000)); + expect(balanceRecords[1].value).to.be.equal(MAX_DAILY_BALANCE_LIMIT); + const actualClaimResult2 = await executeAndCheckClaimAll(context, balanceRecords); + + balanceRecords[1].value = balanceRecords[1].value.add(BigNumber.from(50_000_000_000)); + expect(balanceRecords[1].value).to.be.greaterThan(MAX_DAILY_BALANCE_LIMIT); + const actualClaimResult3 = await executeAndCheckClaimAll(context, balanceRecords); + + expect(actualClaimResult1.yield).to.be.lessThan(actualClaimResult2.yield); + compareClaimPreviews(actualClaimResult2, actualClaimResult3); }); }); }); @@ -1885,7 +1939,7 @@ describe("Contract 'YieldStreamer'", async () => { BALANCE_TRACKER_INIT_DAY - 1, BALANCE_TRACKER_INIT_DAY ) - ).to.reverted + ).to.reverted; }); it("The 'to' day is prior the 'from' day", async () => { @@ -1896,7 +1950,7 @@ describe("Contract 'YieldStreamer'", async () => { YIELD_STREAMER_INIT_DAY, YIELD_STREAMER_INIT_DAY - 1 ) - ).to.reverted + ).to.reverted; }); it("There are no balance records", async () => { @@ -1907,7 +1961,7 @@ describe("Contract 'YieldStreamer'", async () => { YIELD_STREAMER_INIT_DAY, YIELD_STREAMER_INIT_DAY ) - ).to.reverted + ).to.reverted; }); }); }); diff --git a/test/base/BlocklistableUpgradeable.test.ts b/test/base/BlocklistableUpgradeable.test.ts index 0a1b81b..3c93c4b 100644 --- a/test/base/BlocklistableUpgradeable.test.ts +++ b/test/base/BlocklistableUpgradeable.test.ts @@ -5,7 +5,7 @@ import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-wit import { loadFixture } from "@nomicfoundation/hardhat-network-helpers"; import { proveTx } from "../../test-utils/eth"; -async function setUpFixture(func: any) { +async function setUpFixture(func: () => Promise): Promise { if (network.name === "hardhat") { return loadFixture(func); } else { diff --git a/test/base/PausableExtUpgradeable.test.ts b/test/base/PausableExtUpgradeable.test.ts index 633352d..a1b4250 100644 --- a/test/base/PausableExtUpgradeable.test.ts +++ b/test/base/PausableExtUpgradeable.test.ts @@ -5,7 +5,7 @@ import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-wit import { loadFixture } from "@nomicfoundation/hardhat-network-helpers"; import { proveTx } from "../../test-utils/eth"; -async function setUpFixture(func: any) { +async function setUpFixture(func: () => Promise): Promise { if (network.name === "hardhat") { return loadFixture(func); } else { diff --git a/test/base/RescuableUpgradeable.test.ts b/test/base/RescuableUpgradeable.test.ts index f155a73..ef50a2b 100644 --- a/test/base/RescuableUpgradeable.test.ts +++ b/test/base/RescuableUpgradeable.test.ts @@ -5,7 +5,7 @@ import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-wit import { loadFixture } from "@nomicfoundation/hardhat-network-helpers"; import { proveTx } from "../../test-utils/eth"; -async function setUpFixture(func: any) { +async function setUpFixture(func: () => Promise): Promise { if (network.name === "hardhat") { return loadFixture(func); } else { diff --git a/test/mocks/ERC20HookMock.test.ts b/test/mocks/ERC20HookMock.test.ts index 24a4f66..ff9609b 100644 --- a/test/mocks/ERC20HookMock.test.ts +++ b/test/mocks/ERC20HookMock.test.ts @@ -5,7 +5,7 @@ import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-wit import { loadFixture } from "@nomicfoundation/hardhat-network-helpers"; import { proveTx } from "../../test-utils/eth"; -async function setUpFixture(func: any) { +async function setUpFixture(func: () => Promise): Promise { if (network.name === "hardhat") { return loadFixture(func); } else { diff --git a/test/mocks/ERC20TokenMock.test.ts b/test/mocks/ERC20TokenMock.test.ts index 6a7288c..4f20b0c 100644 --- a/test/mocks/ERC20TokenMock.test.ts +++ b/test/mocks/ERC20TokenMock.test.ts @@ -5,7 +5,7 @@ import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-wit import { loadFixture } from "@nomicfoundation/hardhat-network-helpers"; import { proveTx } from "../../test-utils/eth"; -async function setUpFixture(func: any) { +async function setUpFixture(func: () => Promise): Promise { if (network.name === "hardhat") { return loadFixture(func); } else { @@ -24,11 +24,10 @@ describe("Contract 'ERC20TokenMock'", async () => { const REVERT_MESSAGE_INITIALIZABLE_CONTRACT_IS_NOT_INITIALIZING = "Initializable: contract is not initializing"; let tokenFactory: ContractFactory; - let deployer: SignerWithAddress; let user: SignerWithAddress; before(async () => { - [deployer, user] = await ethers.getSigners(); + [, user] = await ethers.getSigners(); tokenFactory = await ethers.getContractFactory("ERC20TokenMock"); });