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

Rate limiter epoch day #70

Merged
merged 3 commits into from
Aug 30, 2024
Merged
Show file tree
Hide file tree
Changes from 2 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
57 changes: 48 additions & 9 deletions contracts/ServiceNodeRewards.sol
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ contract ServiceNodeRewards is Initializable, Ownable2StepUpgradeable, PausableU
uint256 public totalNodes;
uint256 public blsNonSignerThreshold;
uint256 public blsNonSignerThresholdMax;
uint256 public claimThreshold;
uint256 public periodicClaims;
uint256 public epochDay;
uint256 public signatureExpiry;

bytes32 public proofOfPossessionTag;
Expand Down Expand Up @@ -75,6 +78,9 @@ contract ServiceNodeRewards is Initializable, Ownable2StepUpgradeable, PausableU
liquidateTag = buildTag("BLS_SIG_TRYANDINCREMENT_LIQUIDATE");
hashToG2Tag = buildTag("BLS_SIG_HASH_TO_FIELD_TAG");
signatureExpiry = 10 minutes;
claimThreshold = 2_000_000 * 1e9;
periodicClaims = 0;
epochDay = 0;
darcys22 marked this conversation as resolved.
Show resolved Hide resolved

designatedToken = IERC20(token_);
foundationPool = IERC20(foundationPool_);
Expand Down Expand Up @@ -135,6 +141,7 @@ contract ServiceNodeRewards is Initializable, Ownable2StepUpgradeable, PausableU
event RewardsBalanceUpdated(address indexed recipientAddress, uint256 amount, uint256 previousBalance);
event RewardsClaimed(address indexed recipientAddress, uint256 amount);
event BLSNonSignerThresholdMaxUpdated(uint256 newMax);
event ClaimThresholdUpdated(uint256 newThreshold);
event ServiceNodeLiquidated(uint64 indexed serviceNodeID, address operator, BN256G1.G1Point pubkey);
event ServiceNodeRemoval(
uint64 indexed serviceNodeID,
Expand All @@ -150,6 +157,7 @@ contract ServiceNodeRewards is Initializable, Ownable2StepUpgradeable, PausableU
error BLSPubkeyAlreadyExists(uint64 serviceNodeID);
error BLSPubkeyDoesNotMatch(uint64 serviceNodeID, BN256G1.G1Point pubkey);
error CallerNotContributor(uint64 serviceNodeID, address contributor);
error ClaimThresholdExceeded();
error ContractAlreadyStarted();
error ContractNotStarted();
error ContributionTotalMismatch(uint256 required, uint256 provided);
Expand All @@ -163,6 +171,7 @@ contract ServiceNodeRewards is Initializable, Ownable2StepUpgradeable, PausableU
error InvalidBLSProofOfPossession();
error LeaveRequestTooEarly(uint64 serviceNodeID, uint256 timestamp, uint256 currenttime);
error MaxContributorsExceeded();
error MaxClaimExceeded();
error MaxPubkeyAggregationsExceeded();
error NullPublicKey();
error NullAddress();
Expand Down Expand Up @@ -229,22 +238,41 @@ contract ServiceNodeRewards is Initializable, Ownable2StepUpgradeable, PausableU
}

/// @dev Internal function to handle reward claims. Will transfer the
/// available rewards worth of our token to claimingAddress
/// requested amount of our token to claimingAddress, up to the available rewards
/// @param claimingAddress The address claiming the rewards.
function _claimRewards(address claimingAddress) internal {
/// @param amount The amount of rewards to claim.
function _claimRewards(address claimingAddress, uint256 amount) internal {
uint256 claimedRewards = recipients[claimingAddress].claimed;
uint256 totalRewards = recipients[claimingAddress].rewards;
uint256 amountToRedeem = totalRewards - claimedRewards;

recipients[claimingAddress].claimed = totalRewards;
emit RewardsClaimed(claimingAddress, amountToRedeem);
uint256 maxAmount = totalRewards - claimedRewards;
if (amount > maxAmount)
revert MaxClaimExceeded();

uint256 _epochDay = block.timestamp / 86400;
if (_epochDay > epochDay) {
epochDay = _epochDay;
periodicClaims = 0;
}
periodicClaims += amount;
if (periodicClaims > claimThreshold) revert ClaimThresholdExceeded();

SafeERC20.safeTransfer(designatedToken, claimingAddress, amountToRedeem);
recipients[claimingAddress].claimed += amount;
emit RewardsClaimed(claimingAddress, amount);
SafeERC20.safeTransfer(designatedToken, claimingAddress, amount);
}

/// @notice Claim the rewards due for the active wallet invoking the claim.
/// @notice Claim all available rewards for the active wallet invoking the claim.
function claimRewards() public {
_claimRewards(msg.sender);
uint256 claimedRewards = recipients[msg.sender].claimed;
uint256 totalRewards = recipients[msg.sender].rewards;
uint256 amountToRedeem = totalRewards - claimedRewards;
_claimRewards(msg.sender, amountToRedeem);
darcys22 marked this conversation as resolved.
Show resolved Hide resolved
}

/// @notice Claim a specific amount of rewards for the active wallet invoking the claim.
/// @param amount The amount of rewards to claim.
function claimRewards(uint256 amount) public {
_claimRewards(msg.sender, amount);
}

/// MANAGING BLS PUBLIC KEY LIST
Expand Down Expand Up @@ -768,6 +796,17 @@ contract ServiceNodeRewards is Initializable, Ownable2StepUpgradeable, PausableU
emit BLSNonSignerThresholdMaxUpdated(newMax);
}

/// @notice Max Claim amount to use in the check
/// before allowing the user to claim. If the claimed amount over 24 hours
/// exceeds this then the claim will fail
/// @param newMax The new maximum claim threshold
function setClaimThreshold(uint256 newMax) public onlyOwner {
if (newMax <= 0)
revert PositiveNumberRequirement();
claimThreshold = newMax;
emit ClaimThresholdUpdated(newMax);
}

//////////////////////////////////////////////////////////////
// //
// Non-state-changing functions //
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ class ServiceNodeRewardsContract {
ethyl::Transaction removeBLSPublicKeyWithSignature(const std::string& pubkey, const uint64_t timestamp, const std::string& sig, const std::vector<uint64_t>& non_signer_indices);
ethyl::Transaction updateRewardsBalance(const std::string& address, const uint64_t amount, const std::string& sig, const std::vector<uint64_t>& non_signer_indices);
ethyl::Transaction claimRewards();
ethyl::Transaction claimRewards(uint64_t amount);
ethyl::Transaction start();

/// Address of the ERC20 contract that must be set to the address of the
Expand Down
8 changes: 8 additions & 0 deletions test/cpp/src/service_node_rewards_contract.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,14 @@ ethyl::Transaction ServiceNodeRewardsContract::claimRewards() {
return tx;
}

ethyl::Transaction ServiceNodeRewardsContract::claimRewards(uint64_t amount) {
ethyl::Transaction tx(contractAddress, 0, 3000000);
std::string functionSelector = ethyl::utils::toEthFunctionSignature("claimRewards(uint256)");
std::string amount_padded = ethyl::utils::padTo32Bytes(ethyl::utils::decimalToHex(amount), ethyl::utils::PaddingDirection::LEFT);
tx.data = functionSelector + amount_padded;
return tx;
}

ethyl::Transaction ServiceNodeRewardsContract::start() {
ethyl::Transaction tx(contractAddress, 0, 3000000);
std::string functionSelector = ethyl::utils::toEthFunctionSignature("start()");
Expand Down
228 changes: 228 additions & 0 deletions test/cpp/test/src/rewards_contract.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -504,6 +504,234 @@ TEST_CASE( "Rewards Contract", "[ethereum]" ) {
resetContractToSnapshot();
}

SECTION( "Successfully claim the rewards specifying the exact amount" ) {
REQUIRE(rewards_contract.serviceNodesLength() == 0);
ServiceNodeList snl(3);
for(auto& node : snl.nodes) {
const auto pubkey = node.getPublicKeyHex();
const auto proof_of_possession = node.proofOfPossession(config.CHAIN_ID, contract_address, senderAddress, "pubkey");
tx = rewards_contract.addBLSPublicKey(pubkey, proof_of_possession, "pubkey", "sig", 0);
signer.sendTransaction(tx, seckey);
}
REQUIRE(rewards_contract.serviceNodesLength() == 3);
std::vector<unsigned char> secondseckey = ethyl::utils::fromHexString(std::string(config.ADDITIONAL_PRIVATE_KEY1));
const std::string recipientAddress = signer.secretKeyToAddressString(secondseckey);
const uint64_t recipientAmount = 1;
const auto signers = snl.randomSigners(snl.nodes.size() - 1);
const auto sig = snl.updateRewardsBalance(recipientAddress, recipientAmount, config.CHAIN_ID, contract_address, signers);
const auto non_signers = snl.findNonSigners(signers);
tx = rewards_contract.updateRewardsBalance(recipientAddress, recipientAmount, sig, non_signers);
hash = signer.sendTransaction(tx, seckey);
uint64_t amount = erc20_contract.balanceOf(recipientAddress);
REQUIRE(amount == 0);

tx = rewards_contract.claimRewards(recipientAmount);
hash = signer.sendTransaction(tx, secondseckey);
REQUIRE(hash != "");
REQUIRE(defaultProvider.transactionSuccessful(hash));

amount = erc20_contract.balanceOf(recipientAddress);
REQUIRE(amount == recipientAmount);

auto recipient = rewards_contract.viewRecipientData(recipientAddress);
REQUIRE(recipient.rewards == recipientAmount);
REQUIRE(recipient.claimed == amount);

verifyEVMServiceNodesAgainstCPPState(snl);
resetContractToSnapshot();
}

SECTION( "Successfully claim the rewards specifying a lower amount then maximum" ) {
REQUIRE(rewards_contract.serviceNodesLength() == 0);
ServiceNodeList snl(3);
for(auto& node : snl.nodes) {
const auto pubkey = node.getPublicKeyHex();
const auto proof_of_possession = node.proofOfPossession(config.CHAIN_ID, contract_address, senderAddress, "pubkey");
tx = rewards_contract.addBLSPublicKey(pubkey, proof_of_possession, "pubkey", "sig", 0);
signer.sendTransaction(tx, seckey);
}
REQUIRE(rewards_contract.serviceNodesLength() == 3);
std::vector<unsigned char> secondseckey = ethyl::utils::fromHexString(std::string(config.ADDITIONAL_PRIVATE_KEY1));
const std::string recipientAddress = signer.secretKeyToAddressString(secondseckey);
const uint64_t recipientAmount = 2;
const uint64_t lowerAmount = 1;
const auto signers = snl.randomSigners(snl.nodes.size() - 1);
const auto sig = snl.updateRewardsBalance(recipientAddress, recipientAmount, config.CHAIN_ID, contract_address, signers);
const auto non_signers = snl.findNonSigners(signers);
tx = rewards_contract.updateRewardsBalance(recipientAddress, recipientAmount, sig, non_signers);
hash = signer.sendTransaction(tx, seckey);
uint64_t amount = erc20_contract.balanceOf(recipientAddress);
REQUIRE(amount == 0);

tx = rewards_contract.claimRewards(lowerAmount);
hash = signer.sendTransaction(tx, secondseckey);
REQUIRE(hash != "");
REQUIRE(defaultProvider.transactionSuccessful(hash));

amount = erc20_contract.balanceOf(recipientAddress);
REQUIRE(amount == lowerAmount);

auto recipient = rewards_contract.viewRecipientData(recipientAddress);
REQUIRE(recipient.rewards == recipientAmount);
REQUIRE(recipient.claimed == amount);

verifyEVMServiceNodesAgainstCPPState(snl);
resetContractToSnapshot();
}

SECTION( "Fail to claim the rewards specifying a higher amount then maximum" ) {
REQUIRE(rewards_contract.serviceNodesLength() == 0);
ServiceNodeList snl(3);
for(auto& node : snl.nodes) {
const auto pubkey = node.getPublicKeyHex();
const auto proof_of_possession = node.proofOfPossession(config.CHAIN_ID, contract_address, senderAddress, "pubkey");
tx = rewards_contract.addBLSPublicKey(pubkey, proof_of_possession, "pubkey", "sig", 0);
signer.sendTransaction(tx, seckey);
}
REQUIRE(rewards_contract.serviceNodesLength() == 3);
std::vector<unsigned char> secondseckey = ethyl::utils::fromHexString(std::string(config.ADDITIONAL_PRIVATE_KEY1));
const std::string recipientAddress = signer.secretKeyToAddressString(secondseckey);
const uint64_t recipientAmount = 2;
const uint64_t higherAmount = 3;
const auto signers = snl.randomSigners(snl.nodes.size() - 1);
const auto sig = snl.updateRewardsBalance(recipientAddress, recipientAmount, config.CHAIN_ID, contract_address, signers);
const auto non_signers = snl.findNonSigners(signers);
tx = rewards_contract.updateRewardsBalance(recipientAddress, recipientAmount, sig, non_signers);
hash = signer.sendTransaction(tx, seckey);
uint64_t amount = erc20_contract.balanceOf(recipientAddress);
REQUIRE(amount == 0);

tx = rewards_contract.claimRewards(higherAmount);
REQUIRE_THROWS(signer.sendTransaction(tx, secondseckey));

verifyEVMServiceNodesAgainstCPPState(snl);
resetContractToSnapshot();
}

SECTION( "Claim too many rewards in a single transaction and trigger rate limiter" ) {
REQUIRE(rewards_contract.serviceNodesLength() == 0);
ServiceNodeList snl(3);
for(auto& node : snl.nodes) {
const auto pubkey = node.getPublicKeyHex();
const auto proof_of_possession = node.proofOfPossession(config.CHAIN_ID, contract_address, senderAddress, "pubkey");
tx = rewards_contract.addBLSPublicKey(pubkey, proof_of_possession, "pubkey", "sig", 0);
signer.sendTransaction(tx, seckey);
}
REQUIRE(rewards_contract.serviceNodesLength() == 3);
std::vector<unsigned char> secondseckey = ethyl::utils::fromHexString(std::string(config.ADDITIONAL_PRIVATE_KEY1));
const std::string recipientAddress = signer.secretKeyToAddressString(secondseckey);
const uint64_t recipientAmount = 3000000000000000;
const auto signers = snl.randomSigners(snl.nodes.size());
const auto sig = snl.updateRewardsBalance(recipientAddress, recipientAmount, config.CHAIN_ID, contract_address, signers);
const auto non_signers = snl.findNonSigners(signers);
tx = rewards_contract.updateRewardsBalance(recipientAddress, recipientAmount, sig, non_signers);
hash = signer.sendTransaction(tx, seckey);
uint64_t amount = erc20_contract.balanceOf(recipientAddress);
REQUIRE(amount == 0);

tx = rewards_contract.claimRewards();
REQUIRE_THROWS(signer.sendTransaction(tx, secondseckey));

verifyEVMServiceNodesAgainstCPPState(snl);
resetContractToSnapshot();
}

SECTION( "Claim too much rewards over two transactions and trigger rate limiter" ) {
REQUIRE(rewards_contract.serviceNodesLength() == 0);
ServiceNodeList snl(3);
for(auto& node : snl.nodes) {
const auto pubkey = node.getPublicKeyHex();
const auto proof_of_possession = node.proofOfPossession(config.CHAIN_ID, contract_address, senderAddress, "pubkey");
tx = rewards_contract.addBLSPublicKey(pubkey, proof_of_possession, "pubkey", "sig", 0);
signer.sendTransaction(tx, seckey);
}
REQUIRE(rewards_contract.serviceNodesLength() == 3);
std::vector<unsigned char> secondseckey = ethyl::utils::fromHexString(std::string(config.ADDITIONAL_PRIVATE_KEY1));
const std::string recipientAddress = signer.secretKeyToAddressString(secondseckey);
const uint64_t recipientAmount = 1500000000000000;
const auto signers = snl.randomSigners(snl.nodes.size());
const auto sig = snl.updateRewardsBalance(recipientAddress, recipientAmount, config.CHAIN_ID, contract_address, signers);
const auto non_signers = snl.findNonSigners(signers);
tx = rewards_contract.updateRewardsBalance(recipientAddress, recipientAmount, sig, non_signers);
hash = signer.sendTransaction(tx, seckey);
uint64_t amount = erc20_contract.balanceOf(recipientAddress);
REQUIRE(amount == 0);

const uint64_t secondRecipientAmount = 3000000000000000;
tx = erc20_contract.transfer(contract_address, secondRecipientAmount);
hash = signer.sendTransaction(tx, seckey);
REQUIRE(hash != "");
REQUIRE(defaultProvider.transactionSuccessful(hash));

tx = rewards_contract.claimRewards();
hash = signer.sendTransaction(tx, secondseckey);
REQUIRE(hash != "");
REQUIRE(defaultProvider.transactionSuccessful(hash));
amount = erc20_contract.balanceOf(recipientAddress);
REQUIRE(amount == recipientAmount);

const auto secondSig = snl.updateRewardsBalance(recipientAddress, secondRecipientAmount, config.CHAIN_ID, contract_address, signers);
tx = rewards_contract.updateRewardsBalance(recipientAddress, secondRecipientAmount, secondSig, non_signers);
hash = signer.sendTransaction(tx, secondseckey);
// Fast forward 1 days
defaultProvider.evm_increaseTime(std::chrono::hours(1 * 24));

tx = rewards_contract.claimRewards();
hash = signer.sendTransaction(tx, secondseckey);
REQUIRE(hash != "");
REQUIRE(defaultProvider.transactionSuccessful(hash));
amount = erc20_contract.balanceOf(recipientAddress);
REQUIRE(amount == secondRecipientAmount);

verifyEVMServiceNodesAgainstCPPState(snl);
resetContractToSnapshot();
}

SECTION( "Claim too much rewards but over the waiting time should succeed" ) {
REQUIRE(rewards_contract.serviceNodesLength() == 0);
ServiceNodeList snl(3);
for(auto& node : snl.nodes) {
const auto pubkey = node.getPublicKeyHex();
const auto proof_of_possession = node.proofOfPossession(config.CHAIN_ID, contract_address, senderAddress, "pubkey");
tx = rewards_contract.addBLSPublicKey(pubkey, proof_of_possession, "pubkey", "sig", 0);
signer.sendTransaction(tx, seckey);
}
REQUIRE(rewards_contract.serviceNodesLength() == 3);
std::vector<unsigned char> secondseckey = ethyl::utils::fromHexString(std::string(config.ADDITIONAL_PRIVATE_KEY1));
const std::string recipientAddress = signer.secretKeyToAddressString(secondseckey);
const uint64_t recipientAmount = 1500000000000000;
const auto signers = snl.randomSigners(snl.nodes.size());
const auto sig = snl.updateRewardsBalance(recipientAddress, recipientAmount, config.CHAIN_ID, contract_address, signers);
const auto non_signers = snl.findNonSigners(signers);
tx = rewards_contract.updateRewardsBalance(recipientAddress, recipientAmount, sig, non_signers);
hash = signer.sendTransaction(tx, seckey);
uint64_t amount = erc20_contract.balanceOf(recipientAddress);
REQUIRE(amount == 0);

const uint64_t secondRecipientAmount = 3000000000000000;
tx = erc20_contract.transfer(contract_address, secondRecipientAmount);
hash = signer.sendTransaction(tx, seckey);
REQUIRE(hash != "");
REQUIRE(defaultProvider.transactionSuccessful(hash));

tx = rewards_contract.claimRewards();
hash = signer.sendTransaction(tx, secondseckey);
REQUIRE(hash != "");
REQUIRE(defaultProvider.transactionSuccessful(hash));
amount = erc20_contract.balanceOf(recipientAddress);
REQUIRE(amount == recipientAmount);

const auto secondSig = snl.updateRewardsBalance(recipientAddress, secondRecipientAmount, config.CHAIN_ID, contract_address, signers);
tx = rewards_contract.updateRewardsBalance(recipientAddress, secondRecipientAmount, secondSig, non_signers);
hash = signer.sendTransaction(tx, secondseckey);

tx = rewards_contract.claimRewards();
REQUIRE_THROWS(signer.sendTransaction(tx, secondseckey));

verifyEVMServiceNodesAgainstCPPState(snl);
resetContractToSnapshot();
}

SECTION( "Add LOTS of public keys to the smart contract and update the rewards of one of them and successfully claim the rewards" ) {
SUCCEED("Complex test case runs too long on github worker");
return;
Expand Down