Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Limit the daily balance that is taken into account when calculating yield #4

Merged
merged 4 commits into from
Mar 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 16 additions & 5 deletions contracts/YieldStreamer.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand All @@ -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
Expand Down Expand Up @@ -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;
}
Expand All @@ -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;
Expand All @@ -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
Expand All @@ -834,6 +840,11 @@ contract YieldStreamer is
min = value;
}
}

if (min > MAX_DAILY_BALANCE_LIMIT) {
min = MAX_DAILY_BALANCE_LIMIT;
}

return min;
}

Expand Down
10 changes: 5 additions & 5 deletions test/BalanceTracker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ interface DailyBalancesRequest {
dayTo: number;
}

async function setUpFixture(func: any) {
async function setUpFixture<T>(func: () => Promise<T>): Promise<T> {
if (network.name === "hardhat") {
return loadFixture(func);
} else {
Expand Down Expand Up @@ -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);
}
}
Expand Down Expand Up @@ -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(
Expand All @@ -323,7 +323,7 @@ describe("Contract 'BalanceTracker'", async () => {
newBalanceRecord1.value
);
}
if (!!newBalanceRecord2) {
if (newBalanceRecord2) {
await expect(tx)
.to.emit(balanceTracker, "BalanceRecordCreated")
.withArgs(
Expand Down Expand Up @@ -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);
}
Expand Down
110 changes: 82 additions & 28 deletions test/YieldStreamer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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("");
Expand Down Expand Up @@ -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 = {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)) {
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -468,7 +476,7 @@ async function checkYieldRates(
}
}

async function setUpFixture(func: any) {
async function setUpFixture<T>(func: () => Promise<T>): Promise<T> {
if (network.name === "hardhat") {
return loadFixture(func);
} else {
Expand Down Expand Up @@ -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);
Expand All @@ -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);
});
Expand Down Expand Up @@ -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];
});
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -1317,13 +1334,50 @@ describe("Contract 'YieldStreamer'", async () => {
yieldRateRecords: [yieldRateRecordCase1],
balanceRecords: balanceRecordsCase1
};

async function executeAndCheckClaimAll(
context: TestContext,
balanceRecords: BalanceRecord[]
): Promise<ClaimResult> {
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);
});
});
});
Expand Down Expand Up @@ -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 () => {
Expand All @@ -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 () => {
Expand All @@ -1907,7 +1961,7 @@ describe("Contract 'YieldStreamer'", async () => {
YIELD_STREAMER_INIT_DAY,
YIELD_STREAMER_INIT_DAY
)
).to.reverted
).to.reverted;
});
});
});
Expand Down
2 changes: 1 addition & 1 deletion test/base/BlocklistableUpgradeable.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>(func: () => Promise<T>): Promise<T> {
if (network.name === "hardhat") {
return loadFixture(func);
} else {
Expand Down
2 changes: 1 addition & 1 deletion test/base/PausableExtUpgradeable.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>(func: () => Promise<T>): Promise<T> {
if (network.name === "hardhat") {
return loadFixture(func);
} else {
Expand Down
2 changes: 1 addition & 1 deletion test/base/RescuableUpgradeable.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>(func: () => Promise<T>): Promise<T> {
if (network.name === "hardhat") {
return loadFixture(func);
} else {
Expand Down
2 changes: 1 addition & 1 deletion test/mocks/ERC20HookMock.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>(func: () => Promise<T>): Promise<T> {
if (network.name === "hardhat") {
return loadFixture(func);
} else {
Expand Down
Loading
Loading