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

Fix bug with reward for an inactive node #63

Merged
merged 2 commits into from
Dec 29, 2023
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
2 changes: 1 addition & 1 deletion .eslintrc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,6 @@ overrides:
max-lines-per-function: ["off"]
max-statements: ["off"]
no-await-in-loop: ["off"]
no-magic-numbers: ["error", { "ignore": [0, 1] }]
no-magic-numbers: ["error", { "ignore": [0, 0n, 1, 1n, 2, 2n] }]


84 changes: 53 additions & 31 deletions contracts/Paymaster.sol
Original file line number Diff line number Diff line change
Expand Up @@ -131,43 +131,51 @@

event SchainAdded(
string name,
SchainHash hash
SchainHash hash,
Timestamp timestamp
);

event SchainRemoved(
string name,
SchainHash hash
SchainHash hash,
Timestamp timestamp
);

event ValidatorAdded(
ValidatorId id,
address validatorAddress
address validatorAddress,
Timestamp timestamp
);

event ValidatorMarkedAsRemoved(
ValidatorId id
ValidatorId id,
Timestamp timestamp
);

event ValidatorRemoved(
ValidatorId id
ValidatorId id,
Timestamp timestamp
);

event ActiveNodesNumberChanged(
ValidatorId validator,
uint256 oldNumber,
uint256 newNumber
uint256 newNumber,
Timestamp timestamp
);

event MaxReplenishmentPeriodChanged(
Months valueInMonths
);

event SchainPriceSet(
USD priceInUsd
USD priceInUsd,
Timestamp timestamp
);

event SklPriceSet(
USD priceInUsd
USD priceInUsd,
Timestamp timestamp
);

event SklPriceLagSet(
Expand All @@ -186,14 +194,16 @@
SchainHash hash,
Months period,
SKL amount,
Timestamp newLifetime
Timestamp newLifetime,
Timestamp timestamp
);

event RewardClaimed(
ValidatorId validator,
address receiver,
SKL amount,
Timestamp until
Timestamp until,
Timestamp timestamp
);

event VersionSet(
Expand Down Expand Up @@ -226,22 +236,25 @@
revert ValidatorAddressAlreadyExists(validatorAddress);
}

Timestamp currentTimestamp = _getTimestamp();

_validatorData.validators[id].id = id;
delete _validatorData.validators[id].nodesAmount;
delete _validatorData.validators[id].activeNodesAmount;
_validatorData.validators[id].claimedUntil = _getTimestamp();
_validatorData.validators[id].claimedUntil = currentTimestamp;
_validatorData.validators[id].validatorAddress = validatorAddress;
_validatorData.validators[id].nodesHistory.clear();

emit ValidatorAdded(id, validatorAddress);
emit ValidatorAdded(id, validatorAddress, currentTimestamp);
}

function removeValidator(ValidatorId id) external override restricted {
Timestamp currentTimestamp = _getTimestamp();
Validator storage validator = _getValidator(id);
setNodesAmount(id, 0);
validator.deleted = _getTimestamp();
validator.deleted = currentTimestamp;

emit ValidatorMarkedAsRemoved(id);
emit ValidatorMarkedAsRemoved(id, currentTimestamp);
}

function setActiveNodes(ValidatorId validatorId, uint256 amount) external override restricted {
Expand All @@ -261,14 +274,15 @@
function setSchainPrice(USD price) external override restricted {
schainPricePerMonth = price;

emit SchainPriceSet(price);
emit SchainPriceSet(price, _getTimestamp());
}

function setSklPrice(USD price) external override restricted {
Timestamp currentTimestamp = _getTimestamp();
oneSklPrice = price;
sklPriceTimestamp = _getTimestamp();
sklPriceTimestamp = currentTimestamp;

emit SklPriceSet(price);
emit SklPriceSet(price, currentTimestamp);
}

function setAllowedSklPriceLag(Seconds lagSeconds) external override restricted {
Expand Down Expand Up @@ -325,7 +339,13 @@
}
schain.paidUntil = start.add(duration);

emit SchainPaid(schainHash, duration, cost, schain.paidUntil);
emit SchainPaid({
hash: schainHash,
period: duration,
amount: cost,
newLifetime: schain.paidUntil,
timestamp: current
});

_pullTokens(cost);
}
Expand Down Expand Up @@ -446,19 +466,21 @@

function _claimFor(ValidatorId validatorId, address to) private {
Validator storage validator = _getValidator(validatorId);
Timestamp claimUntil = DateTimeUtils.firstDayOfMonth(_getTimestamp());
Timestamp currentTimestamp = _getTimestamp();
Timestamp claimUntil = DateTimeUtils.firstDayOfMonth(currentTimestamp);
_totalRewards.process(claimUntil);

SKL rewards = _getRewardAmount(validator, claimUntil);
validator.claimedUntil = claimUntil;
validator.firstUnpaidDebt = debtsEnd;

emit RewardClaimed(
validatorId,
to,
rewards,
claimUntil
);
emit RewardClaimed({
validator: validatorId,
receiver: to,
amount: rewards,
until: claimUntil,
timestamp: currentTimestamp
});

if (!skaleToken.transfer(to, SKL.unwrap(rewards))) {
revert TransferFailure();
Expand All @@ -470,14 +492,14 @@
if (!_schainHashes.add(SchainHash.unwrap(schain.hash))) {
revert SchainAddingError(schain.hash);
}
emit SchainAdded(schain.name, schain.hash);
emit SchainAdded(schain.name, schain.hash, _getTimestamp());
}

function _removeSchain(Schain storage schain) private {
if(!_schainHashes.remove(SchainHash.unwrap(schain.hash))) {
revert SchainDeletionError(schain.hash);
}
emit SchainRemoved(schain.name, schain.hash);
emit SchainRemoved(schain.name, schain.hash, _getTimestamp());
delete schains[schain.hash];
}

Expand All @@ -489,7 +511,7 @@
revert ValidatorDeletionError(validator.id);
}

emit ValidatorRemoved(validator.id);
emit ValidatorRemoved(validator.id, _getTimestamp());

Check warning on line 514 in contracts/Paymaster.sol

View check run for this annotation

Codecov / codecov/patch

contracts/Paymaster.sol#L514

Added line #L514 was not covered by tests

validator.id = ValidatorId.wrap(0);
delete validator.nodesAmount;
Expand All @@ -507,7 +529,7 @@
uint256 totalNodes = _totalNodesHistory.getLastValue();
_totalNodesHistory.add(currentTime, totalNodes + newAmount - oldAmount);

emit ActiveNodesNumberChanged(validator.id, oldAmount, newAmount);
emit ActiveNodesNumberChanged(validator.id, oldAmount, newAmount, currentTime);
}

function _addDebt(Payment memory debt, DebtId id) private {
Expand Down Expand Up @@ -699,12 +721,12 @@

cursor = nextCursor;
while (totalNodesHistoryIterator.hasNext() && totalNodesHistoryIterator.nextTimestamp <= cursor) {
if (totalNodesHistoryIterator.step()) {
if (totalNodesHistoryIterator.step(_totalNodesHistory)) {
totalNodes = _totalNodesHistory.getValue(totalNodesHistoryIterator);
}
}
while (nodesHistoryIterator.hasNext() && nodesHistoryIterator.nextTimestamp <= cursor) {
if (nodesHistoryIterator.step()) {
if (nodesHistoryIterator.step(validator.nodesHistory)) {
activeNodes = validator.nodesHistory.getValue(nodesHistoryIterator);
}
}
Expand Down
7 changes: 6 additions & 1 deletion contracts/Sequence.sol
Original file line number Diff line number Diff line change
Expand Up @@ -155,13 +155,18 @@ library SequenceLibrary {

// Library internal functions should not have leading underscore
// solhint-disable-next-line private-vars-leading-underscore
function step(Iterator memory iterator) internal pure returns (bool success) {
function step(Iterator memory iterator, Sequence storage sequence) internal view returns (bool success) {
success = hasNext(iterator);
if (iterator.idIndex == _BEFORE_FIRST_ELEMENT) {
iterator.idIndex = 0;
} else {
iterator.idIndex += 1;
}
if (iterator.idIndex + 1 < iterator.sequenceSize) {
iterator.nextTimestamp = _getNodeByIndex(sequence, iterator.idIndex + 1).timestamp;
} else {
iterator.nextTimestamp = Timestamp.wrap(type(uint256).max);
}
}

// Library internal functions should not have leading underscore
Expand Down
54 changes: 52 additions & 2 deletions test/Paymaster.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { MS_PER_SEC, currentTime, nextMonth, skipMonth, skipTimeToSpecificDate } from "./tools/time";
import { MS_PER_SEC, currentTime, monthBegin, nextMonth, skipMonth, skipTime, skipTimeToSpecificDate } from "./tools/time";
import { Paymaster, PaymasterAccessManager, Token } from "../typechain-types";
import {
deployAccessManager,
Expand Down Expand Up @@ -357,11 +357,13 @@ describe("Paymaster", () => {
// Month D

const validatorId = validators.length - 1;
const validatorNodesAmount = BigInt(validators.length);
// It's not accurate value but it's very small
const removedValidatorsReward = ethers.parseEther("1");
const monthCReward = ethers.parseEther("0.01");
const estimated = await paymaster.getRewardAmount(validatorId);
// The validator was removed in the beginning of month C. Receive reward only for month B.
const calculated = monthBReward * (await paymaster.getNodesNumber(validatorId)) / totalNodesNumber;
const calculated = monthBReward * validatorNodesAmount / totalNodesNumber + monthCReward;
expect(estimated).be.lessThanOrEqual(calculated);
expect(calculated - estimated).be.lessThanOrEqual(removedValidatorsReward);
await expect(paymaster.connect(validators[validatorId]).claim(await validators[validatorId].getAddress()))
Expand All @@ -371,5 +373,53 @@ describe("Paymaster", () => {
estimated
);
});

it("should not pay reward for inactive nodes", async () => {
const { paymaster, schains, token, validators } = await loadFixture(addSchainAndValidatorFixture);
const tokensPerMonth = (await paymaster.schainPricePerMonth()) * ethers.parseEther("1") / (await paymaster.oneSklPrice());
const twoYears = 24;
const validatorId = 0;

let totalNodesNumber = BigInt(0);
for (let index = 0; index < validators.length; index += 1) {
totalNodesNumber += await paymaster.getNodesNumber(index);
}

// Month A

await paymaster.connect(user).pay(schains[0], twoYears);

await skipMonth();

// Month B

expect(await paymaster.getRewardAmount(validatorId)).to.be.equal(0);

// Fast forward to the middle of the month

const currentTimestamp = await currentTime();
await skipTime((nextMonth(currentTimestamp) + monthBegin(currentTimestamp)) / 2 - currentTimestamp);
const secondsInMonthB = BigInt(nextMonth(currentTimestamp) - monthBegin(currentTimestamp));

await paymaster.setActiveNodes(validatorId, 0);
const blacklistedTimestamp = (await paymaster.queryFilter(paymaster.filters.ActiveNodesNumberChanged, "latest"))[0].args.timestamp;

await skipMonth();

// Month C

const monthBReward = tokensPerMonth;
const rewardRate = monthBReward / secondsInMonthB;
const estimated = await paymaster.getRewardAmount(validatorId);
const calculated = rewardRate * (blacklistedTimestamp - BigInt(monthBegin(blacklistedTimestamp))) / totalNodesNumber;
expect(estimated).be.lessThanOrEqual(calculated);
expect(calculated - estimated).be.lessThanOrEqual(precision);
await expect(paymaster.connect(validators[validatorId]).claim(await validators[validatorId].getAddress()))
.to.changeTokenBalance(
token,
validators[validatorId],
estimated
);
})
});
});
25 changes: 24 additions & 1 deletion test/Timeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,14 @@ import { loadFixture } from "@nomicfoundation/hardhat-toolbox/network-helpers";


describe("Timeline", () => {
const deployTimelineFixture = async () => await ethers.deployContract("TimelineTester")
const precision = ethers.parseEther("0.000000000001");
const abs = (value: bigint) => {
if(value < 0n) {
return -value;
}
return value;
};
const deployTimelineFixture = async () => await ethers.deployContract("TimelineTester");

describe("basic tests", () => {
it("should calculate an entire segment", async () => {
Expand All @@ -29,6 +36,22 @@ describe("Timeline", () => {
await timeline.process(1);
expect(await timeline.getSum(testFrom, testTo)).to.be.equal(testAnswer);
});

it("should calculate partial segments", async () => {
const timeline = await loadFixture(deployTimelineFixture);
const segment = {
from: 1704067200,
to: 1706745600,
value: ethers.parseEther("2500")
};
const previousMonth = 1703846562;
const middleOfTheMonth = 1705406400;

await timeline.add(segment.from, segment.to, segment.value);
const result = await timeline.getSum(previousMonth, middleOfTheMonth);
const correct = segment.value / 2n;
expect(abs(result - correct)).to.be.lessThan(precision);
});
});

it("random test", async () => {
Expand Down
8 changes: 8 additions & 0 deletions test/tools/time.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,14 @@ export const nextMonth = (timestamp: number | bigint) => {
return new Date(Date.UTC(year, month, 1)).getTime() / MS_PER_SEC;
}

export const monthBegin = (timestamp: number | bigint) => {
const timestampNumber = Number(timestamp) * MS_PER_SEC;
const date = new Date(timestampNumber);
const month = date.getMonth();
const year = date.getFullYear();
return new Date(Date.UTC(year, month, 1)).getTime() / MS_PER_SEC;
}

export const skipMonth = async () => {
const timestamp = await currentTime();
await skipTime(nextMonth(timestamp) - timestamp);
Expand Down
Loading