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
+}