diff --git a/contracts/interfaces/ISFC.sol b/contracts/interfaces/ISFC.sol index aae9bd9..1a5a304 100644 --- a/contracts/interfaces/ISFC.sol +++ b/contracts/interfaces/ISFC.sol @@ -48,12 +48,12 @@ interface ISFC { view returns ( uint256 status, - uint256 deactivatedTime, - uint256 deactivatedEpoch, uint256 receivedStake, + address auth, uint256 createdEpoch, uint256 createdTime, - address auth + uint256 deactivatedTime, + uint256 deactivatedEpoch ); function getValidatorID(address) external view returns (uint256); @@ -162,7 +162,6 @@ interface ISFC { uint256 sealedEpoch, uint256 _totalSupply, address nodeDriver, - address lib, address consts, address _owner ) external; diff --git a/contracts/sfc/NetworkInitializer.sol b/contracts/sfc/NetworkInitializer.sol index fe08295..ce34811 100644 --- a/contracts/sfc/NetworkInitializer.sol +++ b/contracts/sfc/NetworkInitializer.sol @@ -12,7 +12,6 @@ contract NetworkInitializer { uint256 sealedEpoch, uint256 totalSupply, address payable _sfc, - address _lib, address _auth, address _driver, address _evmWriter, @@ -37,6 +36,6 @@ contract NetworkInitializer { consts.updateGasPriceBalancingCounterweight(3600); consts.transferOwnership(_owner); - ISFC(_sfc).initialize(sealedEpoch, totalSupply, _auth, _lib, address(consts), _owner); + ISFC(_sfc).initialize(sealedEpoch, totalSupply, _auth, address(consts), _owner); } } diff --git a/contracts/sfc/SFC.sol b/contracts/sfc/SFC.sol index 6255c0f..55a740a 100644 --- a/contracts/sfc/SFC.sol +++ b/contracts/sfc/SFC.sol @@ -2,134 +2,240 @@ pragma solidity ^0.8.9; import {Ownable} from "../ownership/Ownable.sol"; +import {Initializable} from "../common/Initializable.sol"; import {Decimal} from "../common/Decimal.sol"; -import {SFCBase} from "./SFCBase.sol"; import {NodeDriverAuth} from "./NodeDriverAuth.sol"; import {ConstantsManager} from "./ConstantsManager.sol"; import {GP} from "./GasPriceConstants.sol"; import {Version} from "../version/Version.sol"; /** - * @dev Stakers contract defines data structure and methods for validators / validators. + * @dev SFC contract for Sonic network. */ -contract SFC is SFCBase, Version { - function _delegate(address implementation) internal { - assembly { - // Copy msg.data. We take full control of memory in this inline assembly - // block because it will not return to Solidity code. We overwrite the - // Solidity scratch pad at memory position 0. - calldatacopy(0, 0, calldatasize()) - - // Call the implementation. - // out and outsize are 0 because we don't know the size yet. - let result := delegatecall(gas(), implementation, 0, calldatasize(), 0, 0) - - // Copy the returned data. - returndatacopy(0, 0, returndatasize()) - - switch result - // delegatecall returns 0 on error. - case 0 { - revert(0, returndatasize()) - } - default { - return(0, returndatasize()) - } - } - } +contract SFC is Initializable, Ownable, Version { + uint256 internal constant OK_STATUS = 0; + uint256 internal constant WITHDRAWN_BIT = 1; + uint256 internal constant OFFLINE_BIT = 1 << 3; + uint256 internal constant DOUBLESIGN_BIT = 1 << 7; + uint256 internal constant CHEATER_MASK = DOUBLESIGN_BIT; - // solhint-disable-next-line no-complex-fallback - fallback() external payable { - if (msg.data.length == 0) { - revert TransfersNotAllowed(); - } - _delegate(libAddress); + /** + * @dev The staking for validation + */ + struct Validator { + uint256 status; + uint256 receivedStake; // from all delegators (weight of the validator) + address auth; // self-stake delegator + uint256 createdEpoch; + uint256 createdTime; + uint256 deactivatedTime; + uint256 deactivatedEpoch; } - receive() external payable { - revert TransfersNotAllowed(); - } + NodeDriverAuth internal node; - /* - Getters - */ + // last sealed epoch (currentEpoch - 1) + uint256 public currentSealedEpoch; + mapping(uint256 => Validator) public getValidator; + mapping(address => uint256) public getValidatorID; + mapping(uint256 => bytes) public getValidatorPubkey; - function getEpochValidatorIDs(uint256 epoch) public view returns (uint256[] memory) { - return getEpochSnapshot[epoch].validatorIDs; - } + uint256 public lastValidatorID; - function getEpochReceivedStake(uint256 epoch, uint256 validatorID) public view returns (uint256) { - return getEpochSnapshot[epoch].receivedStake[validatorID]; - } + // total stake of all validators - includes slashed/offline validators + uint256 public totalStake; - function getEpochAccumulatedRewardPerToken(uint256 epoch, uint256 validatorID) public view returns (uint256) { - return getEpochSnapshot[epoch].accumulatedRewardPerToken[validatorID]; - } + // total stake of active (OK_STATUS) validators (total weight) + uint256 public totalActiveStake; - function getEpochAccumulatedUptime(uint256 epoch, uint256 validatorID) public view returns (uint256) { - return getEpochSnapshot[epoch].accumulatedUptime[validatorID]; - } + // delegator => validator ID => stashed rewards (to be claimed/restaked) + mapping(address => mapping(uint256 => uint256)) internal _rewardsStash; - function getEpochAccumulatedOriginatedTxsFee(uint256 epoch, uint256 validatorID) public view returns (uint256) { - return getEpochSnapshot[epoch].accumulatedOriginatedTxsFee[validatorID]; - } + // delegator => validator ID => last epoch number for which were rewards stashed + mapping(address => mapping(uint256 => uint256)) public stashedRewardsUntilEpoch; - function getEpochOfflineTime(uint256 epoch, uint256 validatorID) public view returns (uint256) { - return getEpochSnapshot[epoch].offlineTime[validatorID]; + struct WithdrawalRequest { + uint256 epoch; // epoch where undelegated + uint256 time; // when undelegated + uint256 amount; } - function getEpochOfflineBlocks(uint256 epoch, uint256 validatorID) public view returns (uint256) { - return getEpochSnapshot[epoch].offlineBlocks[validatorID]; + // delegator => validator ID => withdrawal ID => withdrawal request + mapping(address => mapping(uint256 => mapping(uint256 => WithdrawalRequest))) public getWithdrawalRequest; + + // delegator => validator ID => current stake (locked+unlocked) + mapping(address => mapping(uint256 => uint256)) public getStake; + + struct EpochSnapshot { + // validator ID => validator weight in the epoch + mapping(uint256 => uint256) receivedStake; + // validator ID => accumulated ( delegatorsReward * 1e18 / receivedStake ) + mapping(uint256 => uint256) accumulatedRewardPerToken; + // validator ID => accumulated online time + mapping(uint256 => uint256) accumulatedUptime; + // validator ID => gas fees from txs originated by the validator + mapping(uint256 => uint256) accumulatedOriginatedTxsFee; + mapping(uint256 => uint256) offlineTime; + mapping(uint256 => uint256) offlineBlocks; + uint256[] validatorIDs; + uint256 endTime; + uint256 endBlock; + uint256 epochFee; // gas fees from txs in the epoch + uint256 baseRewardPerSecond; // the base reward to divide among validators for each second of the epoch + uint256 totalStake; // total weight of all validators + uint256 totalSupply; // total supply of native tokens } - function getEpochEndBlock(uint256 epoch) public view returns (uint256) { - return getEpochSnapshot[epoch].endBlock; + // the total supply of native tokens in the chain + uint256 public totalSupply; + + // epoch id => epoch snapshot + mapping(uint256 => EpochSnapshot) public getEpochSnapshot; + + // validator ID -> slashing refund ratio (allows to withdraw slashed stake) + mapping(uint256 => uint256) public slashingRefundRatio; + + // the minimal gas price calculated for the current epoch + uint256 public minGasPrice; + + // the treasure contract (receives unlock penalties and a part of epoch fees) + address public treasuryAddress; + + ConstantsManager internal c; + + // the governance contract (to recalculate votes when the stake changes) + address public voteBookAddress; + + // validator ID => amount of pubkey updates + mapping(uint256 => uint256) internal validatorPubkeyChanges; + + // keccak256(pubkey bytes) => validator ID (prevents using the same key by multiple validators) + mapping(bytes32 => uint256) internal pubkeyHashToValidatorID; + + // address authorized to initiate redirection + address public redirectionAuthorizer; + + // delegator => withdrawals receiver + mapping(address => address) public getRedirectionRequest; + + // delegator => withdrawals receiver + mapping(address => address) public getRedirection; + + struct SealEpochRewardsCtx { + uint256[] baseRewardWeights; + uint256 totalBaseRewardWeight; + uint256[] txRewardWeights; + uint256 totalTxRewardWeight; + uint256 epochFee; } - function rewardsStash(address delegator, uint256 validatorID) public view returns (uint256) { - return _rewardsStash[delegator][validatorID]; + // auth + error NotDriverAuth(); + error NotAuthorized(); + + // addresses + error ZeroAddress(); + error SameAddress(); + + // values + error ZeroAmount(); + error ZeroRewards(); + + // pubkeys + error PubkeyUsedByOtherValidator(); + error MalformedPubkey(); + error PubkeyNotChanged(); + error EmptyPubkey(); + error TooManyPubkeyUpdates(); + + // redirections + error AlreadyRedirected(); + error SameRedirectionAuthorizer(); + error Redirected(); + + // validators + error ValidatorNotExists(); + error ValidatorExists(); + error ValidatorNotActive(); + error ValidatorDelegationLimitExceeded(); + error WrongValidatorStatus(); + + // requests + error RequestExists(); + error RequestNotExists(); + + // transfers + error TransfersNotAllowed(); + error TransferFailed(); + + // updater + error SFCAlreadyUpdated(); + error SFCWrongVersion(); + error SFCGovAlreadyUpdated(); + error SFCWrongGovVersion(); + + // governance + error GovVotesRecountFailed(); + + // staking + error InsufficientSelfStake(); + error NotEnoughTimePassed(); + error NotEnoughEpochsPassed(); + error StakeIsFullySlashed(); + + // stashing + error NothingToStash(); + + // slashing + error ValidatorNotSlashed(); + error RefundRatioTooHigh(); + + event DeactivatedValidator(uint256 indexed validatorID, uint256 deactivatedEpoch, uint256 deactivatedTime); + event ChangedValidatorStatus(uint256 indexed validatorID, uint256 status); + event CreatedValidator( + uint256 indexed validatorID, + address indexed auth, + uint256 createdEpoch, + uint256 createdTime + ); + event Delegated(address indexed delegator, uint256 indexed toValidatorID, uint256 amount); + event Undelegated(address indexed delegator, uint256 indexed toValidatorID, uint256 indexed wrID, uint256 amount); + event Withdrawn(address indexed delegator, uint256 indexed toValidatorID, uint256 indexed wrID, uint256 amount); + event ClaimedRewards(address indexed delegator, uint256 indexed toValidatorID, uint256 rewards); + event RestakedRewards(address indexed delegator, uint256 indexed toValidatorID, uint256 rewards); + event BurntFTM(uint256 amount); + event UpdatedSlashingRefundRatio(uint256 indexed validatorID, uint256 refundRatio); + event RefundedSlashedLegacyDelegation(address indexed delegator, uint256 indexed validatorID, uint256 amount); + event AnnouncedRedirection(address indexed from, address indexed to); + + modifier onlyDriver() { + if (!isNode(msg.sender)) { + revert NotDriverAuth(); + } + _; } /* - Constructor - */ - + * Initializer + */ function initialize( uint256 sealedEpoch, uint256 _totalSupply, address nodeDriver, - address lib, address _c, address owner ) external initializer { Ownable.initialize(owner); currentSealedEpoch = sealedEpoch; node = NodeDriverAuth(nodeDriver); - libAddress = lib; c = ConstantsManager(_c); totalSupply = _totalSupply; minGasPrice = GP.initialMinGasPrice(); getEpochSnapshot[sealedEpoch].endTime = _now(); } - function updateLibAddress(address v) external onlyOwner { - libAddress = v; - } - - function updateTreasuryAddress(address v) external onlyOwner { - treasuryAddress = v; - } - - function updateConstsAddress(address v) external onlyOwner { - c = ConstantsManager(v); - } - - function constsAddress() external view returns (address) { - return address(c); - } - - function updateVoteBookAddress(address v) external onlyOwner { - voteBookAddress = v; + receive() external payable { + revert TransfersNotAllowed(); } function migrateValidatorPubkeyUniquenessFlag(uint256 start, uint256 end) external { @@ -175,8 +281,6 @@ contract SFC is SFCBase, Version { redirectionAuthorizer = v; } - event AnnouncedRedirection(address indexed from, address indexed to); - function announceRedirection(address to) external { emit AnnouncedRedirection(msg.sender, to); } @@ -206,6 +310,466 @@ contract SFC is SFCBase, Version { getRedirectionRequest[from] = address(0); } + function sealEpoch( + uint256[] calldata offlineTime, + uint256[] calldata offlineBlocks, + uint256[] calldata uptimes, + uint256[] calldata originatedTxsFee, + uint256 epochGas + ) external onlyDriver { + EpochSnapshot storage snapshot = getEpochSnapshot[currentEpoch()]; + uint256[] memory validatorIDs = snapshot.validatorIDs; + + _sealEpochOffline(snapshot, validatorIDs, offlineTime, offlineBlocks); + { + EpochSnapshot storage prevSnapshot = getEpochSnapshot[currentSealedEpoch]; + uint256 epochDuration = 1; + if (_now() > prevSnapshot.endTime) { + epochDuration = _now() - prevSnapshot.endTime; + } + _sealEpochRewards(epochDuration, snapshot, prevSnapshot, validatorIDs, uptimes, originatedTxsFee); + _sealEpochMinGasPrice(epochDuration, epochGas); + } + + currentSealedEpoch = currentEpoch(); + snapshot.endTime = _now(); + snapshot.endBlock = block.number; + snapshot.baseRewardPerSecond = c.baseRewardPerSecond(); + snapshot.totalSupply = totalSupply; + } + + function sealEpochValidators(uint256[] calldata nextValidatorIDs) external onlyDriver { + EpochSnapshot storage snapshot = getEpochSnapshot[currentEpoch()]; + // fill data for the next snapshot + for (uint256 i = 0; i < nextValidatorIDs.length; i++) { + uint256 validatorID = nextValidatorIDs[i]; + uint256 receivedStake = getValidator[validatorID].receivedStake; + snapshot.receivedStake[validatorID] = receivedStake; + snapshot.totalStake = snapshot.totalStake + receivedStake; + } + snapshot.validatorIDs = nextValidatorIDs; + node.updateMinGasPrice(minGasPrice); + } + + function setGenesisValidator( + address auth, + uint256 validatorID, + bytes calldata pubkey, + uint256 status, + uint256 createdEpoch, + uint256 createdTime, + uint256 deactivatedEpoch, + uint256 deactivatedTime + ) external onlyDriver { + _rawCreateValidator( + auth, + validatorID, + pubkey, + status, + createdEpoch, + createdTime, + deactivatedEpoch, + deactivatedTime + ); + if (validatorID > lastValidatorID) { + lastValidatorID = validatorID; + } + } + + function setGenesisDelegation(address delegator, uint256 toValidatorID, uint256 stake) external onlyDriver { + _rawDelegate(delegator, toValidatorID, stake, false); + _mintNativeToken(stake); + } + + function createValidator(bytes calldata pubkey) external payable { + if (msg.value < c.minSelfStake()) { + revert InsufficientSelfStake(); + } + if (pubkey.length == 0) { + revert EmptyPubkey(); + } + if (pubkeyHashToValidatorID[keccak256(pubkey)] != 0) { + revert PubkeyUsedByOtherValidator(); + } + _createValidator(msg.sender, pubkey); + _delegate(msg.sender, lastValidatorID, msg.value); + } + + function updateSlashingRefundRatio(uint256 validatorID, uint256 refundRatio) external onlyOwner { + if (!isSlashed(validatorID)) { + revert ValidatorNotSlashed(); + } + if (refundRatio > Decimal.unit()) { + revert RefundRatioTooHigh(); + } + slashingRefundRatio[validatorID] = refundRatio; + emit UpdatedSlashingRefundRatio(validatorID, refundRatio); + } + + function recountVotes(address delegator, address validatorAuth, bool strict, uint256 gas) external { + // solhint-disable-next-line avoid-low-level-calls + (bool success, ) = voteBookAddress.call{gas: gas}( + abi.encodeWithSignature("recountVotes(address,address)", delegator, validatorAuth) + ); + if (!success && strict) { + revert GovVotesRecountFailed(); + } + } + + function delegate(uint256 toValidatorID) external payable { + _delegate(msg.sender, toValidatorID, msg.value); + } + + function withdraw(uint256 toValidatorID, uint256 wrID) public { + _withdraw(msg.sender, toValidatorID, wrID, _receiverOf(msg.sender)); + } + + function deactivateValidator(uint256 validatorID, uint256 status) external onlyDriver { + if (status == OK_STATUS) { + revert WrongValidatorStatus(); + } + + _setValidatorDeactivated(validatorID, status); + _syncValidator(validatorID, false); + address validatorAddr = getValidator[validatorID].auth; + _recountVotes(validatorAddr, validatorAddr, false); + } + + function stashRewards(address delegator, uint256 toValidatorID) external { + if (!_stashRewards(delegator, toValidatorID)) { + revert NothingToStash(); + } + } + + // burnFTM allows SFC to burn an arbitrary amount of FTM tokens + function burnFTM(uint256 amount) external onlyOwner { + _burnFTM(amount); + } + + function updateTreasuryAddress(address v) external onlyOwner { + treasuryAddress = v; + } + + function updateConstsAddress(address v) external onlyOwner { + c = ConstantsManager(v); + } + + function updateVoteBookAddress(address v) external onlyOwner { + voteBookAddress = v; + } + + function constsAddress() external view returns (address) { + return address(c); + } + + function claimRewards(uint256 toValidatorID) public { + address delegator = msg.sender; + uint256 rewards = _claimRewards(delegator, toValidatorID); + // It's important that we transfer after erasing (protection against Re-Entrancy) + (bool sent, ) = _receiverOf(delegator).call{value: rewards}(""); + if (!sent) { + revert TransferFailed(); + } + + emit ClaimedRewards(delegator, toValidatorID, rewards); + } + + function rewardsStash(address delegator, uint256 validatorID) public view returns (uint256) { + return _rewardsStash[delegator][validatorID]; + } + + function undelegate(uint256 toValidatorID, uint256 wrID, uint256 amount) public { + address delegator = msg.sender; + + _stashRewards(delegator, toValidatorID); + + if (amount == 0) { + revert ZeroAmount(); + } + + if (getWithdrawalRequest[delegator][toValidatorID][wrID].amount != 0) { + revert RequestExists(); + } + + _rawUndelegate(delegator, toValidatorID, amount, true, false, true); + + getWithdrawalRequest[delegator][toValidatorID][wrID].amount = amount; + getWithdrawalRequest[delegator][toValidatorID][wrID].epoch = currentEpoch(); + getWithdrawalRequest[delegator][toValidatorID][wrID].time = _now(); + + _syncValidator(toValidatorID, false); + + emit Undelegated(delegator, toValidatorID, wrID, amount); + } + + function restakeRewards(uint256 toValidatorID) public { + address delegator = msg.sender; + uint256 rewards = _claimRewards(delegator, toValidatorID); + + _delegate(delegator, toValidatorID, rewards); + emit RestakedRewards(delegator, toValidatorID, rewards); + } + + function currentEpoch() public view returns (uint256) { + return currentSealedEpoch + 1; + } + + function getSelfStake(uint256 validatorID) public view returns (uint256) { + return getStake[getValidator[validatorID].auth][validatorID]; + } + + function getEpochValidatorIDs(uint256 epoch) public view returns (uint256[] memory) { + return getEpochSnapshot[epoch].validatorIDs; + } + + function getEpochReceivedStake(uint256 epoch, uint256 validatorID) public view returns (uint256) { + return getEpochSnapshot[epoch].receivedStake[validatorID]; + } + + function getEpochAccumulatedRewardPerToken(uint256 epoch, uint256 validatorID) public view returns (uint256) { + return getEpochSnapshot[epoch].accumulatedRewardPerToken[validatorID]; + } + + function getEpochAccumulatedUptime(uint256 epoch, uint256 validatorID) public view returns (uint256) { + return getEpochSnapshot[epoch].accumulatedUptime[validatorID]; + } + + function getEpochAccumulatedOriginatedTxsFee(uint256 epoch, uint256 validatorID) public view returns (uint256) { + return getEpochSnapshot[epoch].accumulatedOriginatedTxsFee[validatorID]; + } + + function getEpochOfflineTime(uint256 epoch, uint256 validatorID) public view returns (uint256) { + return getEpochSnapshot[epoch].offlineTime[validatorID]; + } + + function getEpochOfflineBlocks(uint256 epoch, uint256 validatorID) public view returns (uint256) { + return getEpochSnapshot[epoch].offlineBlocks[validatorID]; + } + + function getEpochEndBlock(uint256 epoch) public view returns (uint256) { + return getEpochSnapshot[epoch].endBlock; + } + + function isSlashed(uint256 validatorID) public view returns (bool) { + return getValidator[validatorID].status & CHEATER_MASK != 0; + } + + function pendingRewards(address delegator, uint256 toValidatorID) public view returns (uint256) { + uint256 reward = _newRewards(delegator, toValidatorID); + return _rewardsStash[delegator][toValidatorID] + reward; + } + + function _checkDelegatedStakeLimit(uint256 validatorID) internal view returns (bool) { + return + getValidator[validatorID].receivedStake <= + (getSelfStake(validatorID) * c.maxDelegatedRatio()) / Decimal.unit(); + } + + function isNode(address addr) internal view virtual returns (bool) { + return addr == address(node); + } + + function _delegate(address delegator, uint256 toValidatorID, uint256 amount) internal { + if (!_validatorExists(toValidatorID)) { + revert ValidatorNotExists(); + } + if (getValidator[toValidatorID].status != OK_STATUS) { + revert ValidatorNotActive(); + } + _rawDelegate(delegator, toValidatorID, amount, true); + if (!_checkDelegatedStakeLimit(toValidatorID)) { + revert ValidatorDelegationLimitExceeded(); + } + } + + function _rawDelegate(address delegator, uint256 toValidatorID, uint256 amount, bool strict) internal { + if (amount == 0) { + revert ZeroAmount(); + } + + _stashRewards(delegator, toValidatorID); + + getStake[delegator][toValidatorID] = getStake[delegator][toValidatorID] + amount; + uint256 origStake = getValidator[toValidatorID].receivedStake; + getValidator[toValidatorID].receivedStake = origStake + amount; + totalStake = totalStake + amount; + if (getValidator[toValidatorID].status == OK_STATUS) { + totalActiveStake = totalActiveStake + amount; + } + + _syncValidator(toValidatorID, origStake == 0); + + emit Delegated(delegator, toValidatorID, amount); + + _recountVotes(delegator, getValidator[toValidatorID].auth, strict); + } + + function _rawUndelegate( + address delegator, + uint256 toValidatorID, + uint256 amount, + bool strict, + bool forceful, + bool checkDelegatedStake + ) internal { + getStake[delegator][toValidatorID] -= amount; + getValidator[toValidatorID].receivedStake = getValidator[toValidatorID].receivedStake - amount; + totalStake = totalStake - amount; + if (getValidator[toValidatorID].status == OK_STATUS) { + totalActiveStake = totalActiveStake - amount; + } + + uint256 selfStakeAfterwards = getSelfStake(toValidatorID); + if (selfStakeAfterwards != 0 && getValidator[toValidatorID].status == OK_STATUS) { + if (!(selfStakeAfterwards >= c.minSelfStake())) { + if (forceful) { + revert InsufficientSelfStake(); + } else { + _setValidatorDeactivated(toValidatorID, WITHDRAWN_BIT); + } + } + if (checkDelegatedStake && !_checkDelegatedStakeLimit(toValidatorID)) { + revert ValidatorDelegationLimitExceeded(); + } + } else { + _setValidatorDeactivated(toValidatorID, WITHDRAWN_BIT); + } + + _recountVotes(delegator, getValidator[toValidatorID].auth, strict); + } + + function getSlashingPenalty( + uint256 amount, + bool isCheater, + uint256 refundRatio + ) internal pure returns (uint256 penalty) { + if (!isCheater || refundRatio >= Decimal.unit()) { + return 0; + } + // round penalty upwards (ceiling) to prevent dust amount attacks + penalty = (amount * (Decimal.unit() - refundRatio)) / Decimal.unit() + 1; + if (penalty > amount) { + return amount; + } + return penalty; + } + + function _withdraw(address delegator, uint256 toValidatorID, uint256 wrID, address payable receiver) private { + WithdrawalRequest memory request = getWithdrawalRequest[delegator][toValidatorID][wrID]; + if (request.epoch == 0) { + revert RequestNotExists(); + } + + uint256 requestTime = request.time; + uint256 requestEpoch = request.epoch; + if ( + getValidator[toValidatorID].deactivatedTime != 0 && + getValidator[toValidatorID].deactivatedTime < requestTime + ) { + requestTime = getValidator[toValidatorID].deactivatedTime; + requestEpoch = getValidator[toValidatorID].deactivatedEpoch; + } + + if (_now() < requestTime + c.withdrawalPeriodTime()) { + revert NotEnoughTimePassed(); + } + + if (currentEpoch() < requestEpoch + c.withdrawalPeriodEpochs()) { + revert NotEnoughEpochsPassed(); + } + + uint256 amount = getWithdrawalRequest[delegator][toValidatorID][wrID].amount; + bool isCheater = isSlashed(toValidatorID); + uint256 penalty = getSlashingPenalty(amount, isCheater, slashingRefundRatio[toValidatorID]); + delete getWithdrawalRequest[delegator][toValidatorID][wrID]; + + if (amount <= penalty) { + revert StakeIsFullySlashed(); + } + // It's important that we transfer after erasing (protection against Re-Entrancy) + (bool sent, ) = receiver.call{value: amount - penalty}(""); + if (!sent) { + revert TransferFailed(); + } + _burnFTM(penalty); + + emit Withdrawn(delegator, toValidatorID, wrID, amount); + } + + function _highestPayableEpoch(uint256 validatorID) internal view returns (uint256) { + if (getValidator[validatorID].deactivatedEpoch != 0) { + if (currentSealedEpoch < getValidator[validatorID].deactivatedEpoch) { + return currentSealedEpoch; + } + return getValidator[validatorID].deactivatedEpoch; + } + return currentSealedEpoch; + } + + function _newRewards(address delegator, uint256 toValidatorID) internal view returns (uint256) { + uint256 stashedUntil = stashedRewardsUntilEpoch[delegator][toValidatorID]; + uint256 payableUntil = _highestPayableEpoch(toValidatorID); + uint256 wholeStake = getStake[delegator][toValidatorID]; + uint256 fullReward = _newRewardsOf(wholeStake, toValidatorID, stashedUntil, payableUntil); + return fullReward; + } + + function _newRewardsOf( + uint256 stakeAmount, + uint256 toValidatorID, + uint256 fromEpoch, + uint256 toEpoch + ) internal view returns (uint256) { + if (fromEpoch >= toEpoch) { + return 0; + } + uint256 stashedRate = getEpochSnapshot[fromEpoch].accumulatedRewardPerToken[toValidatorID]; + uint256 currentRate = getEpochSnapshot[toEpoch].accumulatedRewardPerToken[toValidatorID]; + return ((currentRate - stashedRate) * stakeAmount) / Decimal.unit(); + } + + function _stashRewards(address delegator, uint256 toValidatorID) internal returns (bool updated) { + uint256 nonStashedReward = _newRewards(delegator, toValidatorID); + stashedRewardsUntilEpoch[delegator][toValidatorID] = _highestPayableEpoch(toValidatorID); + _rewardsStash[delegator][toValidatorID] += nonStashedReward; + return nonStashedReward != 0; + } + + function _claimRewards(address delegator, uint256 toValidatorID) internal returns (uint256) { + _stashRewards(delegator, toValidatorID); + uint256 rewards = _rewardsStash[delegator][toValidatorID]; + if (rewards == 0) { + revert ZeroRewards(); + } + delete _rewardsStash[delegator][toValidatorID]; + // It's important that we mint after erasing (protection against Re-Entrancy) + _mintNativeToken(rewards); + return rewards; + } + + function _burnFTM(uint256 amount) internal { + if (amount != 0) { + payable(address(0)).transfer(amount); + emit BurntFTM(amount); + } + } + + function epochEndTime(uint256 epoch) internal view returns (uint256) { + return getEpochSnapshot[epoch].endTime; + } + + function _redirected(address addr) internal view returns (bool) { + return getRedirection[addr] != address(0); + } + + function _receiverOf(address addr) internal view returns (address payable) { + address to = getRedirection[addr]; + if (to == address(0)) { + return payable(address(uint160(addr))); + } + return payable(address(uint160(to))); + } + /* Epoch callbacks */ @@ -231,14 +795,6 @@ contract SFC is SFCBase, Version { } } - struct SealEpochRewardsCtx { - uint256[] baseRewardWeights; - uint256 totalBaseRewardWeight; - uint256[] txRewardWeights; - uint256 totalTxRewardWeight; - uint256 epochFee; - } - function _sealEpochRewards( uint256 epochDuration, EpochSnapshot storage snapshot, @@ -296,7 +852,6 @@ contract SFC is SFCBase, Version { if (selfStake != 0) { _rewardsStash[validatorAddr][validatorID] += commissionRewardFull; } - // accounting reward per token for delegators uint256 delegatorsReward = rawReward - commissionRewardFull; // note: use latest stake for the sake of rewards distribution accuracy, not snapshot.receivedStake @@ -351,44 +906,133 @@ contract SFC is SFCBase, Version { minGasPrice = newMinGasPrice; } - function sealEpoch( - uint256[] calldata offlineTime, - uint256[] calldata offlineBlocks, - uint256[] calldata uptimes, - uint256[] calldata originatedTxsFee, - uint256 epochGas - ) external onlyDriver { - EpochSnapshot storage snapshot = getEpochSnapshot[currentEpoch()]; - uint256[] memory validatorIDs = snapshot.validatorIDs; + function _createValidator(address auth, bytes memory pubkey) internal { + uint256 validatorID = ++lastValidatorID; + _rawCreateValidator(auth, validatorID, pubkey, OK_STATUS, currentEpoch(), _now(), 0, 0); + } - _sealEpochOffline(snapshot, validatorIDs, offlineTime, offlineBlocks); - { - EpochSnapshot storage prevSnapshot = getEpochSnapshot[currentSealedEpoch]; - uint256 epochDuration = 1; - if (_now() > prevSnapshot.endTime) { - epochDuration = _now() - prevSnapshot.endTime; + function _rawCreateValidator( + address auth, + uint256 validatorID, + bytes memory pubkey, + uint256 status, + uint256 createdEpoch, + uint256 createdTime, + uint256 deactivatedEpoch, + uint256 deactivatedTime + ) internal { + if (getValidatorID[auth] != 0) { + revert ValidatorExists(); + } + getValidatorID[auth] = validatorID; + getValidator[validatorID].status = status; + getValidator[validatorID].createdEpoch = createdEpoch; + getValidator[validatorID].createdTime = createdTime; + getValidator[validatorID].deactivatedTime = deactivatedTime; + getValidator[validatorID].deactivatedEpoch = deactivatedEpoch; + getValidator[validatorID].auth = auth; + getValidatorPubkey[validatorID] = pubkey; + pubkeyHashToValidatorID[keccak256(pubkey)] = validatorID; + + emit CreatedValidator(validatorID, auth, createdEpoch, createdTime); + if (deactivatedEpoch != 0) { + emit DeactivatedValidator(validatorID, deactivatedEpoch, deactivatedTime); + } + if (status != 0) { + emit ChangedValidatorStatus(validatorID, status); + } + } + + function _calcRawValidatorEpochTxReward( + uint256 epochFee, + uint256 txRewardWeight, + uint256 totalTxRewardWeight + ) internal view returns (uint256) { + if (txRewardWeight == 0) { + return 0; + } + uint256 txReward = (epochFee * txRewardWeight) / totalTxRewardWeight; + // fee reward except burntFeeShare and treasuryFeeShare + return (txReward * (Decimal.unit() - c.burntFeeShare() - c.treasuryFeeShare())) / Decimal.unit(); + } + + function _calcRawValidatorEpochBaseReward( + uint256 epochDuration, + uint256 _baseRewardPerSecond, + uint256 baseRewardWeight, + uint256 totalBaseRewardWeight + ) internal pure returns (uint256) { + if (baseRewardWeight == 0) { + return 0; + } + uint256 totalReward = epochDuration * _baseRewardPerSecond; + return (totalReward * baseRewardWeight) / totalBaseRewardWeight; + } + + function _mintNativeToken(uint256 amount) internal { + // balance will be increased after the transaction is processed + node.incBalance(address(this), amount); + totalSupply = totalSupply + amount; + } + + function _recountVotes(address delegator, address validatorAuth, bool strict) internal { + if (voteBookAddress != address(0)) { + // Don't allow recountVotes to use up all the gas + // solhint-disable-next-line avoid-low-level-calls + (bool success, ) = voteBookAddress.call{gas: 8000000}( + abi.encodeWithSignature("recountVotes(address,address)", delegator, validatorAuth) + ); + // Don't revert if recountVotes failed unless strict mode enabled + if (!success && strict) { + revert GovVotesRecountFailed(); } - _sealEpochRewards(epochDuration, snapshot, prevSnapshot, validatorIDs, uptimes, originatedTxsFee); - _sealEpochMinGasPrice(epochDuration, epochGas); } + } - currentSealedEpoch = currentEpoch(); - snapshot.endTime = _now(); - snapshot.endBlock = block.number; - snapshot.baseRewardPerSecond = c.baseRewardPerSecond(); - snapshot.totalSupply = totalSupply; + function _setValidatorDeactivated(uint256 validatorID, uint256 status) internal { + if (getValidator[validatorID].status == OK_STATUS && status != OK_STATUS) { + totalActiveStake = totalActiveStake - getValidator[validatorID].receivedStake; + } + // status as a number is proportional to severity + if (status > getValidator[validatorID].status) { + getValidator[validatorID].status = status; + if (getValidator[validatorID].deactivatedEpoch == 0) { + getValidator[validatorID].deactivatedEpoch = currentEpoch(); + getValidator[validatorID].deactivatedTime = _now(); + emit DeactivatedValidator( + validatorID, + getValidator[validatorID].deactivatedEpoch, + getValidator[validatorID].deactivatedTime + ); + } + emit ChangedValidatorStatus(validatorID, status); + } } - function sealEpochValidators(uint256[] calldata nextValidatorIDs) external onlyDriver { - EpochSnapshot storage snapshot = getEpochSnapshot[currentEpoch()]; - // fill data for the next snapshot - for (uint256 i = 0; i < nextValidatorIDs.length; i++) { - uint256 validatorID = nextValidatorIDs[i]; - uint256 receivedStake = getValidator[validatorID].receivedStake; - snapshot.receivedStake[validatorID] = receivedStake; - snapshot.totalStake = snapshot.totalStake + receivedStake; + function _syncValidator(uint256 validatorID, bool syncPubkey) public { + if (!_validatorExists(validatorID)) { + revert ValidatorNotExists(); } - snapshot.validatorIDs = nextValidatorIDs; - node.updateMinGasPrice(minGasPrice); + // emit special log for node + uint256 weight = getValidator[validatorID].receivedStake; + if (getValidator[validatorID].status != OK_STATUS) { + weight = 0; + } + node.updateValidatorWeight(validatorID, weight); + if (syncPubkey && weight != 0) { + node.updateValidatorPubkey(validatorID, getValidatorPubkey[validatorID]); + } + } + + function _validatorExists(uint256 validatorID) internal view returns (bool) { + return getValidator[validatorID].createdTime != 0; + } + + function _calcValidatorCommission(uint256 rawReward, uint256 commission) internal pure returns (uint256) { + return (rawReward * commission) / Decimal.unit(); + } + + function _now() internal view virtual returns (uint256) { + return block.timestamp; } } diff --git a/contracts/sfc/SFCBase.sol b/contracts/sfc/SFCBase.sol deleted file mode 100644 index c58ce64..0000000 --- a/contracts/sfc/SFCBase.sol +++ /dev/null @@ -1,185 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.9; - -import {Decimal} from "../common/Decimal.sol"; -import {SFCState} from "./SFCState.sol"; - -contract SFCBase is SFCState { - uint256 internal constant OK_STATUS = 0; - uint256 internal constant WITHDRAWN_BIT = 1; - uint256 internal constant OFFLINE_BIT = 1 << 3; - uint256 internal constant DOUBLESIGN_BIT = 1 << 7; - uint256 internal constant CHEATER_MASK = DOUBLESIGN_BIT; - - // auth - error NotDriverAuth(); - error NotAuthorized(); - - // addresses - error ZeroAddress(); - error SameAddress(); - - // values - error ZeroAmount(); - error ZeroRewards(); - - // pubkeys - error PubkeyUsedByOtherValidator(); - error MalformedPubkey(); - error PubkeyNotChanged(); - error EmptyPubkey(); - error TooManyPubkeyUpdates(); - - // redirections - error AlreadyRedirected(); - error SameRedirectionAuthorizer(); - error Redirected(); - - // validators - error ValidatorNotExists(); - error ValidatorExists(); - error ValidatorNotActive(); - error ValidatorDelegationLimitExceeded(); - error WrongValidatorStatus(); - - // requests - error RequestExists(); - error RequestNotExists(); - - // transfers - error TransfersNotAllowed(); - error TransferFailed(); - - // updater - error SFCAlreadyUpdated(); - error SFCWrongVersion(); - error SFCGovAlreadyUpdated(); - error SFCWrongGovVersion(); - - // governance - error GovVotesRecountFailed(); - - // staking - error InsufficientSelfStake(); - error NotEnoughTimePassed(); - error NotEnoughEpochsPassed(); - error StakeIsFullySlashed(); - - // stashing - error NothingToStash(); - - // slashing - error ValidatorNotSlashed(); - error RefundRatioTooHigh(); - - event DeactivatedValidator(uint256 indexed validatorID, uint256 deactivatedEpoch, uint256 deactivatedTime); - event ChangedValidatorStatus(uint256 indexed validatorID, uint256 status); - - function isNode(address addr) internal view virtual returns (bool) { - return addr == address(node); - } - - modifier onlyDriver() { - if (!isNode(msg.sender)) { - revert NotDriverAuth(); - } - _; - } - - function currentEpoch() public view returns (uint256) { - return currentSealedEpoch + 1; - } - - function _calcRawValidatorEpochTxReward( - uint256 epochFee, - uint256 txRewardWeight, - uint256 totalTxRewardWeight - ) internal view returns (uint256) { - if (txRewardWeight == 0) { - return 0; - } - uint256 txReward = (epochFee * txRewardWeight) / totalTxRewardWeight; - // fee reward except burntFeeShare and treasuryFeeShare - return (txReward * (Decimal.unit() - c.burntFeeShare() - c.treasuryFeeShare())) / Decimal.unit(); - } - - function _calcRawValidatorEpochBaseReward( - uint256 epochDuration, - uint256 _baseRewardPerSecond, - uint256 baseRewardWeight, - uint256 totalBaseRewardWeight - ) internal pure returns (uint256) { - if (baseRewardWeight == 0) { - return 0; - } - uint256 totalReward = epochDuration * _baseRewardPerSecond; - return (totalReward * baseRewardWeight) / totalBaseRewardWeight; - } - - function _mintNativeToken(uint256 amount) internal { - // balance will be increased after the transaction is processed - node.incBalance(address(this), amount); - totalSupply = totalSupply + amount; - } - - function _recountVotes(address delegator, address validatorAuth, bool strict) internal { - if (voteBookAddress != address(0)) { - // Don't allow recountVotes to use up all the gas - // solhint-disable-next-line avoid-low-level-calls - (bool success, ) = voteBookAddress.call{gas: 8000000}( - abi.encodeWithSignature("recountVotes(address,address)", delegator, validatorAuth) - ); - // Don't revert if recountVotes failed unless strict mode enabled - if (!success && strict) { - revert GovVotesRecountFailed(); - } - } - } - - function _setValidatorDeactivated(uint256 validatorID, uint256 status) internal { - if (getValidator[validatorID].status == OK_STATUS && status != OK_STATUS) { - totalActiveStake = totalActiveStake - getValidator[validatorID].receivedStake; - } - // status as a number is proportional to severity - if (status > getValidator[validatorID].status) { - getValidator[validatorID].status = status; - if (getValidator[validatorID].deactivatedEpoch == 0) { - getValidator[validatorID].deactivatedEpoch = currentEpoch(); - getValidator[validatorID].deactivatedTime = _now(); - emit DeactivatedValidator( - validatorID, - getValidator[validatorID].deactivatedEpoch, - getValidator[validatorID].deactivatedTime - ); - } - emit ChangedValidatorStatus(validatorID, status); - } - } - - function _syncValidator(uint256 validatorID, bool syncPubkey) public { - if (!_validatorExists(validatorID)) { - revert ValidatorNotExists(); - } - // emit special log for node - uint256 weight = getValidator[validatorID].receivedStake; - if (getValidator[validatorID].status != OK_STATUS) { - weight = 0; - } - node.updateValidatorWeight(validatorID, weight); - if (syncPubkey && weight != 0) { - node.updateValidatorPubkey(validatorID, getValidatorPubkey[validatorID]); - } - } - - function _validatorExists(uint256 validatorID) internal view returns (bool) { - return getValidator[validatorID].createdTime != 0; - } - - function _calcValidatorCommission(uint256 rawReward, uint256 commission) internal pure returns (uint256) { - return (rawReward * commission) / Decimal.unit(); - } - - function _now() internal view virtual returns (uint256) { - return block.timestamp; - } -} diff --git a/contracts/sfc/SFCLib.sol b/contracts/sfc/SFCLib.sol deleted file mode 100644 index c11ca45..0000000 --- a/contracts/sfc/SFCLib.sol +++ /dev/null @@ -1,426 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.9; - -import {Decimal} from "../common/Decimal.sol"; -import {SFCBase} from "./SFCBase.sol"; - -contract SFCLib is SFCBase { - event CreatedValidator( - uint256 indexed validatorID, - address indexed auth, - uint256 createdEpoch, - uint256 createdTime - ); - event Delegated(address indexed delegator, uint256 indexed toValidatorID, uint256 amount); - event Undelegated(address indexed delegator, uint256 indexed toValidatorID, uint256 indexed wrID, uint256 amount); - event Withdrawn(address indexed delegator, uint256 indexed toValidatorID, uint256 indexed wrID, uint256 amount); - event ClaimedRewards(address indexed delegator, uint256 indexed toValidatorID, uint256 rewards); - event RestakedRewards(address indexed delegator, uint256 indexed toValidatorID, uint256 rewards); - event BurntFTM(uint256 amount); - event UpdatedSlashingRefundRatio(uint256 indexed validatorID, uint256 refundRatio); - event RefundedSlashedLegacyDelegation(address indexed delegator, uint256 indexed validatorID, uint256 amount); - - /* - Constructor - */ - - function setGenesisValidator( - address auth, - uint256 validatorID, - bytes calldata pubkey, - uint256 status, - uint256 createdEpoch, - uint256 createdTime, - uint256 deactivatedEpoch, - uint256 deactivatedTime - ) external onlyDriver { - _rawCreateValidator( - auth, - validatorID, - pubkey, - status, - createdEpoch, - createdTime, - deactivatedEpoch, - deactivatedTime - ); - if (validatorID > lastValidatorID) { - lastValidatorID = validatorID; - } - } - - function setGenesisDelegation(address delegator, uint256 toValidatorID, uint256 stake) external onlyDriver { - _rawDelegate(delegator, toValidatorID, stake, false); - _mintNativeToken(stake); - } - - /* - Methods - */ - - function createValidator(bytes calldata pubkey) external payable { - if (msg.value < c.minSelfStake()) { - revert InsufficientSelfStake(); - } - if (pubkey.length == 0) { - revert EmptyPubkey(); - } - if (pubkeyHashToValidatorID[keccak256(pubkey)] != 0) { - revert PubkeyUsedByOtherValidator(); - } - _createValidator(msg.sender, pubkey); - _delegate(msg.sender, lastValidatorID, msg.value); - } - - function _createValidator(address auth, bytes memory pubkey) internal { - uint256 validatorID = ++lastValidatorID; - _rawCreateValidator(auth, validatorID, pubkey, OK_STATUS, currentEpoch(), _now(), 0, 0); - } - - function _rawCreateValidator( - address auth, - uint256 validatorID, - bytes memory pubkey, - uint256 status, - uint256 createdEpoch, - uint256 createdTime, - uint256 deactivatedEpoch, - uint256 deactivatedTime - ) internal { - if (getValidatorID[auth] != 0) { - revert ValidatorExists(); - } - getValidatorID[auth] = validatorID; - getValidator[validatorID].status = status; - getValidator[validatorID].createdEpoch = createdEpoch; - getValidator[validatorID].createdTime = createdTime; - getValidator[validatorID].deactivatedTime = deactivatedTime; - getValidator[validatorID].deactivatedEpoch = deactivatedEpoch; - getValidator[validatorID].auth = auth; - getValidatorPubkey[validatorID] = pubkey; - pubkeyHashToValidatorID[keccak256(pubkey)] = validatorID; - - emit CreatedValidator(validatorID, auth, createdEpoch, createdTime); - if (deactivatedEpoch != 0) { - emit DeactivatedValidator(validatorID, deactivatedEpoch, deactivatedTime); - } - if (status != 0) { - emit ChangedValidatorStatus(validatorID, status); - } - } - - function getSelfStake(uint256 validatorID) public view returns (uint256) { - return getStake[getValidator[validatorID].auth][validatorID]; - } - - function _checkDelegatedStakeLimit(uint256 validatorID) internal view returns (bool) { - return - getValidator[validatorID].receivedStake <= - (getSelfStake(validatorID) * c.maxDelegatedRatio()) / Decimal.unit(); - } - - function delegate(uint256 toValidatorID) external payable { - _delegate(msg.sender, toValidatorID, msg.value); - } - - function _delegate(address delegator, uint256 toValidatorID, uint256 amount) internal { - if (!_validatorExists(toValidatorID)) { - revert ValidatorNotExists(); - } - if (getValidator[toValidatorID].status != OK_STATUS) { - revert ValidatorNotActive(); - } - _rawDelegate(delegator, toValidatorID, amount, true); - if (!_checkDelegatedStakeLimit(toValidatorID)) { - revert ValidatorDelegationLimitExceeded(); - } - } - - function _rawDelegate(address delegator, uint256 toValidatorID, uint256 amount, bool strict) internal { - if (amount == 0) { - revert ZeroAmount(); - } - - _stashRewards(delegator, toValidatorID); - - getStake[delegator][toValidatorID] = getStake[delegator][toValidatorID] + amount; - uint256 origStake = getValidator[toValidatorID].receivedStake; - getValidator[toValidatorID].receivedStake = origStake + amount; - totalStake = totalStake + amount; - if (getValidator[toValidatorID].status == OK_STATUS) { - totalActiveStake = totalActiveStake + amount; - } - - _syncValidator(toValidatorID, origStake == 0); - - emit Delegated(delegator, toValidatorID, amount); - - _recountVotes(delegator, getValidator[toValidatorID].auth, strict); - } - - function recountVotes(address delegator, address validatorAuth, bool strict, uint256 gas) external { - // solhint-disable-next-line avoid-low-level-calls - (bool success, ) = voteBookAddress.call{gas: gas}( - abi.encodeWithSignature("recountVotes(address,address)", delegator, validatorAuth) - ); - if (!success && strict) { - revert GovVotesRecountFailed(); - } - } - - function _rawUndelegate( - address delegator, - uint256 toValidatorID, - uint256 amount, - bool strict, - bool forceful, - bool checkDelegatedStake - ) internal { - getStake[delegator][toValidatorID] -= amount; - getValidator[toValidatorID].receivedStake = getValidator[toValidatorID].receivedStake - amount; - totalStake = totalStake - amount; - if (getValidator[toValidatorID].status == OK_STATUS) { - totalActiveStake = totalActiveStake - amount; - } - - uint256 selfStakeAfterwards = getSelfStake(toValidatorID); - if (selfStakeAfterwards != 0 && getValidator[toValidatorID].status == OK_STATUS) { - if (!(selfStakeAfterwards >= c.minSelfStake())) { - if (forceful) { - revert InsufficientSelfStake(); - } else { - _setValidatorDeactivated(toValidatorID, WITHDRAWN_BIT); - } - } - if (checkDelegatedStake && !_checkDelegatedStakeLimit(toValidatorID)) { - revert ValidatorDelegationLimitExceeded(); - } - } else { - _setValidatorDeactivated(toValidatorID, WITHDRAWN_BIT); - } - - _recountVotes(delegator, getValidator[toValidatorID].auth, strict); - } - - function undelegate(uint256 toValidatorID, uint256 wrID, uint256 amount) public { - address delegator = msg.sender; - - _stashRewards(delegator, toValidatorID); - - if (amount == 0) { - revert ZeroAmount(); - } - - if (getWithdrawalRequest[delegator][toValidatorID][wrID].amount != 0) { - revert RequestExists(); - } - - _rawUndelegate(delegator, toValidatorID, amount, true, false, true); - - getWithdrawalRequest[delegator][toValidatorID][wrID].amount = amount; - getWithdrawalRequest[delegator][toValidatorID][wrID].epoch = currentEpoch(); - getWithdrawalRequest[delegator][toValidatorID][wrID].time = _now(); - - _syncValidator(toValidatorID, false); - - emit Undelegated(delegator, toValidatorID, wrID, amount); - } - - function isSlashed(uint256 validatorID) public view returns (bool) { - return getValidator[validatorID].status & CHEATER_MASK != 0; - } - - function getSlashingPenalty( - uint256 amount, - bool isCheater, - uint256 refundRatio - ) internal pure returns (uint256 penalty) { - if (!isCheater || refundRatio >= Decimal.unit()) { - return 0; - } - // round penalty upwards (ceiling) to prevent dust amount attacks - penalty = (amount * (Decimal.unit() - refundRatio)) / Decimal.unit() + 1; - if (penalty > amount) { - return amount; - } - return penalty; - } - - function _withdraw(address delegator, uint256 toValidatorID, uint256 wrID, address payable receiver) private { - WithdrawalRequest memory request = getWithdrawalRequest[delegator][toValidatorID][wrID]; - if (request.epoch == 0) { - revert RequestNotExists(); - } - - uint256 requestTime = request.time; - uint256 requestEpoch = request.epoch; - if ( - getValidator[toValidatorID].deactivatedTime != 0 && - getValidator[toValidatorID].deactivatedTime < requestTime - ) { - requestTime = getValidator[toValidatorID].deactivatedTime; - requestEpoch = getValidator[toValidatorID].deactivatedEpoch; - } - - if (_now() < requestTime + c.withdrawalPeriodTime()) { - revert NotEnoughTimePassed(); - } - - if (currentEpoch() < requestEpoch + c.withdrawalPeriodEpochs()) { - revert NotEnoughEpochsPassed(); - } - - uint256 amount = getWithdrawalRequest[delegator][toValidatorID][wrID].amount; - bool isCheater = isSlashed(toValidatorID); - uint256 penalty = getSlashingPenalty(amount, isCheater, slashingRefundRatio[toValidatorID]); - delete getWithdrawalRequest[delegator][toValidatorID][wrID]; - - if (amount <= penalty) { - revert StakeIsFullySlashed(); - } - // It's important that we transfer after erasing (protection against Re-Entrancy) - (bool sent, ) = receiver.call{value: amount - penalty}(""); - if (!sent) { - revert TransferFailed(); - } - _burnFTM(penalty); - - emit Withdrawn(delegator, toValidatorID, wrID, amount); - } - - function withdraw(uint256 toValidatorID, uint256 wrID) public { - _withdraw(msg.sender, toValidatorID, wrID, _receiverOf(msg.sender)); - } - - function deactivateValidator(uint256 validatorID, uint256 status) external onlyDriver { - if (status == OK_STATUS) { - revert WrongValidatorStatus(); - } - - _setValidatorDeactivated(validatorID, status); - _syncValidator(validatorID, false); - address validatorAddr = getValidator[validatorID].auth; - _recountVotes(validatorAddr, validatorAddr, false); - } - - function _highestPayableEpoch(uint256 validatorID) internal view returns (uint256) { - if (getValidator[validatorID].deactivatedEpoch != 0) { - if (currentSealedEpoch < getValidator[validatorID].deactivatedEpoch) { - return currentSealedEpoch; - } - return getValidator[validatorID].deactivatedEpoch; - } - return currentSealedEpoch; - } - - function _newRewards(address delegator, uint256 toValidatorID) internal view returns (uint256) { - uint256 stashedUntil = stashedRewardsUntilEpoch[delegator][toValidatorID]; - uint256 payableUntil = _highestPayableEpoch(toValidatorID); - uint256 wholeStake = getStake[delegator][toValidatorID]; - uint256 fullReward = _newRewardsOf(wholeStake, toValidatorID, stashedUntil, payableUntil); - return fullReward; - } - - function _newRewardsOf( - uint256 stakeAmount, - uint256 toValidatorID, - uint256 fromEpoch, - uint256 toEpoch - ) internal view returns (uint256) { - if (fromEpoch >= toEpoch) { - return 0; - } - uint256 stashedRate = getEpochSnapshot[fromEpoch].accumulatedRewardPerToken[toValidatorID]; - uint256 currentRate = getEpochSnapshot[toEpoch].accumulatedRewardPerToken[toValidatorID]; - return ((currentRate - stashedRate) * stakeAmount) / Decimal.unit(); - } - - function pendingRewards(address delegator, uint256 toValidatorID) public view returns (uint256) { - uint256 reward = _newRewards(delegator, toValidatorID); - return _rewardsStash[delegator][toValidatorID] + reward; - } - - function stashRewards(address delegator, uint256 toValidatorID) external { - if (!_stashRewards(delegator, toValidatorID)) { - revert NothingToStash(); - } - } - - function _stashRewards(address delegator, uint256 toValidatorID) internal returns (bool updated) { - uint256 nonStashedReward = _newRewards(delegator, toValidatorID); - stashedRewardsUntilEpoch[delegator][toValidatorID] = _highestPayableEpoch(toValidatorID); - _rewardsStash[delegator][toValidatorID] += nonStashedReward; - return nonStashedReward != 0; - } - - function _claimRewards(address delegator, uint256 toValidatorID) internal returns (uint256) { - _stashRewards(delegator, toValidatorID); - uint256 rewards = _rewardsStash[delegator][toValidatorID]; - if (rewards == 0) { - revert ZeroRewards(); - } - delete _rewardsStash[delegator][toValidatorID]; - // It's important that we mint after erasing (protection against Re-Entrancy) - _mintNativeToken(rewards); - return rewards; - } - - function claimRewards(uint256 toValidatorID) public { - address delegator = msg.sender; - uint256 rewards = _claimRewards(delegator, toValidatorID); - // It's important that we transfer after erasing (protection against Re-Entrancy) - (bool sent, ) = _receiverOf(delegator).call{value: rewards}(""); - if (!sent) { - revert TransferFailed(); - } - - emit ClaimedRewards(delegator, toValidatorID, rewards); - } - - function restakeRewards(uint256 toValidatorID) public { - address delegator = msg.sender; - uint256 rewards = _claimRewards(delegator, toValidatorID); - - _delegate(delegator, toValidatorID, rewards); - emit RestakedRewards(delegator, toValidatorID, rewards); - } - - // burnFTM allows SFC to burn an arbitrary amount of FTM tokens - function burnFTM(uint256 amount) external onlyOwner { - _burnFTM(amount); - } - - function _burnFTM(uint256 amount) internal { - if (amount != 0) { - payable(address(0)).transfer(amount); - emit BurntFTM(amount); - } - } - - function epochEndTime(uint256 epoch) internal view returns (uint256) { - return getEpochSnapshot[epoch].endTime; - } - - function updateSlashingRefundRatio(uint256 validatorID, uint256 refundRatio) external onlyOwner { - if (!isSlashed(validatorID)) { - revert ValidatorNotSlashed(); - } - if (refundRatio > Decimal.unit()) { - revert RefundRatioTooHigh(); - } - slashingRefundRatio[validatorID] = refundRatio; - emit UpdatedSlashingRefundRatio(validatorID, refundRatio); - } - - function _redirected(address addr) internal view returns (bool) { - return getRedirection[addr] != address(0); - } - - function _receiverOf(address addr) internal view returns (address payable) { - address to = getRedirection[addr]; - if (to == address(0)) { - return payable(address(uint160(addr))); - } - return payable(address(uint160(to))); - } -} diff --git a/contracts/sfc/SFCState.sol b/contracts/sfc/SFCState.sol deleted file mode 100644 index e64ca65..0000000 --- a/contracts/sfc/SFCState.sol +++ /dev/null @@ -1,113 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.9; - -import {Ownable} from "../ownership/Ownable.sol"; -import {Initializable} from "../common/Initializable.sol"; -import {NodeDriverAuth} from "./NodeDriverAuth.sol"; -import {ConstantsManager} from "./ConstantsManager.sol"; - -contract SFCState is Initializable, Ownable { - /** - * @dev The staking for validation - */ - struct Validator { - uint256 status; - uint256 deactivatedTime; - uint256 deactivatedEpoch; - uint256 receivedStake; // from all delegators (weight of the validator) - uint256 createdEpoch; - uint256 createdTime; - address auth; // self-stake delegator - } - - NodeDriverAuth internal node; - - // last sealed epoch (currentEpoch - 1) - uint256 public currentSealedEpoch; - mapping(uint256 => Validator) public getValidator; - mapping(address => uint256) public getValidatorID; - mapping(uint256 => bytes) public getValidatorPubkey; - - uint256 public lastValidatorID; - - // total stake of all validators - includes slashed/offline validators - uint256 public totalStake; - - // total stake of active (OK_STATUS) validators (total weight) - uint256 public totalActiveStake; - - // delegator => validator ID => stashed rewards (to be claimed/restaked) - mapping(address => mapping(uint256 => uint256)) internal _rewardsStash; - - // delegator => validator ID => last epoch number for which were rewards stashed - mapping(address => mapping(uint256 => uint256)) public stashedRewardsUntilEpoch; - - struct WithdrawalRequest { - uint256 epoch; // epoch where undelegated - uint256 time; // when undelegated - uint256 amount; - } - - // delegator => validator ID => withdrawal ID => withdrawal request - mapping(address => mapping(uint256 => mapping(uint256 => WithdrawalRequest))) public getWithdrawalRequest; - - // delegator => validator ID => current stake (locked+unlocked) - mapping(address => mapping(uint256 => uint256)) public getStake; - - struct EpochSnapshot { - // validator ID => validator weight in the epoch - mapping(uint256 => uint256) receivedStake; - // validator ID => accumulated ( delegatorsReward * 1e18 / receivedStake ) - mapping(uint256 => uint256) accumulatedRewardPerToken; - // validator ID => accumulated online time - mapping(uint256 => uint256) accumulatedUptime; - // validator ID => gas fees from txs originated by the validator - mapping(uint256 => uint256) accumulatedOriginatedTxsFee; - mapping(uint256 => uint256) offlineTime; - mapping(uint256 => uint256) offlineBlocks; - uint256[] validatorIDs; - uint256 endTime; - uint256 endBlock; - uint256 epochFee; // gas fees from txs in the epoch - uint256 baseRewardPerSecond; // the base reward to divide among validators for each second of the epoch - uint256 totalStake; // total weight of all validators - uint256 totalSupply; // total supply of native tokens - } - - // the total supply of native tokens in the chain - uint256 public totalSupply; - // epoch id => epoch snapshot - mapping(uint256 => EpochSnapshot) public getEpochSnapshot; - - // validator ID -> slashing refund ratio (allows to withdraw slashed stake) - mapping(uint256 => uint256) public slashingRefundRatio; - - // the minimal gas price calculated for the current epoch - uint256 public minGasPrice; - - // the treasure contract (receives unlock penalties and a part of epoch fees) - address public treasuryAddress; - - // the SFCLib contract - address internal libAddress; - - ConstantsManager internal c; - - // the governance contract (to recalculate votes when the stake changes) - address public voteBookAddress; - - // validator ID => amount of pubkey updates - mapping(uint256 => uint256) internal validatorPubkeyChanges; - - // keccak256(pubkey bytes) => validator ID (prevents using the same key by multiple validators) - mapping(bytes32 => uint256) internal pubkeyHashToValidatorID; - - // address authorized to initiate redirection - address public redirectionAuthorizer; - - // delegator => withdrawals receiver - mapping(address => address) public getRedirectionRequest; - - // delegator => withdrawals receiver - mapping(address => address) public getRedirection; -} diff --git a/contracts/sfc/Updater.sol b/contracts/sfc/Updater.sol index 46c93b3..e3380a2 100644 --- a/contracts/sfc/Updater.sol +++ b/contracts/sfc/Updater.sol @@ -5,7 +5,6 @@ import {Ownable} from "../ownership/Ownable.sol"; import {Decimal} from "../common/Decimal.sol"; import {NodeDriverAuth} from "./NodeDriverAuth.sol"; import {ConstantsManager} from "./ConstantsManager.sol"; -import {SFC} from "./SFC.sol"; import {ISFC} from "../interfaces/ISFC.sol"; import {Version} from "../version/Version.sol"; @@ -23,7 +22,6 @@ interface GovVersion { contract Updater { address public sfcFrom; - address public sfcLib; address public sfcConsts; address public govTo; address public govFrom; @@ -38,7 +36,6 @@ contract Updater { constructor( address _sfcFrom, - address _sfcLib, address _sfcConsts, address _govTo, address _govFrom, @@ -46,7 +43,6 @@ contract Updater { address _owner ) { sfcFrom = _sfcFrom; - sfcLib = _sfcLib; sfcConsts = _sfcConsts; govTo = _govTo; govFrom = _govFrom; @@ -55,7 +51,6 @@ contract Updater { address sfcTo = address(0xFC00FACE00000000000000000000000000000000); if ( sfcFrom == address(0) || - sfcLib == address(0) || sfcConsts == address(0) || govTo == address(0) || govFrom == address(0) || @@ -103,7 +98,6 @@ contract Updater { nodeAuth.upgradeCode(sfcTo, sfcFrom); ISFC(sfcTo).updateConstsAddress(sfcConsts); ISFC(sfcTo).updateVoteBookAddress(voteBook); - SFC(sfcTo).updateLibAddress(sfcLib); nodeAuth.upgradeCode(govTo, govFrom); GovI(govTo).upgrade(voteBook); diff --git a/contracts/test/UnitTestSFC.sol b/contracts/test/UnitTestSFC.sol index 1531981..6b4567c 100644 --- a/contracts/test/UnitTestSFC.sol +++ b/contracts/test/UnitTestSFC.sol @@ -3,13 +3,12 @@ pragma solidity ^0.8.9; import {Decimal} from "../common/Decimal.sol"; import {SFC} from "../sfc/SFC.sol"; -import {SFCBase} from "../sfc/SFCBase.sol"; -import {SFCLib} from "../sfc/SFCLib.sol"; +import {ISFC} from "../interfaces/ISFC.sol"; import {NodeDriverAuth} from "../sfc/NodeDriverAuth.sol"; import {NodeDriver} from "../sfc/NodeDriver.sol"; import {UnitTestConstantsManager} from "./UnitTestConstantsManager.sol"; -contract UnitTestSFCBase { +contract UnitTestSFC is SFC { uint256 internal time; bool public allowedNonNodeCalls; @@ -36,22 +35,7 @@ contract UnitTestSFCBase { function disableNonNodeCalls() external { allowedNonNodeCalls = false; } -} - -contract UnitTestSFC is SFC, UnitTestSFCBase { - function _now() internal view override returns (uint256) { - return time; - } - - function isNode(address addr) internal view override returns (bool) { - if (allowedNonNodeCalls) { - return true; - } - return SFCBase.isNode(addr); - } -} -contract UnitTestSFCLib is SFCLib, UnitTestSFCBase { function _now() internal view override returns (uint256) { return time; } @@ -60,7 +44,7 @@ contract UnitTestSFCLib is SFCLib, UnitTestSFCBase { if (allowedNonNodeCalls) { return true; } - return SFCBase.isNode(addr); + return SFC.isNode(addr); } } @@ -69,7 +53,6 @@ contract UnitTestNetworkInitializer { uint256 sealedEpoch, uint256 totalSupply, address payable _sfc, - address _lib, address _auth, address _driver, address _evmWriter, @@ -94,189 +77,6 @@ contract UnitTestNetworkInitializer { consts.updateGasPriceBalancingCounterweight(6 * 60 * 60); consts.transferOwnership(_owner); - SFCUnitTestI(_sfc).initialize(sealedEpoch, totalSupply, _auth, _lib, address(consts), _owner); + ISFC(_sfc).initialize(sealedEpoch, totalSupply, _auth, address(consts), _owner); } } - -interface SFCUnitTestI { - function currentSealedEpoch() external view returns (uint256); - - function getEpochSnapshot( - uint256 - ) - external - view - returns ( - uint256 endTime, - uint256 endBlock, - uint256 epochFee, - uint256 _baseRewardPerSecond, - uint256 totalStake, - uint256 totalSupply - ); - - function getStake(address, uint256) external view returns (uint256); - - function getValidator( - uint256 - ) - external - view - returns ( - uint256 status, - uint256 deactivatedTime, - uint256 deactivatedEpoch, - uint256 receivedStake, - uint256 createdEpoch, - uint256 createdTime, - address auth - ); - - function getValidatorID(address) external view returns (uint256); - - function getValidatorPubkey(uint256) external view returns (bytes memory); - - function getWithdrawalRequest( - address, - uint256, - uint256 - ) external view returns (uint256 epoch, uint256 time, uint256 amount); - - function isOwner() external view returns (bool); - - function lastValidatorID() external view returns (uint256); - - function minGasPrice() external view returns (uint256); - - function owner() external view returns (address); - - function renounceOwnership() external; - - function slashingRefundRatio(uint256) external view returns (uint256); - - function stashedRewardsUntilEpoch(address, uint256) external view returns (uint256); - - function targetGasPowerPerSecond() external view returns (uint256); - - function totalActiveStake() external view returns (uint256); - - function totalStake() external view returns (uint256); - - function totalSupply() external view returns (uint256); - - function transferOwnership(address newOwner) external; - - function treasuryAddress() external view returns (address); - - function version() external pure returns (bytes3); - - function currentEpoch() external view returns (uint256); - - function updateConstsAddress(address v) external; - - function constsAddress() external view returns (address); - - function getEpochValidatorIDs(uint256 epoch) external view returns (uint256[] memory); - - function getEpochReceivedStake(uint256 epoch, uint256 validatorID) external view returns (uint256); - - function getEpochAccumulatedRewardPerToken(uint256 epoch, uint256 validatorID) external view returns (uint256); - - function getEpochAccumulatedUptime(uint256 epoch, uint256 validatorID) external view returns (uint256); - - function getEpochAccumulatedOriginatedTxsFee(uint256 epoch, uint256 validatorID) external view returns (uint256); - - function getEpochOfflineTime(uint256 epoch, uint256 validatorID) external view returns (uint256); - - function getEpochOfflineBlocks(uint256 epoch, uint256 validatorID) external view returns (uint256); - - function getEpochEndBlock(uint256 epoch) external view returns (uint256); - - function rewardsStash(address delegator, uint256 validatorID) external view returns (uint256); - - function createValidator(bytes calldata pubkey) external payable; - - function getSelfStake(uint256 validatorID) external view returns (uint256); - - function delegate(uint256 toValidatorID) external payable; - - function undelegate(uint256 toValidatorID, uint256 wrID, uint256 amount) external; - - function isSlashed(uint256 validatorID) external view returns (bool); - - function withdraw(uint256 toValidatorID, uint256 wrID) external; - - function deactivateValidator(uint256 validatorID, uint256 status) external; - - function pendingRewards(address delegator, uint256 toValidatorID) external view returns (uint256); - - function stashRewards(address delegator, uint256 toValidatorID) external; - - function claimRewards(uint256 toValidatorID) external; - - function restakeRewards(uint256 toValidatorID) external; - - function offlinePenaltyThreshold() external view returns (uint256 blocksNum, uint256 time); - - function updateBaseRewardPerSecond(uint256 value) external; - - function updateOfflinePenaltyThreshold(uint256 blocksNum, uint256 time) external; - - function updateSlashingRefundRatio(uint256 validatorID, uint256 refundRatio) external; - - function updateTreasuryAddress(address v) external; - - function burnFTM(uint256 amount) external; - - function sealEpoch( - uint256[] calldata offlineTime, - uint256[] calldata offlineBlocks, - uint256[] calldata uptimes, - uint256[] calldata originatedTxsFee, - uint256 epochGas - ) external; - - function sealEpochValidators(uint256[] calldata nextValidatorIDs) external; - - function initialize( - uint256 sealedEpoch, - uint256 _totalSupply, - address nodeDriver, - address lib, - address consts, - address _owner - ) external; - - function setGenesisValidator( - address auth, - uint256 validatorID, - bytes calldata pubkey, - uint256 status, - uint256 createdEpoch, - uint256 createdTime, - uint256 deactivatedEpoch, - uint256 deactivatedTime - ) external; - - function setGenesisDelegation(address delegator, uint256 toValidatorID, uint256 stake) external; - - function _syncValidator(uint256 validatorID, bool syncPubkey) external; - - function getTime() external view returns (uint256); - - function getBlockTime() external view returns (uint256); - - function rebaseTime() external; - - function advanceTime(uint256) external; - - function enableNonNodeCalls() external; - - function disableNonNodeCalls() external; - - function allowedNonNodeCalls() external view returns (bool); - - function updateVoteBookAddress(address v) external; - - function voteBookAddress() external view returns (address); -} diff --git a/hardhat.config.ts b/hardhat.config.ts index 05313bc..283a533 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -5,6 +5,7 @@ import '@nomicfoundation/hardhat-ethers'; import 'hardhat-contract-sizer'; import 'hardhat-gas-reporter'; import 'solidity-coverage'; +import '@typechain/hardhat'; dotenv.config(); @@ -19,6 +20,11 @@ const config: HardhatUserConfig = { }, }, }, + networks: { + hardhat: { + allowUnlimitedContractSize: true, + }, + }, gasReporter: { currency: 'USD', enabled: !!process.env.REPORT_GAS, diff --git a/test/NodeDriver.ts b/test/NodeDriver.ts index d71f431..75b1ee3 100644 --- a/test/NodeDriver.ts +++ b/test/NodeDriver.ts @@ -1,19 +1,18 @@ import { ethers } from 'hardhat'; import { expect } from 'chai'; import { loadFixture } from '@nomicfoundation/hardhat-network-helpers'; -import { EVMWriter, NetworkInitializer, NodeDriver, NodeDriverAuth, SFCLib, UnitTestSFC } from '../typechain-types'; +import { IEVMWriter, NetworkInitializer, NodeDriver, NodeDriverAuth, UnitTestSFC } from '../typechain-types'; describe('NodeDriver', () => { const fixture = async () => { const [owner, nonOwner] = await ethers.getSigners(); const sfc: UnitTestSFC = await ethers.deployContract('UnitTestSFC'); const nodeDriver: NodeDriver = await ethers.deployContract('NodeDriver'); - const evmWriter: EVMWriter = await ethers.deployContract('StubEvmWriter'); + const evmWriter: IEVMWriter = await ethers.deployContract('StubEvmWriter'); const nodeDriverAuth: NodeDriverAuth = await ethers.deployContract('NodeDriverAuth'); - const sfcLib: SFCLib = await ethers.deployContract('UnitTestSFCLib'); const initializer: NetworkInitializer = await ethers.deployContract('NetworkInitializer'); - await initializer.initializeAll(12, 0, sfc, sfcLib, nodeDriverAuth, nodeDriver, evmWriter, owner); + await initializer.initializeAll(12, 0, sfc, nodeDriverAuth, nodeDriver, evmWriter, owner); return { owner, @@ -22,7 +21,6 @@ describe('NodeDriver', () => { nodeDriver, evmWriter, nodeDriverAuth, - sfcLib, }; }; diff --git a/test/SFC.ts b/test/SFC.ts index 557ad9f..b4623c8 100644 --- a/test/SFC.ts +++ b/test/SFC.ts @@ -2,11 +2,10 @@ import { ethers } from 'hardhat'; import { expect } from 'chai'; import { loadFixture } from '@nomicfoundation/hardhat-network-helpers'; import { - EVMWriter, + IEVMWriter, NodeDriver, NodeDriverAuth, - SFCLib, - SFCUnitTestI, + UnitTestSFC, UnitTestConstantsManager, UnitTestNetworkInitializer, } from '../typechain-types'; @@ -16,14 +15,13 @@ import { BlockchainNode, ValidatorMetrics } from './helpers/BlockchainNode'; describe('SFC', () => { const fixture = async () => { const [owner, user] = await ethers.getSigners(); - const sfc: SFCUnitTestI = await ethers.getContractAt('SFCUnitTestI', await ethers.deployContract('UnitTestSFC')); + const sfc: UnitTestSFC = await ethers.deployContract('UnitTestSFC'); const nodeDriver: NodeDriver = await ethers.deployContract('NodeDriver'); - const evmWriter: EVMWriter = await ethers.deployContract('StubEvmWriter'); + const evmWriter: IEVMWriter = await ethers.deployContract('StubEvmWriter'); const nodeDriverAuth: NodeDriverAuth = await ethers.deployContract('NodeDriverAuth'); - const sfcLib: SFCLib = await ethers.deployContract('UnitTestSFCLib'); const initializer: UnitTestNetworkInitializer = await ethers.deployContract('UnitTestNetworkInitializer'); - await initializer.initializeAll(0, 0, sfc, sfcLib, nodeDriverAuth, nodeDriver, evmWriter, owner); + await initializer.initializeAll(0, 0, sfc, nodeDriverAuth, nodeDriver, evmWriter, owner); const constants: UnitTestConstantsManager = await ethers.getContractAt( 'UnitTestConstantsManager', await sfc.constsAddress(), @@ -37,7 +35,6 @@ describe('SFC', () => { evmWriter, nodeDriver, nodeDriverAuth, - sfcLib, constants, }; }; @@ -52,7 +49,7 @@ describe('SFC', () => { to: this.sfc, value: 1, }), - ).to.revertedWithCustomError(this.sfcLib, 'TransfersNotAllowed'); + ).to.revertedWithCustomError(this.sfc, 'TransfersNotAllowed'); }); describe('Genesis validator', () => { @@ -77,14 +74,11 @@ describe('SFC', () => { }); it('Should revert when sealEpoch not called by node', async function () { - await expect(this.sfc.sealEpoch([1], [1], [1], [1], 0)).to.be.revertedWithCustomError( - this.sfcLib, - 'NotDriverAuth', - ); + await expect(this.sfc.sealEpoch([1], [1], [1], [1], 0)).to.be.revertedWithCustomError(this.sfc, 'NotDriverAuth'); }); it('Should revert when SealEpochValidators not called by node', async function () { - await expect(this.sfc.sealEpochValidators([1])).to.be.revertedWithCustomError(this.sfcLib, 'NotDriverAuth'); + await expect(this.sfc.sealEpochValidators([1])).to.be.revertedWithCustomError(this.sfc, 'NotDriverAuth'); }); }); @@ -179,13 +173,13 @@ describe('SFC', () => { this.sfc .connect(this.validator) .createValidator(ethers.Wallet.createRandom().publicKey, { value: ethers.parseEther('0.1') }), - ).to.be.revertedWithCustomError(this.sfcLib, 'InsufficientSelfStake'); + ).to.be.revertedWithCustomError(this.sfc, 'InsufficientSelfStake'); }); it('Should revert when public key is empty while creating a validator', async function () { await expect( this.sfc.connect(this.validator).createValidator('0x', { value: ethers.parseEther('0.4') }), - ).to.be.revertedWithCustomError(this.sfcLib, 'EmptyPubkey'); + ).to.be.revertedWithCustomError(this.sfc, 'EmptyPubkey'); }); it('Should succeed and create two validators and return id of last validator', async function () { @@ -216,7 +210,7 @@ describe('SFC', () => { it('Should revert when staking to non-existing validator', async function () { await expect( this.sfc.connect(this.secondValidator).delegate(1, { value: ethers.parseEther('0.1') }), - ).to.be.revertedWithCustomError(this.sfcLib, 'ValidatorNotExists'); + ).to.be.revertedWithCustomError(this.sfc, 'ValidatorNotExists'); }); it('Should succeed and stake with different delegators', async function () { @@ -331,13 +325,13 @@ describe('SFC', () => { 0, 0, ), - ).to.be.revertedWithCustomError(this.sfcLib, 'NotDriverAuth'); + ).to.be.revertedWithCustomError(this.sfc, 'NotDriverAuth'); }); it('Should revert when setGenesisDelegation is not called not node', async function () { const delegator = ethers.Wallet.createRandom(); await expect(this.sfc.setGenesisDelegation(delegator, 1, 100, 0, 0, 0, 0, 0, 1000)).to.be.revertedWithCustomError( - this.sfcLib, + this.sfc, 'NotDriverAuth', ); }); @@ -443,7 +437,7 @@ describe('SFC', () => { this.sfc .connect(validator) .createValidator(ethers.Wallet.createRandom().publicKey, { value: ethers.parseEther('0.1') }), - ).to.be.revertedWithCustomError(this.sfcLib, 'InsufficientSelfStake'); + ).to.be.revertedWithCustomError(this.sfc, 'InsufficientSelfStake'); await node.handleTx( await this.sfc.connect(validator).createValidator(pubkey, { value: ethers.parseEther('0.3175') }), @@ -453,7 +447,7 @@ describe('SFC', () => { this.sfc .connect(validator) .createValidator(ethers.Wallet.createRandom().publicKey, { value: ethers.parseEther('0.5') }), - ).to.be.revertedWithCustomError(this.sfcLib, 'ValidatorExists'); + ).to.be.revertedWithCustomError(this.sfc, 'ValidatorExists'); await node.handleTx( await this.sfc.connect(secondValidator).createValidator(secondPubkey, { value: ethers.parseEther('0.5') }), @@ -771,7 +765,7 @@ describe('SFC', () => { it('Should revert when deactivating validator if not Node', async function () { await this.sfc.disableNonNodeCalls(); await expect(this.sfc.deactivateValidator(this.validatorId, 0)).to.be.revertedWithCustomError( - this.sfcLib, + this.sfc, 'NotDriverAuth', ); }); @@ -1011,7 +1005,7 @@ describe('SFC', () => { it('Should revert when calling deactivateValidator with wrong status', async function () { await expect(this.sfc.deactivateValidator(1, 0)).to.be.revertedWithCustomError( - this.sfcLib, + this.sfc, 'WrongValidatorStatus', ); }); @@ -1099,11 +1093,11 @@ describe('SFC', () => { describe('Epoch getters', () => { it('Should revert when trying to unlock stake if not lockedup', async function () { - await expect(this.sfc.unlockStake(1, 10)).to.be.revertedWithCustomError(this.sfcLib, 'NotLockedUp'); + await expect(this.sfc.unlockStake(1, 10)).to.be.revertedWithCustomError(this.sfc, 'NotLockedUp'); }); it('Should revert when trying to unlock stake if amount is 0', async function () { - await expect(this.sfc.unlockStake(1, 0)).to.be.revertedWithCustomError(this.sfcLib, 'ZeroAmount'); + await expect(this.sfc.unlockStake(1, 0)).to.be.revertedWithCustomError(this.sfc, 'ZeroAmount'); }); it('Should succeed and return slashed status', async function () { @@ -1111,12 +1105,12 @@ describe('SFC', () => { }); it('Should revert when delegating to an unexisting validator', async function () { - await expect(this.sfc.delegate(4)).to.be.revertedWithCustomError(this.sfcLib, 'ValidatorNotExists'); + await expect(this.sfc.delegate(4)).to.be.revertedWithCustomError(this.sfc, 'ValidatorNotExists'); }); it('Should revert when delegating to an unexisting validator (2)', async function () { await expect(this.sfc.delegate(4, { value: ethers.parseEther('1') })).to.be.revertedWithCustomError( - this.sfcLib, + this.sfc, 'ValidatorNotExists', ); }); @@ -1217,23 +1211,20 @@ describe('SFC', () => { it('Should revert when withdrawing nonexistent request', async function () { await expect(this.sfc.withdraw(this.validatorId, 0)).to.be.revertedWithCustomError( - this.sfcLib, + this.sfc, 'RequestNotExists', ); }); it('Should revert when undelegating 0 amount', async function () { await this.blockchainNode.sealEpoch(1_000); - await expect(this.sfc.undelegate(this.validatorId, 0, 0)).to.be.revertedWithCustomError( - this.sfcLib, - 'ZeroAmount', - ); + await expect(this.sfc.undelegate(this.validatorId, 0, 0)).to.be.revertedWithCustomError(this.sfc, 'ZeroAmount'); }); it('Should revert when undelegating if not enough unlocked stake', async function () { await this.blockchainNode.sealEpoch(1_000); await expect(this.sfc.undelegate(this.validatorId, 0, 10)).to.be.revertedWithCustomError( - this.sfcLib, + this.sfc, 'NotEnoughUnlockedStake', ); }); @@ -1243,7 +1234,7 @@ describe('SFC', () => { await this.sfc.connect(this.thirdDelegator).delegate(this.validatorId, { value: ethers.parseEther('1') }); await expect( this.sfc.connect(this.thirdDelegator).unlockStake(this.validatorId, 10), - ).to.be.revertedWithCustomError(this.sfcLib, 'NotLockedUp'); + ).to.be.revertedWithCustomError(this.sfc, 'NotLockedUp'); }); it('Should succeed and return the unlocked stake', async function () { @@ -1259,7 +1250,7 @@ describe('SFC', () => { await this.blockchainNode.sealEpoch(1_000); await expect( this.sfc.connect(this.thirdDelegator).claimRewards(this.validatorId), - ).to.be.revertedWithCustomError(this.sfcLib, 'ZeroRewards'); + ).to.be.revertedWithCustomError(this.sfc, 'ZeroRewards'); }); }); @@ -1277,7 +1268,7 @@ describe('SFC', () => { this.sfc .connect(this.thirdDelegator) .lockStake(this.validatorId, 2 * 60 * 60 * 24 * 365, ethers.parseEther('0')), - ).to.be.revertedWithCustomError(this.sfcLib, 'ZeroAmount'); + ).to.be.revertedWithCustomError(this.sfc, 'ZeroAmount'); }); it('Should revert when locking for more than a year', async function () { @@ -1287,7 +1278,7 @@ describe('SFC', () => { this.sfc .connect(this.thirdDelegator) .lockStake(this.thirdValidatorId, 2 * 60 * 60 * 24 * 365, ethers.parseEther('1')), - ).to.be.revertedWithCustomError(this.sfcLib, 'IncorrectDuration'); + ).to.be.revertedWithCustomError(this.sfc, 'IncorrectDuration'); }); it('Should revert when locking for more than a validator lockup period', async function () { @@ -1297,7 +1288,7 @@ describe('SFC', () => { this.sfc .connect(this.thirdDelegator) .lockStake(this.thirdValidatorId, 60 * 60 * 24 * 364, ethers.parseEther('1')), - ).to.be.revertedWithCustomError(this.sfcLib, 'ValidatorLockupTooShort'); + ).to.be.revertedWithCustomError(this.sfc, 'ValidatorLockupTooShort'); await this.sfc .connect(this.thirdDelegator) .lockStake(this.thirdValidatorId, 60 * 60 * 24 * 363, ethers.parseEther('1')); @@ -1321,7 +1312,7 @@ describe('SFC', () => { await this.blockchainNode.sealEpoch(60 * 60 * 24 * 14); await expect( this.sfc.unlockStake(this.thirdValidatorId, ethers.parseEther('10')), - ).to.be.revertedWithCustomError(this.sfcLib, 'NotLockedUp'); + ).to.be.revertedWithCustomError(this.sfc, 'NotLockedUp'); }); it('Should revert when unlocking more than locked stake', async function () { @@ -1333,7 +1324,7 @@ describe('SFC', () => { await this.blockchainNode.sealEpoch(60 * 60 * 24 * 14); await expect( this.sfc.connect(this.thirdDelegator).unlockStake(this.thirdValidatorId, ethers.parseEther('10')), - ).to.be.revertedWithCustomError(this.sfcLib, 'NotEnoughLockedStake'); + ).to.be.revertedWithCustomError(this.sfc, 'NotEnoughLockedStake'); }); it('Should succeed and scale unlocking penalty', async function () { @@ -1364,7 +1355,7 @@ describe('SFC', () => { await expect( this.sfc.connect(this.thirdDelegator).unlockStake(this.thirdValidatorId, ethers.parseEther('0.51')), - ).to.be.revertedWithCustomError(this.sfcLib, 'NotEnoughLockedStake'); + ).to.be.revertedWithCustomError(this.sfc, 'NotEnoughLockedStake'); expect( await this.sfc .connect(this.thirdDelegator) @@ -1405,7 +1396,7 @@ describe('SFC', () => { await expect( this.sfc.connect(this.thirdDelegator).unlockStake(this.thirdValidatorId, ethers.parseEther('0.51')), - ).to.be.revertedWithCustomError(this.sfcLib, 'NotEnoughLockedStake'); + ).to.be.revertedWithCustomError(this.sfc, 'NotEnoughLockedStake'); expect( await this.sfc .connect(this.thirdDelegator) @@ -1423,7 +1414,7 @@ describe('SFC', () => { await expect( this.sfc.connect(this.thirdDelegator).unlockStake(this.thirdValidatorId, ethers.parseEther('1.51')), - ).to.be.revertedWithCustomError(this.sfcLib, 'NotEnoughLockedStake'); + ).to.be.revertedWithCustomError(this.sfc, 'NotEnoughLockedStake'); expect( await this.sfc .connect(this.thirdDelegator) @@ -1485,16 +1476,13 @@ describe('SFC', () => { await expect( this.sfc.connect(this.validator).updateSlashingRefundRatio(this.thirdValidatorId, 1), - ).to.be.revertedWithCustomError(this.sfcLib, 'ValidatorNotSlashed'); + ).to.be.revertedWithCustomError(this.sfc, 'ValidatorNotSlashed'); await this.blockchainNode.sealEpoch(60 * 60 * 24 * 14); }); it('Should revert when syncing if validator does not exist', async function () { - await expect(this.sfc._syncValidator(33, false)).to.be.revertedWithCustomError( - this.sfcLib, - 'ValidatorNotExists', - ); + await expect(this.sfc._syncValidator(33, false)).to.be.revertedWithCustomError(this.sfc, 'ValidatorNotExists'); }); }); }); @@ -1697,7 +1685,7 @@ describe('SFC', () => { await this.blockchainNode.sealEpoch(60 * 60 * 24); await expect( this.sfc.connect(this.firstDelegator).relockStake(this.validatorId, 60 * 60 * 24 * 20, ethers.parseEther('0')), - ).to.be.revertedWithCustomError(this.sfcLib, 'TooFrequentReLocks'); + ).to.be.revertedWithCustomError(this.sfc, 'TooFrequentReLocks'); // 4 await this.sfc.advanceTime(60 * 60 * 24 * 14); @@ -1707,7 +1695,7 @@ describe('SFC', () => { await this.blockchainNode.sealEpoch(60 * 60 * 24); await expect( this.sfc.connect(this.firstDelegator).relockStake(this.validatorId, 60 * 60 * 24 * 20, ethers.parseEther('0')), - ).to.be.revertedWithCustomError(this.sfcLib, 'TooFrequentReLocks'); + ).to.be.revertedWithCustomError(this.sfc, 'TooFrequentReLocks'); for (let i = 5; i <= 40; i++) { // 5-40