diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000000..c5476864ec7 --- /dev/null +++ b/.prettierignore @@ -0,0 +1 @@ +contracts/KIP/protocol/**/*.sol diff --git a/.solhintignore b/.solhintignore new file mode 100644 index 00000000000..c5476864ec7 --- /dev/null +++ b/.solhintignore @@ -0,0 +1 @@ +contracts/KIP/protocol/**/*.sol diff --git a/audit/KIP81_certik_20230208.pdf b/audit/KIP81_certik_20230208.pdf new file mode 100644 index 00000000000..0f45f2e3ce9 Binary files /dev/null and b/audit/KIP81_certik_20230208.pdf differ diff --git a/audit/KIP81_theori_20230428.pdf b/audit/KIP81_theori_20230428.pdf new file mode 100644 index 00000000000..1257e2fdc00 Binary files /dev/null and b/audit/KIP81_theori_20230428.pdf differ diff --git a/contracts/KIP/mocks/KIP37MintableMock.sol b/contracts/KIP/mocks/KIP37MintableMock.sol index 3ea93f99fe9..27601061a19 100644 --- a/contracts/KIP/mocks/KIP37MintableMock.sol +++ b/contracts/KIP/mocks/KIP37MintableMock.sol @@ -18,7 +18,7 @@ contract KIP37MintableMock is KIP37Mintable { function create( uint256 id, uint256 initialSupply, - string calldata uri_ + string memory uri_ ) public override returns (bool) { return super.create(id, initialSupply, uri_); } @@ -33,16 +33,16 @@ contract KIP37MintableMock is KIP37Mintable { function mint( uint256 id, - address[] calldata toList, - uint256[] calldata amounts + address[] memory toList, + uint256[] memory amounts ) public override { super.mint(id, toList, amounts); } function mintBatch( address to, - uint256[] calldata ids, - uint256[] calldata amounts + uint256[] memory ids, + uint256[] memory amounts ) public override { super.mintBatch(to, ids, amounts); } diff --git a/contracts/KIP/protocol/KIP103/ITreasuryRebalance.sol b/contracts/KIP/protocol/KIP103/ITreasuryRebalance.sol new file mode 100644 index 00000000000..df95dbc6512 --- /dev/null +++ b/contracts/KIP/protocol/KIP103/ITreasuryRebalance.sol @@ -0,0 +1,190 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity ^0.8.0; + +/** + * @dev External interface of TreasuryRebalance + */ +interface ITreasuryRebalance { + /** + * @dev Emitted when the contract is deployed + * `rebalanceBlockNumber` is the target block number of the execution the rebalance in Core + * `deployedBlockNumber` is the current block number when its deployed + */ + event ContractDeployed( + Status status, + uint256 rebalanceBlockNumber, + uint256 deployedBlockNumber + ); + + /** + * @dev Emitted when a Retired is registered + */ + event RetiredRegistered(address retired); + + /** + * @dev Emitted when a Retired is removed + */ + event RetiredRemoved(address retired); + + /** + * @dev Emitted when a Newbie is registered + */ + event NewbieRegistered(address newbie, uint256 fundAllocation); + + /** + * @dev Emitted when a Newbie is removed + */ + event NewbieRemoved(address newbie); + + /** + * @dev Emitted when a admin approves the retired address. + */ + event Approved(address retired, address approver, uint256 approversCount); + + /** + * @dev Emitted when the contract status changes + */ + event StatusChanged(Status status); + + /** + * @dev Emitted when the contract is finalized + * memo - is the result of the treasury fund rebalancing + */ + event Finalized(string memo, Status status); + + // Status of the contract + enum Status { + Initialized, + Registered, + Approved, + Finalized + } + + /** + * Retired struct to store retired address and their approver addresses + */ + struct Retired { + address retired; + address[] approvers; + } + + /** + * Newbie struct to newbie receiver address and their fund allocation + */ + struct Newbie { + address newbie; + uint256 amount; + } + + // State variables + function status() external view returns (Status); // current status of the contract + + function rebalanceBlockNumber() external view returns (uint256); // the target block number of the execution of rebalancing + + function memo() external view returns (string memory); // result of the treasury fund rebalance + + /** + * @dev to get retired details by retiredAddress + */ + function getRetired( + address retiredAddress + ) external view returns (address, address[] memory); + + /** + * @dev to get newbie details by newbieAddress + */ + function getNewbie( + address newbieAddress + ) external view returns (address, uint256); + + /** + * @dev returns the sum of retirees balances + */ + function sumOfRetiredBalance() + external + view + returns (uint256 retireesBalance); + + /** + * @dev returns the sum of newbie funds + */ + function getTreasuryAmount() external view returns (uint256 treasuryAmount); + + /** + * @dev returns the length of retirees list + */ + function getRetiredCount() external view returns (uint256); + + /** + * @dev returns the length of newbies list + */ + function getNewbieCount() external view returns (uint256); + + /** + * @dev verify all retirees are approved by admin + */ + function checkRetiredsApproved() external view; + + // State changing functions + /** + * @dev registers retired details + * Can only be called by the current owner at Initialized state + */ + function registerRetired(address retiredAddress) external; + + /** + * @dev remove the retired details from the array + * Can only be called by the current owner at Initialized state + */ + function removeRetired(address retiredAddress) external; + + /** + * @dev registers newbie address and its fund distribution + * Can only be called by the current owner at Initialized state + */ + function registerNewbie(address newbieAddress, uint256 amount) external; + + /** + * @dev remove the newbie details from the array + * Can only be called by the current owner at Initialized state + */ + function removeNewbie(address newbieAddress) external; + + /** + * @dev approves a retiredAddress,the address can be a EOA or a contract address. + * - If the retiredAddress is a EOA, the caller should be the EOA address + * - If the retiredAddress is a Contract, the caller should be one of the contract `admin` + */ + function approve(address retiredAddress) external; + + /** + * @dev sets the status to Registered, + * After this stage, registrations will be restricted. + * Can only be called by the current owner at Initialized state + */ + function finalizeRegistration() external; + + /** + * @dev sets the status to Approved, + * Can only be called by the current owner at Registered state + */ + function finalizeApproval() external; + + /** + * @dev sets the status of the contract to Finalize. Once finalized the storage data + * of the contract cannot be modified + * Can only be called by the current owner at Approved state after the execution of rebalance in the core + * - memo format: { "retirees": [ { "retired": "0xaddr", "balance": 0xamount }, + * { "retired": "0xaddr", "balance": 0xamount }, ... ], + * "newbies": [ { "newbie": "0xaddr", "fundAllocated": 0xamount }, + * { "newbie": "0xaddr", "fundAllocated": 0xamount }, ... ], + * "burnt": 0xamount, "success": true/false } + */ + function finalizeContract(string memory memo) external; + + /** + * @dev resets all storage values to empty objects except targetBlockNumber + */ + function reset() external; +} diff --git a/contracts/KIP/protocol/KIP103/Ownable.sol b/contracts/KIP/protocol/KIP103/Ownable.sol new file mode 100644 index 00000000000..ffec4138db2 --- /dev/null +++ b/contracts/KIP/protocol/KIP103/Ownable.sol @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.0; + +/** + * @dev Contract module which provides a basic access control mechanism, where + * there is an account (an owner) that can be granted exclusive access to + * specific functions. + * + * This module is used through inheritance. It will make available the modifier + * `onlyOwner`, which can be aplied to your functions to restrict their use to + * the owner. + */ +contract Ownable { + address private _owner; + + event OwnershipTransferred( + address indexed previousOwner, + address indexed newOwner + ); + + /** + * @dev Initializes the contract setting the deployer as the initial owner. + */ + constructor() { + _owner = msg.sender; + emit OwnershipTransferred(address(0), _owner); + } + + /** + * @dev Returns the address of the current owner. + */ + function owner() public view returns (address) { + return _owner; + } + + /** + * @dev Throws if called by any account other than the owner. + */ + modifier onlyOwner() { + require(isOwner(), "Ownable: caller is not the owner"); + _; + } + + /** + * @dev Returns true if the caller is the current owner. + */ + function isOwner() public view returns (bool) { + return msg.sender == _owner; + } + + /** + * @dev Leaves the contract without owner. It will not be possible to call + * `onlyOwner` functions anymore. Can only be called by the current owner. + * + * > Note: Renouncing ownership will leave the contract without an owner, + * thereby removing any functionality that is only available to the owner. + */ + function renounceOwnership() public onlyOwner { + emit OwnershipTransferred(_owner, address(0)); + _owner = address(0); + } + + /** + * @dev Transfers ownership of the contract to a new account (`newOwner`). + * Can only be called by the current owner. + */ + function transferOwnership(address newOwner) public onlyOwner { + _transferOwnership(newOwner); + } + + /** + * @dev Transfers ownership of the contract to a new account (`newOwner`). + */ + function _transferOwnership(address newOwner) internal { + require( + newOwner != address(0), + "Ownable: new owner is the zero address" + ); + emit OwnershipTransferred(_owner, newOwner); + _owner = newOwner; + } +} diff --git a/contracts/KIP/protocol/KIP103/SenderTest1.sol b/contracts/KIP/protocol/KIP103/SenderTest1.sol new file mode 100644 index 00000000000..7ff9e94388f --- /dev/null +++ b/contracts/KIP/protocol/KIP103/SenderTest1.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity ^0.8.0; + +/** + * Test contract to represent KGF contract implementing getState() + */ +contract SenderTest1 { + address[] _adminList; + uint256 public minReq = 1; + + constructor() { + _adminList.push(msg.sender); + } + + /* + * Getter functions + */ + function getState() external view returns (address[] memory, uint256) { + return (_adminList, minReq); + } + + function emptyAdminList() public { + _adminList.pop(); + } + + function changeMinReq(uint256 req) public { + minReq = req; + } + + function addAdmin(address admin) public { + _adminList.push(admin); + } + + /* + * Deposit function + */ + /// @dev Fallback function that allows to deposit KLAY + fallback() external payable { + require(msg.value > 0, "Invalid value."); + } +} diff --git a/contracts/KIP/protocol/KIP103/SenderTest2.sol b/contracts/KIP/protocol/KIP103/SenderTest2.sol new file mode 100644 index 00000000000..bbb9b634f8d --- /dev/null +++ b/contracts/KIP/protocol/KIP103/SenderTest2.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity ^0.8.0; + +/** + * Test contract to represent KIR contract implementing getState() + */ +contract SenderTest2 { + address[] _adminList; + + constructor() { + _adminList.push(msg.sender); + } + + /* + * Getter functions + */ + function getState() external view returns (address[] memory, uint256) { + return (_adminList, 1); + } + + /* + * Deposit function + */ + /// @dev Fallback function that allows to deposit KLAY + fallback() external payable { + require(msg.value > 0, "Invalid value."); + } +} diff --git a/contracts/KIP/protocol/KIP103/TreasuryRebalance.sol b/contracts/KIP/protocol/KIP103/TreasuryRebalance.sol new file mode 100644 index 00000000000..dc25224a430 --- /dev/null +++ b/contracts/KIP/protocol/KIP103/TreasuryRebalance.sol @@ -0,0 +1,451 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity ^0.8.0; + +import "./Ownable.sol"; +import "./ITreasuryRebalance.sol"; + +/** + * @title Interface to get adminlist and quorom + */ +interface IRetiredContract { + function getState() + external + view + returns (address[] memory adminList, uint256 quorom); +} + +/** + * @title Smart contract to record the rebalance of treasury funds. + * This contract is to mainly record the addresses which holds the treasury funds + * before and after rebalancing. It facilates approval and redistributing to new addresses. + * Core will execute the re-distribution by reading this contract. + */ +contract TreasuryRebalance is Ownable, ITreasuryRebalance { + /** + * Storage + */ + Retired[] public retirees; // array of the Retired struct + Newbie[] public newbies; // array of Newbie struct + Status public status; // current status of the contract + uint256 public rebalanceBlockNumber; // the target block number of the execution of rebalancing. + string public memo; // result of the treasury fund rebalance. + + /** + * Modifiers + */ + modifier onlyAtStatus(Status _status) { + require(status == _status, "Not in the designated status"); + _; + } + + /** + * Constructor + * @param _rebalanceBlockNumber is the target block number of the execution the rebalance in Core + */ + constructor(uint256 _rebalanceBlockNumber) { + require(_rebalanceBlockNumber > block.number, "rebalance blockNumber should be greater than current block"); + rebalanceBlockNumber = _rebalanceBlockNumber; + status = Status.Initialized; + emit ContractDeployed(status, _rebalanceBlockNumber, block.timestamp); + } + + //State changing Functions + /** + * @dev registers retired details + * @param _retiredAddress is the address of the retired + */ + function registerRetired( + address _retiredAddress + ) public onlyOwner onlyAtStatus(Status.Initialized) { + require( + !retiredExists(_retiredAddress), + "Retired address is already registered" + ); + Retired storage retired = retirees.push(); + retired.retired = _retiredAddress; + emit RetiredRegistered(retired.retired); + } + + /** + * @dev remove the retired details from the array + * @param _retiredAddress is the address of the retired + */ + function removeRetired( + address _retiredAddress + ) public onlyOwner onlyAtStatus(Status.Initialized) { + uint256 retiredIndex = getRetiredIndex(_retiredAddress); + require(retiredIndex != type(uint256).max, "Retired not registered"); + retirees[retiredIndex] = retirees[retirees.length - 1]; + retirees.pop(); + + emit RetiredRemoved(_retiredAddress); + } + + /** + * @dev registers newbie address and its fund distribution + * @param _newbieAddress is the address of the newbie + * @param _amount is the fund to be allocated to the newbie + */ + function registerNewbie( + address _newbieAddress, + uint256 _amount + ) public onlyOwner onlyAtStatus(Status.Initialized) { + require( + !newbieExists(_newbieAddress), + "Newbie address is already registered" + ); + require(_amount != 0, "Amount cannot be set to 0"); + + Newbie memory newbie = Newbie(_newbieAddress, _amount); + newbies.push(newbie); + + emit NewbieRegistered(_newbieAddress, _amount); + } + + /** + * @dev remove the newbie details from the array + * @param _newbieAddress is the address of the newbie + */ + function removeNewbie( + address _newbieAddress + ) public onlyOwner onlyAtStatus(Status.Initialized) { + uint256 newbieIndex = getNewbieIndex(_newbieAddress); + require(newbieIndex != type(uint256).max, "Newbie not registered"); + newbies[newbieIndex] = newbies[newbies.length - 1]; + newbies.pop(); + + emit NewbieRemoved(_newbieAddress); + } + + /** + * @dev retiredAddress can be a EOA or a contract address. To approve: + * If the retiredAddress is a EOA, the msg.sender should be the EOA address + * If the retiredAddress is a Contract, the msg.sender should be one of the contract `admin`. + * It uses the getState() function in the retiredAddress contract to get the admin details. + * @param _retiredAddress is the address of the retired + */ + function approve( + address _retiredAddress + ) public onlyAtStatus(Status.Registered) { + require( + retiredExists(_retiredAddress), + "retired needs to be registered before approval" + ); + + //Check whether the retired address is EOA or contract address + bool isContract = isContractAddr(_retiredAddress); + if (!isContract) { + //check whether the msg.sender is the retired if its a EOA + require( + msg.sender == _retiredAddress, + "retiredAddress is not the msg.sender" + ); + _updateApprover(_retiredAddress, msg.sender); + } else { + (address[] memory adminList, ) = _getState(_retiredAddress); + require(adminList.length != 0, "admin list cannot be empty"); + + //check if the msg.sender is one of the admin of the retiredAddress contract + require( + _validateAdmin(msg.sender, adminList), + "msg.sender is not the admin" + ); + _updateApprover(_retiredAddress, msg.sender); + } + } + + /** + * @dev validate if the msg.sender is admin if the retiredAddress is a contract + * @param _approver is the msg.sender + * @return isAdmin is true if the msg.sender is one of the admin + */ + function _validateAdmin( + address _approver, + address[] memory _adminList + ) private pure returns (bool isAdmin) { + for (uint256 i = 0; i < _adminList.length; i++) { + if (_approver == _adminList[i]) { + isAdmin = true; + } + } + } + + /** + * @dev gets the adminList and quorom by calling `getState()` method in retiredAddress contract + * @param _retiredAddress is the address of the contract + * @return adminList list of the retiredAddress contract admins + * @return req min required number of approvals + */ + function _getState( + address _retiredAddress + ) private view returns (address[] memory adminList, uint256 req) { + IRetiredContract retiredContract = IRetiredContract(_retiredAddress); + (adminList, req) = retiredContract.getState(); + } + + /** + * @dev Internal function to update the approver details of a retired + * _retiredAddress is the address of the retired + * _approver is the admin of the retiredAddress + */ + function _updateApprover( + address _retiredAddress, + address _approver + ) private { + uint256 index = getRetiredIndex(_retiredAddress); + require(index != type(uint256).max, "Retired not registered"); + address[] memory approvers = retirees[index].approvers; + for (uint256 i = 0; i < approvers.length; i++) { + require(approvers[i] != _approver, "Already approved"); + } + retirees[index].approvers.push(_approver); + emit Approved( + _retiredAddress, + _approver, + retirees[index].approvers.length + ); + } + + /** + * @dev finalizeRegistration sets the status to Registered, + * After this stage, registrations will be restricted. + */ + function finalizeRegistration() + public + onlyOwner + onlyAtStatus(Status.Initialized) + { + status = Status.Registered; + emit StatusChanged(status); + } + + /** + * @dev finalizeApproval sets the status to Approved, + * After this stage, approvals will be restricted. + */ + function finalizeApproval() + public + onlyOwner + onlyAtStatus(Status.Registered) + { + require( + getTreasuryAmount() < sumOfRetiredBalance(), + "treasury amount should be less than the sum of all retired address balances" + ); + checkRetiredsApproved(); + status = Status.Approved; + emit StatusChanged(status); + } + + /** + * @dev verify if quorom reached for the retired approvals + */ + function checkRetiredsApproved() public view { + for (uint256 i = 0; i < retirees.length; i++) { + Retired memory retired = retirees[i]; + bool isContract = isContractAddr(retired.retired); + if (isContract) { + (address[] memory adminList, uint256 req) = _getState( + retired.retired + ); + require( + retired.approvers.length >= req, + "min required admins should approve" + ); + //if min quorom reached, make sure all approvers are still valid + address[] memory approvers = retired.approvers; + uint256 validApprovals = 0; + for (uint256 j = 0; j < approvers.length; j++) { + if (_validateAdmin(approvers[j], adminList)) { + validApprovals++; + } + } + require( + validApprovals >= req, + "min required admins should approve" + ); + } else { + require(retired.approvers.length == 1, "EOA should approve"); + } + } + } + + /** + * @dev sets the status of the contract to Finalize. Once finalized the storage data + * of the contract cannot be modified + * @param _memo is the result of the rebalance after executing successfully in the core. + */ + function finalizeContract( + string memory _memo + ) public onlyOwner onlyAtStatus(Status.Approved) { + memo = _memo; + status = Status.Finalized; + emit Finalized(memo, status); + require( + block.number > rebalanceBlockNumber, + "Contract can only finalize after executing rebalancing" + ); + } + + /** + * @dev resets all storage values to empty objects except targetBlockNumber + */ + function reset() public onlyOwner { + //reset cannot be called at Finalized status or after target block.number + require( + ((status != Status.Finalized) && + (block.number < rebalanceBlockNumber)), + "Contract is finalized, cannot reset values" + ); + + //`delete` keyword is used to set a storage variable or a dynamic array to its default value. + delete retirees; + delete newbies; + delete memo; + status = Status.Initialized; + } + + //Getters + /** + * @dev to get retired details by retiredAddress + * @param _retiredAddress is the address of the retired + */ + function getRetired( + address _retiredAddress + ) public view returns (address, address[] memory) { + uint256 index = getRetiredIndex(_retiredAddress); + require(index != type(uint256).max, "Retired not registered"); + Retired memory retired = retirees[index]; + return (retired.retired, retired.approvers); + } + + /** + * @dev check whether retiredAddress is registered + * @param _retiredAddress is the address of the retired + */ + function retiredExists(address _retiredAddress) public view returns (bool) { + require(_retiredAddress != address(0), "Invalid address"); + for (uint256 i = 0; i < retirees.length; i++) { + if (retirees[i].retired == _retiredAddress) { + return true; + } + } + } + + /** + * @dev get index of the retired in the retirees array + * @param _retiredAddress is the address of the retired + */ + function getRetiredIndex( + address _retiredAddress + ) public view returns (uint256) { + for (uint256 i = 0; i < retirees.length; i++) { + if (retirees[i].retired == _retiredAddress) { + return i; + } + } + return type(uint256).max; + } + + /** + * @dev to calculate the sum of retirees balances + * @return retireesBalance the sum of balances of retireds + */ + function sumOfRetiredBalance() + public + view + returns (uint256 retireesBalance) + { + for (uint256 i = 0; i < retirees.length; i++) { + retireesBalance += retirees[i].retired.balance; + } + return retireesBalance; + } + + /** + * @dev to get newbie details by newbieAddress + * @param _newbieAddress is the address of the newbie + * @return newbie is the address of the newbie + * @return amount is the fund allocated to the newbie + */ + function getNewbie( + address _newbieAddress + ) public view returns (address, uint256) { + uint256 index = getNewbieIndex(_newbieAddress); + require(index != type(uint256).max, "Newbie not registered"); + Newbie memory newbie = newbies[index]; + return (newbie.newbie, newbie.amount); + } + + /** + * @dev check whether _newbieAddress is registered + * @param _newbieAddress is the address of the newbie + */ + function newbieExists(address _newbieAddress) public view returns (bool) { + require(_newbieAddress != address(0), "Invalid address"); + for (uint256 i = 0; i < newbies.length; i++) { + if (newbies[i].newbie == _newbieAddress) { + return true; + } + } + } + + /** + * @dev get index of the newbie in the newbies array + * @param _newbieAddress is the address of the newbie + */ + function getNewbieIndex( + address _newbieAddress + ) public view returns (uint256) { + for (uint256 i = 0; i < newbies.length; i++) { + if (newbies[i].newbie == _newbieAddress) { + return i; + } + } + return type(uint256).max; + } + + /** + * @dev to calculate the sum of newbie funds + * @return treasuryAmount the sum of funds allocated to newbies + */ + function getTreasuryAmount() public view returns (uint256 treasuryAmount) { + for (uint256 i = 0; i < newbies.length; i++) { + treasuryAmount += newbies[i].amount; + } + return treasuryAmount; + } + + /** + * @dev gets the length of retirees list + */ + function getRetiredCount() public view returns (uint256) { + return retirees.length; + } + + /** + * @dev gets the length of newbies list + */ + function getNewbieCount() public view returns (uint256) { + return newbies.length; + } + + /** + * @dev fallback function to revert any payments + */ + fallback() external payable { + revert("This contract does not accept any payments"); + } + + /** + * @dev Helper function to check the address is contract addr or EOA + */ + function isContractAddr(address _addr) public view returns (bool) { + uint256 size; + assembly { + size := extcodesize(_addr) + } + return size > 0; + } +} diff --git a/contracts/KIP/protocol/KIP113/IAddressBook.sol b/contracts/KIP/protocol/KIP113/IAddressBook.sol new file mode 100644 index 00000000000..1aa2ecdd3b2 --- /dev/null +++ b/contracts/KIP/protocol/KIP113/IAddressBook.sol @@ -0,0 +1,26 @@ +// Copyright 2023 The klaytn Authors +// This file is part of the klaytn library. +// +// The klaytn library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The klaytn library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the klaytn library. If not, see . + +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.19; + +interface IAddressBook { + function getState() external view returns (address[] memory adminList, uint256 requirement); + + function getCnInfo( + address _cnNodeId + ) external view returns (address cnNodeId, address cnStakingcontract, address cnRewardAddress); +} diff --git a/contracts/KIP/protocol/KIP113/IKIP113.sol b/contracts/KIP/protocol/KIP113/IKIP113.sol new file mode 100644 index 00000000000..0c9a7a6e7d1 --- /dev/null +++ b/contracts/KIP/protocol/KIP113/IKIP113.sol @@ -0,0 +1,36 @@ +// Copyright 2023 The klaytn Authors +// This file is part of the klaytn library. +// +// The klaytn library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The klaytn library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the klaytn library. If not, see . + +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.19; + +/// @title KIP-113 BLS public key registry +/// @dev See https://github.com/klaytn/kips/issues/113 +interface IKIP113 { + struct BlsPublicKeyInfo { + /// @dev compressed BLS12-381 public key (48 bytes) + bytes publicKey; + /// @dev proof-of-possession (96 bytes) + /// must be a result of PopProve algorithm as per + /// draft-irtf-cfrg-bls-signature-05 section 3.3.3. + /// with ciphersuite "BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_POP_" + bytes pop; + } + + /// @dev Returns all the stored addresses, public keys, and proof-of-possessions at once. + /// _Note_ The function is not able to verify the validity of the public key and the proof-of-possession due to the lack of [EIP-2537](https://eips.ethereum.org/EIPS/eip-2537). See [validation](https://kips.klaytn.foundation/KIPs/kip-113#validation) for off-chain validation. + function getAllBlsInfo() external view returns (address[] memory nodeIdList, BlsPublicKeyInfo[] memory pubkeyList); +} diff --git a/contracts/KIP/protocol/KIP113/SimpleBlsRegistry.sol b/contracts/KIP/protocol/KIP113/SimpleBlsRegistry.sol new file mode 100644 index 00000000000..29cfebdcfeb --- /dev/null +++ b/contracts/KIP/protocol/KIP113/SimpleBlsRegistry.sol @@ -0,0 +1,98 @@ +// Copyright 2023 The klaytn Authors +// This file is part of the klaytn library. +// +// The klaytn library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The klaytn library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the klaytn library. If not, see . + +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.19; + +import "./IKIP113.sol"; +import "./IAddressBook.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; + +contract SimpleBlsRegistry is Ownable, IKIP113 { + IAddressBook public constant abook = IAddressBook(0x0000000000000000000000000000000000000400); + bytes32 public constant ZERO48HASH = 0xc980e59163ce244bb4bb6211f48c7b46f88a4f40943e84eb99bdc41e129bd293; // keccak256(hex"00"*48) + bytes32 public constant ZERO96HASH = 0x46700b4d40ac5c35af2c22dda2787a91eb567b06c924a8fb8ae9a05b20c08c21; // keccak256(hex"00"*96) + + address[] public allNodeIds; + mapping(address => BlsPublicKeyInfo) public record; // cnNodeId => BlsPublicKeyInfo + + event Registered(address cnNodeId, bytes publicKey, bytes pop); + event Unregistered(address cnNodeId, bytes publicKey, bytes pop); + + modifier onlyValidPublicKey(bytes calldata publicKey) { + require(publicKey.length == 48, "Public key must be 48 bytes"); + require(keccak256(publicKey) != ZERO48HASH, "Public key cannot be zero"); + _; + } + + modifier onlyValidPop(bytes calldata pop) { + require(pop.length == 96, "Pop must be 96 bytes"); + require(keccak256(pop) != ZERO96HASH, "Pop cannot be zero"); + _; + } + + function register( + address cnNodeId, + bytes calldata publicKey, + bytes calldata pop + ) external onlyOwner onlyValidPublicKey(publicKey) onlyValidPop(pop) { + require(isCN(cnNodeId), "cnNodeId is not in AddressBook"); + if (record[cnNodeId].publicKey.length == 0) { + allNodeIds.push(cnNodeId); + } + + record[cnNodeId] = BlsPublicKeyInfo(publicKey, pop); + emit Registered(cnNodeId, publicKey, pop); + } + + function unregister(address cnNodeId) external onlyOwner { + require(!isCN(cnNodeId), "CN is still in AddressBook"); + require(record[cnNodeId].publicKey.length != 0, "CN is not registered"); + + _removeCnNodeId(cnNodeId); + emit Unregistered(cnNodeId, record[cnNodeId].publicKey, record[cnNodeId].pop); + delete record[cnNodeId]; + } + + function _removeCnNodeId(address cnNodeId) private { + for (uint256 i = 0; i < allNodeIds.length; i++) { + if (allNodeIds[i] == cnNodeId) { + allNodeIds[i] = allNodeIds[allNodeIds.length - 1]; + allNodeIds.pop(); + break; + } + } + } + + function getAllBlsInfo() external view returns (address[] memory nodeIdList, BlsPublicKeyInfo[] memory pubkeyList) { + nodeIdList = new address[](allNodeIds.length); + pubkeyList = new BlsPublicKeyInfo[](allNodeIds.length); + + for (uint256 i = 0; i < nodeIdList.length; i++) { + nodeIdList[i] = allNodeIds[i]; + pubkeyList[i] = record[allNodeIds[i]]; + } + } + + function isCN(address target) public view returns (bool) { + // getCnInfo if not CN + try abook.getCnInfo(target) { + return true; + } catch { + return false; + } + } +} diff --git a/contracts/KIP/protocol/KIP113/mock/AddressBookMock.sol b/contracts/KIP/protocol/KIP113/mock/AddressBookMock.sol new file mode 100644 index 00000000000..2804194edfd --- /dev/null +++ b/contracts/KIP/protocol/KIP113/mock/AddressBookMock.sol @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.19; + +import "../IAddressBook.sol"; + +contract AddressBookMock is IAddressBook { + address public constant dummy = 0x0000000000000000000000000000000000000000; + // addresses derived from the mnemonic test-junk + // addresses must be aligned with fixtures.ts:getActors() + address public constant abookAdmin = 0x70997970C51812dc3A010C7d01b50e0d17dc79C8; + address public constant cn0 = 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC; + address public constant cn1 = 0x90F79bf6EB2c4f870365E785982E1f101E93b906; + address public constant cn2 = 0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65; + + function getState() external pure returns (address[] memory, uint256) { + address[] memory adminList = new address[](1); + adminList[0] = abookAdmin; + uint256 requirement = 1; + return (adminList, requirement); + } + + function getCnInfo(address _cnNodeId) external pure returns (address, address, address) { + address[3] memory cnList = [cn0, cn1, cn2]; + + for (uint256 i = 0; i < cnList.length; i++) { + if (_cnNodeId == cnList[i]) { + return (cnList[i], dummy, dummy); + } + } + + revert("Invalid CN node ID."); + } +} + +contract AddressBookMockOneCN is IAddressBook { + address public constant dummy = 0x0000000000000000000000000000000000000000; + // addresses derived from the mnemonic test-junk + // addresses must be aligned with fixtures.ts:getActors() + address public constant abookAdmin = 0x70997970C51812dc3A010C7d01b50e0d17dc79C8; + address public constant cn0 = 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC; + + function getState() external pure returns (address[] memory, uint256) { + address[] memory adminList = new address[](1); + adminList[0] = abookAdmin; + uint256 requirement = 1; + return (adminList, requirement); + } + + function getCnInfo(address _cnNodeId) external pure returns (address, address, address) { + address[1] memory cnList = [cn0]; + + for (uint256 i = 0; i < cnList.length; i++) { + if (_cnNodeId == cnList[i]) { + return (cnList[i], dummy, dummy); + } + } + + revert("Invalid CN node ID."); + } +} diff --git a/contracts/KIP/protocol/KIP113/mock/SimpleBlsRegistryMock.sol b/contracts/KIP/protocol/KIP113/mock/SimpleBlsRegistryMock.sol new file mode 100644 index 00000000000..86acee427e4 --- /dev/null +++ b/contracts/KIP/protocol/KIP113/mock/SimpleBlsRegistryMock.sol @@ -0,0 +1,45 @@ +// Copyright 2023 The klaytn Authors +// This file is part of the klaytn library. +// +// The klaytn library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The klaytn library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the klaytn library. If not, see . + +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.19; + +import "../SimpleBlsRegistry.sol"; + +contract SimpleBlsRegistryMock is IKIP113 { + function getAllBlsInfo() external pure returns (address[] memory, BlsPublicKeyInfo[] memory) { + address[] memory ret1 = new address[](3); + ret1[0] = 0x1111111111111111111111111111111111111111; + ret1[1] = 0x2222222222222222222222222222222222222222; + ret1[2] = 0x3333333333333333333333333333333333333333; + + BlsPublicKeyInfo[] memory ret2 = new BlsPublicKeyInfo[](3); + ret2[0] = BlsPublicKeyInfo( + hex"111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111", + hex"111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111" + ); + ret2[1] = BlsPublicKeyInfo( + hex"222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222", + hex"222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222" + ); + ret2[2] = BlsPublicKeyInfo( + hex"333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333", + hex"333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333" + ); + + return (ret1, ret2); + } +} diff --git a/contracts/KIP/protocol/KIP149/IRegistry.sol b/contracts/KIP/protocol/KIP149/IRegistry.sol new file mode 100644 index 00000000000..37c339c722e --- /dev/null +++ b/contracts/KIP/protocol/KIP149/IRegistry.sol @@ -0,0 +1,67 @@ +// Copyright 2023 The klaytn Authors +// This file is part of the klaytn library. +// +// The klaytn library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The klaytn library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the klaytn library. If not, see . + +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.18; + +abstract contract IRegistry { + /* ========== VARIABLES ========== */ + /// The following variables are baked here because their storage layouts matter in protocol consensus + /// when inject initial states (pre-deployed system contracts, owner) of the Registry. + /// @dev Mapping of system contracts + mapping(string => Record[]) public records; + + /// @dev Array of system contract names + string[] public names; + + /// @dev Owner of contract + address internal _owner; + + /* ========== TYPES ========== */ + /// @dev Struct of system contracts + struct Record { + address addr; + uint256 activation; + } + + /* ========== EVENTS ========== */ + /// @dev Emitted when the contract owner is updated by `transferOwnership`. + event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); + + /// @dev Emitted when a new system contract is registered. + event Registered(string name, address indexed addr, uint256 indexed activation); + + /* ========== MUTATORS ========== */ + /// @dev Registers a new system contract. + function register(string memory name, address addr, uint256 activation) external virtual; + + /// @dev Transfers ownership to newOwner. + function transferOwnership(address newOwner) external virtual; + + /* ========== GETTERS ========== */ + /// @dev Returns an address for active system contracts registered as name if exists. + /// It returns a zero address if there's no active system contract with name. + function getActiveAddr(string memory name) external virtual returns (address); + + /// @dev Returns all system contracts registered as name. + function getAllRecords(string memory name) external view virtual returns (Record[] memory); + + /// @dev Returns all names of registered system contracts. + function getAllNames() external view virtual returns (string[] memory); + + /// @dev Returns owner of contract. + function owner() external view virtual returns (address); +} diff --git a/contracts/KIP/protocol/KIP149/Registry.sol b/contracts/KIP/protocol/KIP149/Registry.sol new file mode 100644 index 00000000000..3bf9d9032da --- /dev/null +++ b/contracts/KIP/protocol/KIP149/Registry.sol @@ -0,0 +1,149 @@ +// Copyright 2023 The klaytn Authors +// This file is part of the klaytn library. +// +// The klaytn library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The klaytn library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the klaytn library. If not, see . + +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.18; + +import "./IRegistry.sol"; + +/** + * @dev Registry is a contract that manages the addresses of system contracts. + * Note: The pre-deployed system contracts will be directly injected into the registry in HF block. + * + * register: Registers a new system contract. + * - Only can be registered by governance. + * - If predecessor is not yet active, overwrite it. + * + * Code organization + * - Modifiers + * - Mutators + * - Getters + */ +contract Registry is IRegistry { + /* ========== MODIFIERS ========== */ + // /** + // * @dev Throws if not called by systemTx. + // * TODO: Decide whether to use this modifier or not. + // */ + // modifier onlySystemTx() { + // _; + // } + + /** + * @dev Throws if called by any account other than the owner. + */ + modifier onlyOwner() { + require(msg.sender == owner(), "Not owner"); + _; + } + + /** + * @dev Throws if the given string is empty. + */ + modifier notEmptyString(string memory name) { + bytes memory b = abi.encodePacked(name); + require(b.length != 0, "Empty string"); + _; + } + + /* ========== MUTATORS ========== */ + /** + * @dev Registers a new system contract to the records. + * @param name The name of the contract to register. + * @param addr The address of the contract to register. + * @param activation The activation block number of the contract. + * NOTE: Register a zero address if you want to deprecate the contract without replacing it. + */ + function register( + string memory name, + address addr, + uint256 activation + ) external override onlyOwner notEmptyString(name) { + // Don't allow the current block since it affects to other txs in the same block. + require(activation > block.number, "Can't register contract from past"); + + uint256 length = records[name].length; + + if (length == 0) { + names.push(name); + records[name].push(Record(addr, activation)); + } else { + Record storage last = records[name][length - 1]; + if (last.activation <= block.number) { + // Last record is active. Append new record. + records[name].push(Record(addr, activation)); + } else { + // Last record is not yet active. Overwrite last record. + last.addr = addr; + last.activation = activation; + } + } + + emit Registered(name, addr, activation); + } + + /** + * @dev Transfers ownership of the contract to a newOwner. + * @param newOwner The address to transfer ownership to. + */ + function transferOwnership(address newOwner) external override onlyOwner { + require(newOwner != address(0), "Zero address"); + _owner = newOwner; + + emit OwnershipTransferred(msg.sender, newOwner); + } + + /* ========== GETTERS ========== */ + /** + * @dev Returns the address of contract if active at current block. + * @param name The name of the contract to check. + * Note: If there is no active contract, it returns address(0). + */ + function getActiveAddr(string memory name) public view virtual override returns (address) { + uint256 length = records[name].length; + + // activation is always in ascending order. + for (uint256 i = length; i > 0; i--) { + if (records[name][i - 1].activation <= block.number) { + return records[name][i - 1].addr; + } + } + + return address(0); + } + + /** + * @dev Returns all contract with same name. + * @param name The name of the contract to check. + */ + function getAllRecords(string memory name) public view override returns (Record[] memory) { + return records[name]; + } + + /** + * @dev Returns the all system contract names. (include deprecated contracts) + */ + function getAllNames() public view override returns (string[] memory) { + return names; + } + + /** + * @dev Returns the owner of the contract. + */ + function owner() public view override returns (address) { + return _owner; + } +} diff --git a/contracts/KIP/protocol/KIP81/CnStakingV2.sol b/contracts/KIP/protocol/KIP81/CnStakingV2.sol new file mode 100644 index 00000000000..7351356ebe2 --- /dev/null +++ b/contracts/KIP/protocol/KIP81/CnStakingV2.sol @@ -0,0 +1,1044 @@ +// Copyright 2022 The klaytn Authors +// This file is part of the klaytn library. +// +// The klaytn library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The klaytn library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the klaytn library. If not, see . + +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.0; +import "./ICnStakingV2.sol"; + +// Features +// 1. Administration +// - Manage multisig admins. +// - Every multisig operations must be approved (either submit or confirm) +// by exactly `requirement` number of admins. +// - Additionally a `contractValidator` takes part in contract initialization. +// - Functions +// - multisig AddAdmin: add an admin address +// - multisig DeleteAdmin: delete an admin address +// - multisig UpdateRequirement: change multisig threshold +// - multisig ClearRequest: cancel all pending (NotConfirmed) multisig requests +// +// 2. Lockup stakes (Initial lockup) +// - Initial lockup is a set of long-term fixed lockups. +// - Every admins and the contractValidator must agree to the conditions +// for this contract to initialize. +// - KLAYs must be deposited for this contract to initialize. +// - Functions +// - reviewInitialConditions(): Agree to the initlal lockup conditions +// - depositLockupStakingAndInit(): Deposit requried amount +// - multisig WithdrawLockupStaking: Withdraw unlocked amount +// +// 3. Non-lockup stakes (Free stake) +// - Free stakes can be added or removed at any time on admins' discretion. +// - Free stakes can be added either by calling stakeKlay() or sending +// a transaction to this contract with nonzero KLAY (via fallback). +// - It takes STAKE_LOCKUP after withdrawal request to actually take out the KLAY. +// - Functions +// - multisig ApproveStakingWithdrawal: Schedule a withdrawal +// - multisig CancelApprovedStakingWithdrawal: Cancel a withdrawal request +// - withdrawApprovedStaking(): Take out the KLAY or cancel an expired withdrawal request. +// +// 4. External accounts +// - Several addresses constitute the identity of this CN. +// - Among them, RewardAddress can be modified via CnStaking contract. +// - Functions +// - multisig UpdateRewardAddress: Setup pendingRewardAddress +// - acceptRewardAddress(): Request AddressBook to change reward address. +// - multisig UpdateStakingTracker: Change the StakingTracker contract to report stakes. +// - multisig UpdateVoterAddress: Change the Voter account and notify to StakingTracker. + +// Code organization +// - Constants +// - States +// - Modifiers +// - Mutators +// - Constructor and initializers +// - Specific multisig operations +// - Generic multisig facility +// - Private helpers +// - Other public functions +// - Getters + +contract CnStakingV2 is ICnStakingV2 { + // Constants + // - Constants are defined as virtual functions to allow easier unit tests. + uint256 constant public ONE_WEEK = 1 weeks; + function MAX_ADMIN() + public view virtual override returns(uint256) { return 50; } + function CONTRACT_TYPE() + public view virtual override returns(string memory) { return "CnStakingContract"; } + function VERSION() + public view virtual override returns(uint256) { return 2; } + function ADDRESS_BOOK_ADDRESS() + public view virtual override returns(address) { return 0x0000000000000000000000000000000000000400; } + function STAKE_LOCKUP() + public view virtual override returns(uint256) { return ONE_WEEK; } + + // State variables + + // Multisig admin list + address public contractValidator; // temporary admin only used during initialization + address[] public adminList; // all persistent admins + uint256 public requirement; // this number of admins must approve a request + mapping (address => bool) public isAdmin; + + // Multisig requests + uint256 public lastClearedId; // For efficient ClearRequest + uint256 public requestCount; + mapping(uint256 => Request) private requestMap; + struct Request { + Functions functionId; + bytes32 firstArg; + bytes32 secondArg; + bytes32 thirdArg; + address requestProposer; + address[] confirmers; + RequestState state; + } + + // Initial lockup + LockupConditions public lockupConditions; + uint256 public initialLockupStaking; + uint256 public remainingLockupStaking; + bool public isInitialized; + struct LockupConditions { + uint256[] unlockTime; + uint256[] unlockAmount; + bool allReviewed; + uint256 reviewedCount; + mapping(address => bool) reviewedAdmin; + } + + // Free stakes + uint256 public staking; + uint256 public unstaking; + uint256 public withdrawalRequestCount; + mapping(uint256 => WithdrawalRequest) private withdrawalRequestMap; + struct WithdrawalRequest { + address to; + uint256 value; + uint256 withdrawableFrom; + WithdrawalStakingState state; + } + + // External accounts + uint256 public override gcId; // used to group staking contracts + address public override nodeId; // informational + address public override rewardAddress; // informational + address public override pendingRewardAddress; // used in updateRewardAddress in progress + address public override stakingTracker; // used to call refreshStake(), refreshVoter() + address public override voterAddress; // read by StakingTracker + + modifier onlyMultisigTx() { + require(msg.sender == address(this), "Not a multisig-transaction."); + _; + } + + modifier onlyAdmin(address _admin) { + require(isAdmin[_admin], "Address is not admin."); + _; + } + + modifier adminDoesNotExist(address _admin) { + require(!isAdmin[_admin], "Admin already exists."); + _; + } + + modifier notNull(address _address) { + require(_address != address(0), "Address is null"); + _; + } + + modifier notConfirmedRequest(uint256 _id) { + require(requestMap[_id].state == RequestState.NotConfirmed, "Must be at not-confirmed state."); + _; + } + + modifier validRequirement(uint256 _adminCount, uint256 _requirement) { + require(_adminCount <= MAX_ADMIN() + && _requirement <= _adminCount + && _requirement != 0 + && _adminCount != 0, "Invalid requirement."); + _; + } + + modifier beforeInit() { + require(isInitialized == false, "Contract has been initialized."); + _; + } + + modifier afterInit() { + require(isInitialized == true, "Contract is not initialized."); + _; + } + + // Initialization functions + + /// @dev Fill in initial values for the contract + /// Emits a DeployContract event. + /// @param _contractValidator A temporary admin to perform initial condition checks + /// @param _nodeId The NodeID of this CN + /// @param _rewardAddress The RewardBase of this CN + /// @param _cnAdminlist Initial list of admins + /// @param _requirement Number of required multisig confirmations + /// @param _unlockTime List of initial lockup deadlines in block timestamp + /// @param _unlockAmount List of initial lockup amounts in peb + constructor(address _contractValidator, address _nodeId, address _rewardAddress, + address[] memory _cnAdminlist, uint256 _requirement, + uint256[] memory _unlockTime, uint256[] memory _unlockAmount) + notNull(_contractValidator) + notNull(_nodeId) + notNull(_rewardAddress) + validRequirement(_cnAdminlist.length, _requirement) { + + // Sanitize _cnAdminlist + isAdmin[_contractValidator] = true; + for (uint256 i = 0; i < _cnAdminlist.length; i++) { + require(!isAdmin[_cnAdminlist[i]] && + _cnAdminlist[i] != address(0), "Address is null or not unique."); + isAdmin[_cnAdminlist[i]] = true; + } + + // Sanitize _unlockTime and _unlockAmount + require(_unlockTime.length != 0 && + _unlockAmount.length != 0 && + _unlockTime.length == _unlockAmount.length, "Invalid unlock time and amount."); + uint256 unlockTime = block.timestamp; + + for (uint256 i = 0; i < _unlockAmount.length; i++) { + require(unlockTime < _unlockTime[i], "Unlock time is not in ascending order."); + require(_unlockAmount[i] > 0, "Amount is not positive number."); + unlockTime = _unlockTime[i]; + } + + contractValidator = _contractValidator; + nodeId = _nodeId; + rewardAddress = _rewardAddress; + + adminList = _cnAdminlist; + requirement = _requirement; + + lockupConditions.unlockTime = _unlockTime; + lockupConditions.unlockAmount = _unlockAmount; + isInitialized = false; + + emit DeployContract(CONTRACT_TYPE(), _contractValidator, _nodeId, _rewardAddress, + _cnAdminlist, _requirement, _unlockTime, _unlockAmount); + } + + /// @dev Set the initial stakingTracker address + /// Emits a UpdateStakingTracker event. + /// This step can be skipped if automatic StakingTracker refresh is not needed. + function setStakingTracker(address _tracker) external override + beforeInit() + onlyAdmin(msg.sender) + notNull(_tracker) { + require(validStakingTracker(_tracker), "Invalid contract"); + + stakingTracker = _tracker; + emit UpdateStakingTracker(_tracker); + } + + /// @dev Set the gcId + /// The gcId never changes once initialized. + /// Emits a UpdateCouncilId event. + function setGCId(uint256 _gcId) external override + beforeInit() + onlyAdmin(msg.sender) { + require(_gcId != 0, "GC ID cannot be zero"); + gcId = _gcId; + emit UpdateGCId(_gcId); + } + + /// @dev Agree on the initial lockup conditions + /// The contractValidator and every initial admins (cnAdminList) must agree + /// for this contract to initialize. + /// Emits a ReviewInitialConditions event. + /// Emits a CompleteReviewInitialConditions if everyone has reviewed. + function reviewInitialConditions() external override + beforeInit() + onlyAdmin(msg.sender) { + require(lockupConditions.reviewedAdmin[msg.sender] == false, + "Msg.sender already reviewed."); + lockupConditions.reviewedAdmin[msg.sender] = true; + lockupConditions.reviewedCount ++; + emit ReviewInitialConditions(msg.sender); + + if (lockupConditions.reviewedCount == adminList.length + 1) { + lockupConditions.allReviewed = true; + emit CompleteReviewInitialConditions(); + } + } + + /// @dev Completes the contract initialization by depositing initial lockup amounts. + /// Everyone must have agreed on initial lockup conditions, + /// The transaction must send exactly the initial lockup amount of KLAY. + /// Emits a DepositLockupStakingAndInit event. + function depositLockupStakingAndInit() external payable override + beforeInit() { + require(gcId != 0, "GC ID cannot be zero"); + require(lockupConditions.allReviewed == true, "Reviewing is not finished."); + + uint256 requiredStakingAmount; + for (uint256 i = 0; i < lockupConditions.unlockAmount.length; i++) { + requiredStakingAmount += lockupConditions.unlockAmount[i]; + } + require(msg.value == requiredStakingAmount, "Value does not match."); + initialLockupStaking = requiredStakingAmount; + remainingLockupStaking = requiredStakingAmount; + + // Remove the temporary admin (i.e. contractValidator) + isAdmin[contractValidator] = false; + delete contractValidator; + + isInitialized = true; + emit DepositLockupStakingAndInit(msg.sender, msg.value); + } + + // Multisig operations + + /// @dev Submit a request to add an admin to adminList + /// @param _admin new admin address + function submitAddAdmin(address _admin) external override + afterInit() + onlyAdmin(msg.sender) + notNull(_admin) + adminDoesNotExist(_admin) + validRequirement(adminList.length + 1, requirement) { + uint256 id = submitRequest(Functions.AddAdmin, toBytes32(_admin), 0, 0); + confirmRequest(id); + } + + /// @dev Add an admin to adminList + /// @param _admin new admin address + /// Emits an AddAdmin event. + /// All outstanding requests (i.e. NotConfirmed) are canceled. + function addAdmin(address _admin) external override + onlyMultisigTx() + notNull(_admin) + adminDoesNotExist(_admin) + validRequirement(adminList.length + 1, requirement) { + isAdmin[_admin] = true; + adminList.push(_admin); + clearRequest(); + emit AddAdmin(_admin); + } + + /// @dev Submit a request to delete an admin from adminList + /// @param _admin the admin address + function submitDeleteAdmin(address _admin) external override + afterInit() + onlyAdmin(msg.sender) + notNull(_admin) + onlyAdmin(_admin) + validRequirement(adminList.length - 1, requirement) { + uint256 id = submitRequest(Functions.DeleteAdmin, toBytes32(_admin), 0, 0); + confirmRequest(id); + } + + /// @dev Delete an admin from adminList + /// @param _admin the admin address + /// Emits a DeleteAdmin event. + /// All outstanding requests (i.e. NotConfirmed) are canceled. + function deleteAdmin(address _admin) external override + onlyMultisigTx() + notNull(_admin) + onlyAdmin(_admin) + validRequirement(adminList.length - 1, requirement) { + deleteArrayElement(adminList, _admin); + isAdmin[_admin] = false; + clearRequest(); + emit DeleteAdmin(_admin); + } + + /// @dev submit a request to update the confirmation threshold + /// @param _requirement new confirmation threshold + function submitUpdateRequirement(uint256 _requirement) external override + afterInit() + onlyAdmin(msg.sender) + validRequirement(adminList.length, _requirement) { + require(_requirement != requirement, "Invalid value"); + uint256 id = submitRequest(Functions.UpdateRequirement, bytes32(_requirement), 0, 0); + confirmRequest(id); + } + + /// @dev update the confirmation threshold + /// @param _requirement new confirmation threshold + /// Emits an UpdateRequirement event. + /// All outstanding requests (i.e. NotConfirmed) are canceled. + function updateRequirement(uint256 _requirement) external override + onlyMultisigTx() + validRequirement(adminList.length, _requirement) { + requirement = _requirement; + clearRequest(); + emit UpdateRequirement(_requirement); + } + + /// @dev submit a request to cancel all outstanding (i.e. NotConfirmed) requests + function submitClearRequest() external override + afterInit() + onlyAdmin(msg.sender) { + uint256 id = submitRequest(Functions.ClearRequest, 0, 0, 0); + confirmRequest(id); + } + + /// @dev cancel all outstanding (i.e. NotConfirmed) requests + /// Emits a ClearRequest event. + function clearRequest() public override + onlyMultisigTx() { + for (uint256 i = lastClearedId; i < requestCount; i++){ + if (requestMap[i].state == RequestState.NotConfirmed) { + requestMap[i].state = RequestState.Canceled; + } + } + lastClearedId = requestCount; + emit ClearRequest(); + } + + /// @dev Submit a request to withdraw a part of initial lockup stakes + /// + /// Max withdrawable amount is (unlocked - withdrawn), + /// where unlocked = amounts that lockup period has passed, + /// and withdrawn = (initial - remaining). + /// + /// @param _to The recipient address + /// @param _value The amount + function submitWithdrawLockupStaking(address payable _to, uint256 _value) external override + afterInit() + onlyAdmin(msg.sender) + notNull(_to) { + ( , , , , uint256 withdrawableAmount) = getLockupStakingInfo(); + require(_value > 0 && _value <= withdrawableAmount, "Invalid value."); + + uint256 id = submitRequest(Functions.WithdrawLockupStaking, + toBytes32(_to), bytes32(_value), 0); + confirmRequest(id); + } + + /// @dev Withdraw a part of initial lockup stakes + /// Emits a WithdrawLockupStaking event. + function withdrawLockupStaking(address payable _to, uint256 _value) external override + onlyMultisigTx() + notNull(_to) { + ( , , , , uint256 withdrawableAmount) = getLockupStakingInfo(); + require(_value > 0 && _value <= withdrawableAmount, "Value is not withdrawable."); + + remainingLockupStaking -= _value; + + (bool success, ) = _to.call{ value: _value }(""); + require(success, "Transfer failed."); + + safeRefreshStake(); + emit WithdrawLockupStaking(_to, _value); + } + + /// @dev submit a request to withdraw a part of free stakes. + /// + /// Creates a new WithdrawalRequest + /// The WithdrawalRequest is withdrawable from request creation + STAKE_LOCKUP. + /// The WithdrawalRequest expires from request creation + 2 * STAKE_LOCKUP. + /// + /// Max withdrawable amount is (staked - unstaking). + /// Once the WithdrawalRequest is created, unstaking amount increases. + /// + /// @param _to The recipient address + /// @param _value The amount + function submitApproveStakingWithdrawal(address _to, uint256 _value) external override + afterInit() + onlyAdmin(msg.sender) + notNull(_to) { + require(_value > 0 && _value <= staking, "Invalid value."); + require(unstaking + _value <= staking, "Too much outstanding withdrawal"); + uint256 id = submitRequest(Functions.ApproveStakingWithdrawal, + toBytes32(_to), bytes32(_value), 0); + confirmRequest(id); + } + + /// @dev Withdraw a part of free stakes. + /// Emits a ApproveStakingWithdrawal event. + function approveStakingWithdrawal(address _to, uint256 _value) external override + onlyMultisigTx() + notNull(_to) { + require(_value > 0 && _value <= staking, "Invalid value."); + require(unstaking + _value <= staking, "Too much outstanding withdrawal"); + uint256 id = withdrawalRequestCount; + withdrawalRequestCount ++; + + uint256 time = block.timestamp + STAKE_LOCKUP(); + withdrawalRequestMap[id] = WithdrawalRequest({ + to : _to, + value : _value, + withdrawableFrom : time, + state: WithdrawalStakingState.Unknown + }); + unstaking += _value; + safeRefreshStake(); + emit ApproveStakingWithdrawal(id, _to, _value, time); + } + + /// @dev submit a request to cancel a withdrawal request + /// The withdrawal request ID can be obtained from ApproveStakingWithdrawal event + /// or getApprovedStakingWithdrawalIds(). + /// Unstaking amount decreases. + function submitCancelApprovedStakingWithdrawal(uint256 _id) external override + afterInit() + onlyAdmin(msg.sender) { + WithdrawalRequest storage request = withdrawalRequestMap[_id]; + require(request.to != address(0), "Withdrawal request does not exist."); + require(request.state == WithdrawalStakingState.Unknown, "Invalid state."); + + uint256 id = submitRequest(Functions.CancelApprovedStakingWithdrawal, bytes32(_id), 0, 0); + confirmRequest(id); + } + + /// @dev cancel a withdrawal request + /// Emits a CancelApprovedStakingWithdrawal event. + function cancelApprovedStakingWithdrawal(uint256 _id) external override + onlyMultisigTx() { + WithdrawalRequest storage request = withdrawalRequestMap[_id]; + require(request.to != address(0), "Withdrawal request does not exist."); + require(request.state == WithdrawalStakingState.Unknown, "Invalid state."); + + request.state = WithdrawalStakingState.Canceled; + unstaking -= request.value; + safeRefreshStake(); + emit CancelApprovedStakingWithdrawal(_id, request.to, request.value); + } + + /// @dev submit a request to update the reward address of this CN + function submitUpdateRewardAddress(address _addr) external override + afterInit() + onlyAdmin(msg.sender) { + uint256 id = submitRequest(Functions.UpdateRewardAddress, toBytes32(_addr), 0, 0); + confirmRequest(id); + } + + /// @dev Update the reward address in the AddressBook + /// Emits an UpdateRewardAddress event. + /// Need to call acceptRewardAddress() to reflect the change to AddressBook. + /// The address can be null, which cancels the reward address update attempt. + function updateRewardAddress(address _addr) external override + onlyMultisigTx() { + pendingRewardAddress = _addr; + emit UpdateRewardAddress(_addr); + } + + /// @dev submit a request to update the staking tracker this CN reports to + /// Should not be called if there is an active proposal + function submitUpdateStakingTracker(address _tracker) external override + afterInit() + onlyAdmin(msg.sender) + notNull(_tracker) { + require(validStakingTracker(_tracker), "Invalid contract"); + if (stakingTracker != address(0)) { + IStakingTracker(stakingTracker).refreshStake(address(this)); + require(IStakingTracker(stakingTracker).getLiveTrackerIds().length == 0, "Cannot update tracker when there is an active tracker"); + } + + uint256 id = submitRequest(Functions.UpdateStakingTracker, toBytes32(_tracker), 0, 0); + confirmRequest(id); + } + + /// @dev Update the staking tracker + /// Emits an UpdateStakingTracker event. + /// Should not be called if there is an active proposal + function updateStakingTracker(address _tracker) external override + onlyMultisigTx() + notNull(_tracker) { + require(validStakingTracker(_tracker), "Invalid contract"); + if (stakingTracker != address(0)) { + IStakingTracker(stakingTracker).refreshStake(address(this)); + require(IStakingTracker(stakingTracker).getLiveTrackerIds().length == 0, "Cannot update tracker when there is an active tracker"); + } + + stakingTracker = _tracker; + emit UpdateStakingTracker(_tracker); + } + + /// @dev submit a request to update the voter address of this CN + function submitUpdateVoterAddress(address _addr) external override + afterInit() + onlyAdmin(msg.sender) { + if (stakingTracker != address(0) && _addr != address(0)) { + address oldGCId = IStakingTracker(stakingTracker).voterToGCId(_addr); + require(oldGCId == address(0), "Voter address already taken"); + } + uint256 id = submitRequest(Functions.UpdateVoterAddress, toBytes32(_addr), 0, 0); + confirmRequest(id); + } + + /// @dev Update the voter address of this CN + /// Emits an UpdateVoterAddress event. + function updateVoterAddress(address _addr) external override + onlyMultisigTx() { + voterAddress = _addr; + + if (stakingTracker != address(0)) { + IStakingTracker(stakingTracker).refreshVoter(address(this)); + } + emit UpdateVoterAddress(_addr); + } + + // Generic multisig facility + + /// @dev Submits a request + /// Emits a SubmitRequest event. + /// @return the request ID + function submitRequest(Functions _functionId, + bytes32 _firstArg, bytes32 _secondArg, bytes32 _thirdArg) private + returns(uint256) { + uint256 id = requestCount; + requestCount ++; + + requestMap[id] = Request({ + functionId : _functionId, + firstArg : _firstArg, + secondArg : _secondArg, + thirdArg : _thirdArg, + requestProposer : msg.sender, + confirmers : new address[](0), + state: RequestState.NotConfirmed + }); + emit SubmitRequest(id, msg.sender, _functionId, _firstArg, _secondArg, _thirdArg); + return id; + } + + /// @dev Confirm a submitted request by another admin + /// Note that a submitXYZ() automatically calls confirmRequest(). + /// Therefore an explicit confirmRequest() is only relevant when requirement >= 2. + /// + /// Emits a ConfirmRequest event. + /// The necessary data can be obtained from SubmitRequest event or getRequestInfo(). + /// + /// @param _id The request ID + /// @param _functionId The function ID in enum Functions + /// @param _firstArg The first argument + /// @param _secondArg The second argument + /// @param _thirdArg The third argument + function confirmRequest(uint256 _id, Functions _functionId, + bytes32 _firstArg, bytes32 _secondArg, bytes32 _thirdArg) public override + notConfirmedRequest(_id) + onlyAdmin(msg.sender) { + require(!hasConfirmed(_id, msg.sender), "Msg.sender already confirmed."); + require( + requestMap[_id].functionId == _functionId && + requestMap[_id].firstArg == _firstArg && + requestMap[_id].secondArg == _secondArg && + requestMap[_id].thirdArg == _thirdArg, "Function id and arguments do not match."); + + requestMap[_id].confirmers.push(msg.sender); + emit ConfirmRequest(_id, msg.sender, _functionId, + _firstArg, _secondArg, _thirdArg, requestMap[_id].confirmers); + + if (requestMap[_id].confirmers.length >= requirement) { + executeRequest(_id); + } + } + + /// @dev Shortcut of confirmRequest(...) + /// Used by submitXYZ() functions. + function confirmRequest(uint256 id) private { + confirmRequest(id, requestMap[id].functionId, + requestMap[id].firstArg, requestMap[id].secondArg, requestMap[id].thirdArg); + } + + /// @dev Revoke a confirmation to a request + /// If the sender is the proposer of the request, the request is canceled. + /// Otherwise, the sender is simply deleted from the confirmers list. + /// + /// Emits a CancelRequest or RevokeConfirmation event. + /// The necessary data can be obtained from SubmitRequest event or getRequestInfo(). + /// + /// @param _id The request ID + /// @param _functionId The function ID in enum Functions + /// @param _firstArg The first argument + /// @param _secondArg The second argument + /// @param _thirdArg The third argument + function revokeConfirmation(uint256 _id, Functions _functionId, + bytes32 _firstArg, bytes32 _secondArg, bytes32 _thirdArg) external override + notConfirmedRequest(_id) + onlyAdmin(msg.sender) { + require(hasConfirmed(_id, msg.sender), "Msg.sender has not confirmed."); + require( + requestMap[_id].functionId == _functionId && + requestMap[_id].firstArg == _firstArg && + requestMap[_id].secondArg == _secondArg && + requestMap[_id].thirdArg == _thirdArg, "Function id and arguments do not match."); + + if (requestMap[_id].requestProposer == msg.sender) { + requestMap[_id].state = RequestState.Canceled; + emit CancelRequest(_id, msg.sender, requestMap[_id].functionId, + requestMap[_id].firstArg, requestMap[_id].secondArg, requestMap[_id].thirdArg); + } else { + deleteArrayElement(requestMap[_id].confirmers, msg.sender); + emit RevokeConfirmation(_id, msg.sender, requestMap[_id].functionId, + requestMap[_id].firstArg, requestMap[_id].secondArg, requestMap[_id].thirdArg, + requestMap[_id].confirmers); + } + } + + /// @dev execute a requested function + /// Used by confirmRequest when enough confirmations are made. + /// Emits a ExecuteRequestSuccess or ExecuteRequestFailure event. + function executeRequest(uint256 _id) private { + bool ok = false; + bytes memory out; + Functions funcId = requestMap[_id].functionId; + bytes32 a1 = requestMap[_id].firstArg; + bytes32 a2 = requestMap[_id].secondArg; + bytes32 a3 = requestMap[_id].thirdArg; + + if (funcId == Functions.AddAdmin) { + (ok, out) = address(this).call(abi.encodeWithSignature("addAdmin(address)", a1)); + } else if (funcId == Functions.DeleteAdmin) { + (ok, out) = address(this).call(abi.encodeWithSignature("deleteAdmin(address)", a1)); + } else if (funcId == Functions.UpdateRequirement) { + (ok, out) = address(this).call(abi.encodeWithSignature("updateRequirement(uint256)", a1)); + } else if (funcId == Functions.ClearRequest) { + (ok, out) = address(this).call(abi.encodeWithSignature("clearRequest()")); + } else if (funcId == Functions.WithdrawLockupStaking) { + (ok, out) = address(this).call( + abi.encodeWithSignature("withdrawLockupStaking(address,uint256)", a1, a2)); + } else if (funcId == Functions.ApproveStakingWithdrawal) { + (ok, out) = address(this).call( + abi.encodeWithSignature("approveStakingWithdrawal(address,uint256)", a1, a2)); + } else if (funcId == Functions.CancelApprovedStakingWithdrawal) { + (ok, out) = address(this).call( + abi.encodeWithSignature("cancelApprovedStakingWithdrawal(uint256)", a1)); + } else if (funcId == Functions.UpdateRewardAddress) { + (ok, out) = address(this).call( + abi.encodeWithSignature("updateRewardAddress(address)", a1)); + } else if (funcId == Functions.UpdateStakingTracker) { + (ok, out) = address(this).call( + abi.encodeWithSignature("updateStakingTracker(address)", a1)); + } else if (funcId == Functions.UpdateVoterAddress) { + (ok, out) = address(this).call( + abi.encodeWithSignature("updateVoterAddress(address)", a1)); + } else { + revert("Unsupported function"); + } + + if (ok) { + requestMap[_id].state = RequestState.Executed; + emit ExecuteRequestSuccess(_id, msg.sender, funcId, a1, a2, a3); + } else { + requestMap[_id].state = RequestState.ExecutionFailed; + emit ExecuteRequestFailure(_id, msg.sender, funcId, a1, a2, a3); + } + } + + // Helper functions + + function toBytes32(address _x) private pure returns(bytes32) { + return bytes32(uint256(uint160(_x))); + } + + function hasConfirmed(uint256 _id, address addr) private view returns(bool) { + for (uint i = 0; i < requestMap[_id].confirmers.length; i++) { + if (requestMap[_id].confirmers[i] == addr) { + return true; + } + } + return false; + } + + function deleteArrayElement(address[] storage array, address target) private { + for (uint i = 0; i < array.length; i++) { + if (array[i] == target) { + if (i != array.length - 1) { + array[i] = array[array.length - 1]; + } + array.pop(); + return; + } + } + } + + /// @dev Checks if a given address is valid StakingTracker contract + function validStakingTracker(address _tracker) private view returns(bool) { + string memory _type = IStakingTracker(_tracker).CONTRACT_TYPE(); + uint256 _version = IStakingTracker(_tracker).VERSION(); + return (keccak256(bytes(_type)) == keccak256(bytes("StakingTracker")) && + _version == 1); + } + + // Public functions + + /// @dev Add more free stakes + /// Emits a StakeKlay event. + function stakeKlay() public payable override + afterInit() { + require(msg.value > 0, "Invalid amount."); + staking += msg.value; + safeRefreshStake(); + emit StakeKlay(msg.sender, msg.value); + } + + /// @dev The fallback which add more free stakes + /// + /// Note that This fallback only accept transactions with empty calldata. + /// contract calls with wrong function signature is reverted despite this fallback. + receive() external payable override + afterInit() { + stakeKlay(); + } + + /// @dev Refresh the balance of this contract recorded in StakingTracker + /// This function should never revert to allow financial features to work + /// even if stakingTracker is accidentally malfunctioning. + function safeRefreshStake() private { + stakingTracker.call(abi.encodeWithSignature("refreshStake(address)", address(this))); + } + + /// @dev Take out an approved withdrawal amounts. + /// + /// If STAKE_LOCKUP has passed since WithdrawalRequest was created, + /// an admin can call this function to execute the withdrawal. + /// + /// If 2*STAKE_LOCKUP has passed since WithdrawalRequest was created, + /// the withdrawal is canceled by calling this function. + /// + /// Either way, unstaking amount decreases. + /// + /// The withdrawal request ID can be obtained from ApproveStakingWithdrawal event + /// or getApprovedStakingWithdrawalIds(). + function withdrawApprovedStaking(uint256 _id) external override + onlyAdmin(msg.sender) { + WithdrawalRequest storage request = withdrawalRequestMap[_id]; + require(request.to != address(0), "Withdrawal request does not exist."); + require(request.state == WithdrawalStakingState.Unknown, "Invalid state."); + require(request.value <= staking, "Value is not withdrawable."); + require(request.withdrawableFrom <= block.timestamp, "Not withdrawable yet."); + + uint256 withdrawableUntil = request.withdrawableFrom + STAKE_LOCKUP(); + if (withdrawableUntil <= block.timestamp) { + request.state = WithdrawalStakingState.Canceled; + unstaking -= request.value; + + safeRefreshStake(); + emit CancelApprovedStakingWithdrawal(_id, request.to, request.value); + } else { + request.state = WithdrawalStakingState.Transferred; + staking -= request.value; + unstaking -= request.value; + + (bool success, ) = request.to.call{ value: request.value }(""); + require(success, "Transfer failed."); + + safeRefreshStake(); + emit WithdrawApprovedStaking(_id, request.to, request.value); + } + } + + /// @dev Finish updating the reward address + /// Must be called from either the pendingRewardAddress, or one of the AddressBook admins. + /// This step guarantees that the rewardAddress is owned by the current CN. + /// + /// Emits an AcceptRewardAddress event. + /// Also emits a ReviseRewardAddress event from the AddressBook. + function acceptRewardAddress(address _addr) external override { + require(canAcceptRewardAddress(), "Unauthorized to accept reward address"); + require(_addr == pendingRewardAddress, "Given address does not match the pending"); + + IAddressBook(ADDRESS_BOOK_ADDRESS()).reviseRewardAddress(pendingRewardAddress); + rewardAddress = pendingRewardAddress; + pendingRewardAddress = address(0); + + emit UpdateRewardAddress(rewardAddress); + } + + function canAcceptRewardAddress() private returns(bool) { + if (msg.sender == pendingRewardAddress) { + return true; + } + (address[] memory abookAdminList, ) = IAddressBook(ADDRESS_BOOK_ADDRESS()).getState(); + for (uint256 i = 0; i < abookAdminList.length; i++) { + if (msg.sender == abookAdminList[i]) { + return true; + } + } + return false; + } + + // Public getters + + /// @dev Return the reviewers of the initial lockup conditions + /// @return reviewers addresses + function getReviewers() external view override + beforeInit() + returns(address[] memory) { + address[] memory reviewers = new address[](lockupConditions.reviewedCount); + uint256 id = 0; + if (lockupConditions.reviewedAdmin[contractValidator] == true) { + reviewers[id] = contractValidator; + id ++; + } + for (uint256 i = 0; i < adminList.length; i ++) { + if (lockupConditions.reviewedAdmin[adminList[i]] == true) { + reviewers[id] = adminList[i]; + id ++; + } + } + return reviewers; + } + + /// @dev Return the overall adminstrative states + function getState() external view override returns( + address _contractValidator, address _nodeId, address _rewardAddress, + address[] memory _adminList, uint256 _requirement, + uint256[] memory _unlockTime, uint256[] memory _unlockAmount, + bool _allReviewed, bool _isInitialized) { + return(contractValidator, nodeId, rewardAddress, + adminList, requirement, + lockupConditions.unlockTime, lockupConditions.unlockAmount, + lockupConditions.allReviewed, isInitialized); + } + + /// @dev Query request IDs that matches given state. + /// + /// For efficiency, only IDs in range (_from <= id < _to) are searched. + /// If _to == 0 or _to >= requestCount, then the search range is (_from <= id < requestCount). + /// + /// @param _from search begin index + /// @param _to search end index; but search till the end if _to == 0 or _to >= requestCount. + /// @param _state request state + /// @return ids request IDs satisfying the conditions + function getRequestIds(uint256 _from, uint256 _to, RequestState _state) + external view override returns(uint256[] memory ids) { + uint256 begin = _from; + uint256 end = _to; + if (_to == 0 || _to >= requestCount) { + end = requestCount; + } + + // Because memory array cannot grow, we must calculate size first. + uint cnt = 0; + for (uint i = begin; i < end; i++) { + if (requestMap[i].state == _state) { + cnt++; + } + } + ids = new uint256[](cnt); + cnt = 0; + for (uint i = begin; i < end; i++) { + if (requestMap[i].state == _state) { + ids[cnt] = i; + cnt++; + } + } + return ids; + } + + /// @dev Query a request details + /// @param _id requestID + function getRequestInfo(uint256 _id) external view override returns( + Functions functionId, bytes32 firstArg, bytes32 secondArg, bytes32 thirdArg, + address proposer, address[] memory confirmers, RequestState state) { + + Request storage r = requestMap[_id]; + return(r.functionId, r.firstArg, r.secondArg, r.thirdArg, + r.requestProposer, r.confirmers, r.state); + } + + /// @dev Query initial lockup status + /// @return unlockTime List of unlocking times in timestamp + /// @return unlockAmount List of unlocking amounts + /// @return initial Initial lockup amount + /// @return remaining Remaining lockup amount = (initial - withdrawn) + /// @return withdrawable Max withdrawable amount = (unlocked - withdrawn) + function getLockupStakingInfo() public view override + afterInit() returns( + uint256[] memory unlockTime, uint256[] memory unlockAmount, + uint256 initial, uint256 remaining, uint256 withdrawable) { + + uint256 unlockedAmount = 0; + for (uint256 i = 0; i < lockupConditions.unlockTime.length; i++){ + if (block.timestamp > lockupConditions.unlockTime[i]) { + unlockedAmount += lockupConditions.unlockAmount[i]; + } + } + + uint256 withdrawnAmount = initialLockupStaking - remainingLockupStaking; + uint256 withdrawableAmount = unlockedAmount - withdrawnAmount; + + return (lockupConditions.unlockTime, lockupConditions.unlockAmount, + initialLockupStaking, remainingLockupStaking, withdrawableAmount); + } + + /// @dev Query withdrawal IDs that matches given state. + /// + /// For efficiency, only IDs in range (_from <= id < _to) are searched. + /// If _to == 0 or _to >= requestCount, then the search range is (_from <= id < requestCount). + /// + /// @param _from search begin index + /// @param _to search end index; but search till the end if _to == 0 or _to >= requestCount. + /// @param _state withdrawal state + /// @return ids withdrawal IDs satisfying the conditions + function getApprovedStakingWithdrawalIds(uint256 _from, uint256 _to, WithdrawalStakingState _state) + external view override returns(uint256[] memory ids) { + uint256 begin = _from; + uint256 end = _to; + if (_to == 0 || _to >= withdrawalRequestCount) { + end = withdrawalRequestCount; + } + + // Because memory array cannot grow, we must calculate size first. + uint cnt = 0; + for (uint i = begin; i < end; i++) { + if (withdrawalRequestMap[i].state == _state) { + cnt += 1; + } + } + ids = new uint256[](cnt); + cnt = 0; + for (uint i = begin; i < end; i++) { + if (withdrawalRequestMap[i].state == _state) { + ids[cnt] = i; + cnt++; + } + } + return ids; + } + + /// @dev Query a withdrawal request details + /// @param _index withdrawal request ID + /// @return to recipient + /// @return value withdrawing amount + /// @return withdrawableFrom withdrawable timestamp + /// @return state the request state + function getApprovedStakingWithdrawalInfo(uint256 _index) external view override returns( + address to, uint256 value, uint256 withdrawableFrom, WithdrawalStakingState state) { + return ( + withdrawalRequestMap[_index].to, + withdrawalRequestMap[_index].value, + withdrawalRequestMap[_index].withdrawableFrom, + withdrawalRequestMap[_index].state + ); + } +} + +interface IAddressBook { + function getState() external view returns(address[] memory, uint256); + function reviseRewardAddress(address) external; +} + +interface IStakingTracker { + function refreshStake(address staking) external; + function refreshVoter(address voter) external; + function CONTRACT_TYPE() external view returns(string memory); + function VERSION() external view returns(uint256); + function voterToGCId(address voter) external view returns(address nodeId); + function getLiveTrackerIds() external view returns(uint256[] memory); +} diff --git a/contracts/KIP/protocol/KIP81/GovParam.sol b/contracts/KIP/protocol/KIP81/GovParam.sol new file mode 100644 index 00000000000..f7a8e8b90f3 --- /dev/null +++ b/contracts/KIP/protocol/KIP81/GovParam.sol @@ -0,0 +1,242 @@ +// Copyright 2022 The klaytn Authors +// This file is part of the klaytn library. +// +// The klaytn library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The klaytn library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the klaytn library. If not, see . + +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/access/Ownable.sol"; +import "./IGovParam.sol"; + +/// @dev Contract to store and update governance parameters +/// This contract can be called by node to read the param values in the current block +/// Also, the governance contract can change the parameter values. +contract GovParam is Ownable, IGovParam { + /// @dev Returns all parameter names that ever existed + string[] public override paramNames; + + mapping(string => Param[]) private _checkpoints; + + /// @dev Returns all parameter names that ever existed, including those that are currently non-existing + function getAllParamNames() external view override returns (string[] memory) { + return paramNames; + } + + /// @dev Returns all checkpoints of the parameter + /// @param name The parameter name + function checkpoints(string calldata name) public view override returns (Param[] memory) { + return _checkpoints[name]; + } + + /// @dev Returns the last checkpoint whose activation block has passed. + /// WARNING: Before calling this function, you must ensure that + /// _checkpoints[name].length > 0 + function _param(string memory name) private view returns (Param storage) { + Param[] storage ckpts = _checkpoints[name]; + uint256 len = ckpts.length; + + // there can be up to one checkpoint whose activation block has not passed yet + // because setParam() will overwrite if there already exists such a checkpoint + // thus, if the last checkpoint's activation is in the future, + // it is guaranteed that the next-to-last is activated + if (ckpts[len - 1].activation <= block.number) { + return ckpts[len - 1]; + } else { + return ckpts[len - 2]; + } + } + + /// @dev Returns the parameter viewed by the current block + /// @param name The parameter name + /// @return (1) Whether the parameter exists, and if the parameter exists, (2) its value + function getParam(string calldata name) external view override returns (bool, bytes memory) { + if (_checkpoints[name].length == 0) { + return (false, ""); + } + + Param memory p = _param(name); + return (p.exists, p.val); + } + + /// @dev Average of two integers without overflow + /// https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.7.3/contracts/utils/math/Math.sol#L34 + function average(uint256 a, uint256 b) internal pure returns (uint256) { + // (a + b) / 2 can overflow. + return (a & b) + (a ^ b) / 2; + } + + /// @dev Returns the parameters used for generating the "blockNumber" block + /// WARNING: for future blocks, the result may change + function getParamAt(string memory name, uint256 blockNumber) public view override returns (bool, bytes memory) { + uint256 len = _checkpoints[name].length; + if (len == 0) { + return (false, ""); + } + + // See https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/extensions/ERC20Votes.sol#L99 + // We run a binary search to look for the earliest checkpoint taken after `blockNumber`. + // During the loop, the index of the wanted checkpoint remains in the range [low-1, high). + // With each iteration, either `low` or `high` is moved towards the middle of the range to maintain the invariant. + // - If the middle checkpoint is after `blockNumber`, we look in [low, mid) + // - If the middle checkpoint is before or equal to `blockNumber`, we look in [mid+1, high) + // Once we reach a single value (when low == high), we've found the right checkpoint at the index high-1, if not + // out of bounds (in which case we're looking too far in the past and the result is 0). + // Note that if the latest checkpoint available is exactly for `blockNumber`, we end up with an index that is + // past the end of the array, so we technically don't find a checkpoint after `blockNumber`, but it works out + // the same. + uint256 low = 0; + uint256 high = len; + + Param[] storage ckpts = _checkpoints[name]; + + while (low < high) { + uint256 mid = average(low, high); + if (ckpts[mid].activation > blockNumber) { + high = mid; + } else { + low = mid + 1; + } + } + + // high can't be zero. For high to be zero, The "high = mid" line should be executed when mid is zero. + // When mid = 0, ckpts[mid].activation is always 0 due to the sentinel checkpoint. + // Therefore, ckpts[mid].activation <= blockNumber, + // and the "high = mid" line is never executed. + return (ckpts[high - 1].exists, ckpts[high - 1].val); + } + + /// @dev Returns existing parameters viewed by the current block + function getAllParams() external view override returns (string[] memory, bytes[] memory) { + // solidity doesn't allow memory arrays to be resized + // so we calculate the size in advance (existCount) + // See https://docs.soliditylang.org/en/latest/types.html#allocating-memory-arrays + uint256 existCount = 0; + for (uint256 i = 0; i < paramNames.length; i++) { + Param storage tmp = _param(paramNames[i]); + if (tmp.exists) { + existCount++; + } + } + + string[] memory names = new string[](existCount); + bytes[] memory vals = new bytes[](existCount); + + uint256 idx = 0; + for (uint256 i = 0; i < paramNames.length; i++) { + Param storage tmp = _param(paramNames[i]); + if (tmp.exists) { + names[idx] = paramNames[i]; + vals[idx] = tmp.val; + idx++; + } + } + return (names, vals); + } + + /// @dev Returns parameters used for generating the "blockNumber" block + /// WARNING: for future blocks, the result may change + function getAllParamsAt(uint256 blockNumber) external view override returns (string[] memory, bytes[] memory) { + // solidity doesn't allow memory arrays to be resized + // so we calculate the size in advance (existCount) + // See https://docs.soliditylang.org/en/latest/types.html#allocating-memory-arrays + uint256 existCount = 0; + for (uint256 i = 0; i < paramNames.length; i++) { + (bool exists, ) = getParamAt(paramNames[i], blockNumber); + if (exists) { + existCount++; + } + } + + string[] memory names = new string[](existCount); + bytes[] memory vals = new bytes[](existCount); + + uint256 idx = 0; + for (uint256 i = 0; i < paramNames.length; i++) { + (bool exists, bytes memory val) = getParamAt(paramNames[i], blockNumber); + if (exists) { + names[idx] = paramNames[i]; + vals[idx] = val; + idx++; + } + } + + return (names, vals); + } + + /// @dev Returns all parameters as stored in the contract + function getAllCheckpoints() external view override returns (string[] memory, Param[][] memory) { + Param[][] memory ckptsArr = new Param[][](paramNames.length); + for (uint256 i = 0; i < paramNames.length; i++) { + ckptsArr[i] = _checkpoints[paramNames[i]]; + } + return (paramNames, ckptsArr); + } + + /// @dev Returns all parameters as stored in the contract + function setParam(string calldata name, bool exists, bytes calldata val, uint256 activation) + public + override + onlyOwner + { + require(bytes(name).length > 0, "GovParam: name cannot be empty"); + require( + activation > block.number, + "GovParam: activation must be in the future" + ); + require( + !exists || val.length > 0, + "GovParam: val must not be empty if exists=true" + ); + require( + exists || val.length == 0, + "GovParam: val must be empty if exists=false" + ); + + Param memory newParam = Param(activation, exists, val); + Param[] storage ckpts = _checkpoints[name]; + + // for a new parameter, push occurs twice + // (1) sentinel checkpoint + // (2) newParam + // this ensures that if name is in paramNames, then ckpts.length >= 2 + if (ckpts.length == 0) { + paramNames.push(name); + + // insert a sentinel checkpoint + ckpts.push(Param(0, false, "")); + } + + uint256 lastPos = ckpts.length - 1; + // if the last checkpoint's activation is in the past, push newParam + // otherwise, overwrite the last checkpoint with newParam + if (ckpts[lastPos].activation <= block.number) { + ckpts.push(newParam); + } else { + ckpts[lastPos] = newParam; + } + + emit SetParam(name, exists, val, activation); + } + + /// @dev Updates the parameter to the given state at the relative activation block + function setParamIn(string calldata name, bool exists, bytes calldata val, uint256 relativeActivation) + external + override + onlyOwner + { + uint256 activation = block.number + relativeActivation; + setParam(name, exists, val, activation); + } +} diff --git a/contracts/KIP/protocol/KIP81/ICnStakingV2.sol b/contracts/KIP/protocol/KIP81/ICnStakingV2.sol new file mode 100644 index 00000000000..5f5687c5242 --- /dev/null +++ b/contracts/KIP/protocol/KIP81/ICnStakingV2.sol @@ -0,0 +1,150 @@ +// Copyright 2022 The klaytn Authors +// This file is part of the klaytn library. +// +// The klaytn library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The klaytn library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the klaytn library. If not, see . + +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.0; + +interface ICnStakingV2 { + // Initialization + event DeployContract(string contractType, address contractValidator, address nodeId, address rewardAddress, address[] cnAdminList, uint256 requirement, uint256[] unlockTime, uint256[] unlockAmount); + event ReviewInitialConditions(address indexed from); + event CompleteReviewInitialConditions(); + event DepositLockupStakingAndInit(address from, uint256 value); + + // Multisig operation in general + event SubmitRequest(uint256 indexed id, address indexed from, Functions functionId, bytes32 firstArg, bytes32 secondArg, bytes32 thirdArg); + event ConfirmRequest(uint256 indexed id, address indexed from, Functions functionId, bytes32 firstArg, bytes32 secondArg, bytes32 thirdArg, address[] confirmers); + event RevokeConfirmation(uint256 indexed id, address indexed from, Functions functionId, bytes32 firstArg, bytes32 secondArg, bytes32 thirdArg, address[] confirmers); + event CancelRequest(uint256 indexed id, address indexed from, Functions functionId, bytes32 firstArg, bytes32 secondArg, bytes32 thirdArg); + event ExecuteRequestSuccess(uint256 indexed id, address indexed from, Functions functionId, bytes32 firstArg, bytes32 secondArg, bytes32 thirdArg); + event ExecuteRequestFailure(uint256 indexed id, address indexed from, Functions functionId, bytes32 firstArg, bytes32 secondArg, bytes32 thirdArg); + event ClearRequest(); + + // Specific multisig operations + event AddAdmin (address indexed admin); + event DeleteAdmin(address indexed admin); + event UpdateRequirement(uint256 requirement); + event WithdrawLockupStaking(address indexed to, uint256 value); + event ApproveStakingWithdrawal(uint256 approvedWithdrawalId, address to, uint256 value, uint256 withdrawableFrom); + event CancelApprovedStakingWithdrawal(uint256 approvedWithdrawalId, address to, uint256 value); + event UpdateRewardAddress(address rewardAddress); + event UpdateStakingTracker(address stakingTracker); + event UpdateVoterAddress(address voterAddress); + event UpdateGCId(uint256 gcId); + + // Public functions + event StakeKlay(address from, uint256 value); + event WithdrawApprovedStaking(uint256 approvedWithdrawalId, address to, uint256 value); + event AcceptRewardAddress(address rewardAddress); + + // Emitted from AddressBook + event ReviseRewardAddress(address cnNodeId, address prevRewardAddress, address curRewardAddress); + + enum RequestState { Unknown, NotConfirmed, Executed, ExecutionFailed, Canceled } + enum Functions { + Unknown, + AddAdmin, + DeleteAdmin, + UpdateRequirement, + ClearRequest, + WithdrawLockupStaking, + ApproveStakingWithdrawal, + CancelApprovedStakingWithdrawal, + UpdateRewardAddress, + UpdateStakingTracker, + UpdateVoterAddress + } + enum WithdrawalStakingState { Unknown, Transferred, Canceled } + + // Constants + function MAX_ADMIN() external returns(uint256); + function CONTRACT_TYPE() external returns(string memory); + function VERSION() external returns(uint256); + function ADDRESS_BOOK_ADDRESS() external returns(address); + function STAKE_LOCKUP() external returns(uint256); + + // Initialization + function setStakingTracker(address _tracker) external; + function setGCId(uint256 _gcId) external; + function reviewInitialConditions() external; + function depositLockupStakingAndInit() external payable; + + // Submit multisig request + function submitAddAdmin(address _admin) external; + function submitDeleteAdmin(address _admin) external; + function submitUpdateRequirement(uint256 _requirement) external; + function submitClearRequest() external; + function submitWithdrawLockupStaking(address payable _to, uint256 _value) external; + function submitApproveStakingWithdrawal(address _to, uint256 _value) external; + function submitCancelApprovedStakingWithdrawal(uint256 _approvedWithdrawalId) external; + function submitUpdateRewardAddress(address _rewardAddress) external; + function submitUpdateStakingTracker(address _tracker) external; + function submitUpdateVoterAddress(address _voterAddress) external; + + // Specific multisig operations + function addAdmin(address _admin) external; + function deleteAdmin(address _admin) external; + function updateRequirement(uint256 _requirement) external; + function clearRequest() external; + function withdrawLockupStaking(address payable _to, uint256 _value) external; + function approveStakingWithdrawal(address _to, uint256 _value) external; + function cancelApprovedStakingWithdrawal(uint256 _approvedWithdrawalId) external; + function updateRewardAddress(address _rewardAddress) external; + function updateStakingTracker(address _tracker) external; + function updateVoterAddress(address _voterAddress) external; + + // Confirm multisig request + function confirmRequest(uint256 _id, Functions _functionId, + bytes32 _firstArg, bytes32 _secondArg, bytes32 _thirdArg) external; + function revokeConfirmation(uint256 _id, Functions _functionId, + bytes32 _firstArg, bytes32 _secondArg, bytes32 _thirdArg) external; + + // Public functions + function stakeKlay() external payable; + receive() external payable; + function withdrawApprovedStaking(uint256 _approvedWithdrawalId) external; + function acceptRewardAddress(address _rewardAddress) external; + + // Getters + function gcId() external view returns(uint256); + function nodeId() external view returns(address); + function rewardAddress() external view returns(address); + function pendingRewardAddress() external view returns(address); + function stakingTracker() external view returns(address); + function voterAddress() external view returns(address); + + function getReviewers() external view returns(address[] memory reviewers); + function getState() external view returns( + address contractValidator, address nodeId, address rewardAddress, + address[] memory adminList, uint256 requirement, + uint256[] memory unlockTime, uint256[] memory unlockAmount, + bool allReviewed, bool isInitialized); + + function getRequestIds(uint256 _from, uint256 _to, RequestState _state) + external view returns(uint256[] memory ids); + function getRequestInfo(uint256 _id) external view returns( + Functions functionId, bytes32 firstArg, bytes32 secondArg, bytes32 thirdArg, + address proposer, address[] memory confirmers, RequestState state); + + function getLockupStakingInfo() external view returns( + uint256[] memory unlockTime, uint256[] memory unlockAmount, + uint256 initial, uint256 remaining, uint256 withdrawable); + + function getApprovedStakingWithdrawalIds(uint256 _from, uint256 _to, WithdrawalStakingState _state) + external view returns(uint256[] memory ids); + function getApprovedStakingWithdrawalInfo(uint256 _index) external view returns( + address to, uint256 value, uint256 withdrawableFrom, WithdrawalStakingState state); +} diff --git a/contracts/KIP/protocol/KIP81/IGovParam.sol b/contracts/KIP/protocol/KIP81/IGovParam.sol new file mode 100644 index 00000000000..c1989bbf207 --- /dev/null +++ b/contracts/KIP/protocol/KIP81/IGovParam.sol @@ -0,0 +1,61 @@ +// Copyright 2022 The klaytn Authors +// This file is part of the klaytn library. +// +// The klaytn library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The klaytn library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the klaytn library. If not, see . + +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.0; + +/** + * @dev Interface of the GovParam Contract + */ +interface IGovParam { + struct Param { + uint256 activation; + bool exists; + bytes val; + } + + event SetParam(string name, bool exists, bytes value, uint256 activation); + + function setParam( + string calldata name, bool exists, bytes calldata value, + uint256 activation) external; + + function setParamIn( + string calldata name, bool exists, bytes calldata value, + uint256 relativeActivation) external; + + /// All (including soft-deleted) param names ever existed + function paramNames(uint256 idx) external view returns (string memory); + function getAllParamNames() external view returns (string[] memory); + + /// Raw checkpoints + function checkpoints(string calldata name) external view + returns(Param[] memory); + function getAllCheckpoints() external view + returns(string[] memory, Param[][] memory); + + /// Any given stored (including soft-deleted) params + function getParam(string calldata name) external view + returns(bool, bytes memory); + function getParamAt(string calldata name, uint256 blockNumber) external view + returns(bool, bytes memory); + + /// All existing params + function getAllParams() external view + returns (string[] memory, bytes[] memory); + function getAllParamsAt(uint256 blockNumber) external view + returns(string[] memory, bytes[] memory); +} diff --git a/contracts/KIP/protocol/KIP81/IStakingTracker.sol b/contracts/KIP/protocol/KIP81/IStakingTracker.sol new file mode 100644 index 00000000000..edacc6a4f64 --- /dev/null +++ b/contracts/KIP/protocol/KIP81/IStakingTracker.sol @@ -0,0 +1,64 @@ +// Copyright 2022 The klaytn Authors +// This file is part of the klaytn library. +// +// The klaytn library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The klaytn library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the klaytn library. If not, see . + +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.0; + +interface IStakingTracker { + // Events + event CreateTracker(uint256 indexed trackerId, + uint256 trackStart, uint256 trackEnd, uint256[] gcIds); + event RetireTracker(uint256 indexed trackerId); + event RefreshStake(uint256 indexed trackerId, uint256 indexed gcId, address staking, + uint256 stakingBalance, uint256 gcBalance, uint256 gcVote, + uint256 totalVotes); + event RefreshVoter(uint256 indexed gcId, address staking, address voter); + + // Constants + function CONTRACT_TYPE() external view returns(string memory); + function VERSION() external view returns(uint256); + function ADDRESS_BOOK_ADDRESS() external view returns(address); + function MIN_STAKE() external view returns(uint256); + + // Mutators + function createTracker(uint256 trackStart, uint256 trackEnd) external returns(uint256 trackerId); + function refreshStake(address staking) external; + function refreshVoter(address staking) external; + + // Getters + function getLastTrackerId() external view returns(uint256); + function getAllTrackerIds() external view returns(uint256[] memory); + function getLiveTrackerIds() external view returns(uint256[] memory); + + function getTrackerSummary(uint256 trackerId) external view returns( + uint256 trackStart, + uint256 trackEnd, + uint256 numGCs, + uint256 totalVotes, + uint256 numEligible); + function getTrackedGC(uint256 trackerId, uint256 gcId) external view returns( + uint256 gcBalance, + uint256 gcVotes); + function getAllTrackedGCs(uint256 trackerId) external view returns( + uint256[] memory gcIds, + uint256[] memory gcBalances, + uint256[] memory gcVotes); + + function stakingToGCId(uint256 trackerId, address staking) external view returns(uint256 gcId); + + function voterToGCId(address voter) external view returns(uint256 gcId); + function gcIdToVoter(uint256 gcId) external view returns(address voter); +} diff --git a/contracts/KIP/protocol/KIP81/IVoting.sol b/contracts/KIP/protocol/KIP81/IVoting.sol new file mode 100644 index 00000000000..a84208133c4 --- /dev/null +++ b/contracts/KIP/protocol/KIP81/IVoting.sol @@ -0,0 +1,167 @@ +// Copyright 2022 The klaytn Authors +// This file is part of the klaytn library. +// +// The klaytn library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The klaytn library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the klaytn library. If not, see . + +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.0; + +interface IVoting { + // Types + + enum ProposalState { + Pending, + Active, + Canceled, + Failed, + Passed, + Queued, + Expired, + Executed + } + + enum VoteChoice { No, Yes, Abstain } + + struct Receipt { + bool hasVoted; + uint8 choice; + uint256 votes; + } + + // Events + + /// @dev Emitted when a proposal is created + /// @param signatures Array of empty strings; for compatibility with OpenZeppelin + event ProposalCreated( + uint256 proposalId, address proposer, + address[] targets, uint256[] values, string[] signatures, bytes[] calldatas, + uint256 voteStart, uint256 voteEnd, string description); + + /// @dev Emitted when a proposal is canceled + event ProposalCanceled(uint256 proposalId); + + /// @dev Emitted when a proposal is queued + /// @param eta The block number where transaction becomes executable. + event ProposalQueued(uint256 proposalId, uint256 eta); + + /// @dev Emitted when a proposal is executed + event ProposalExecuted(uint256 proposalId); + + /// @dev Emitted when a vote is cast + /// @param reason An empty string; for compatibility with OpenZeppelin + event VoteCast(address indexed voter, uint256 proposalId, + uint8 choice, uint256 votes, string reason); + + /// @dev Emitted when the StakingTracker is changed + event UpdateStakingTracker(address oldAddr, address newAddr); + + /// @dev Emitted when the secretary is changed + event UpdateSecretary(address oldAddr, address newAddr); + + /// @dev Emitted when the AccessRule is changed + event UpdateAccessRule( + bool secretaryPropose, bool voterPropose, + bool secretaryExecute, bool voterExecute); + + /// @dev Emitted when the TimingRule is changed + event UpdateTimingRule( + uint256 minVotingDelay, uint256 maxVotingDelay, + uint256 minVotingPeriod, uint256 maxVotingPeriod); + + // Mutators + + function propose( + string memory description, + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + uint256 votingDelay, + uint256 votingPeriod + ) external returns (uint256 proposalId); + + function cancel(uint256 proposalId) external; + function castVote(uint256 proposalId, uint8 choice) external; + function queue(uint256 proposalId) external; + function execute(uint256 proposalId) external payable; + + function updateStakingTracker(address newAddr) external; + function updateSecretary(address newAddr) external; + function updateAccessRule( + bool secretaryPropose, bool voterPropose, + bool secretaryExecute, bool voterExecute) external; + function updateTimingRule( + uint256 minVotingDelay, uint256 maxVotingDelay, + uint256 minVotingPeriod, uint256 maxVotingPeriod) external; + + // Getters + + function stakingTracker() external view returns(address); + function secretary() external view returns(address); + function accessRule() external view returns( + bool secretaryPropose, bool voterPropose, + bool secretaryExecute, bool voterExecute); + function timingRule() external view returns( + uint256 minVotingDelay, uint256 maxVotingDelay, + uint256 minVotingPeriod, uint256 maxVotingPeriod); + function queueTimeout() external view returns(uint256); + function execDelay() external view returns(uint256); + function execTimeout() external view returns(uint256); + + function lastProposalId() external view returns(uint256); + function state(uint256 proposalId) external view returns(ProposalState); + function checkQuorum(uint256 proposalId) external view returns(bool); + function getVotes(uint256 proposalId, address voter) external view returns(uint256, uint256); + + function getProposalContent(uint256 proposalId) external view returns( + uint256 id, + address proposer, + string memory description); + function getActions(uint256 proposalId) external view returns( + address[] memory targets, + uint256[] memory values, + string[] memory signatures, + bytes[] memory calldatas); + function getProposalSchedule(uint256 proposalId) external view returns( + uint256 voteStart, + uint256 voteEnd, + uint256 queueDeadline, + uint256 eta, + uint256 execDeadline, + bool canceled, + bool queued, + bool executed); + function getProposalTally(uint256 proposalId) external view returns( + uint256 totalYes, + uint256 totalNo, + uint256 totalAbstain, + uint256 quorumCount, + uint256 quorumPower, + uint256[] memory voters); + function getReceipt(uint256 proposalId, uint256 voter) external view returns( + bool hasVoted, + uint8 choice, + uint256 votes); + function getTrackerSummary(uint256 proposalId) external view returns( + uint256 trackStart, + uint256 trackEnd, + uint256 numGCs, + uint256 totalVotes, + uint256 numEligible); + function getAllTrackedGCs(uint256 proposalId) external view returns( + uint256[] memory gcIds, + uint256[] memory gcBalances, + uint256[] memory gcVotes); + function voterToGCId(address voter) external view returns(uint256 gcId); + function gcIdToVoter(uint256 gcId) external view returns(address voter); +} diff --git a/contracts/KIP/protocol/KIP81/StakingTracker.sol b/contracts/KIP/protocol/KIP81/StakingTracker.sol new file mode 100644 index 00000000000..674e2ba7bfe --- /dev/null +++ b/contracts/KIP/protocol/KIP81/StakingTracker.sol @@ -0,0 +1,435 @@ +// Copyright 2022 The klaytn Authors +// This file is part of the klaytn library. +// +// The klaytn library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The klaytn library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the klaytn library. If not, see . + +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/access/Ownable.sol"; +import "./IStakingTracker.sol"; + +contract StakingTracker is IStakingTracker, Ownable { + + struct Tracker { + // Tracked block range. + // Balance changes are only updated if trackStart <= block.number < trackEnd. + uint256 trackStart; + uint256 trackEnd; + + // List of eligible GCs and their staking addresses. + // Determined at crateTracker() and does not change. + uint256[] gcIds; + mapping(uint256 => bool) gcExists; + mapping(address => uint256) stakingToGCId; + + // Balances and voting powers. + // First collected at crateTracker() and updated at refreshStake() until trackEnd. + mapping(address => uint256) stakingBalances; // staking address balances + mapping(uint256 => uint256) gcBalances; // consolidated GC balances + mapping(uint256 => uint256) gcVotes; // GC voting powers + uint256 totalVotes; + uint256 numEligible; + } + + // Store tracker objects + mapping(uint256 => Tracker) internal trackers; // indexed by trackerId + uint256[] internal allTrackerIds; // append-only list of trackerIds + uint256[] internal liveTrackerIds; // trackerIds with block.number < trackEnd. Not in order. + + // 1-to-1 mapping between gcId and voter account + mapping(uint256 => address) public override gcIdToVoter; + mapping(address => uint256) public override voterToGCId; + + // Constants + function CONTRACT_TYPE() + external view virtual override returns(string memory) { return "StakingTracker"; } + function VERSION() + external view virtual override returns(uint256) { return 1; } + function ADDRESS_BOOK_ADDRESS() + public view virtual override returns(address) { return 0x0000000000000000000000000000000000000400; } + function MIN_STAKE() + public view virtual override returns(uint256) { return 5000000 ether; } + + // Mutators + + /// @dev Creates a new Tracker and populate initial values from AddressBook + /// Only allowed to the contract owner. + function createTracker(uint256 trackStart, uint256 trackEnd) + public virtual override onlyOwner returns(uint256 trackerId) + { + trackerId = getLastTrackerId() + 1; + allTrackerIds.push(trackerId); + liveTrackerIds.push(trackerId); + + Tracker storage tracker = trackers[trackerId]; + tracker.trackStart = trackStart; + tracker.trackEnd = trackEnd; + + populateFromAddressBook(trackerId); + calcAllVotes(trackerId); + + emit CreateTracker(trackerId, trackStart, trackEnd, tracker.gcIds); + return trackerId; + } + + /// @dev Populate a tracker with staking balances from AddressBook + function populateFromAddressBook(uint256 trackerId) internal { + Tracker storage tracker = trackers[trackerId]; + + (,address[] memory stakingContracts,) = getAddressBookLists(); + + for (uint256 i = 0; i < stakingContracts.length; i++) { + address staking = stakingContracts[i]; + + (bool isV2, uint256 balance, uint256 gcId, address stakingTracker, ) = readCnStaking(staking); + if (!isV2) { + // Ignore V1 contract + continue; + } + if (stakingTracker != address(this)) { + // Ignore CnStaking that does not point to this StakingTracker. + // Hinders an attack where the CnStaking evades real-time voting + // power calculation via staking withdrawal. + continue; + } + + if (!tracker.gcExists[gcId]) { + tracker.gcExists[gcId] = true; + tracker.gcIds.push(gcId); + } + + tracker.stakingToGCId[staking] = gcId; + tracker.stakingBalances[staking] = balance; + tracker.gcBalances[gcId] += balance; + } + } + + /// @dev Populate a tracker with voting powers + function calcAllVotes(uint256 trackerId) internal { + Tracker storage tracker = trackers[trackerId]; + uint256 numEligible = 0; + uint256 totalVotes = 0; + + for (uint256 i = 0; i < tracker.gcIds.length; i++) { + uint256 gcId = tracker.gcIds[i]; + if (tracker.gcBalances[gcId] >= MIN_STAKE()) { + numEligible ++; + } + } + for (uint256 i = 0; i < tracker.gcIds.length; i++) { + uint256 gcId = tracker.gcIds[i]; + uint256 balance = tracker.gcBalances[gcId]; + uint256 votes = calcVotes(numEligible, balance); + tracker.gcVotes[gcId] = votes; + totalVotes += votes; + } + + tracker.numEligible = numEligible; + tracker.totalVotes = totalVotes; // only write final result to save gas + } + + /// @dev Re-evaluate Tracker contents related to the staking contract + /// Anyone can call this function, but `staking` must be a staking contract + /// registered in tracker. + function refreshStake(address staking) external virtual override { + uint256 i = 0; + while (i < liveTrackerIds.length) { + uint256 currId = liveTrackerIds[i]; + + // Remove expired tracker as soon as we discover it + if (!isTrackerLive(currId)) { + uint256 lastId = liveTrackerIds[liveTrackerIds.length-1]; + liveTrackerIds[i] = lastId; + liveTrackerIds.pop(); + emit RetireTracker(currId); + continue; + } + + updateTracker(currId, staking); + i++; + } + } + + /// @dev Re-evalute balances and subsequently voting power + function updateTracker(uint256 trackerId, address staking) private { + Tracker storage tracker = trackers[trackerId]; + + // Resolve GC + uint256 gcId = tracker.stakingToGCId[staking]; + if (gcId == 0) { + return; + } + + // Update balance + uint256 oldBalance = tracker.stakingBalances[staking]; + (, uint256 newBalance, , , ) = readCnStaking(staking); + tracker.stakingBalances[staking] = newBalance; + + uint256 oldGcBalance = tracker.gcBalances[gcId]; + tracker.gcBalances[gcId] -= oldBalance; + tracker.gcBalances[gcId] += newBalance; + uint256 newGcBalance = tracker.gcBalances[gcId]; + + // Update vote cap if necessary + recalcAllVotesIfNeeded(trackerId, oldGcBalance, newGcBalance); + + // Update votes + uint256 oldVotes = tracker.gcVotes[gcId]; + uint256 newVotes = calcVotes(tracker.numEligible, newGcBalance); + tracker.gcVotes[gcId] = newVotes; + tracker.totalVotes -= oldVotes; + tracker.totalVotes += newVotes; + + emit RefreshStake(trackerId, gcId, staking, + newBalance, newGcBalance, newVotes, tracker.totalVotes); + } + + function recalcAllVotesIfNeeded(uint256 trackerId, uint256 oldGcBalance, uint256 newGcBalance) internal { + Tracker storage tracker = trackers[trackerId]; + + bool wasEligible = oldGcBalance >= MIN_STAKE(); + bool isEligible = newGcBalance >= MIN_STAKE(); + if (wasEligible != isEligible) { + if (wasEligible) { // eligible -> not eligible + tracker.numEligible -= 1; + } else { // not eligible -> eligible + tracker.numEligible += 1; + } + recalcAllVotes(trackerId); + } + } + + /// @dev Recalculate votes with new numEligible + function recalcAllVotes(uint256 trackerId) internal { + Tracker storage tracker = trackers[trackerId]; + + uint256 totalVotes = tracker.totalVotes; + for (uint256 i = 0; i < tracker.gcIds.length; i++) { + uint256 gcId = tracker.gcIds[i]; + uint256 gcBalance = tracker.gcBalances[gcId]; + uint256 oldVotes = tracker.gcVotes[gcId]; + uint256 newVotes = calcVotes(tracker.numEligible, gcBalance); + + if (oldVotes != newVotes) { + tracker.gcVotes[gcId] = newVotes; + totalVotes -= oldVotes; + totalVotes += newVotes; + } + } + + tracker.totalVotes = totalVotes; // only write final result to save gas + } + + /// @dev Re-evaluate voter account mapping related to the staking contract + /// Anyone can call this function, but `staking` must be a staking contract + /// registered to the current AddressBook. + /// + /// Updates the voter account of the GC of the `staking` with respect to + /// the corrent AddressBook. + /// + /// If the GC already had a voter account, the account will be unregistered. + /// If the new voter account is already appointed for another GC, + /// this function reverts. + function refreshVoter(address staking) external virtual override { + (, address[] memory stakingContracts, ) = getAddressBookLists(); + bool stakingInAddressBook = false; + for (uint256 i = 0; i < stakingContracts.length; i++) { + if (stakingContracts[i] == staking) { + stakingInAddressBook = true; + break; + } + } + require(stakingInAddressBook, "Not a staking contract"); + + (bool isV2, , uint256 gcId, , address newVoter) = readCnStaking(staking); + require(isV2, "Invalid CnStaking contract"); + + updateVoter(gcId, newVoter); + + emit RefreshVoter(gcId, staking, newVoter); + } + + function updateVoter(uint256 gcId, address newVoter) internal { + // Unlink existing two-way mapping + address oldVoter = gcIdToVoter[gcId]; + if (oldVoter != address(0)) { + voterToGCId[oldVoter] = 0; + gcIdToVoter[gcId] = address(0); + } + + // Create new mapping + if (newVoter != address(0)) { + require(voterToGCId[newVoter] == 0, "Voter address already taken"); + voterToGCId[newVoter] = gcId; + gcIdToVoter[gcId] = newVoter; + } + } + + // Helper fucntions + + /// @dev Query the 3-tuples (node, staking, reward) from AddressBook + function getAddressBookLists() internal view returns( + address[] memory nodeIds, + address[] memory stakingContracts, + address[] memory rewardAddrs) + { + (nodeIds, stakingContracts, rewardAddrs, /* kgf */, /* kir */) = + IAddressBook(ADDRESS_BOOK_ADDRESS()).getAllAddressInfo(); + require(nodeIds.length == stakingContracts.length && + nodeIds.length == rewardAddrs.length, "Invalid data"); + } + + /// @dev Test if the given contract is a CnStakingV2 instance + /// Does not check if the contract is registered in AddressBook. + function isCnStakingV2(address staking) public view returns(bool) { + bool ok; + bytes memory out; + + (ok, out) = staking.staticcall(abi.encodeWithSignature("CONTRACT_TYPE()")); + if (!ok || out.length == 0) { + return false; + } + string memory _type = abi.decode(out, (string)); + if (keccak256(bytes(_type)) != keccak256(bytes("CnStakingContract"))) { + return false; + } + + (ok, out) = staking.staticcall(abi.encodeWithSignature("VERSION()")); + if (!ok || out.length == 0) { + return false; + } + uint256 _version = abi.decode(out, (uint256)); + if (_version < 2) { + return false; + } + + return true; + } + + /// @dev Read various fields from a CnStaking contract + function readCnStaking(address staking) public view virtual returns( + bool isV2, + uint256 effectiveBalance, + uint256 gcId, + address stakingTracker, + address voterAddress) + { + if (isCnStakingV2(staking)) { + return (true, + staking.balance - ICnStakingV2(staking).unstaking(), + ICnStakingV2(staking).gcId(), + ICnStakingV2(staking).stakingTracker(), + ICnStakingV2(staking).voterAddress()); + } + return (false, 0, 0, address(0), address(0)); + } + + /// @dev Calculate voting power from staking amounts. + /// One integer vote is granted for each MIN_STAKE() balance. But the number of votes + /// is at most ([number of eligible GCs] - 1). + function calcVotes(uint256 numEligible, uint256 balance) private view returns(uint256) { + uint256 voteCap = 1; + if (numEligible > 1) { + voteCap = numEligible - 1; + } + + uint256 votes = balance / MIN_STAKE(); + if (votes > voteCap) { + votes = voteCap; + } + return votes; + } + + /// @dev Determine if given tracker is updatable with respect to current block. + function isTrackerLive(uint256 trackerId) private view returns(bool) { + Tracker storage tracker = trackers[trackerId]; + return (tracker.trackStart <= block.number && block.number < tracker.trackEnd); + } + + // Getter functions + + function getLastTrackerId() public view override returns(uint256) { + return allTrackerIds.length; + } + function getAllTrackerIds() external view override returns(uint256[] memory) { + return allTrackerIds; + } + function getLiveTrackerIds() external view override returns(uint256[] memory) { + return liveTrackerIds; + } + + function getTrackerSummary(uint256 trackerId) public view override returns( + uint256 trackStart, + uint256 trackEnd, + uint256 numGCs, + uint256 totalVotes, + uint256 numEligible) + { + Tracker storage tracker = trackers[trackerId]; + return (tracker.trackStart, + tracker.trackEnd, + tracker.gcIds.length, + tracker.totalVotes, + tracker.numEligible); + } + + function getTrackedGC(uint256 trackerId, uint256 gcId) external view override returns( + uint256 gcBalance, + uint256 gcVotes) + { + Tracker storage tracker = trackers[trackerId]; + return (tracker.gcBalances[gcId], + tracker.gcVotes[gcId]); + } + + function getAllTrackedGCs(uint256 trackerId) public view override returns( + uint256[] memory gcIds, + uint256[] memory gcBalances, + uint256[] memory gcVotes) + { + Tracker storage tracker = trackers[trackerId]; + uint256 numGCs = tracker.gcIds.length; + gcIds = tracker.gcIds; + + gcBalances = new uint256[](numGCs); + gcVotes = new uint256[](numGCs); + for (uint256 i = 0; i < numGCs; i++) { + uint256 gcId = tracker.gcIds[i]; + gcBalances[i] = tracker.gcBalances[gcId]; + gcVotes[i] = tracker.gcVotes[gcId]; + } + } + + function stakingToGCId(uint256 trackerId, address staking) + external view override returns(uint256) + { + Tracker storage tracker = trackers[trackerId]; + return tracker.stakingToGCId[staking]; + } +} + +interface IAddressBook { + function getAllAddressInfo() external view returns( + address[] memory, address[] memory, address[] memory, address, address); +} + +interface ICnStakingV2 { + function VERSION() external view returns(uint256); + function rewardAddress() external view returns(address); + function stakingTracker() external view returns(address); + function voterAddress() external view returns(address); + function gcId() external view returns(uint256); + function unstaking() external view returns(uint256); +} diff --git a/contracts/KIP/protocol/KIP81/Voting.sol b/contracts/KIP/protocol/KIP81/Voting.sol new file mode 100644 index 00000000000..572eaada024 --- /dev/null +++ b/contracts/KIP/protocol/KIP81/Voting.sol @@ -0,0 +1,627 @@ +// Copyright 2022 The klaytn Authors +// This file is part of the klaytn library. +// +// The klaytn library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The klaytn library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the klaytn library. If not, see . + +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/utils/Strings.sol"; +import "./IVoting.sol"; +import "./StakingTracker.sol"; + +contract Voting is IVoting { + // Types + + struct Proposal { + // Contents + address proposer; + string description; + address[] targets; // Transaction 'to' addresses + uint256[] values; // Transaction 'value' amounts + bytes[] calldatas; // Transaction 'input' data + + // Schedule + uint256 voteStart; // propose()d block + votingDelay + uint256 voteEnd; // voteStart + votingPeriod + uint256 queueDeadline; // voteEnd + queueTimeout + uint256 eta; // queue()d block + execDelay + uint256 execDeadline; // queue()d block + execDelay + execTimeout + bool canceled; // true if successfully cancel()ed + bool queued; // true if successfully queue()d + bool executed; // true if successfully execute()d + + // Vote counting + address stakingTracker; + uint256 trackerId; + uint256 totalYes; + uint256 totalNo; + uint256 totalAbstain; + uint256 quorumCount; + uint256 quorumPower; + uint256[] voters; + mapping(uint256 => Receipt) receipts; + } + + struct AccessRule { + // True if the secretary can propose() + bool secretaryPropose; + // True if any eligible voter at the time of the submission can propose() a proposal + bool voterPropose; + + // True if the secretary can queue() and execute() + bool secretaryExecute; + // True if any eligible voter of a given proposal can queue() and execute() the proposal. + bool voterExecute; + } + + struct TimingRule { + uint256 minVotingDelay; + uint256 maxVotingDelay; + uint256 minVotingPeriod; + uint256 maxVotingPeriod; + } + + // States + + mapping(uint256 => Proposal) private proposals; + uint256 public nextProposalId; + + /// @dev The address of StakingTracker. + /// Intended for internal use only, but is public for debugging purposes. + /// This address is used by newly created proposals. + address public override stakingTracker; + + /// @dev The address of the Voting secretary. + /// The secretary can be zero address to signify the absence of the secretary. + address public override secretary; + + /// @dev The access control rule of some important functions. + AccessRule public override accessRule; + /// @dev The timing rules of proposal schedule + TimingRule public override timingRule; + + uint256 public constant DAY = 86400; + /// @dev Grace period to queue() passed proposals in block numbers + uint256 public override queueTimeout = 14*DAY; + /// @dev A minimum delay before a queued transaction can be executed in block numbers + uint256 public override execDelay = 2*DAY; + /// @dev Grace period to execute() queued proposals since `execDelay` in block numbers + uint256 public override execTimeout = 14*DAY; + + constructor(address _tracker, address _secretary) { + if (_tracker != address(0)) { + stakingTracker = _tracker; + } else { + // This contract becomes the owner + stakingTracker = address(new StakingTracker()); + } + + secretary = _secretary; + + nextProposalId = 1; + + // Initial rules + accessRule.secretaryPropose = true; + accessRule.voterPropose = false; + accessRule.secretaryExecute = true; + accessRule.voterExecute = false; + validateAccessRule(); + + timingRule.minVotingDelay = 1*DAY; + timingRule.maxVotingDelay = 28*DAY; + timingRule.minVotingPeriod = 1*DAY; + timingRule.maxVotingPeriod = 28*DAY; + validateTimingRule(); + } + + /// @dev Check for propose() access permission + function checkProposeAccess(uint256 proposalId) internal view { + checkAccess(proposalId, accessRule.secretaryPropose, accessRule.voterPropose); + } + /// @dev Check for queue() and execute() access permission + function checkExecuteAccess(uint256 proposalId) internal view { + checkAccess(proposalId, accessRule.secretaryExecute, accessRule.voterExecute); + } + + /// @dev Check that sender has access to a certain operation for the given proposal. + /// + /// @param proposalId The proposal ID which the operation changes + /// @param secretaryAccess True if the operation is allowed to the secretary + /// @param voterAccess True if the operation is allowed to any voter of the proposal + function checkAccess(uint256 proposalId, bool secretaryAccess, bool voterAccess) + internal view { + // if ( sA && vA), msg.sender must be the secretary or a voter. + // Note that in this case, the revert message would be + // "Not a registered voter" or "Not eligible to vote". + // if ( sA && !vA), msg.sender must be the secretary. + // if (!sa && vA), msg.sender must be a voter. + if (secretaryAccess && msg.sender == secretary) { + return; + } else if (voterAccess) { + // check that the sender is an eligible voter of the given proposal. + (uint256 gcId, uint256 votes) = getVotes(proposalId, msg.sender); + require(gcId != 0, "Not a registered voter"); + require(votes > 0, "Not eligible to vote"); + } else { + revert("Not the secretary"); + } + } + + // Modifiers + + /// @dev Sender must have execute permission of the proposal + modifier onlyExecutor(uint256 proposalId) { + checkExecuteAccess(proposalId); + _; + } + + /// @dev The proposal must exist and is in the speciefied state + modifier onlyState(uint256 proposalId, ProposalState s) { + require(proposals[proposalId].proposer != address(0), "No such proposal"); + require(state(proposalId) == s, "Not allowed in current state"); + _; + } + + /// @dev Sender must be this contract, i.e. executed via governance proposal + modifier onlyGovernance() { + require(address(this) == msg.sender, "Not a governance transaction"); + _; + } + + /// @dev Sender must be this contract or the secretary. + modifier onlyGovernanceOrSecretary() { + require(msg.sender == address(this) || msg.sender == secretary, + "Not a governance transaction or secretary"); + _; + } + + // Mutators + + /// @dev Create a Proposal + /// @param description Proposal text + /// @param targets List of transaction target addresses + /// @param values List of KLAY values to send along with transactions + /// @param calldatas List of transaction calldatas + /// @param votingDelay Delay from proposal submission to voting start in block numbers + /// @param votingPeriod Duration of the voting in block numbers + function propose( + string memory description, + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + uint256 votingDelay, + uint256 votingPeriod + ) external override returns (uint256 proposalId) { + + require(targets.length == values.length && + targets.length == calldatas.length, "Invalid actions"); + require(timingRule.minVotingDelay <= votingDelay && + votingDelay <= timingRule.maxVotingDelay, "Invalid votingDelay"); + require(timingRule.minVotingPeriod <= votingPeriod && + votingPeriod <= timingRule.maxVotingPeriod, "Invalid votingPeriod"); + + proposalId = nextProposalId; + nextProposalId ++; + Proposal storage p = proposals[proposalId]; + + p.proposer = msg.sender; + p.description = description; + p.targets = targets; + p.values = values; + p.calldatas = calldatas; + + p.voteStart = block.number + votingDelay; + p.voteEnd = p.voteStart + votingPeriod; + p.queueDeadline = p.voteEnd + queueTimeout; + + // Finalize voter list and track balance changes during the preparation period + p.stakingTracker = stakingTracker; + p.trackerId = IStakingTracker(p.stakingTracker).createTracker(block.number, p.voteStart); + + // Permission check must be done here since it requires trackerId. + checkProposeAccess(proposalId); + + emit ProposalCreated(proposalId, p.proposer, + p.targets, p.values, new string[](p.targets.length), p.calldatas, + p.voteStart, p.voteEnd, p.description); + } + + /// @dev Cancel a proposal + /// The proposal must be in Pending state + /// Only the proposer of the proposal can cancel the proposal. + function cancel(uint256 proposalId) external override + onlyState(proposalId, ProposalState.Pending) { + Proposal storage p = proposals[proposalId]; + require(p.proposer == msg.sender, "Not the proposer"); + + p.canceled = true; + emit ProposalCanceled(proposalId); + } + + /// @dev Cast a vote to a proposal + /// The proposal must be in Active state + /// A node can only vote once for a proposal + /// choice must be one of VoteChoice. + function castVote(uint256 proposalId, uint8 choice) external override + onlyState(proposalId, ProposalState.Active) { + Proposal storage p = proposals[proposalId]; + + // cache quorums to (1) save gas for checkQuorum, + // (2) prevent any unintended outcome of updating stakingTracker address. + if (p.quorumCount == 0) { + (uint256 quorumCount, uint256 quorumPower) = getQuorum(proposalId); + p.quorumCount = quorumCount; + p.quorumPower = quorumPower; + } + + (uint256 gcId, uint256 votes) = getVotes(proposalId, msg.sender); + require(gcId != 0, "Not a registered voter"); + require(votes > 0, "Not eligible to vote"); + + require(choice == uint8(VoteChoice.Yes) || + choice == uint8(VoteChoice.No) || + choice == uint8(VoteChoice.Abstain), "Not a valid choice"); + + require(!p.receipts[gcId].hasVoted, "Already voted"); + p.receipts[gcId].hasVoted = true; + p.receipts[gcId].choice = choice; + p.receipts[gcId].votes = votes; + + incrementTally(proposalId, choice, votes); + p.voters.push(gcId); + + emit VoteCast(msg.sender, proposalId, choice, votes, + Strings.toHexString(gcId, 32)); + } + + function incrementTally(uint256 proposalId, uint8 choice, uint256 votes) private { + Proposal storage p = proposals[proposalId]; + if (choice == uint8(VoteChoice.Yes)) { + p.totalYes += votes; + } else if (choice == uint8(VoteChoice.No)) { + p.totalNo += votes; + } else if (choice == uint8(VoteChoice.Abstain)) { + p.totalAbstain += votes; + } + } + + /// @dev Queue a passed proposal + /// The proposal must be in Passed state + /// Current block must be before `queueDeadline` of this proposal + /// If secretary is null, any GC with at least 1 vote can queue. + /// Otherwise only secretary can queue. + function queue(uint256 proposalId) external override + onlyState(proposalId, ProposalState.Passed) + onlyExecutor(proposalId) { + + Proposal storage p = proposals[proposalId]; + require(p.targets.length > 0, "Proposal has no action"); + + p.eta = block.number + execDelay; + p.execDeadline = p.eta + execTimeout; + p.queued = true; + + emit ProposalQueued(proposalId, p.eta); + } + + /// @dev Execute a queued proposal + /// The proposal must be in Queued state + /// Current block must be after `eta` and before `execDeadline` of this proposal + /// If secretary is null, any GC with at least 1 vote can execute. + /// Otherwise only secretary can execute. + function execute(uint256 proposalId) external payable override + onlyState(proposalId, ProposalState.Queued) + onlyExecutor(proposalId) { + + Proposal storage p = proposals[proposalId]; + require(block.number >= p.eta, "Not yet executable"); + + for (uint256 i = 0; i < p.targets.length; i++) { + (bool success, bytes memory result) = + p.targets[i].call{value: p.values[i]}(p.calldatas[i]); + handleCallResult(success, result); + } + + p.executed = true; + + emit ProposalExecuted(proposalId); + } + + function handleCallResult(bool success, bytes memory result) private pure { + if (success) { + return; + } + + if (result.length == 0) { + // Call failed without message. + revert("Transaction failed"); + } else { + // https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.7.3/contracts/utils/Address.sol + // Toss the result, which would contain error instances. + assembly { + let result_size := mload(result) + revert(add(32, result), result_size) + } + } + } + + // Governance functions + + /// @dev Update the StakingTracker address + /// Should not be called if there is an active proposal + function updateStakingTracker(address newAddr) public override onlyGovernance { + // Retire expired trackers + require(newAddr != address(0), "Address is null"); + IStakingTracker(stakingTracker).refreshStake(address(0)); + require(IStakingTracker(stakingTracker).getLiveTrackerIds().length == 0, "Cannot update tracker when there is an active tracker"); + address oldAddr = stakingTracker; + stakingTracker = newAddr; + emit UpdateStakingTracker(oldAddr, newAddr); + } + + /// @dev Update the secretary account + /// Must be called by address(this), i.e. via governance proposal. + function updateSecretary(address newAddr) public override onlyGovernance { + address oldAddr = secretary; + secretary = newAddr; + validateAccessRule(); + emit UpdateSecretary(oldAddr, newAddr); + } + + /// @dev Update the access rule + function updateAccessRule( + bool secretaryPropose, bool voterPropose, + bool secretaryExecute, bool voterExecute) + public override onlyGovernanceOrSecretary { + AccessRule storage ar = accessRule; + ar.secretaryPropose = secretaryPropose; + ar.voterPropose = voterPropose; + ar.secretaryExecute = secretaryExecute; + ar.voterExecute = voterExecute; + + validateAccessRule(); + + emit UpdateAccessRule(ar.secretaryPropose, ar.voterPropose, + ar.secretaryExecute, ar.voterExecute); + } + + function validateAccessRule() internal view { + AccessRule storage ar = accessRule; + require((ar.secretaryPropose && secretary != address(0)) || ar.voterPropose, "No propose access"); + require((ar.secretaryExecute && secretary != address(0)) || ar.voterExecute, "No execute access"); + } + + /// @dev Update the timing rule + function updateTimingRule( + uint256 minVotingDelay, uint256 maxVotingDelay, + uint256 minVotingPeriod, uint256 maxVotingPeriod) + public override onlyGovernanceOrSecretary { + TimingRule storage tr = timingRule; + tr.minVotingDelay = minVotingDelay; + tr.maxVotingDelay = maxVotingDelay; + tr.minVotingPeriod = minVotingPeriod; + tr.maxVotingPeriod = maxVotingPeriod; + + validateTimingRule(); + + emit UpdateTimingRule(tr.minVotingDelay, tr.maxVotingDelay, + tr. minVotingPeriod, tr.maxVotingPeriod); + } + + function validateTimingRule() internal view { + TimingRule storage tr = timingRule; + require(tr.minVotingDelay >= 1*DAY, "Invalid timing"); + require(tr.minVotingPeriod >= 1*DAY, "Invalid timing"); + require(tr.minVotingDelay <= tr.maxVotingDelay, "Invalid timing"); + require(tr.minVotingPeriod <= tr.maxVotingPeriod, "Invalid timing"); + } + + // Getters + + /// @dev The id of the last created proposal + /// Retrurns 0 if there is no proposal. + function lastProposalId() external view override returns(uint256) { + return nextProposalId - 1; + } + + /// @dev State of a proposal + function state(uint256 proposalId) public view override returns(ProposalState) { + Proposal storage p = proposals[proposalId]; + + if (p.executed) { + return ProposalState.Executed; + } else if (p.canceled) { + return ProposalState.Canceled; + } else if (block.number < p.voteStart) { + return ProposalState.Pending; + } else if (block.number <= p.voteEnd) { + return ProposalState.Active; + } else if (!checkQuorum(proposalId)) { + return ProposalState.Failed; + } + + if (!p.queued) { + if (block.number <= p.queueDeadline || p.targets.length == 0) { + return ProposalState.Passed; + } else { + return ProposalState.Expired; + } + } else { + if (block.number <= p.execDeadline) { + return ProposalState.Queued; + } else { + return ProposalState.Expired; + } + } + } + + /// @dev Check if a proposal is passed + /// Note that its return value represents the current voting status, + /// and is subject to change until the voting ends. + function checkQuorum(uint256 proposalId) public view override returns(bool) { + Proposal storage p = proposals[proposalId]; + + (uint256 quorumCount, uint256 quorumPower) = getQuorum(proposalId); + uint256 totalVotes = p.totalYes + p.totalNo + p.totalAbstain; + uint256 quorumYes = p.totalNo + p.totalAbstain + 1; // more than half of all votes + + bool countPass = (p.voters.length >= quorumCount); + bool powerPass = (totalVotes >= quorumPower); + bool approval = (p.totalYes >= quorumYes); + + return ((countPass || powerPass) && approval); + } + + /// @dev Calculate count and power quorums for a proposal + function getQuorum(uint256 proposalId) private view returns( + uint256 quorumCount, uint256 quorumPower) { + + Proposal storage p = proposals[proposalId]; + if (p.quorumCount != 0) { // return cached numbers + return (p.quorumCount, p.quorumPower); + } + + ( , , , uint256 totalVotes, uint256 numEligible) = + IStakingTracker(p.stakingTracker).getTrackerSummary(p.trackerId); + + quorumCount = (numEligible + 2) / 3; // more than or equal to 1/3 of all GC members + quorumPower = (totalVotes + 2) / 3; // more than or equal to 1/3 of all voting powers + return (quorumCount, quorumPower); + } + + /// @dev Resolve the voter account into its gcId and voting powers + /// Returns the currently assigned gcId. Returns the voting powers + /// effective at the given proposal. Returns zero gcId and 0 votes + /// if the voter account is not assigned to any eligible GC. + /// + /// @param proposalId The proposal id + /// @return gcId The gcId assigned to this voter account + /// @return votes The amount of voting powers the voter account represents + function getVotes(uint256 proposalId, address voter) public view override returns( + uint256 gcId, uint256 votes) { + Proposal storage p = proposals[proposalId]; + + gcId = IStakingTracker(p.stakingTracker).voterToGCId(voter); + ( , votes) = IStakingTracker(p.stakingTracker).getTrackedGC(p.trackerId, gcId); + } + + /// @dev General contents of a proposal + function getProposalContent(uint256 proposalId) external override view returns( + uint256 id, + address proposer, + string memory description) + { + Proposal storage p = proposals[proposalId]; + return (proposalId, + p.proposer, + p.description); + } + + /// @dev Transactions in a proposal + /// signatures is Array of empty strings; for compatibility with OpenZeppelin + function getActions(uint256 proposalId) external override view returns( + address[] memory targets, + uint256[] memory values, + string[] memory signatures, + bytes[] memory calldatas) + { + Proposal storage p = proposals[proposalId]; + return (p.targets, + p.values, + new string[](p.targets.length), + p.calldatas); + } + + /// @dev Timing and state related properties of a proposal + function getProposalSchedule(uint256 proposalId) external view override returns( + uint256 voteStart, + uint256 voteEnd, + uint256 queueDeadline, + uint256 eta, + uint256 execDeadline, + bool canceled, + bool queued, + bool executed) + { + Proposal storage p = proposals[proposalId]; + return (p.voteStart, + p.voteEnd, + p.queueDeadline, + p.eta, + p.execDeadline, + p.canceled, + p.queued, + p.executed); + } + + /// @dev Vote counting related properties of a proposal + function getProposalTally(uint256 proposalId) external view override returns( + uint256 totalYes, + uint256 totalNo, + uint256 totalAbstain, + uint256 quorumCount, + uint256 quorumPower, + uint256[] memory voters) + { + Proposal storage p = proposals[proposalId]; + (quorumCount, quorumPower) = getQuorum(proposalId); + return (p.totalYes, + p.totalNo, + p.totalAbstain, + quorumCount, + quorumPower, + p.voters); + } + + /// @dev Individual vote receipt + function getReceipt(uint256 proposalId, uint256 gcId) external view override returns( + bool hasVoted, + uint8 choice, + uint256 votes) + { + Proposal storage p = proposals[proposalId]; + Receipt storage r = p.receipts[gcId]; + return (r.hasVoted, + r.choice, + r.votes); + } + + function getTrackerSummary(uint256 proposalId) external view override returns( + uint256 trackStart, + uint256 trackEnd, + uint256 numGCs, + uint256 totalVotes, + uint256 numEligible) + { + Proposal storage p = proposals[proposalId]; + return IStakingTracker(p.stakingTracker).getTrackerSummary(p.trackerId); + } + + function getAllTrackedGCs(uint256 proposalId) external view override returns( + uint256[] memory gcIds, + uint256[] memory gcBalances, + uint256[] memory gcVotes) + { + Proposal storage p = proposals[proposalId]; + return IStakingTracker(p.stakingTracker).getAllTrackedGCs(p.trackerId); + } + + function voterToGCId(address voter) external view override returns(uint256 gcId) { + return IStakingTracker(stakingTracker).voterToGCId(voter); + } + function gcIdToVoter(uint256 gcId) external view override returns(address voter) { + return IStakingTracker(stakingTracker).gcIdToVoter(gcId); + } +} diff --git a/contracts/KIP/protocol/KIP81/mock/CnStakingV2Mock.sol b/contracts/KIP/protocol/KIP81/mock/CnStakingV2Mock.sol new file mode 100644 index 00000000000..10a991049e3 --- /dev/null +++ b/contracts/KIP/protocol/KIP81/mock/CnStakingV2Mock.sol @@ -0,0 +1,37 @@ +// Copyright 2022 The klaytn Authors +// This file is part of the klaytn library. +// +// The klaytn library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The klaytn library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the klaytn library. If not, see . + +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.0; +import "../ICnStakingV2.sol"; +import "../CnStakingV2.sol"; + +contract CnStakingV2Mock is CnStakingV2 { + address private addressBookAddress = 0x0000000000000000000000000000000000000400; + function mockSetAddressBookAddress(address _addr) external { addressBookAddress = _addr; } + function ADDRESS_BOOK_ADDRESS() public view virtual override returns(address) { return addressBookAddress; } + + uint256 maxAdmin = 50; + function mockSetMaxAdmin(uint256 _max) external { maxAdmin = _max; } + function MAX_ADMIN() public view virtual override returns(uint256) { return maxAdmin; } + + constructor(address _contractValidator, address _nodeId, address _rewardAddress, + address[] memory _cnAdminlist, uint256 _requirement, + uint256[] memory _unlockTime, uint256[] memory _unlockAmount) + CnStakingV2(_contractValidator, _nodeId, _rewardAddress, + _cnAdminlist, _requirement, + _unlockTime, _unlockAmount) { } +} diff --git a/contracts/KIP/protocol/KIP81/mock/GovParamMock.sol b/contracts/KIP/protocol/KIP81/mock/GovParamMock.sol new file mode 100644 index 00000000000..5e72be06b10 --- /dev/null +++ b/contracts/KIP/protocol/KIP81/mock/GovParamMock.sol @@ -0,0 +1,39 @@ +// Copyright 2022 The klaytn Authors +// This file is part of the klaytn library. +// +// The klaytn library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The klaytn library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the klaytn library. If not, see . + +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.0; + +contract GovParamMock { + string[] keys; + bytes[] values; + + constructor() { + keys.push("kip71.basefeedenominator"); + keys.push("kip71.upperboundbasefee"); + + values.push("0x30"); + } + + function getAllParamsAt(uint256 blockNumber) + external + view + returns (string[] memory, bytes[] memory) + { + // revert("nononono"); // revert test + return (keys, values); + } +} diff --git a/contracts/KIP/protocol/KIP81/mock/RecipientMock.sol b/contracts/KIP/protocol/KIP81/mock/RecipientMock.sol new file mode 100644 index 00000000000..ee736645af0 --- /dev/null +++ b/contracts/KIP/protocol/KIP81/mock/RecipientMock.sol @@ -0,0 +1,66 @@ +// Copyright 2022 The klaytn Authors +// This file is part of the klaytn library. +// +// The klaytn library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The klaytn library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the klaytn library. If not, see . + +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.0; + +// This contract happily receive KLAY transfer transactions. +contract WelcomingRecipient { + event CoinReceived(address sender, uint256 amount); + + function deposit() external payable { + emit CoinReceived(msg.sender, msg.value); + } + + receive() external payable { + emit CoinReceived(msg.sender, msg.value); + } +} + +// This contract reverts upon KLAY transfer transactions. +contract DenyingRecipient { + function deposit() external payable { + revert("You cannot deposit"); + } + + receive() external payable { + revert("I do not accept money"); + } +} + +// Test ability to check contract type and version +contract TypeVersionMock { + string public CONTRACT_TYPE; + uint256 public VERSION; + constructor(string memory t, uint256 v) { + CONTRACT_TYPE = t; + VERSION = v; + } +} + +contract TypeMock { + string public CONTRACT_TYPE; + constructor(string memory t) { + CONTRACT_TYPE = t; + } +} + +contract VersionMock { + uint256 public VERSION; + constructor(uint256 v) { + VERSION = v; + } +} diff --git a/contracts/KIP/protocol/KIP81/mock/StakingTrackerMock.sol b/contracts/KIP/protocol/KIP81/mock/StakingTrackerMock.sol new file mode 100644 index 00000000000..92ae9b5aab8 --- /dev/null +++ b/contracts/KIP/protocol/KIP81/mock/StakingTrackerMock.sol @@ -0,0 +1,65 @@ +// Copyright 2022 The klaytn Authors +// This file is part of the klaytn library. +// +// The klaytn library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The klaytn library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the klaytn library. If not, see . + +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.0; + +import "../IStakingTracker.sol"; +import "../StakingTracker.sol"; + +contract StakingTrackerMock is StakingTracker { + address private addressBookAddress = 0x0000000000000000000000000000000000000400; + function mockSetAddressBookAddress(address _addr) external { addressBookAddress = _addr; } + function ADDRESS_BOOK_ADDRESS() public view virtual override returns(address) { return addressBookAddress; } + + function mockSetOwner(address newOwner) external { _transferOwnership(newOwner); } + + function mockSetVoter(uint256 gcId, address newVoter) external { + updateVoter(gcId, newVoter); + emit RefreshVoter(gcId, address(0), newVoter); + } +} + +contract StakingTrackerMockReceiver { + event RefreshStake(); + event RefreshVoter(); + + function refreshStake(address) external { emit RefreshStake(); } + function refreshVoter(address) external { emit RefreshVoter(); } + function CONTRACT_TYPE() external pure returns(string memory) { return "StakingTracker"; } + function VERSION() external pure returns(uint256) { return 1; } + function voterToGCId(address voter) external view returns(uint256) { return 0; } + function getLiveTrackerIds() external view returns(uint256[] memory) { return new uint256[](0); } +} + +contract StakingTrackerMockActive { + event RefreshStake(); + + function CONTRACT_TYPE() external pure returns(string memory) { return "StakingTracker"; } + function VERSION() external pure returns(uint256) { return 1; } + function refreshStake(address) external { emit RefreshStake(); } + function getLiveTrackerIds() external view returns(uint256[] memory) { return new uint256[](1); } +} + +contract StakingTrackerMockWrong { + function CONTRACT_TYPE() external pure returns(string memory) { return "Wrong"; } + function VERSION() external pure returns(uint256) { return 1; } +} + +contract StakingTrackerMockInvalid { + function CONTRACT_TYPE() external pure returns(string memory) { return ""; } + // no VERSION() function +} diff --git a/contracts/KIP/protocol/KIP81/mock/VotingMock.sol b/contracts/KIP/protocol/KIP81/mock/VotingMock.sol new file mode 100644 index 00000000000..5fa3ed8c23a --- /dev/null +++ b/contracts/KIP/protocol/KIP81/mock/VotingMock.sol @@ -0,0 +1,29 @@ +// Copyright 2022 The klaytn Authors +// This file is part of the klaytn library. +// +// The klaytn library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The klaytn library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the klaytn library. If not, see . + +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.0; +import "../IVoting.sol"; +import "../Voting.sol"; + +contract VotingMock is Voting { + constructor(address _tracker, address _secretary) + Voting(_tracker, _secretary) { + queueTimeout = 60; + execDelay = 15; + execTimeout = 60; + } +} diff --git a/hardhat.config.js b/hardhat.config.js index 0ed8f266724..ded71970317 100644 --- a/hardhat.config.js +++ b/hardhat.config.js @@ -33,7 +33,7 @@ const argv = require('yargs/yargs')() compiler: { alias: 'compileVersion', type: 'string', - default: '0.8.9', + default: '0.8.19', }, coinmarketcap: { alias: 'coinmarketcapApiKey', diff --git a/package-lock.json b/package-lock.json index 9ec114a14d1..895451144c5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@klaytn/contracts", - "version": "1.0.4", + "version": "1.0.6", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@klaytn/contracts", - "version": "1.0.4", + "version": "1.0.6", "license": "MIT", "bin": { "klaytn-contracts-migrate-imports": "scripts/migrate-imports.js" @@ -14,6 +14,7 @@ "devDependencies": { "@nomiclabs/hardhat-truffle5": "^2.0.5", "@nomiclabs/hardhat-web3": "^2.0.0", + "@openzeppelin/contracts": "^4.9.5", "@openzeppelin/docs-utils": "^0.1.0", "@openzeppelin/test-helpers": "^0.5.13", "chai": "^4.2.0", @@ -1788,6 +1789,12 @@ "node": ">=6 <7 || >=8" } }, + "node_modules/@openzeppelin/contracts": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/@openzeppelin/contracts/-/contracts-4.9.5.tgz", + "integrity": "sha512-ZK+W5mVhRppff9BE6YdR8CC52C8zAvsVAiWhEtQ5+oNxFE6h1WdeWo+FJSF8KKvtxxVYZ7MTP/5KoVpAU3aSWg==", + "dev": true + }, "node_modules/@openzeppelin/docs-utils": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/@openzeppelin/docs-utils/-/docs-utils-0.1.0.tgz", @@ -19503,6 +19510,12 @@ } } }, + "@openzeppelin/contracts": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/@openzeppelin/contracts/-/contracts-4.9.5.tgz", + "integrity": "sha512-ZK+W5mVhRppff9BE6YdR8CC52C8zAvsVAiWhEtQ5+oNxFE6h1WdeWo+FJSF8KKvtxxVYZ7MTP/5KoVpAU3aSWg==", + "dev": true + }, "@openzeppelin/docs-utils": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/@openzeppelin/docs-utils/-/docs-utils-0.1.0.tgz", diff --git a/package.json b/package.json index b9b5bdea1ee..8b1c8d7c81c 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "devDependencies": { "@nomiclabs/hardhat-truffle5": "^2.0.5", "@nomiclabs/hardhat-web3": "^2.0.0", + "@openzeppelin/contracts": "^4.9.5", "@openzeppelin/docs-utils": "^0.1.0", "@openzeppelin/test-helpers": "^0.5.13", "chai": "^4.2.0", @@ -77,4 +78,4 @@ "web3": "^1.3.0", "yargs": "^17.0.0" } -} \ No newline at end of file +}