From 883717f7678d883d33be9be4b63a246e02193ddd Mon Sep 17 00:00:00 2001 From: Mike-CZ Date: Fri, 25 Oct 2024 10:31:17 +0200 Subject: [PATCH 1/8] Move interfaces into separate directory --- .../interfaces/{IEVMWriter.sol => IEvmWriter.sol} | 0 .../NodeDriverI.sol => interfaces/INodeDriver.sol} | 2 +- contracts/interfaces/INodeDriverExecutable.sol | 6 ++++++ contracts/{sfc/SFCI.sol => interfaces/ISfc.sol} | 2 +- contracts/sfc/NetworkInitializer.sol | 4 ++-- contracts/sfc/NodeDriver.sol | 2 +- contracts/sfc/NodeDriverAuth.sol | 13 +++++-------- contracts/sfc/Updater.sol | 6 +++--- contracts/test/StubEvmWriter.sol | 2 +- 9 files changed, 20 insertions(+), 17 deletions(-) rename contracts/interfaces/{IEVMWriter.sol => IEvmWriter.sol} (100%) rename contracts/{sfc/NodeDriverI.sol => interfaces/INodeDriver.sol} (98%) create mode 100644 contracts/interfaces/INodeDriverExecutable.sol rename contracts/{sfc/SFCI.sol => interfaces/ISfc.sol} (99%) diff --git a/contracts/interfaces/IEVMWriter.sol b/contracts/interfaces/IEvmWriter.sol similarity index 100% rename from contracts/interfaces/IEVMWriter.sol rename to contracts/interfaces/IEvmWriter.sol diff --git a/contracts/sfc/NodeDriverI.sol b/contracts/interfaces/INodeDriver.sol similarity index 98% rename from contracts/sfc/NodeDriverI.sol rename to contracts/interfaces/INodeDriver.sol index 987eeb1..ae328bb 100644 --- a/contracts/sfc/NodeDriverI.sol +++ b/contracts/interfaces/INodeDriver.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.9; -interface NodeDriverI { +interface INodeDriver { function setGenesisValidator( address _auth, uint256 validatorID, diff --git a/contracts/interfaces/INodeDriverExecutable.sol b/contracts/interfaces/INodeDriverExecutable.sol new file mode 100644 index 0000000..9bc78cc --- /dev/null +++ b/contracts/interfaces/INodeDriverExecutable.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.9; + +interface INodeDriverExecutable { + function execute() external; +} diff --git a/contracts/sfc/SFCI.sol b/contracts/interfaces/ISfc.sol similarity index 99% rename from contracts/sfc/SFCI.sol rename to contracts/interfaces/ISfc.sol index 205fff2..f8b34ec 100644 --- a/contracts/sfc/SFCI.sol +++ b/contracts/interfaces/ISfc.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.9; -interface SFCI { +interface ISfc { event CreatedValidator( uint256 indexed validatorID, address indexed auth, diff --git a/contracts/sfc/NetworkInitializer.sol b/contracts/sfc/NetworkInitializer.sol index 51d6b5a..f247036 100644 --- a/contracts/sfc/NetworkInitializer.sol +++ b/contracts/sfc/NetworkInitializer.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.9; -import {SFCI} from "./SFCI.sol"; +import {ISfc} from "../interfaces/ISfc.sol"; import {NodeDriver, NodeDriverAuth} from "./NodeDriver.sol"; import {ConstantsManager} from "./ConstantsManager.sol"; import {Decimal} from "../common/Decimal.sol"; @@ -40,6 +40,6 @@ contract NetworkInitializer { consts.updateGasPriceBalancingCounterweight(3600); consts.transferOwnership(_owner); - SFCI(_sfc).initialize(sealedEpoch, totalSupply, _auth, _lib, address(consts), _owner); + ISfc(_sfc).initialize(sealedEpoch, totalSupply, _auth, _lib, address(consts), _owner); } } diff --git a/contracts/sfc/NodeDriver.sol b/contracts/sfc/NodeDriver.sol index c58e44c..3c2230c 100644 --- a/contracts/sfc/NodeDriver.sol +++ b/contracts/sfc/NodeDriver.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.9; import {Initializable} from "../common/Initializable.sol"; import {NodeDriverAuth} from "./NodeDriverAuth.sol"; -import {IEvmWriter} from "../interfaces/IEVMWriter.sol"; +import {IEvmWriter} from "../interfaces/IEvmWriter.sol"; contract NodeDriver is Initializable { NodeDriverAuth internal backend; diff --git a/contracts/sfc/NodeDriverAuth.sol b/contracts/sfc/NodeDriverAuth.sol index 2801fff..ef5cd03 100644 --- a/contracts/sfc/NodeDriverAuth.sol +++ b/contracts/sfc/NodeDriverAuth.sol @@ -3,15 +3,12 @@ pragma solidity ^0.8.9; import {Initializable} from "../common/Initializable.sol"; import {Ownable} from "../ownership/Ownable.sol"; -import {SFCI} from "./SFCI.sol"; +import {ISfc} from "../interfaces/ISfc.sol"; import {NodeDriver} from "./NodeDriver.sol"; - -interface NodeDriverExecutable { - function execute() external; -} +import {INodeDriverExecutable} from "../interfaces/INodeDriverExecutable.sol"; contract NodeDriverAuth is Initializable, Ownable { - SFCI internal sfc; + ISfc internal sfc; NodeDriver internal driver; error NotSFC(); @@ -25,7 +22,7 @@ contract NodeDriverAuth is Initializable, Ownable { function initialize(address payable _sfc, address _driver, address _owner) external initializer { Ownable.initialize(_owner); driver = NodeDriver(_driver); - sfc = SFCI(_sfc); + sfc = ISfc(_sfc); } modifier onlySFC() { @@ -48,7 +45,7 @@ contract NodeDriverAuth is Initializable, Ownable { function _execute(address executable, address newOwner, bytes32 selfCodeHash, bytes32 driverCodeHash) internal { _transferOwnership(executable); - NodeDriverExecutable(executable).execute(); + INodeDriverExecutable(executable).execute(); _transferOwnership(newOwner); //require(driver.backend() == address(this), "ownership of driver is lost"); if (_getCodeHash(address(this)) != selfCodeHash) { diff --git a/contracts/sfc/Updater.sol b/contracts/sfc/Updater.sol index 634167a..e681f7c 100644 --- a/contracts/sfc/Updater.sol +++ b/contracts/sfc/Updater.sol @@ -6,7 +6,7 @@ import {Decimal} from "../common/Decimal.sol"; import {NodeDriverAuth} from "./NodeDriverAuth.sol"; import {ConstantsManager} from "./ConstantsManager.sol"; import {SFC} from "./SFC.sol"; -import {SFCI} from "./SFCI.sol"; +import {ISfc} from "../interfaces/ISfc.sol"; import {Version} from "../version/Version.sol"; interface GovI { @@ -104,8 +104,8 @@ contract Updater { NodeDriverAuth nodeAuth = NodeDriverAuth(0xD100ae0000000000000000000000000000000000); nodeAuth.upgradeCode(sfcTo, sfcFrom); - SFCI(sfcTo).updateConstsAddress(sfcConsts); - SFCI(sfcTo).updateVoteBookAddress(voteBook); + ISfc(sfcTo).updateConstsAddress(sfcConsts); + ISfc(sfcTo).updateVoteBookAddress(voteBook); SFC(sfcTo).updateLibAddress(sfcLib); nodeAuth.upgradeCode(govTo, govFrom); diff --git a/contracts/test/StubEvmWriter.sol b/contracts/test/StubEvmWriter.sol index 1c50617..c0e0b4b 100644 --- a/contracts/test/StubEvmWriter.sol +++ b/contracts/test/StubEvmWriter.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.9; -import {IEvmWriter} from "../interfaces/IEVMWriter.sol"; +import {IEvmWriter} from "../interfaces/IEvmWriter.sol"; contract StubEvmWriter is IEvmWriter { function setBalance(address acc, uint256 value) external {} From 5815adb650c77decb82d639e99c13c48ea8b38cf Mon Sep 17 00:00:00 2001 From: Mike-CZ Date: Sun, 27 Oct 2024 17:15:33 +0100 Subject: [PATCH 2/8] Rename interfaces --- contracts/interfaces/{IEvmWriter.sol => IEVMWriter.sol} | 2 +- contracts/interfaces/{ISfc.sol => ISFC.sol} | 2 +- contracts/sfc/NetworkInitializer.sol | 4 ++-- contracts/sfc/NodeDriver.sol | 6 +++--- contracts/sfc/NodeDriverAuth.sol | 6 +++--- contracts/sfc/Updater.sol | 6 +++--- contracts/test/StubEvmWriter.sol | 4 ++-- 7 files changed, 15 insertions(+), 15 deletions(-) rename contracts/interfaces/{IEvmWriter.sol => IEVMWriter.sol} (94%) rename contracts/interfaces/{ISfc.sol => ISFC.sol} (99%) diff --git a/contracts/interfaces/IEvmWriter.sol b/contracts/interfaces/IEVMWriter.sol similarity index 94% rename from contracts/interfaces/IEvmWriter.sol rename to contracts/interfaces/IEVMWriter.sol index 575a7b4..02f14ad 100644 --- a/contracts/interfaces/IEvmWriter.sol +++ b/contracts/interfaces/IEVMWriter.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.9; -interface IEvmWriter { +interface IEVMWriter { function setBalance(address acc, uint256 value) external; function copyCode(address acc, address from) external; diff --git a/contracts/interfaces/ISfc.sol b/contracts/interfaces/ISFC.sol similarity index 99% rename from contracts/interfaces/ISfc.sol rename to contracts/interfaces/ISFC.sol index f8b34ec..4cceb8f 100644 --- a/contracts/interfaces/ISfc.sol +++ b/contracts/interfaces/ISFC.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.9; -interface ISfc { +interface ISFC { event CreatedValidator( uint256 indexed validatorID, address indexed auth, diff --git a/contracts/sfc/NetworkInitializer.sol b/contracts/sfc/NetworkInitializer.sol index f247036..7377e2b 100644 --- a/contracts/sfc/NetworkInitializer.sol +++ b/contracts/sfc/NetworkInitializer.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.9; -import {ISfc} from "../interfaces/ISfc.sol"; +import {ISFC} from "../interfaces/ISFC.sol"; import {NodeDriver, NodeDriverAuth} from "./NodeDriver.sol"; import {ConstantsManager} from "./ConstantsManager.sol"; import {Decimal} from "../common/Decimal.sol"; @@ -40,6 +40,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, _lib, address(consts), _owner); } } diff --git a/contracts/sfc/NodeDriver.sol b/contracts/sfc/NodeDriver.sol index 3c2230c..d8b7013 100644 --- a/contracts/sfc/NodeDriver.sol +++ b/contracts/sfc/NodeDriver.sol @@ -3,11 +3,11 @@ pragma solidity ^0.8.9; import {Initializable} from "../common/Initializable.sol"; import {NodeDriverAuth} from "./NodeDriverAuth.sol"; -import {IEvmWriter} from "../interfaces/IEvmWriter.sol"; +import {IEVMWriter} from "../interfaces/IEVMWriter.sol"; contract NodeDriver is Initializable { NodeDriverAuth internal backend; - IEvmWriter internal evmWriter; + IEVMWriter internal evmWriter; error NotNode(); error NotBackend(); @@ -36,7 +36,7 @@ contract NodeDriver is Initializable { function initialize(address _backend, address _evmWriterAddress) external initializer { backend = NodeDriverAuth(_backend); emit UpdatedBackend(_backend); - evmWriter = IEvmWriter(_evmWriterAddress); + evmWriter = IEVMWriter(_evmWriterAddress); } function setBalance(address acc, uint256 value) external onlyBackend { diff --git a/contracts/sfc/NodeDriverAuth.sol b/contracts/sfc/NodeDriverAuth.sol index ef5cd03..8a2a6ce 100644 --- a/contracts/sfc/NodeDriverAuth.sol +++ b/contracts/sfc/NodeDriverAuth.sol @@ -3,12 +3,12 @@ pragma solidity ^0.8.9; import {Initializable} from "../common/Initializable.sol"; import {Ownable} from "../ownership/Ownable.sol"; -import {ISfc} from "../interfaces/ISfc.sol"; +import {ISFC} from "../interfaces/ISFC.sol"; import {NodeDriver} from "./NodeDriver.sol"; import {INodeDriverExecutable} from "../interfaces/INodeDriverExecutable.sol"; contract NodeDriverAuth is Initializable, Ownable { - ISfc internal sfc; + ISFC internal sfc; NodeDriver internal driver; error NotSFC(); @@ -22,7 +22,7 @@ contract NodeDriverAuth is Initializable, Ownable { function initialize(address payable _sfc, address _driver, address _owner) external initializer { Ownable.initialize(_owner); driver = NodeDriver(_driver); - sfc = ISfc(_sfc); + sfc = ISFC(_sfc); } modifier onlySFC() { diff --git a/contracts/sfc/Updater.sol b/contracts/sfc/Updater.sol index e681f7c..6b148a5 100644 --- a/contracts/sfc/Updater.sol +++ b/contracts/sfc/Updater.sol @@ -6,7 +6,7 @@ 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 {ISFC} from "../interfaces/ISFC.sol"; import {Version} from "../version/Version.sol"; interface GovI { @@ -104,8 +104,8 @@ contract Updater { NodeDriverAuth nodeAuth = NodeDriverAuth(0xD100ae0000000000000000000000000000000000); nodeAuth.upgradeCode(sfcTo, sfcFrom); - ISfc(sfcTo).updateConstsAddress(sfcConsts); - ISfc(sfcTo).updateVoteBookAddress(voteBook); + ISFC(sfcTo).updateConstsAddress(sfcConsts); + ISFC(sfcTo).updateVoteBookAddress(voteBook); SFC(sfcTo).updateLibAddress(sfcLib); nodeAuth.upgradeCode(govTo, govFrom); diff --git a/contracts/test/StubEvmWriter.sol b/contracts/test/StubEvmWriter.sol index c0e0b4b..20f03fc 100644 --- a/contracts/test/StubEvmWriter.sol +++ b/contracts/test/StubEvmWriter.sol @@ -1,9 +1,9 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.9; -import {IEvmWriter} from "../interfaces/IEvmWriter.sol"; +import {IEVMWriter} from "../interfaces/IEVMWriter.sol"; -contract StubEvmWriter is IEvmWriter { +contract StubEvmWriter is IEVMWriter { function setBalance(address acc, uint256 value) external {} function copyCode(address acc, address from) external {} From 97aef3d15a7f985fad92e25e93e8c7a71a60d6a0 Mon Sep 17 00:00:00 2001 From: Mike-CZ Date: Sun, 27 Oct 2024 21:19:28 +0100 Subject: [PATCH 3/8] Merge `SFCLib` and `SFC` contracts --- contracts/interfaces/ISFC.sol | 1 - contracts/sfc/NetworkInitializer.sol | 3 +- contracts/sfc/SFC.sol | 1503 ++++++++++++++++++++++---- contracts/sfc/SFCBase.sol | 240 ---- contracts/sfc/SFCLib.sol | 842 --------------- contracts/sfc/SFCState.sol | 137 --- contracts/sfc/Updater.sol | 5 - contracts/test/UnitTestSFC.sol | 244 +---- hardhat.config.ts | 6 + test/NodeDriver.ts | 8 +- test/SFC.ts | 88 +- 11 files changed, 1374 insertions(+), 1703 deletions(-) delete mode 100644 contracts/sfc/SFCBase.sol delete mode 100644 contracts/sfc/SFCLib.sol delete mode 100644 contracts/sfc/SFCState.sol diff --git a/contracts/interfaces/ISFC.sol b/contracts/interfaces/ISFC.sol index 4cceb8f..54817a5 100644 --- a/contracts/interfaces/ISFC.sol +++ b/contracts/interfaces/ISFC.sol @@ -200,7 +200,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 7377e2b..86187ba 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, @@ -40,6 +39,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 0135406..e15f120 100644 --- a/contracts/sfc/SFC.sol +++ b/contracts/sfc/SFC.sol @@ -2,209 +2,1136 @@ 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()) +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; + + /** + * @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; + + struct Rewards { + uint256 lockupExtraReward; + uint256 lockupBaseReward; + uint256 unlockedReward; + } + + // 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 => Rewards)) 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; + + struct LockedDelegation { + uint256 lockedStake; + uint256 fromEpoch; + uint256 endTime; + uint256 duration; + } + + // delegator => validator ID => current stake (locked+unlocked) + mapping(address => mapping(uint256 => uint256)) public getStake; + + // delegator => validator ID => locked stake info + mapping(address => mapping(uint256 => LockedDelegation)) public getLockupInfo; + + mapping(address => mapping(uint256 => Rewards)) public getStashedLockupRewards; + + 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; + + ConstantsManager internal c; + + // the governance contract (to recalculate votes when the stake changes) + address public voteBookAddress; + + struct Penalty { + uint256 amount; + uint256 end; + } + // delegator => validatorID => penalties info + mapping(address => mapping(uint256 => Penalty[])) public getStashedPenalties; + + // 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; + } + + // 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 LockedStakeGreaterThanTotalStake(); + error InsufficientSelfStake(); + error NotEnoughUnlockedStake(); + error NotEnoughLockedStake(); + error NotEnoughTimePassed(); + error NotEnoughEpochsPassed(); + error StakeIsFullySlashed(); + error IncorrectDuration(); + error ValidatorLockupTooShort(); + error TooManyReLocks(); + error TooFrequentReLocks(); + error LockupDurationDecreased(); + error AlreadyLockedUp(); + error NotLockedUp(); + + // 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 lockupExtraReward, + uint256 lockupBaseReward, + uint256 unlockedReward + ); + event RestakedRewards( + address indexed delegator, + uint256 indexed toValidatorID, + uint256 lockupExtraReward, + uint256 lockupBaseReward, + uint256 unlockedReward + ); + event BurntFTM(uint256 amount); + event LockedUpStake(address indexed delegator, uint256 indexed validatorID, uint256 duration, uint256 amount); + event UnlockedStake(address indexed delegator, uint256 indexed validatorID, uint256 amount, uint256 penalty); + 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(); + } + _; + } + + /* + * Initializer + */ + function initialize( + uint256 sealedEpoch, + uint256 _totalSupply, + address nodeDriver, + address _c, + address owner + ) external initializer { + Ownable.initialize(owner); + currentSealedEpoch = sealedEpoch; + node = NodeDriverAuth(nodeDriver); + c = ConstantsManager(_c); + totalSupply = _totalSupply; + minGasPrice = GP.initialMinGasPrice(); + getEpochSnapshot[sealedEpoch].endTime = _now(); + } + + receive() external payable { + revert TransfersNotAllowed(); + } + + function migrateValidatorPubkeyUniquenessFlag(uint256 start, uint256 end) external { + for (uint256 vid = start; vid < end; vid++) { + bytes memory pubkey = getValidatorPubkey[vid]; + if (pubkey.length > 0 && pubkeyHashToValidatorID[keccak256(pubkey)] != vid) { + if (pubkeyHashToValidatorID[keccak256(pubkey)] != 0) { + revert PubkeyUsedByOtherValidator(); + } + pubkeyHashToValidatorID[keccak256(pubkey)] = vid; + } + } + } + + function updateValidatorPubkey(bytes calldata pubkey) external { + if (pubkey.length != 66 || pubkey[0] != 0xc0) { + revert MalformedPubkey(); + } + uint256 validatorID = getValidatorID[msg.sender]; + if (!_validatorExists(validatorID)) { + revert ValidatorNotExists(); + } + if (keccak256(pubkey) == keccak256(getValidatorPubkey[validatorID])) { + revert PubkeyNotChanged(); + } + if (pubkeyHashToValidatorID[keccak256(pubkey)] != 0) { + revert PubkeyUsedByOtherValidator(); + } + if (validatorPubkeyChanges[validatorID] != 0) { + revert TooManyPubkeyUpdates(); + } + + validatorPubkeyChanges[validatorID]++; + pubkeyHashToValidatorID[keccak256(pubkey)] = validatorID; + getValidatorPubkey[validatorID] = pubkey; + _syncValidator(validatorID, true); + } + + function setRedirectionAuthorizer(address v) external onlyOwner { + if (redirectionAuthorizer == v) { + revert SameRedirectionAuthorizer(); + } + redirectionAuthorizer = v; + } + + function announceRedirection(address to) external { + emit AnnouncedRedirection(msg.sender, to); + } + + function initiateRedirection(address from, address to) external { + if (msg.sender != redirectionAuthorizer) { + revert NotAuthorized(); + } + if (getRedirection[from] == to) { + revert AlreadyRedirected(); + } + if (from == to) { + revert SameAddress(); + } + getRedirectionRequest[from] = to; + } + + function redirect(address to) external { + address from = msg.sender; + if (to == address(0)) { + revert ZeroAddress(); + } + if (getRedirectionRequest[from] != to) { + revert RequestNotExists(); + } + getRedirection[from] = to; + 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, + uint256 lockedStake, + uint256 lockupFromEpoch, + uint256 lockupEndTime, + uint256 lockupDuration, + uint256 earlyUnlockPenalty, + uint256 rewards + ) external onlyDriver { + _rawDelegate(delegator, toValidatorID, stake, false); + _rewardsStash[delegator][toValidatorID].unlockedReward = rewards; + _mintNativeToken(stake); + if (lockedStake != 0) { + if (lockedStake > stake) { + revert LockedStakeGreaterThanTotalStake(); + } + LockedDelegation storage ld = getLockupInfo[delegator][toValidatorID]; + ld.lockedStake = lockedStake; + ld.fromEpoch = lockupFromEpoch; + ld.endTime = lockupEndTime; + ld.duration = lockupDuration; + getStashedLockupRewards[delegator][toValidatorID].lockupExtraReward = earlyUnlockPenalty; + emit LockedUpStake(delegator, toValidatorID, lockupDuration, lockedStake); + } + } + + 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 unlockStake(uint256 toValidatorID, uint256 amount) external returns (uint256) { + address delegator = msg.sender; + LockedDelegation storage ld = getLockupInfo[delegator][toValidatorID]; + + if (amount == 0) { + revert ZeroAmount(); + } + if (!isLockedUp(delegator, toValidatorID)) { + revert NotLockedUp(); + } + if (amount > ld.lockedStake) { + revert NotEnoughLockedStake(); + } + if (_redirected(delegator)) { + revert Redirected(); + } + + _stashRewards(delegator, toValidatorID); + + uint256 penalty = _popWholeUnlockPenalty(delegator, toValidatorID, amount, ld.lockedStake); + if (penalty > amount) { + penalty = amount; + } + ld.lockedStake -= amount; + if (penalty != 0) { + _rawUndelegate(delegator, toValidatorID, penalty, true, false, false); + (bool success, ) = treasuryAddress.call{value: penalty}(""); + if (!success) { + revert TransferFailed(); + } + } + + emit UnlockedStake(delegator, toValidatorID, amount, penalty); + return penalty; + } + + 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; + Rewards memory rewards = _claimRewards(delegator, toValidatorID); + // It's important that we transfer after erasing (protection against Re-Entrancy) + (bool sent, ) = _receiverOf(delegator).call{ + value: rewards.lockupExtraReward + rewards.lockupBaseReward + rewards.unlockedReward + }(""); + + if (!sent) { + revert TransferFailed(); + } + + emit ClaimedRewards( + delegator, + toValidatorID, + rewards.lockupExtraReward, + rewards.lockupBaseReward, + rewards.unlockedReward + ); + } + + function rewardsStash(address delegator, uint256 validatorID) public view returns (uint256) { + Rewards memory stash = _rewardsStash[delegator][validatorID]; + return stash.lockupBaseReward + stash.lockupExtraReward + stash.unlockedReward; + } + + function undelegate(uint256 toValidatorID, uint256 wrID, uint256 amount) public { + address delegator = msg.sender; + + _stashRewards(delegator, toValidatorID); + + if (amount == 0) { + revert ZeroAmount(); + } + + if (amount > getUnlockedStake(delegator, toValidatorID)) { + revert NotEnoughUnlockedStake(); + } + + 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; + Rewards memory rewards = _claimRewards(delegator, toValidatorID); + + uint256 lockupReward = rewards.lockupExtraReward + rewards.lockupBaseReward; + _delegate(delegator, toValidatorID, lockupReward + rewards.unlockedReward); + getLockupInfo[delegator][toValidatorID].lockedStake += lockupReward; + emit RestakedRewards( + delegator, + toValidatorID, + rewards.lockupExtraReward, + rewards.lockupBaseReward, + rewards.unlockedReward + ); + } + + function lockStake(uint256 toValidatorID, uint256 lockupDuration, uint256 amount) public { + address delegator = msg.sender; + if (amount == 0) { + revert ZeroAmount(); + } + if (isLockedUp(delegator, toValidatorID)) { + revert AlreadyLockedUp(); + } + _lockStake(delegator, toValidatorID, lockupDuration, amount, false); + } + + function relockStake(uint256 toValidatorID, uint256 lockupDuration, uint256 amount) public { + address delegator = msg.sender; + if (!isLockedUp(delegator, toValidatorID)) { + revert NotLockedUp(); + } + _lockStake(delegator, toValidatorID, lockupDuration, amount, true); + } + + 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) { + Rewards memory reward = _pendingRewards(delegator, toValidatorID); + return reward.unlockedReward + reward.lockupBaseReward + reward.lockupExtraReward; + } + + function getUnlockedStake(address delegator, uint256 toValidatorID) public view returns (uint256) { + if (!isLockedUp(delegator, toValidatorID)) { + return getStake[delegator][toValidatorID]; + } + return getStake[delegator][toValidatorID] - getLockupInfo[delegator][toValidatorID].lockedStake; + } + + function getLockedStake(address delegator, uint256 toValidatorID) public view returns (uint256) { + if (!isLockedUp(delegator, toValidatorID)) { + return 0; + } + return getLockupInfo[delegator][toValidatorID].lockedStake; + } + + function isLockedUp(address delegator, uint256 toValidatorID) public view returns (bool) { + return + getLockupInfo[delegator][toValidatorID].endTime != 0 && + getLockupInfo[delegator][toValidatorID].lockedStake != 0 && + _now() <= getLockupInfo[delegator][toValidatorID].endTime; + } + + 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); + } } - default { - return(0, returndatasize()) + 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(); } - } - // solhint-disable-next-line no-complex-fallback - fallback() external payable { - if (msg.data.length == 0) { - revert TransfersNotAllowed(); + 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; } - _delegate(libAddress); - } - receive() external payable { - revert TransfersNotAllowed(); - } + if (_now() < requestTime + c.withdrawalPeriodTime()) { + revert NotEnoughTimePassed(); + } - /* - Getters - */ + if (currentEpoch() < requestEpoch + c.withdrawalPeriodEpochs()) { + revert NotEnoughEpochsPassed(); + } - function getEpochValidatorIDs(uint256 epoch) public view returns (uint256[] memory) { - return getEpochSnapshot[epoch].validatorIDs; - } + uint256 amount = getWithdrawalRequest[delegator][toValidatorID][wrID].amount; + bool isCheater = isSlashed(toValidatorID); + uint256 penalty = getSlashingPenalty(amount, isCheater, slashingRefundRatio[toValidatorID]); + delete getWithdrawalRequest[delegator][toValidatorID][wrID]; - function getEpochReceivedStake(uint256 epoch, uint256 validatorID) public view returns (uint256) { - return getEpochSnapshot[epoch].receivedStake[validatorID]; - } + 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); - function getEpochAccumulatedRewardPerToken(uint256 epoch, uint256 validatorID) public view returns (uint256) { - return getEpochSnapshot[epoch].accumulatedRewardPerToken[validatorID]; + emit Withdrawn(delegator, toValidatorID, wrID, amount); } - function getEpochAccumulatedUptime(uint256 epoch, uint256 validatorID) public view returns (uint256) { - return getEpochSnapshot[epoch].accumulatedUptime[validatorID]; + 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 getEpochAccumulatedOriginatedTxsFee(uint256 epoch, uint256 validatorID) public view returns (uint256) { - return getEpochSnapshot[epoch].accumulatedOriginatedTxsFee[validatorID]; + // find highest epoch such that _isLockedUpAtEpoch returns true (using binary search) + function _highestLockupEpoch(address delegator, uint256 validatorID) internal view returns (uint256) { + uint256 fromEpoch = getLockupInfo[delegator][validatorID].fromEpoch; + uint256 r = currentSealedEpoch; + if (_isLockedUpAtEpoch(delegator, validatorID, r)) { + return r; + } + if (!_isLockedUpAtEpoch(delegator, validatorID, fromEpoch)) { + return 0; + } + if (fromEpoch > r) { + return 0; + } + while (fromEpoch < r) { + uint256 m = (fromEpoch + r) / 2; + if (_isLockedUpAtEpoch(delegator, validatorID, m)) { + fromEpoch = m + 1; + } else { + r = m; + } + } + if (r == 0) { + return 0; + } + return r - 1; } - function getEpochOfflineTime(uint256 epoch, uint256 validatorID) public view returns (uint256) { - return getEpochSnapshot[epoch].offlineTime[validatorID]; - } + function _newRewards(address delegator, uint256 toValidatorID) internal view returns (Rewards memory) { + uint256 stashedUntil = stashedRewardsUntilEpoch[delegator][toValidatorID]; + uint256 payableUntil = _highestPayableEpoch(toValidatorID); + uint256 lockedUntil = _highestLockupEpoch(delegator, toValidatorID); + if (lockedUntil > payableUntil) { + lockedUntil = payableUntil; + } + if (lockedUntil < stashedUntil) { + lockedUntil = stashedUntil; + } - function getEpochOfflineBlocks(uint256 epoch, uint256 validatorID) public view returns (uint256) { - return getEpochSnapshot[epoch].offlineBlocks[validatorID]; - } + LockedDelegation storage ld = getLockupInfo[delegator][toValidatorID]; + uint256 wholeStake = getStake[delegator][toValidatorID]; + uint256 unlockedStake = wholeStake - ld.lockedStake; + uint256 fullReward; - function getEpochEndBlock(uint256 epoch) public view returns (uint256) { - return getEpochSnapshot[epoch].endBlock; - } + // count reward for locked stake during lockup epochs + fullReward = _newRewardsOf(ld.lockedStake, toValidatorID, stashedUntil, lockedUntil); + Rewards memory plReward = _scaleLockupReward(fullReward, ld.duration); + // count reward for unlocked stake during lockup epochs + fullReward = _newRewardsOf(unlockedStake, toValidatorID, stashedUntil, lockedUntil); + Rewards memory puReward = _scaleLockupReward(fullReward, 0); + // count lockup reward for unlocked stake during unlocked epochs + fullReward = _newRewardsOf(wholeStake, toValidatorID, lockedUntil, payableUntil); + Rewards memory wuReward = _scaleLockupReward(fullReward, 0); - function rewardsStash(address delegator, uint256 validatorID) public view returns (uint256) { - Rewards memory stash = _rewardsStash[delegator][validatorID]; - return stash.lockupBaseReward + stash.lockupExtraReward + stash.unlockedReward; + return sumRewards(plReward, puReward, wuReward); } - /* - Constructor - */ + 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 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 _pendingRewards(address delegator, uint256 toValidatorID) internal view returns (Rewards memory) { + Rewards memory reward = _newRewards(delegator, toValidatorID); + return sumRewards(_rewardsStash[delegator][toValidatorID], reward); } - function updateLibAddress(address v) external onlyOwner { - libAddress = v; + function _stashRewards(address delegator, uint256 toValidatorID) internal returns (bool updated) { + Rewards memory nonStashedReward = _newRewards(delegator, toValidatorID); + stashedRewardsUntilEpoch[delegator][toValidatorID] = _highestPayableEpoch(toValidatorID); + _rewardsStash[delegator][toValidatorID] = sumRewards(_rewardsStash[delegator][toValidatorID], nonStashedReward); + getStashedLockupRewards[delegator][toValidatorID] = sumRewards( + getStashedLockupRewards[delegator][toValidatorID], + nonStashedReward + ); + if (!isLockedUp(delegator, toValidatorID)) { + delete getLockupInfo[delegator][toValidatorID]; + delete getStashedLockupRewards[delegator][toValidatorID]; + } + _truncateLegacyPenalty(delegator, toValidatorID); + return + nonStashedReward.lockupBaseReward != 0 || + nonStashedReward.lockupExtraReward != 0 || + nonStashedReward.unlockedReward != 0; } - function updateTreasuryAddress(address v) external onlyOwner { - treasuryAddress = v; + function _claimRewards(address delegator, uint256 toValidatorID) internal returns (Rewards memory rewards) { + _stashRewards(delegator, toValidatorID); + rewards = _rewardsStash[delegator][toValidatorID]; + uint256 totalReward = rewards.unlockedReward + rewards.lockupBaseReward + rewards.lockupExtraReward; + if (totalReward == 0) { + revert ZeroRewards(); + } + delete _rewardsStash[delegator][toValidatorID]; + // It's important that we mint after erasing (protection against Re-Entrancy) + _mintNativeToken(totalReward); + return rewards; } - function updateConstsAddress(address v) external onlyOwner { - c = ConstantsManager(v); + function _burnFTM(uint256 amount) internal { + if (amount != 0) { + payable(address(0)).transfer(amount); + emit BurntFTM(amount); + } } - function constsAddress() external view returns (address) { - return address(c); + function epochEndTime(uint256 epoch) internal view returns (uint256) { + return getEpochSnapshot[epoch].endTime; } - function updateVoteBookAddress(address v) external onlyOwner { - voteBookAddress = v; + function _isLockedUpAtEpoch(address delegator, uint256 toValidatorID, uint256 epoch) internal view returns (bool) { + return + getLockupInfo[delegator][toValidatorID].fromEpoch <= epoch && + epochEndTime(epoch) <= getLockupInfo[delegator][toValidatorID].endTime; } - function migrateValidatorPubkeyUniquenessFlag(uint256 start, uint256 end) external { - for (uint256 vid = start; vid < end; vid++) { - bytes memory pubkey = getValidatorPubkey[vid]; - if (pubkey.length > 0 && pubkeyHashToValidatorID[keccak256(pubkey)] != vid) { - if (pubkeyHashToValidatorID[keccak256(pubkey)] != 0) { - revert PubkeyUsedByOtherValidator(); - } - pubkeyHashToValidatorID[keccak256(pubkey)] = vid; - } + function _lockStake( + address delegator, + uint256 toValidatorID, + uint256 lockupDuration, + uint256 amount, + bool relock + ) internal { + if (_redirected(delegator)) { + revert Redirected(); } - } - function updateValidatorPubkey(bytes calldata pubkey) external { - if (pubkey.length != 66 || pubkey[0] != 0xc0) { - revert MalformedPubkey(); + if (amount > getUnlockedStake(delegator, toValidatorID)) { + revert NotEnoughUnlockedStake(); } - uint256 validatorID = getValidatorID[msg.sender]; - if (!_validatorExists(validatorID)) { - revert ValidatorNotExists(); + + if (getValidator[toValidatorID].status != OK_STATUS) { + revert ValidatorNotActive(); } - if (keccak256(pubkey) == keccak256(getValidatorPubkey[validatorID])) { - revert PubkeyNotChanged(); + + if (lockupDuration < c.minLockupDuration() || lockupDuration > c.maxLockupDuration()) { + revert IncorrectDuration(); } - if (pubkeyHashToValidatorID[keccak256(pubkey)] != 0) { - revert PubkeyUsedByOtherValidator(); + + uint256 endTime = _now() + lockupDuration; + address validatorAddr = getValidator[toValidatorID].auth; + if ( + delegator != validatorAddr && + getLockupInfo[validatorAddr][toValidatorID].endTime + 30 * 24 * 60 * 60 < endTime + ) { + revert ValidatorLockupTooShort(); } - if (validatorPubkeyChanges[validatorID] != 0) { - revert TooManyPubkeyUpdates(); + + _stashRewards(delegator, toValidatorID); + _delStalePenalties(delegator, toValidatorID); + + // stash the previous penalty and clean getStashedLockupRewards + LockedDelegation storage ld = getLockupInfo[delegator][toValidatorID]; + if (relock) { + Penalty[] storage penalties = getStashedPenalties[delegator][toValidatorID]; + + uint256 penalty = _popNonStashedUnlockPenalty(delegator, toValidatorID, ld.lockedStake, ld.lockedStake); + if (penalty != 0) { + penalties.push(Penalty(penalty, ld.endTime)); + if (penalties.length > 30) { + revert TooManyReLocks(); + } + if ( + amount <= ld.lockedStake / 100 && penalties.length > 3 && endTime < ld.endTime + 14 * 24 * 60 * 60 + ) { + revert TooFrequentReLocks(); + } + } } - validatorPubkeyChanges[validatorID]++; - pubkeyHashToValidatorID[keccak256(pubkey)] = validatorID; - getValidatorPubkey[validatorID] = pubkey; - _syncValidator(validatorID, true); + // check lockup duration after _stashRewards, which has erased previous lockup if it has unlocked already + if (lockupDuration < ld.duration) { + revert LockupDurationDecreased(); + } + + ld.lockedStake = ld.lockedStake + amount; + ld.fromEpoch = currentEpoch(); + ld.endTime = endTime; + ld.duration = lockupDuration; + + emit LockedUpStake(delegator, toValidatorID, lockupDuration, amount); } - function setRedirectionAuthorizer(address v) external onlyOwner { - if (redirectionAuthorizer == v) { - revert SameRedirectionAuthorizer(); - } - redirectionAuthorizer = v; + function _popNonStashedUnlockPenalty( + address delegator, + uint256 toValidatorID, + uint256 unlockAmount, + uint256 totalAmount + ) internal returns (uint256) { + Rewards storage r = getStashedLockupRewards[delegator][toValidatorID]; + uint256 lockupExtraRewardShare = (r.lockupExtraReward * unlockAmount) / totalAmount; + uint256 lockupBaseRewardShare = (r.lockupBaseReward * unlockAmount) / totalAmount; + uint256 penalty = lockupExtraRewardShare + lockupBaseRewardShare / 2; + r.lockupExtraReward = r.lockupExtraReward - lockupExtraRewardShare; + r.lockupBaseReward = r.lockupBaseReward - lockupBaseRewardShare; + return penalty; } - event AnnouncedRedirection(address indexed from, address indexed to); + function _popStashedUnlockPenalty( + address delegator, + uint256 toValidatorID, + uint256 unlockAmount, + uint256 totalAmount + ) internal returns (uint256) { + _delStalePenalties(delegator, toValidatorID); + Penalty[] storage penalties = getStashedPenalties[delegator][toValidatorID]; + uint256 total = 0; + for (uint256 i = 0; i < penalties.length; i++) { + uint256 penalty = (penalties[i].amount * unlockAmount) / totalAmount; + penalties[i].amount = penalties[i].amount - penalty; + total = total + penalty; + } + return total; + } - function announceRedirection(address to) external { - emit AnnouncedRedirection(msg.sender, to); + function _popWholeUnlockPenalty( + address delegator, + uint256 toValidatorID, + uint256 unlockAmount, + uint256 totalAmount + ) internal returns (uint256) { + uint256 nonStashed = _popNonStashedUnlockPenalty(delegator, toValidatorID, unlockAmount, totalAmount); + uint256 stashed = _popStashedUnlockPenalty(delegator, toValidatorID, unlockAmount, totalAmount); + return nonStashed + stashed; } - function initiateRedirection(address from, address to) external { - if (msg.sender != redirectionAuthorizer) { - revert NotAuthorized(); - } - if (getRedirection[from] == to) { - revert AlreadyRedirected(); - } - if (from == to) { - revert SameAddress(); + function _delStalePenalties(address delegator, uint256 toValidatorID) public { + Penalty[] storage penalties = getStashedPenalties[delegator][toValidatorID]; + for (uint256 i = 0; i < penalties.length; ) { + if (penalties[i].end < _now() || penalties[i].amount == 0) { + penalties[i] = penalties[penalties.length - 1]; + penalties.pop(); + } else { + i++; + } } - getRedirectionRequest[from] = to; } - function redirect(address to) external { - address from = msg.sender; + 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)) { - revert ZeroAddress(); - } - if (getRedirectionRequest[from] != to) { - revert RequestNotExists(); + return payable(address(uint160(addr))); } - getRedirection[from] = to; - getRedirectionRequest[from] = address(0); + return payable(address(uint160(to))); } /* @@ -232,14 +1159,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, @@ -368,44 +1287,266 @@ 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); + } + } + + // code below can be erased after 1 year since deployment of multipenalties + + function _getAvgEpochStep(uint256 duration) internal view virtual returns (uint256) { + // estimate number of epochs such that we would make approximately 15 iterations + uint256 tryEpochs = currentSealedEpoch / 5; + if (tryEpochs > 10000) { + tryEpochs = 10000; + } + uint256 tryEndTime = getEpochSnapshot[currentSealedEpoch - tryEpochs].endTime; + if (tryEndTime == 0 || tryEpochs == 0) { + return 0; + } + uint256 secondsPerEpoch = (_now() - tryEndTime) / tryEpochs; + return duration / (secondsPerEpoch * 15 + 1); + } + + function _getAvgReceivedStake(uint256 validatorID, uint256 duration, uint256 step) internal view returns (uint256) { + uint256 receivedStakeSum = getValidator[validatorID].receivedStake; + uint256 samples = 1; + + uint256 until = _now() - duration; + for (uint256 i = 1; i <= 30; i++) { + uint256 e = currentSealedEpoch - i * step; + EpochSnapshot storage s = getEpochSnapshot[e]; + if (s.endTime < until) { + break; + } + uint256 sample = s.receivedStake[validatorID]; + if (sample != 0) { + samples++; + receivedStakeSum += sample; } - _sealEpochRewards(epochDuration, snapshot, prevSnapshot, validatorIDs, uptimes, originatedTxsFee); - _sealEpochMinGasPrice(epochDuration, epochGas); } + return receivedStakeSum / samples; + } - currentSealedEpoch = currentEpoch(); - snapshot.endTime = _now(); - snapshot.endBlock = block.number; - snapshot.baseRewardPerSecond = c.baseRewardPerSecond(); - snapshot.totalSupply = totalSupply; + function _getAvgUptime( + uint256 validatorID, + uint256 duration, + uint256 step + ) internal view virtual returns (uint256) { + uint256 until = _now() - duration; + uint256 oldUptimeCounter = 0; + uint256 newUptimeCounter = 0; + for (uint256 i = 0; i <= 30; i++) { + uint256 e = currentSealedEpoch - i * step; + EpochSnapshot storage s = getEpochSnapshot[e]; + uint256 endTime = s.endTime; + if (endTime < until) { + if (i <= 2) { + return duration; + } + break; + } + uint256 uptimeCounter = s.accumulatedUptime[validatorID]; + if (uptimeCounter != 0) { + oldUptimeCounter = uptimeCounter; + if (newUptimeCounter == 0) { + newUptimeCounter = uptimeCounter; + } + } + } + uint256 uptime = newUptimeCounter - oldUptimeCounter; + if (uptime > (duration * 4) / 5) { + return duration; + } + return uptime; } - 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 _truncateLegacyPenalty(address delegator, uint256 toValidatorID) internal { + Rewards storage r = getStashedLockupRewards[delegator][toValidatorID]; + uint256 storedPenalty = r.lockupExtraReward + r.lockupBaseReward / 2; + if (storedPenalty == 0) { + return; } - snapshot.validatorIDs = nextValidatorIDs; - node.updateMinGasPrice(minGasPrice); + LockedDelegation storage ld = getLockupInfo[delegator][toValidatorID]; + uint256 duration = ld.duration; + uint256 lockedStake = ld.lockedStake; + uint256 step = _getAvgEpochStep(duration); + if (step == 0) { + return; + } + uint256 rps = (_getAvgUptime(toValidatorID, duration, step) * 2092846271) / duration; // corresponds to 6.6% APR + uint256 selfStake = getStake[delegator][toValidatorID]; + + uint256 avgFullReward = (((selfStake * rps * duration) / 1e18) * (Decimal.unit() - c.validatorCommission())) / + Decimal.unit(); // reward for self-stake + if (getValidator[toValidatorID].auth == delegator) { + // reward for received portion of stake + uint256 receivedStakeAvg = (_getAvgReceivedStake(toValidatorID, duration, step) * 11) / 10; + avgFullReward += (((receivedStakeAvg * rps * duration) / 1e18) * c.validatorCommission()) / Decimal.unit(); + } + avgFullReward = (avgFullReward * lockedStake) / selfStake; + Rewards memory avgReward = _scaleLockupReward(avgFullReward, duration); + uint256 maxReasonablePenalty = avgReward.lockupBaseReward / 2 + avgReward.lockupExtraReward; + maxReasonablePenalty = maxReasonablePenalty; + if (storedPenalty > maxReasonablePenalty) { + r.lockupExtraReward = (r.lockupExtraReward * maxReasonablePenalty) / storedPenalty; + r.lockupBaseReward = (r.lockupBaseReward * maxReasonablePenalty) / storedPenalty; + } + } + + 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 sumRewards(Rewards memory a, Rewards memory b) internal pure returns (Rewards memory) { + return + Rewards( + a.lockupExtraReward + b.lockupExtraReward, + a.lockupBaseReward + b.lockupBaseReward, + a.unlockedReward + b.unlockedReward + ); + } + + function sumRewards(Rewards memory _a, Rewards memory _b, Rewards memory _c) internal pure returns (Rewards memory) { + return sumRewards(sumRewards(_a, _b), _c); + } + + function _scaleLockupReward( + uint256 fullReward, + uint256 lockupDuration + ) internal view returns (Rewards memory reward) { + reward = Rewards(0, 0, 0); + uint256 unlockedRewardRatio = c.unlockedRewardRatio(); + if (lockupDuration != 0) { + uint256 maxLockupExtraRatio = Decimal.unit() - unlockedRewardRatio; + uint256 lockupExtraRatio = (maxLockupExtraRatio * lockupDuration) / c.maxLockupDuration(); + uint256 totalScaledReward = (fullReward * (unlockedRewardRatio + lockupExtraRatio)) / Decimal.unit(); + reward.lockupBaseReward = (fullReward * unlockedRewardRatio) / Decimal.unit(); + reward.lockupExtraReward = totalScaledReward - reward.lockupBaseReward; + } else { + reward.unlockedReward = (fullReward * unlockedRewardRatio) / Decimal.unit(); + } + return reward; + } + + 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/SFCBase.sol b/contracts/sfc/SFCBase.sol deleted file mode 100644 index 75016da..0000000 --- a/contracts/sfc/SFCBase.sol +++ /dev/null @@ -1,240 +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 LockedStakeGreaterThanTotalStake(); - error InsufficientSelfStake(); - error NotEnoughUnlockedStake(); - error NotEnoughLockedStake(); - error NotEnoughTimePassed(); - error NotEnoughEpochsPassed(); - error StakeIsFullySlashed(); - error IncorrectDuration(); - error ValidatorLockupTooShort(); - error TooManyReLocks(); - error TooFrequentReLocks(); - error LockupDurationDecreased(); - error AlreadyLockedUp(); - error NotLockedUp(); - - // 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 sumRewards(Rewards memory a, Rewards memory b) internal pure returns (Rewards memory) { - return - Rewards( - a.lockupExtraReward + b.lockupExtraReward, - a.lockupBaseReward + b.lockupBaseReward, - a.unlockedReward + b.unlockedReward - ); - } - - function sumRewards(Rewards memory a, Rewards memory b, Rewards memory c) internal pure returns (Rewards memory) { - return sumRewards(sumRewards(a, b), c); - } - - function _scaleLockupReward( - uint256 fullReward, - uint256 lockupDuration - ) internal view returns (Rewards memory reward) { - reward = Rewards(0, 0, 0); - uint256 unlockedRewardRatio = c.unlockedRewardRatio(); - if (lockupDuration != 0) { - uint256 maxLockupExtraRatio = Decimal.unit() - unlockedRewardRatio; - uint256 lockupExtraRatio = (maxLockupExtraRatio * lockupDuration) / c.maxLockupDuration(); - uint256 totalScaledReward = (fullReward * (unlockedRewardRatio + lockupExtraRatio)) / Decimal.unit(); - reward.lockupBaseReward = (fullReward * unlockedRewardRatio) / Decimal.unit(); - reward.lockupExtraReward = totalScaledReward - reward.lockupBaseReward; - } else { - reward.unlockedReward = (fullReward * unlockedRewardRatio) / Decimal.unit(); - } - return reward; - } - - 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 getLockedStake(address delegator, uint256 toValidatorID) public view returns (uint256) { - if (!isLockedUp(delegator, toValidatorID)) { - return 0; - } - return getLockupInfo[delegator][toValidatorID].lockedStake; - } - - function isLockedUp(address delegator, uint256 toValidatorID) public view returns (bool) { - return - getLockupInfo[delegator][toValidatorID].endTime != 0 && - getLockupInfo[delegator][toValidatorID].lockedStake != 0 && - _now() <= getLockupInfo[delegator][toValidatorID].endTime; - } - - 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 33f660c..0000000 --- a/contracts/sfc/SFCLib.sol +++ /dev/null @@ -1,842 +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 lockupExtraReward, - uint256 lockupBaseReward, - uint256 unlockedReward - ); - event RestakedRewards( - address indexed delegator, - uint256 indexed toValidatorID, - uint256 lockupExtraReward, - uint256 lockupBaseReward, - uint256 unlockedReward - ); - event BurntFTM(uint256 amount); - event LockedUpStake(address indexed delegator, uint256 indexed validatorID, uint256 duration, uint256 amount); - event UnlockedStake(address indexed delegator, uint256 indexed validatorID, uint256 amount, uint256 penalty); - 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, - uint256 lockedStake, - uint256 lockupFromEpoch, - uint256 lockupEndTime, - uint256 lockupDuration, - uint256 earlyUnlockPenalty, - uint256 rewards - ) external onlyDriver { - _rawDelegate(delegator, toValidatorID, stake, false); - _rewardsStash[delegator][toValidatorID].unlockedReward = rewards; - _mintNativeToken(stake); - if (lockedStake != 0) { - if (lockedStake > stake) { - revert LockedStakeGreaterThanTotalStake(); - } - LockedDelegation storage ld = getLockupInfo[delegator][toValidatorID]; - ld.lockedStake = lockedStake; - ld.fromEpoch = lockupFromEpoch; - ld.endTime = lockupEndTime; - ld.duration = lockupDuration; - getStashedLockupRewards[delegator][toValidatorID].lockupExtraReward = earlyUnlockPenalty; - emit LockedUpStake(delegator, toValidatorID, lockupDuration, lockedStake); - } - } - - /* - 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 (amount > getUnlockedStake(delegator, toValidatorID)) { - revert NotEnoughUnlockedStake(); - } - - 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; - } - - // find highest epoch such that _isLockedUpAtEpoch returns true (using binary search) - function _highestLockupEpoch(address delegator, uint256 validatorID) internal view returns (uint256) { - uint256 fromEpoch = getLockupInfo[delegator][validatorID].fromEpoch; - uint256 r = currentSealedEpoch; - if (_isLockedUpAtEpoch(delegator, validatorID, r)) { - return r; - } - if (!_isLockedUpAtEpoch(delegator, validatorID, fromEpoch)) { - return 0; - } - if (fromEpoch > r) { - return 0; - } - while (fromEpoch < r) { - uint256 m = (fromEpoch + r) / 2; - if (_isLockedUpAtEpoch(delegator, validatorID, m)) { - fromEpoch = m + 1; - } else { - r = m; - } - } - if (r == 0) { - return 0; - } - return r - 1; - } - - function _newRewards(address delegator, uint256 toValidatorID) internal view returns (Rewards memory) { - uint256 stashedUntil = stashedRewardsUntilEpoch[delegator][toValidatorID]; - uint256 payableUntil = _highestPayableEpoch(toValidatorID); - uint256 lockedUntil = _highestLockupEpoch(delegator, toValidatorID); - if (lockedUntil > payableUntil) { - lockedUntil = payableUntil; - } - if (lockedUntil < stashedUntil) { - lockedUntil = stashedUntil; - } - - LockedDelegation storage ld = getLockupInfo[delegator][toValidatorID]; - uint256 wholeStake = getStake[delegator][toValidatorID]; - uint256 unlockedStake = wholeStake - ld.lockedStake; - uint256 fullReward; - - // count reward for locked stake during lockup epochs - fullReward = _newRewardsOf(ld.lockedStake, toValidatorID, stashedUntil, lockedUntil); - Rewards memory plReward = _scaleLockupReward(fullReward, ld.duration); - // count reward for unlocked stake during lockup epochs - fullReward = _newRewardsOf(unlockedStake, toValidatorID, stashedUntil, lockedUntil); - Rewards memory puReward = _scaleLockupReward(fullReward, 0); - // count lockup reward for unlocked stake during unlocked epochs - fullReward = _newRewardsOf(wholeStake, toValidatorID, lockedUntil, payableUntil); - Rewards memory wuReward = _scaleLockupReward(fullReward, 0); - - return sumRewards(plReward, puReward, wuReward); - } - - 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) internal view returns (Rewards memory) { - Rewards memory reward = _newRewards(delegator, toValidatorID); - return sumRewards(_rewardsStash[delegator][toValidatorID], reward); - } - - function pendingRewards(address delegator, uint256 toValidatorID) public view returns (uint256) { - Rewards memory reward = _pendingRewards(delegator, toValidatorID); - return reward.unlockedReward + reward.lockupBaseReward + reward.lockupExtraReward; - } - - function stashRewards(address delegator, uint256 toValidatorID) external { - if (!_stashRewards(delegator, toValidatorID)) { - revert NothingToStash(); - } - } - - function _stashRewards(address delegator, uint256 toValidatorID) internal returns (bool updated) { - Rewards memory nonStashedReward = _newRewards(delegator, toValidatorID); - stashedRewardsUntilEpoch[delegator][toValidatorID] = _highestPayableEpoch(toValidatorID); - _rewardsStash[delegator][toValidatorID] = sumRewards(_rewardsStash[delegator][toValidatorID], nonStashedReward); - getStashedLockupRewards[delegator][toValidatorID] = sumRewards( - getStashedLockupRewards[delegator][toValidatorID], - nonStashedReward - ); - if (!isLockedUp(delegator, toValidatorID)) { - delete getLockupInfo[delegator][toValidatorID]; - delete getStashedLockupRewards[delegator][toValidatorID]; - } - _truncateLegacyPenalty(delegator, toValidatorID); - return - nonStashedReward.lockupBaseReward != 0 || - nonStashedReward.lockupExtraReward != 0 || - nonStashedReward.unlockedReward != 0; - } - - function _claimRewards(address delegator, uint256 toValidatorID) internal returns (Rewards memory rewards) { - _stashRewards(delegator, toValidatorID); - rewards = _rewardsStash[delegator][toValidatorID]; - uint256 totalReward = rewards.unlockedReward + rewards.lockupBaseReward + rewards.lockupExtraReward; - if (totalReward == 0) { - revert ZeroRewards(); - } - delete _rewardsStash[delegator][toValidatorID]; - // It's important that we mint after erasing (protection against Re-Entrancy) - _mintNativeToken(totalReward); - return rewards; - } - - function claimRewards(uint256 toValidatorID) public { - address delegator = msg.sender; - Rewards memory rewards = _claimRewards(delegator, toValidatorID); - // It's important that we transfer after erasing (protection against Re-Entrancy) - (bool sent, ) = _receiverOf(delegator).call{ - value: rewards.lockupExtraReward + rewards.lockupBaseReward + rewards.unlockedReward - }(""); - - if (!sent) { - revert TransferFailed(); - } - - emit ClaimedRewards( - delegator, - toValidatorID, - rewards.lockupExtraReward, - rewards.lockupBaseReward, - rewards.unlockedReward - ); - } - - function restakeRewards(uint256 toValidatorID) public { - address delegator = msg.sender; - Rewards memory rewards = _claimRewards(delegator, toValidatorID); - - uint256 lockupReward = rewards.lockupExtraReward + rewards.lockupBaseReward; - _delegate(delegator, toValidatorID, lockupReward + rewards.unlockedReward); - getLockupInfo[delegator][toValidatorID].lockedStake += lockupReward; - emit RestakedRewards( - delegator, - toValidatorID, - rewards.lockupExtraReward, - rewards.lockupBaseReward, - rewards.unlockedReward - ); - } - - // 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 _isLockedUpAtEpoch(address delegator, uint256 toValidatorID, uint256 epoch) internal view returns (bool) { - return - getLockupInfo[delegator][toValidatorID].fromEpoch <= epoch && - epochEndTime(epoch) <= getLockupInfo[delegator][toValidatorID].endTime; - } - - function getUnlockedStake(address delegator, uint256 toValidatorID) public view returns (uint256) { - if (!isLockedUp(delegator, toValidatorID)) { - return getStake[delegator][toValidatorID]; - } - return getStake[delegator][toValidatorID] - getLockupInfo[delegator][toValidatorID].lockedStake; - } - - function _lockStake( - address delegator, - uint256 toValidatorID, - uint256 lockupDuration, - uint256 amount, - bool relock - ) internal { - if (_redirected(delegator)) { - revert Redirected(); - } - - if (amount > getUnlockedStake(delegator, toValidatorID)) { - revert NotEnoughUnlockedStake(); - } - - if (getValidator[toValidatorID].status != OK_STATUS) { - revert ValidatorNotActive(); - } - - if (lockupDuration < c.minLockupDuration() || lockupDuration > c.maxLockupDuration()) { - revert IncorrectDuration(); - } - - uint256 endTime = _now() + lockupDuration; - address validatorAddr = getValidator[toValidatorID].auth; - if ( - delegator != validatorAddr && - getLockupInfo[validatorAddr][toValidatorID].endTime + 30 * 24 * 60 * 60 < endTime - ) { - revert ValidatorLockupTooShort(); - } - - _stashRewards(delegator, toValidatorID); - _delStalePenalties(delegator, toValidatorID); - - // stash the previous penalty and clean getStashedLockupRewards - LockedDelegation storage ld = getLockupInfo[delegator][toValidatorID]; - if (relock) { - Penalty[] storage penalties = getStashedPenalties[delegator][toValidatorID]; - - uint256 penalty = _popNonStashedUnlockPenalty(delegator, toValidatorID, ld.lockedStake, ld.lockedStake); - if (penalty != 0) { - penalties.push(Penalty(penalty, ld.endTime)); - if (penalties.length > 30) { - revert TooManyReLocks(); - } - if ( - amount <= ld.lockedStake / 100 && penalties.length > 3 && endTime < ld.endTime + 14 * 24 * 60 * 60 - ) { - revert TooFrequentReLocks(); - } - } - } - - // check lockup duration after _stashRewards, which has erased previous lockup if it has unlocked already - if (lockupDuration < ld.duration) { - revert LockupDurationDecreased(); - } - - ld.lockedStake = ld.lockedStake + amount; - ld.fromEpoch = currentEpoch(); - ld.endTime = endTime; - ld.duration = lockupDuration; - - emit LockedUpStake(delegator, toValidatorID, lockupDuration, amount); - } - - function lockStake(uint256 toValidatorID, uint256 lockupDuration, uint256 amount) public { - address delegator = msg.sender; - if (amount == 0) { - revert ZeroAmount(); - } - if (isLockedUp(delegator, toValidatorID)) { - revert AlreadyLockedUp(); - } - _lockStake(delegator, toValidatorID, lockupDuration, amount, false); - } - - function relockStake(uint256 toValidatorID, uint256 lockupDuration, uint256 amount) public { - address delegator = msg.sender; - if (!isLockedUp(delegator, toValidatorID)) { - revert NotLockedUp(); - } - _lockStake(delegator, toValidatorID, lockupDuration, amount, true); - } - - function _popNonStashedUnlockPenalty( - address delegator, - uint256 toValidatorID, - uint256 unlockAmount, - uint256 totalAmount - ) internal returns (uint256) { - Rewards storage r = getStashedLockupRewards[delegator][toValidatorID]; - uint256 lockupExtraRewardShare = (r.lockupExtraReward * unlockAmount) / totalAmount; - uint256 lockupBaseRewardShare = (r.lockupBaseReward * unlockAmount) / totalAmount; - uint256 penalty = lockupExtraRewardShare + lockupBaseRewardShare / 2; - r.lockupExtraReward = r.lockupExtraReward - lockupExtraRewardShare; - r.lockupBaseReward = r.lockupBaseReward - lockupBaseRewardShare; - return penalty; - } - - function _popStashedUnlockPenalty( - address delegator, - uint256 toValidatorID, - uint256 unlockAmount, - uint256 totalAmount - ) internal returns (uint256) { - _delStalePenalties(delegator, toValidatorID); - Penalty[] storage penalties = getStashedPenalties[delegator][toValidatorID]; - uint256 total = 0; - for (uint256 i = 0; i < penalties.length; i++) { - uint256 penalty = (penalties[i].amount * unlockAmount) / totalAmount; - penalties[i].amount = penalties[i].amount - penalty; - total = total + penalty; - } - return total; - } - - function _popWholeUnlockPenalty( - address delegator, - uint256 toValidatorID, - uint256 unlockAmount, - uint256 totalAmount - ) internal returns (uint256) { - uint256 nonStashed = _popNonStashedUnlockPenalty(delegator, toValidatorID, unlockAmount, totalAmount); - uint256 stashed = _popStashedUnlockPenalty(delegator, toValidatorID, unlockAmount, totalAmount); - return nonStashed + stashed; - } - - function unlockStake(uint256 toValidatorID, uint256 amount) external returns (uint256) { - address delegator = msg.sender; - LockedDelegation storage ld = getLockupInfo[delegator][toValidatorID]; - - if (amount == 0) { - revert ZeroAmount(); - } - if (!isLockedUp(delegator, toValidatorID)) { - revert NotLockedUp(); - } - if (amount > ld.lockedStake) { - revert NotEnoughLockedStake(); - } - if (_redirected(delegator)) { - revert Redirected(); - } - - _stashRewards(delegator, toValidatorID); - - uint256 penalty = _popWholeUnlockPenalty(delegator, toValidatorID, amount, ld.lockedStake); - if (penalty > amount) { - penalty = amount; - } - ld.lockedStake -= amount; - if (penalty != 0) { - _rawUndelegate(delegator, toValidatorID, penalty, true, false, false); - (bool success, ) = treasuryAddress.call{value: penalty}(""); - if (!success) { - revert TransferFailed(); - } - } - - emit UnlockedStake(delegator, toValidatorID, amount, penalty); - return penalty; - } - - 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 _delStalePenalties(address delegator, uint256 toValidatorID) public { - Penalty[] storage penalties = getStashedPenalties[delegator][toValidatorID]; - for (uint256 i = 0; i < penalties.length; ) { - if (penalties[i].end < _now() || penalties[i].amount == 0) { - penalties[i] = penalties[penalties.length - 1]; - penalties.pop(); - } else { - i++; - } - } - } - - 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))); - } - - // code below can be erased after 1 year since deployment of multipenalties - - function _getAvgEpochStep(uint256 duration) internal view virtual returns (uint256) { - // estimate number of epochs such that we would make approximately 15 iterations - uint256 tryEpochs = currentSealedEpoch / 5; - if (tryEpochs > 10000) { - tryEpochs = 10000; - } - uint256 tryEndTime = getEpochSnapshot[currentSealedEpoch - tryEpochs].endTime; - if (tryEndTime == 0 || tryEpochs == 0) { - return 0; - } - uint256 secondsPerEpoch = (_now() - tryEndTime) / tryEpochs; - return duration / (secondsPerEpoch * 15 + 1); - } - - function _getAvgReceivedStake(uint256 validatorID, uint256 duration, uint256 step) internal view returns (uint256) { - uint256 receivedStakeSum = getValidator[validatorID].receivedStake; - uint256 samples = 1; - - uint256 until = _now() - duration; - for (uint256 i = 1; i <= 30; i++) { - uint256 e = currentSealedEpoch - i * step; - EpochSnapshot storage s = getEpochSnapshot[e]; - if (s.endTime < until) { - break; - } - uint256 sample = s.receivedStake[validatorID]; - if (sample != 0) { - samples++; - receivedStakeSum += sample; - } - } - return receivedStakeSum / samples; - } - - function _getAvgUptime( - uint256 validatorID, - uint256 duration, - uint256 step - ) internal view virtual returns (uint256) { - uint256 until = _now() - duration; - uint256 oldUptimeCounter = 0; - uint256 newUptimeCounter = 0; - for (uint256 i = 0; i <= 30; i++) { - uint256 e = currentSealedEpoch - i * step; - EpochSnapshot storage s = getEpochSnapshot[e]; - uint256 endTime = s.endTime; - if (endTime < until) { - if (i <= 2) { - return duration; - } - break; - } - uint256 uptimeCounter = s.accumulatedUptime[validatorID]; - if (uptimeCounter != 0) { - oldUptimeCounter = uptimeCounter; - if (newUptimeCounter == 0) { - newUptimeCounter = uptimeCounter; - } - } - } - uint256 uptime = newUptimeCounter - oldUptimeCounter; - if (uptime > (duration * 4) / 5) { - return duration; - } - return uptime; - } - - function _truncateLegacyPenalty(address delegator, uint256 toValidatorID) internal { - Rewards storage r = getStashedLockupRewards[delegator][toValidatorID]; - uint256 storedPenalty = r.lockupExtraReward + r.lockupBaseReward / 2; - if (storedPenalty == 0) { - return; - } - LockedDelegation storage ld = getLockupInfo[delegator][toValidatorID]; - uint256 duration = ld.duration; - uint256 lockedStake = ld.lockedStake; - uint256 step = _getAvgEpochStep(duration); - if (step == 0) { - return; - } - uint256 rps = (_getAvgUptime(toValidatorID, duration, step) * 2092846271) / duration; // corresponds to 6.6% APR - uint256 selfStake = getStake[delegator][toValidatorID]; - - uint256 avgFullReward = (((selfStake * rps * duration) / 1e18) * (Decimal.unit() - c.validatorCommission())) / - Decimal.unit(); // reward for self-stake - if (getValidator[toValidatorID].auth == delegator) { - // reward for received portion of stake - uint256 receivedStakeAvg = (_getAvgReceivedStake(toValidatorID, duration, step) * 11) / 10; - avgFullReward += (((receivedStakeAvg * rps * duration) / 1e18) * c.validatorCommission()) / Decimal.unit(); - } - avgFullReward = (avgFullReward * lockedStake) / selfStake; - Rewards memory avgReward = _scaleLockupReward(avgFullReward, duration); - uint256 maxReasonablePenalty = avgReward.lockupBaseReward / 2 + avgReward.lockupExtraReward; - maxReasonablePenalty = maxReasonablePenalty; - if (storedPenalty > maxReasonablePenalty) { - r.lockupExtraReward = (r.lockupExtraReward * maxReasonablePenalty) / storedPenalty; - r.lockupBaseReward = (r.lockupBaseReward * maxReasonablePenalty) / storedPenalty; - } - } -} diff --git a/contracts/sfc/SFCState.sol b/contracts/sfc/SFCState.sol deleted file mode 100644 index 68906db..0000000 --- a/contracts/sfc/SFCState.sol +++ /dev/null @@ -1,137 +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; - - struct Rewards { - uint256 lockupExtraReward; - uint256 lockupBaseReward; - uint256 unlockedReward; - } - - // 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 => Rewards)) 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; - - struct LockedDelegation { - uint256 lockedStake; - uint256 fromEpoch; - uint256 endTime; - uint256 duration; - } - - // delegator => validator ID => current stake (locked+unlocked) - mapping(address => mapping(uint256 => uint256)) public getStake; - - // delegator => validator ID => locked stake info - mapping(address => mapping(uint256 => LockedDelegation)) public getLockupInfo; - - mapping(address => mapping(uint256 => Rewards)) public getStashedLockupRewards; - - 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; - - struct Penalty { - uint256 amount; - uint256 end; - } - // delegator => validatorID => penalties info - mapping(address => mapping(uint256 => Penalty[])) public getStashedPenalties; - - // 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 6b148a5..d4c4428 100644 --- a/contracts/sfc/Updater.sol +++ b/contracts/sfc/Updater.sol @@ -23,7 +23,6 @@ interface GovVersion { contract Updater { address public sfcFrom; - address public sfcLib; address public sfcConsts; address public govTo; address public govFrom; @@ -38,7 +37,6 @@ contract Updater { constructor( address _sfcFrom, - address _sfcLib, address _sfcConsts, address _govTo, address _govFrom, @@ -46,7 +44,6 @@ contract Updater { address _owner ) { sfcFrom = _sfcFrom; - sfcLib = _sfcLib; sfcConsts = _sfcConsts; govTo = _govTo; govFrom = _govFrom; @@ -55,7 +52,6 @@ contract Updater { address sfcTo = address(0xFC00FACE00000000000000000000000000000000); if ( sfcFrom == address(0) || - sfcLib == address(0) || sfcConsts == address(0) || govTo == address(0) || govFrom == address(0) || @@ -106,7 +102,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 db4176d..f75896b 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,9 +35,7 @@ contract UnitTestSFCBase { function disableNonNodeCalls() external { allowedNonNodeCalls = false; } -} -contract UnitTestSFC is SFC, UnitTestSFCBase { function _now() internal view override returns (uint256) { return time; } @@ -47,26 +44,13 @@ contract UnitTestSFC is SFC, UnitTestSFCBase { if (allowedNonNodeCalls) { return true; } - return SFCBase.isNode(addr); + return SFC.isNode(addr); } -} -contract UnitTestSFCLib is SFCLib, UnitTestSFCBase { function highestLockupEpoch(address delegator, uint256 validatorID) external view returns (uint256) { return _highestLockupEpoch(delegator, validatorID); } - 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); - } - function _getAvgEpochStep(uint256) internal pure override returns (uint256) { return 1; } @@ -81,7 +65,6 @@ contract UnitTestNetworkInitializer { uint256 sealedEpoch, uint256 totalSupply, address payable _sfc, - address _lib, address _auth, address _driver, address _evmWriter, @@ -109,225 +92,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 getLockupInfo( - address, - uint256 - ) external view returns (uint256 lockedStake, uint256 fromEpoch, uint256 endTime, uint256 duration); - - function getStake(address, uint256) external view returns (uint256); - - function getStashedLockupRewards( - address, - uint256 - ) external view returns (uint256 lockupExtraReward, uint256 lockupBaseReward, uint256 unlockedReward); - - 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 totalSlashedStake() 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 getLockedStake(address delegator, uint256 toValidatorID) 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 isLockedUp(address delegator, uint256 toValidatorID) external view returns (bool); - - function getUnlockedStake(address delegator, uint256 toValidatorID) external view returns (uint256); - - function lockStake(uint256 toValidatorID, uint256 lockupDuration, uint256 amount) external; - - function relockStake(uint256 toValidatorID, uint256 lockupDuration, uint256 amount) external; - - function unlockStake(uint256 toValidatorID, uint256 amount) external returns (uint256); - - 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, - uint256 lockedStake, - uint256 lockupFromEpoch, - uint256 lockupEndTime, - uint256 lockupDuration, - uint256 earlyUnlockPenalty, - uint256 rewards - ) 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 highestLockupEpoch(address, uint256) external view returns (uint256); - - 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..a9126a3 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 From 892017d83d65736cc0e44bfcec124543c51714f8 Mon Sep 17 00:00:00 2001 From: Mike-CZ Date: Sun, 27 Oct 2024 21:22:49 +0100 Subject: [PATCH 4/8] Fix lint errors --- contracts/sfc/SFC.sol | 6 +++++- hardhat.config.ts | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/contracts/sfc/SFC.sol b/contracts/sfc/SFC.sol index e15f120..d787fe7 100644 --- a/contracts/sfc/SFC.sol +++ b/contracts/sfc/SFC.sol @@ -1467,7 +1467,11 @@ contract SFC is Initializable, Ownable, Version { ); } - function sumRewards(Rewards memory _a, Rewards memory _b, Rewards memory _c) internal pure returns (Rewards memory) { + function sumRewards( + Rewards memory _a, + Rewards memory _b, + Rewards memory _c + ) internal pure returns (Rewards memory) { return sumRewards(sumRewards(_a, _b), _c); } diff --git a/hardhat.config.ts b/hardhat.config.ts index a9126a3..283a533 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -5,7 +5,7 @@ import '@nomicfoundation/hardhat-ethers'; import 'hardhat-contract-sizer'; import 'hardhat-gas-reporter'; import 'solidity-coverage'; -import "@typechain/hardhat"; +import '@typechain/hardhat'; dotenv.config(); From 998e2adb00d7e75eea0b1a7efd22c3b5ff1b69ae Mon Sep 17 00:00:00 2001 From: Mike-CZ Date: Sun, 27 Oct 2024 21:24:53 +0100 Subject: [PATCH 5/8] Fix solhint warning --- contracts/sfc/Updater.sol | 1 - 1 file changed, 1 deletion(-) diff --git a/contracts/sfc/Updater.sol b/contracts/sfc/Updater.sol index d4c4428..b178199 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"; From 07158954d38374194f9dc414c6845f6d36063873 Mon Sep 17 00:00:00 2001 From: Mike-CZ Date: Thu, 31 Oct 2024 14:02:49 +0100 Subject: [PATCH 6/8] Merge removed lock --- contracts/sfc/SFC.sol | 566 ++---------------------------------------- 1 file changed, 24 insertions(+), 542 deletions(-) diff --git a/contracts/sfc/SFC.sol b/contracts/sfc/SFC.sol index d787fe7..77ce3cc 100644 --- a/contracts/sfc/SFC.sol +++ b/contracts/sfc/SFC.sol @@ -34,12 +34,6 @@ contract SFC is Initializable, Ownable, Version { NodeDriverAuth internal node; - struct Rewards { - uint256 lockupExtraReward; - uint256 lockupBaseReward; - uint256 unlockedReward; - } - // last sealed epoch (currentEpoch - 1) uint256 public currentSealedEpoch; mapping(uint256 => Validator) public getValidator; @@ -55,7 +49,7 @@ contract SFC is Initializable, Ownable, Version { uint256 public totalActiveStake; // delegator => validator ID => stashed rewards (to be claimed/restaked) - mapping(address => mapping(uint256 => Rewards)) internal _rewardsStash; + 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; @@ -69,21 +63,9 @@ contract SFC is Initializable, Ownable, Version { // delegator => validator ID => withdrawal ID => withdrawal request mapping(address => mapping(uint256 => mapping(uint256 => WithdrawalRequest))) public getWithdrawalRequest; - struct LockedDelegation { - uint256 lockedStake; - uint256 fromEpoch; - uint256 endTime; - uint256 duration; - } - // delegator => validator ID => current stake (locked+unlocked) mapping(address => mapping(uint256 => uint256)) public getStake; - // delegator => validator ID => locked stake info - mapping(address => mapping(uint256 => LockedDelegation)) public getLockupInfo; - - mapping(address => mapping(uint256 => Rewards)) public getStashedLockupRewards; - struct EpochSnapshot { // validator ID => validator weight in the epoch mapping(uint256 => uint256) receivedStake; @@ -124,13 +106,6 @@ contract SFC is Initializable, Ownable, Version { // the governance contract (to recalculate votes when the stake changes) address public voteBookAddress; - struct Penalty { - uint256 amount; - uint256 end; - } - // delegator => validatorID => penalties info - mapping(address => mapping(uint256 => Penalty[])) public getStashedPenalties; - // validator ID => amount of pubkey updates mapping(uint256 => uint256) internal validatorPubkeyChanges; @@ -203,20 +178,10 @@ contract SFC is Initializable, Ownable, Version { error GovVotesRecountFailed(); // staking - error LockedStakeGreaterThanTotalStake(); error InsufficientSelfStake(); - error NotEnoughUnlockedStake(); - error NotEnoughLockedStake(); error NotEnoughTimePassed(); error NotEnoughEpochsPassed(); error StakeIsFullySlashed(); - error IncorrectDuration(); - error ValidatorLockupTooShort(); - error TooManyReLocks(); - error TooFrequentReLocks(); - error LockupDurationDecreased(); - error AlreadyLockedUp(); - error NotLockedUp(); // stashing error NothingToStash(); @@ -236,23 +201,9 @@ contract SFC is Initializable, Ownable, Version { 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 lockupExtraReward, - uint256 lockupBaseReward, - uint256 unlockedReward - ); - event RestakedRewards( - address indexed delegator, - uint256 indexed toValidatorID, - uint256 lockupExtraReward, - uint256 lockupBaseReward, - uint256 unlockedReward - ); + 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 LockedUpStake(address indexed delegator, uint256 indexed validatorID, uint256 duration, uint256 amount); - event UnlockedStake(address indexed delegator, uint256 indexed validatorID, uint256 amount, uint256 penalty); 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); @@ -425,32 +376,9 @@ contract SFC is Initializable, Ownable, Version { } } - function setGenesisDelegation( - address delegator, - uint256 toValidatorID, - uint256 stake, - uint256 lockedStake, - uint256 lockupFromEpoch, - uint256 lockupEndTime, - uint256 lockupDuration, - uint256 earlyUnlockPenalty, - uint256 rewards - ) external onlyDriver { + function setGenesisDelegation(address delegator, uint256 toValidatorID, uint256 stake) external onlyDriver { _rawDelegate(delegator, toValidatorID, stake, false); - _rewardsStash[delegator][toValidatorID].unlockedReward = rewards; _mintNativeToken(stake); - if (lockedStake != 0) { - if (lockedStake > stake) { - revert LockedStakeGreaterThanTotalStake(); - } - LockedDelegation storage ld = getLockupInfo[delegator][toValidatorID]; - ld.lockedStake = lockedStake; - ld.fromEpoch = lockupFromEpoch; - ld.endTime = lockupEndTime; - ld.duration = lockupDuration; - getStashedLockupRewards[delegator][toValidatorID].lockupExtraReward = earlyUnlockPenalty; - emit LockedUpStake(delegator, toValidatorID, lockupDuration, lockedStake); - } } function createValidator(bytes calldata pubkey) external payable { @@ -518,42 +446,6 @@ contract SFC is Initializable, Ownable, Version { _burnFTM(amount); } - function unlockStake(uint256 toValidatorID, uint256 amount) external returns (uint256) { - address delegator = msg.sender; - LockedDelegation storage ld = getLockupInfo[delegator][toValidatorID]; - - if (amount == 0) { - revert ZeroAmount(); - } - if (!isLockedUp(delegator, toValidatorID)) { - revert NotLockedUp(); - } - if (amount > ld.lockedStake) { - revert NotEnoughLockedStake(); - } - if (_redirected(delegator)) { - revert Redirected(); - } - - _stashRewards(delegator, toValidatorID); - - uint256 penalty = _popWholeUnlockPenalty(delegator, toValidatorID, amount, ld.lockedStake); - if (penalty > amount) { - penalty = amount; - } - ld.lockedStake -= amount; - if (penalty != 0) { - _rawUndelegate(delegator, toValidatorID, penalty, true, false, false); - (bool success, ) = treasuryAddress.call{value: penalty}(""); - if (!success) { - revert TransferFailed(); - } - } - - emit UnlockedStake(delegator, toValidatorID, amount, penalty); - return penalty; - } - function updateTreasuryAddress(address v) external onlyOwner { treasuryAddress = v; } @@ -572,28 +464,18 @@ contract SFC is Initializable, Ownable, Version { function claimRewards(uint256 toValidatorID) public { address delegator = msg.sender; - Rewards memory rewards = _claimRewards(delegator, toValidatorID); + uint256 rewards = _claimRewards(delegator, toValidatorID); // It's important that we transfer after erasing (protection against Re-Entrancy) - (bool sent, ) = _receiverOf(delegator).call{ - value: rewards.lockupExtraReward + rewards.lockupBaseReward + rewards.unlockedReward - }(""); - + (bool sent, ) = _receiverOf(delegator).call{value: rewards}(""); if (!sent) { revert TransferFailed(); } - emit ClaimedRewards( - delegator, - toValidatorID, - rewards.lockupExtraReward, - rewards.lockupBaseReward, - rewards.unlockedReward - ); + emit ClaimedRewards(delegator, toValidatorID, rewards); } function rewardsStash(address delegator, uint256 validatorID) public view returns (uint256) { - Rewards memory stash = _rewardsStash[delegator][validatorID]; - return stash.lockupBaseReward + stash.lockupExtraReward + stash.unlockedReward; + return _rewardsStash[delegator][validatorID]; } function undelegate(uint256 toValidatorID, uint256 wrID, uint256 amount) public { @@ -605,10 +487,6 @@ contract SFC is Initializable, Ownable, Version { revert ZeroAmount(); } - if (amount > getUnlockedStake(delegator, toValidatorID)) { - revert NotEnoughUnlockedStake(); - } - if (getWithdrawalRequest[delegator][toValidatorID][wrID].amount != 0) { revert RequestExists(); } @@ -626,37 +504,10 @@ contract SFC is Initializable, Ownable, Version { function restakeRewards(uint256 toValidatorID) public { address delegator = msg.sender; - Rewards memory rewards = _claimRewards(delegator, toValidatorID); - - uint256 lockupReward = rewards.lockupExtraReward + rewards.lockupBaseReward; - _delegate(delegator, toValidatorID, lockupReward + rewards.unlockedReward); - getLockupInfo[delegator][toValidatorID].lockedStake += lockupReward; - emit RestakedRewards( - delegator, - toValidatorID, - rewards.lockupExtraReward, - rewards.lockupBaseReward, - rewards.unlockedReward - ); - } - - function lockStake(uint256 toValidatorID, uint256 lockupDuration, uint256 amount) public { - address delegator = msg.sender; - if (amount == 0) { - revert ZeroAmount(); - } - if (isLockedUp(delegator, toValidatorID)) { - revert AlreadyLockedUp(); - } - _lockStake(delegator, toValidatorID, lockupDuration, amount, false); - } + uint256 rewards = _claimRewards(delegator, toValidatorID); - function relockStake(uint256 toValidatorID, uint256 lockupDuration, uint256 amount) public { - address delegator = msg.sender; - if (!isLockedUp(delegator, toValidatorID)) { - revert NotLockedUp(); - } - _lockStake(delegator, toValidatorID, lockupDuration, amount, true); + _delegate(delegator, toValidatorID, rewards); + emit RestakedRewards(delegator, toValidatorID, rewards); } function currentEpoch() public view returns (uint256) { @@ -704,29 +555,8 @@ contract SFC is Initializable, Ownable, Version { } function pendingRewards(address delegator, uint256 toValidatorID) public view returns (uint256) { - Rewards memory reward = _pendingRewards(delegator, toValidatorID); - return reward.unlockedReward + reward.lockupBaseReward + reward.lockupExtraReward; - } - - function getUnlockedStake(address delegator, uint256 toValidatorID) public view returns (uint256) { - if (!isLockedUp(delegator, toValidatorID)) { - return getStake[delegator][toValidatorID]; - } - return getStake[delegator][toValidatorID] - getLockupInfo[delegator][toValidatorID].lockedStake; - } - - function getLockedStake(address delegator, uint256 toValidatorID) public view returns (uint256) { - if (!isLockedUp(delegator, toValidatorID)) { - return 0; - } - return getLockupInfo[delegator][toValidatorID].lockedStake; - } - - function isLockedUp(address delegator, uint256 toValidatorID) public view returns (bool) { - return - getLockupInfo[delegator][toValidatorID].endTime != 0 && - getLockupInfo[delegator][toValidatorID].lockedStake != 0 && - _now() <= getLockupInfo[delegator][toValidatorID].endTime; + uint256 reward = _newRewards(delegator, toValidatorID); + return _rewardsStash[delegator][toValidatorID] + reward; } function _checkDelegatedStakeLimit(uint256 validatorID) internal view returns (bool) { @@ -876,60 +706,12 @@ contract SFC is Initializable, Ownable, Version { return currentSealedEpoch; } - // find highest epoch such that _isLockedUpAtEpoch returns true (using binary search) - function _highestLockupEpoch(address delegator, uint256 validatorID) internal view returns (uint256) { - uint256 fromEpoch = getLockupInfo[delegator][validatorID].fromEpoch; - uint256 r = currentSealedEpoch; - if (_isLockedUpAtEpoch(delegator, validatorID, r)) { - return r; - } - if (!_isLockedUpAtEpoch(delegator, validatorID, fromEpoch)) { - return 0; - } - if (fromEpoch > r) { - return 0; - } - while (fromEpoch < r) { - uint256 m = (fromEpoch + r) / 2; - if (_isLockedUpAtEpoch(delegator, validatorID, m)) { - fromEpoch = m + 1; - } else { - r = m; - } - } - if (r == 0) { - return 0; - } - return r - 1; - } - - function _newRewards(address delegator, uint256 toValidatorID) internal view returns (Rewards memory) { + function _newRewards(address delegator, uint256 toValidatorID) internal view returns (uint256) { uint256 stashedUntil = stashedRewardsUntilEpoch[delegator][toValidatorID]; uint256 payableUntil = _highestPayableEpoch(toValidatorID); - uint256 lockedUntil = _highestLockupEpoch(delegator, toValidatorID); - if (lockedUntil > payableUntil) { - lockedUntil = payableUntil; - } - if (lockedUntil < stashedUntil) { - lockedUntil = stashedUntil; - } - - LockedDelegation storage ld = getLockupInfo[delegator][toValidatorID]; uint256 wholeStake = getStake[delegator][toValidatorID]; - uint256 unlockedStake = wholeStake - ld.lockedStake; - uint256 fullReward; - - // count reward for locked stake during lockup epochs - fullReward = _newRewardsOf(ld.lockedStake, toValidatorID, stashedUntil, lockedUntil); - Rewards memory plReward = _scaleLockupReward(fullReward, ld.duration); - // count reward for unlocked stake during lockup epochs - fullReward = _newRewardsOf(unlockedStake, toValidatorID, stashedUntil, lockedUntil); - Rewards memory puReward = _scaleLockupReward(fullReward, 0); - // count lockup reward for unlocked stake during unlocked epochs - fullReward = _newRewardsOf(wholeStake, toValidatorID, lockedUntil, payableUntil); - Rewards memory wuReward = _scaleLockupReward(fullReward, 0); - - return sumRewards(plReward, puReward, wuReward); + uint256 fullReward = _newRewardsOf(wholeStake, toValidatorID, stashedUntil, payableUntil); + return fullReward; } function _newRewardsOf( @@ -946,40 +728,22 @@ contract SFC is Initializable, Ownable, Version { return ((currentRate - stashedRate) * stakeAmount) / Decimal.unit(); } - function _pendingRewards(address delegator, uint256 toValidatorID) internal view returns (Rewards memory) { - Rewards memory reward = _newRewards(delegator, toValidatorID); - return sumRewards(_rewardsStash[delegator][toValidatorID], reward); - } - function _stashRewards(address delegator, uint256 toValidatorID) internal returns (bool updated) { - Rewards memory nonStashedReward = _newRewards(delegator, toValidatorID); + uint256 nonStashedReward = _newRewards(delegator, toValidatorID); stashedRewardsUntilEpoch[delegator][toValidatorID] = _highestPayableEpoch(toValidatorID); - _rewardsStash[delegator][toValidatorID] = sumRewards(_rewardsStash[delegator][toValidatorID], nonStashedReward); - getStashedLockupRewards[delegator][toValidatorID] = sumRewards( - getStashedLockupRewards[delegator][toValidatorID], - nonStashedReward - ); - if (!isLockedUp(delegator, toValidatorID)) { - delete getLockupInfo[delegator][toValidatorID]; - delete getStashedLockupRewards[delegator][toValidatorID]; - } - _truncateLegacyPenalty(delegator, toValidatorID); - return - nonStashedReward.lockupBaseReward != 0 || - nonStashedReward.lockupExtraReward != 0 || - nonStashedReward.unlockedReward != 0; + _rewardsStash[delegator][toValidatorID] += nonStashedReward; + return nonStashedReward != 0; } - function _claimRewards(address delegator, uint256 toValidatorID) internal returns (Rewards memory rewards) { + function _claimRewards(address delegator, uint256 toValidatorID) internal returns (uint256) { _stashRewards(delegator, toValidatorID); - rewards = _rewardsStash[delegator][toValidatorID]; - uint256 totalReward = rewards.unlockedReward + rewards.lockupBaseReward + rewards.lockupExtraReward; - if (totalReward == 0) { + 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(totalReward); + _mintNativeToken(rewards); return rewards; } @@ -994,134 +758,6 @@ contract SFC is Initializable, Ownable, Version { return getEpochSnapshot[epoch].endTime; } - function _isLockedUpAtEpoch(address delegator, uint256 toValidatorID, uint256 epoch) internal view returns (bool) { - return - getLockupInfo[delegator][toValidatorID].fromEpoch <= epoch && - epochEndTime(epoch) <= getLockupInfo[delegator][toValidatorID].endTime; - } - - function _lockStake( - address delegator, - uint256 toValidatorID, - uint256 lockupDuration, - uint256 amount, - bool relock - ) internal { - if (_redirected(delegator)) { - revert Redirected(); - } - - if (amount > getUnlockedStake(delegator, toValidatorID)) { - revert NotEnoughUnlockedStake(); - } - - if (getValidator[toValidatorID].status != OK_STATUS) { - revert ValidatorNotActive(); - } - - if (lockupDuration < c.minLockupDuration() || lockupDuration > c.maxLockupDuration()) { - revert IncorrectDuration(); - } - - uint256 endTime = _now() + lockupDuration; - address validatorAddr = getValidator[toValidatorID].auth; - if ( - delegator != validatorAddr && - getLockupInfo[validatorAddr][toValidatorID].endTime + 30 * 24 * 60 * 60 < endTime - ) { - revert ValidatorLockupTooShort(); - } - - _stashRewards(delegator, toValidatorID); - _delStalePenalties(delegator, toValidatorID); - - // stash the previous penalty and clean getStashedLockupRewards - LockedDelegation storage ld = getLockupInfo[delegator][toValidatorID]; - if (relock) { - Penalty[] storage penalties = getStashedPenalties[delegator][toValidatorID]; - - uint256 penalty = _popNonStashedUnlockPenalty(delegator, toValidatorID, ld.lockedStake, ld.lockedStake); - if (penalty != 0) { - penalties.push(Penalty(penalty, ld.endTime)); - if (penalties.length > 30) { - revert TooManyReLocks(); - } - if ( - amount <= ld.lockedStake / 100 && penalties.length > 3 && endTime < ld.endTime + 14 * 24 * 60 * 60 - ) { - revert TooFrequentReLocks(); - } - } - } - - // check lockup duration after _stashRewards, which has erased previous lockup if it has unlocked already - if (lockupDuration < ld.duration) { - revert LockupDurationDecreased(); - } - - ld.lockedStake = ld.lockedStake + amount; - ld.fromEpoch = currentEpoch(); - ld.endTime = endTime; - ld.duration = lockupDuration; - - emit LockedUpStake(delegator, toValidatorID, lockupDuration, amount); - } - - function _popNonStashedUnlockPenalty( - address delegator, - uint256 toValidatorID, - uint256 unlockAmount, - uint256 totalAmount - ) internal returns (uint256) { - Rewards storage r = getStashedLockupRewards[delegator][toValidatorID]; - uint256 lockupExtraRewardShare = (r.lockupExtraReward * unlockAmount) / totalAmount; - uint256 lockupBaseRewardShare = (r.lockupBaseReward * unlockAmount) / totalAmount; - uint256 penalty = lockupExtraRewardShare + lockupBaseRewardShare / 2; - r.lockupExtraReward = r.lockupExtraReward - lockupExtraRewardShare; - r.lockupBaseReward = r.lockupBaseReward - lockupBaseRewardShare; - return penalty; - } - - function _popStashedUnlockPenalty( - address delegator, - uint256 toValidatorID, - uint256 unlockAmount, - uint256 totalAmount - ) internal returns (uint256) { - _delStalePenalties(delegator, toValidatorID); - Penalty[] storage penalties = getStashedPenalties[delegator][toValidatorID]; - uint256 total = 0; - for (uint256 i = 0; i < penalties.length; i++) { - uint256 penalty = (penalties[i].amount * unlockAmount) / totalAmount; - penalties[i].amount = penalties[i].amount - penalty; - total = total + penalty; - } - return total; - } - - function _popWholeUnlockPenalty( - address delegator, - uint256 toValidatorID, - uint256 unlockAmount, - uint256 totalAmount - ) internal returns (uint256) { - uint256 nonStashed = _popNonStashedUnlockPenalty(delegator, toValidatorID, unlockAmount, totalAmount); - uint256 stashed = _popStashedUnlockPenalty(delegator, toValidatorID, unlockAmount, totalAmount); - return nonStashed + stashed; - } - - function _delStalePenalties(address delegator, uint256 toValidatorID) public { - Penalty[] storage penalties = getStashedPenalties[delegator][toValidatorID]; - for (uint256 i = 0; i < penalties.length; ) { - if (penalties[i].end < _now() || penalties[i].amount == 0) { - penalties[i] = penalties[penalties.length - 1]; - penalties.pop(); - } else { - i++; - } - } - } - function _redirected(address addr) internal view returns (bool) { return getRedirection[addr] != address(0); } @@ -1214,24 +850,7 @@ contract SFC is Initializable, Ownable, Version { uint256 commissionRewardFull = _calcValidatorCommission(rawReward, c.validatorCommission()); uint256 selfStake = getStake[validatorAddr][validatorID]; if (selfStake != 0) { - uint256 lCommissionRewardFull = (commissionRewardFull * getLockedStake(validatorAddr, validatorID)) / - selfStake; - uint256 uCommissionRewardFull = commissionRewardFull - lCommissionRewardFull; - Rewards memory lCommissionReward = _scaleLockupReward( - lCommissionRewardFull, - getLockupInfo[validatorAddr][validatorID].duration - ); - Rewards memory uCommissionReward = _scaleLockupReward(uCommissionRewardFull, 0); - _rewardsStash[validatorAddr][validatorID] = sumRewards( - _rewardsStash[validatorAddr][validatorID], - lCommissionReward, - uCommissionReward - ); - getStashedLockupRewards[validatorAddr][validatorID] = sumRewards( - getStashedLockupRewards[validatorAddr][validatorID], - lCommissionReward, - uCommissionReward - ); + _rewardsStash[validatorAddr][validatorID] += commissionRewardFull; } // accounting reward per token for delegators uint256 delegatorsReward = rawReward - commissionRewardFull; @@ -1324,108 +943,6 @@ contract SFC is Initializable, Ownable, Version { } } - // code below can be erased after 1 year since deployment of multipenalties - - function _getAvgEpochStep(uint256 duration) internal view virtual returns (uint256) { - // estimate number of epochs such that we would make approximately 15 iterations - uint256 tryEpochs = currentSealedEpoch / 5; - if (tryEpochs > 10000) { - tryEpochs = 10000; - } - uint256 tryEndTime = getEpochSnapshot[currentSealedEpoch - tryEpochs].endTime; - if (tryEndTime == 0 || tryEpochs == 0) { - return 0; - } - uint256 secondsPerEpoch = (_now() - tryEndTime) / tryEpochs; - return duration / (secondsPerEpoch * 15 + 1); - } - - function _getAvgReceivedStake(uint256 validatorID, uint256 duration, uint256 step) internal view returns (uint256) { - uint256 receivedStakeSum = getValidator[validatorID].receivedStake; - uint256 samples = 1; - - uint256 until = _now() - duration; - for (uint256 i = 1; i <= 30; i++) { - uint256 e = currentSealedEpoch - i * step; - EpochSnapshot storage s = getEpochSnapshot[e]; - if (s.endTime < until) { - break; - } - uint256 sample = s.receivedStake[validatorID]; - if (sample != 0) { - samples++; - receivedStakeSum += sample; - } - } - return receivedStakeSum / samples; - } - - function _getAvgUptime( - uint256 validatorID, - uint256 duration, - uint256 step - ) internal view virtual returns (uint256) { - uint256 until = _now() - duration; - uint256 oldUptimeCounter = 0; - uint256 newUptimeCounter = 0; - for (uint256 i = 0; i <= 30; i++) { - uint256 e = currentSealedEpoch - i * step; - EpochSnapshot storage s = getEpochSnapshot[e]; - uint256 endTime = s.endTime; - if (endTime < until) { - if (i <= 2) { - return duration; - } - break; - } - uint256 uptimeCounter = s.accumulatedUptime[validatorID]; - if (uptimeCounter != 0) { - oldUptimeCounter = uptimeCounter; - if (newUptimeCounter == 0) { - newUptimeCounter = uptimeCounter; - } - } - } - uint256 uptime = newUptimeCounter - oldUptimeCounter; - if (uptime > (duration * 4) / 5) { - return duration; - } - return uptime; - } - - function _truncateLegacyPenalty(address delegator, uint256 toValidatorID) internal { - Rewards storage r = getStashedLockupRewards[delegator][toValidatorID]; - uint256 storedPenalty = r.lockupExtraReward + r.lockupBaseReward / 2; - if (storedPenalty == 0) { - return; - } - LockedDelegation storage ld = getLockupInfo[delegator][toValidatorID]; - uint256 duration = ld.duration; - uint256 lockedStake = ld.lockedStake; - uint256 step = _getAvgEpochStep(duration); - if (step == 0) { - return; - } - uint256 rps = (_getAvgUptime(toValidatorID, duration, step) * 2092846271) / duration; // corresponds to 6.6% APR - uint256 selfStake = getStake[delegator][toValidatorID]; - - uint256 avgFullReward = (((selfStake * rps * duration) / 1e18) * (Decimal.unit() - c.validatorCommission())) / - Decimal.unit(); // reward for self-stake - if (getValidator[toValidatorID].auth == delegator) { - // reward for received portion of stake - uint256 receivedStakeAvg = (_getAvgReceivedStake(toValidatorID, duration, step) * 11) / 10; - avgFullReward += (((receivedStakeAvg * rps * duration) / 1e18) * c.validatorCommission()) / Decimal.unit(); - } - avgFullReward = (avgFullReward * lockedStake) / selfStake; - Rewards memory avgReward = _scaleLockupReward(avgFullReward, duration); - uint256 maxReasonablePenalty = avgReward.lockupBaseReward / 2 + avgReward.lockupExtraReward; - maxReasonablePenalty = maxReasonablePenalty; - if (storedPenalty > maxReasonablePenalty) { - r.lockupExtraReward = (r.lockupExtraReward * maxReasonablePenalty) / storedPenalty; - r.lockupBaseReward = (r.lockupBaseReward * maxReasonablePenalty) / storedPenalty; - } - } - function _calcRawValidatorEpochTxReward( uint256 epochFee, uint256 txRewardWeight, @@ -1458,41 +975,6 @@ contract SFC is Initializable, Ownable, Version { totalSupply = totalSupply + amount; } - function sumRewards(Rewards memory a, Rewards memory b) internal pure returns (Rewards memory) { - return - Rewards( - a.lockupExtraReward + b.lockupExtraReward, - a.lockupBaseReward + b.lockupBaseReward, - a.unlockedReward + b.unlockedReward - ); - } - - function sumRewards( - Rewards memory _a, - Rewards memory _b, - Rewards memory _c - ) internal pure returns (Rewards memory) { - return sumRewards(sumRewards(_a, _b), _c); - } - - function _scaleLockupReward( - uint256 fullReward, - uint256 lockupDuration - ) internal view returns (Rewards memory reward) { - reward = Rewards(0, 0, 0); - uint256 unlockedRewardRatio = c.unlockedRewardRatio(); - if (lockupDuration != 0) { - uint256 maxLockupExtraRatio = Decimal.unit() - unlockedRewardRatio; - uint256 lockupExtraRatio = (maxLockupExtraRatio * lockupDuration) / c.maxLockupDuration(); - uint256 totalScaledReward = (fullReward * (unlockedRewardRatio + lockupExtraRatio)) / Decimal.unit(); - reward.lockupBaseReward = (fullReward * unlockedRewardRatio) / Decimal.unit(); - reward.lockupExtraReward = totalScaledReward - reward.lockupBaseReward; - } else { - reward.unlockedReward = (fullReward * unlockedRewardRatio) / Decimal.unit(); - } - return reward; - } - function _recountVotes(address delegator, address validatorAuth, bool strict) internal { if (voteBookAddress != address(0)) { // Don't allow recountVotes to use up all the gas From 5e2299060f7e37d9468cb22502ba931ffdc5f5d2 Mon Sep 17 00:00:00 2001 From: Mike-CZ Date: Thu, 31 Oct 2024 14:53:06 +0100 Subject: [PATCH 7/8] Reorganize `Validator` struct fields --- contracts/interfaces/ISFC.sol | 6 +++--- contracts/sfc/SFC.sol | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/contracts/interfaces/ISFC.sol b/contracts/interfaces/ISFC.sol index 93ef4f6..b86b815 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, uint256 createdEpoch, uint256 createdTime, - address auth + address auth, + uint256 deactivatedTime, + uint256 deactivatedEpoch ); function getValidatorID(address) external view returns (uint256); diff --git a/contracts/sfc/SFC.sol b/contracts/sfc/SFC.sol index 77ce3cc..220e084 100644 --- a/contracts/sfc/SFC.sol +++ b/contracts/sfc/SFC.sol @@ -24,12 +24,12 @@ contract SFC is Initializable, Ownable, Version { */ 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 + uint256 deactivatedTime; + uint256 deactivatedEpoch; } NodeDriverAuth internal node; From c2369156048b9d4978bed4b8b1cc7d7686da4580 Mon Sep 17 00:00:00 2001 From: Mike-CZ Date: Thu, 31 Oct 2024 14:57:44 +0100 Subject: [PATCH 8/8] Reorganize `Validator` struct fields --- contracts/interfaces/ISFC.sol | 2 +- contracts/sfc/SFC.sol | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/interfaces/ISFC.sol b/contracts/interfaces/ISFC.sol index b86b815..1a5a304 100644 --- a/contracts/interfaces/ISFC.sol +++ b/contracts/interfaces/ISFC.sol @@ -49,9 +49,9 @@ interface ISFC { returns ( uint256 status, uint256 receivedStake, + address auth, uint256 createdEpoch, uint256 createdTime, - address auth, uint256 deactivatedTime, uint256 deactivatedEpoch ); diff --git a/contracts/sfc/SFC.sol b/contracts/sfc/SFC.sol index 220e084..55a740a 100644 --- a/contracts/sfc/SFC.sol +++ b/contracts/sfc/SFC.sol @@ -25,9 +25,9 @@ contract SFC is Initializable, Ownable, Version { struct Validator { uint256 status; uint256 receivedStake; // from all delegators (weight of the validator) + address auth; // self-stake delegator uint256 createdEpoch; uint256 createdTime; - address auth; // self-stake delegator uint256 deactivatedTime; uint256 deactivatedEpoch; }