github.com/klaytn/klaytn@v1.12.1/contracts/kip103/TreasuryRebalance.sol (about)

     1  // SPDX-License-Identifier: GPL-3.0
     2  
     3  pragma solidity ^0.8.0;
     4  
     5  import "./Ownable.sol";
     6  import "./ITreasuryRebalance.sol";
     7  
     8  /**
     9   * @title Interface to get adminlist and quorom
    10   */
    11  interface IRetiredContract {
    12      function getState()
    13          external
    14          view
    15          returns (address[] memory adminList, uint256 quorom);
    16  }
    17  
    18  /**
    19   * @title Smart contract to record the rebalance of treasury funds.
    20   * This contract is to mainly record the addresses which holds the treasury funds
    21   * before and after rebalancing. It facilates approval and redistributing to new addresses.
    22   * Core will execute the re-distribution by reading this contract.
    23   */
    24  contract TreasuryRebalance is Ownable, ITreasuryRebalance {
    25      /**
    26       * Storage
    27       */
    28      Retired[] public retirees; // array of the Retired struct
    29      Newbie[] public newbies; // array of Newbie struct
    30      Status public status; // current status of the contract
    31      uint256 public rebalanceBlockNumber; // the target block number of the execution of rebalancing.
    32      string public memo; // result of the treasury fund rebalance.
    33  
    34      /**
    35       * Modifiers
    36       */
    37      modifier onlyAtStatus(Status _status) {
    38          require(status == _status, "Not in the designated status");
    39          _;
    40      }
    41  
    42      /**
    43       *  Constructor
    44       * @param _rebalanceBlockNumber is the target block number of the execution the rebalance in Core
    45       */
    46      constructor(uint256 _rebalanceBlockNumber) {
    47          rebalanceBlockNumber = _rebalanceBlockNumber;
    48          status = Status.Initialized;
    49          emit ContractDeployed(status, _rebalanceBlockNumber, block.timestamp);
    50      }
    51  
    52      //State changing Functions
    53      /**
    54       * @dev registers retired details
    55       * @param _retiredAddress is the address of the retired
    56       */
    57      function registerRetired(
    58          address _retiredAddress
    59      ) public onlyOwner onlyAtStatus(Status.Initialized) {
    60          require(
    61              !retiredExists(_retiredAddress),
    62              "Retired address is already registered"
    63          );
    64          Retired storage retired = retirees.push();
    65          retired.retired = _retiredAddress;
    66          emit RetiredRegistered(retired.retired);
    67      }
    68  
    69      /**
    70       * @dev remove the retired details from the array
    71       * @param _retiredAddress is the address of the retired
    72       */
    73      function removeRetired(
    74          address _retiredAddress
    75      ) public onlyOwner onlyAtStatus(Status.Initialized) {
    76          uint256 retiredIndex = getRetiredIndex(_retiredAddress);
    77          require(retiredIndex != type(uint256).max, "Retired not registered");
    78          retirees[retiredIndex] = retirees[retirees.length - 1];
    79          retirees.pop();
    80  
    81          emit RetiredRemoved(_retiredAddress);
    82      }
    83  
    84      /**
    85       * @dev registers newbie address and its fund distribution
    86       * @param _newbieAddress is the address of the newbie
    87       * @param _amount is the fund to be allocated to the newbie
    88       */
    89      function registerNewbie(
    90          address _newbieAddress,
    91          uint256 _amount
    92      ) public onlyOwner onlyAtStatus(Status.Initialized) {
    93          require(
    94              !newbieExists(_newbieAddress),
    95              "Newbie address is already registered"
    96          );
    97          require(_amount != 0, "Amount cannot be set to 0");
    98  
    99          Newbie memory newbie = Newbie(_newbieAddress, _amount);
   100          newbies.push(newbie);
   101  
   102          emit NewbieRegistered(_newbieAddress, _amount);
   103      }
   104  
   105      /**
   106       * @dev remove the newbie details from the array
   107       * @param _newbieAddress is the address of the newbie
   108       */
   109      function removeNewbie(
   110          address _newbieAddress
   111      ) public onlyOwner onlyAtStatus(Status.Initialized) {
   112          uint256 newbieIndex = getNewbieIndex(_newbieAddress);
   113          require(newbieIndex != type(uint256).max, "Newbie not registered");
   114          newbies[newbieIndex] = newbies[newbies.length - 1];
   115          newbies.pop();
   116  
   117          emit NewbieRemoved(_newbieAddress);
   118      }
   119  
   120      /**
   121       * @dev retiredAddress can be a EOA or a contract address. To approve:
   122       *      If the retiredAddress is a EOA, the msg.sender should be the EOA address
   123       *      If the retiredAddress is a Contract, the msg.sender should be one of the contract `admin`.
   124       *      It uses the getState() function in the retiredAddress contract to get the admin details.
   125       * @param _retiredAddress is the address of the retired
   126       */
   127      function approve(
   128          address _retiredAddress
   129      ) public onlyAtStatus(Status.Registered) {
   130          require(
   131              retiredExists(_retiredAddress),
   132              "retired needs to be registered before approval"
   133          );
   134  
   135          //Check whether the retired address is EOA or contract address
   136          bool isContract = isContractAddr(_retiredAddress);
   137          if (!isContract) {
   138              //check whether the msg.sender is the retired if its a EOA
   139              require(
   140                  msg.sender == _retiredAddress,
   141                  "retiredAddress is not the msg.sender"
   142              );
   143              _updateApprover(_retiredAddress, msg.sender);
   144          } else {
   145              (address[] memory adminList, ) = _getState(_retiredAddress);
   146              require(adminList.length != 0, "admin list cannot be empty");
   147  
   148              //check if the msg.sender is one of the admin of the retiredAddress contract
   149              require(
   150                  _validateAdmin(msg.sender, adminList),
   151                  "msg.sender is not the admin"
   152              );
   153              _updateApprover(_retiredAddress, msg.sender);
   154          }
   155      }
   156  
   157      /**
   158       * @dev validate if the msg.sender is admin if the retiredAddress is a contract
   159       * @param _approver is the msg.sender
   160       * @return isAdmin is true if the msg.sender is one of the admin
   161       */
   162      function _validateAdmin(
   163          address _approver,
   164          address[] memory _adminList
   165      ) private pure returns (bool isAdmin) {
   166          for (uint256 i = 0; i < _adminList.length; i++) {
   167              if (_approver == _adminList[i]) {
   168                  isAdmin = true;
   169              }
   170          }
   171      }
   172  
   173      /**
   174       * @dev gets the adminList and quorom by calling `getState()` method in retiredAddress contract
   175       * @param _retiredAddress is the address of the contract
   176       * @return adminList list of the retiredAddress contract admins
   177       * @return req min required number of approvals
   178       */
   179      function _getState(
   180          address _retiredAddress
   181      ) private view returns (address[] memory adminList, uint256 req) {
   182          IRetiredContract retiredContract = IRetiredContract(_retiredAddress);
   183          (adminList, req) = retiredContract.getState();
   184      }
   185  
   186      /**
   187       * @dev Internal function to update the approver details of a retired
   188       * _retiredAddress is the address of the retired
   189       * _approver is the admin of the retiredAddress
   190       */
   191      function _updateApprover(
   192          address _retiredAddress,
   193          address _approver
   194      ) private {
   195          uint256 index = getRetiredIndex(_retiredAddress);
   196          require(index != type(uint256).max, "Retired not registered");
   197          address[] memory approvers = retirees[index].approvers;
   198          for (uint256 i = 0; i < approvers.length; i++) {
   199              require(approvers[i] != _approver, "Already approved");
   200          }
   201          retirees[index].approvers.push(_approver);
   202          emit Approved(
   203              _retiredAddress,
   204              _approver,
   205              retirees[index].approvers.length
   206          );
   207      }
   208  
   209      /**
   210       * @dev finalizeRegistration sets the status to Registered,
   211       *      After this stage, registrations will be restricted.
   212       */
   213      function finalizeRegistration()
   214          public
   215          onlyOwner
   216          onlyAtStatus(Status.Initialized)
   217      {
   218          status = Status.Registered;
   219          emit StatusChanged(status);
   220      }
   221  
   222      /**
   223       * @dev finalizeApproval sets the status to Approved,
   224       *      After this stage, approvals will be restricted.
   225       */
   226      function finalizeApproval()
   227          public
   228          onlyOwner
   229          onlyAtStatus(Status.Registered)
   230      {
   231          require(
   232              getTreasuryAmount() < sumOfRetiredBalance(),
   233              "treasury amount should be less than the sum of all retired address balances"
   234          );
   235          checkRetiredsApproved();
   236          status = Status.Approved;
   237          emit StatusChanged(status);
   238      }
   239  
   240      /**
   241       * @dev verify if quorom reached for the retired approvals
   242       */
   243      function checkRetiredsApproved() public view {
   244          for (uint256 i = 0; i < retirees.length; i++) {
   245              Retired memory retired = retirees[i];
   246              bool isContract = isContractAddr(retired.retired);
   247              if (isContract) {
   248                  (address[] memory adminList, uint256 req) = _getState(
   249                      retired.retired
   250                  );
   251                  require(
   252                      retired.approvers.length >= req,
   253                      "min required admins should approve"
   254                  );
   255                  //if min quorom reached, make sure all approvers are still valid
   256                  address[] memory approvers = retired.approvers;
   257                  uint256 validApprovals = 0;
   258                  for (uint256 j = 0; j < approvers.length; j++) {
   259                      if (_validateAdmin(approvers[j], adminList)) {
   260                          validApprovals++;
   261                      }
   262                  }
   263                  require(
   264                      validApprovals >= req,
   265                      "min required admins should approve"
   266                  );
   267              } else {
   268                  require(retired.approvers.length == 1, "EOA should approve");
   269              }
   270          }
   271      }
   272  
   273      /**
   274       * @dev sets the status of the contract to Finalize. Once finalized the storage data
   275       * of the contract cannot be modified
   276       * @param _memo is the result of the rebalance after executing successfully in the core.
   277       */
   278      function finalizeContract(
   279          string memory _memo
   280      ) public onlyOwner onlyAtStatus(Status.Approved) {
   281          memo = _memo;
   282          status = Status.Finalized;
   283          emit Finalized(memo, status);
   284          require(
   285              block.number > rebalanceBlockNumber,
   286              "Contract can only finalize after executing rebalancing"
   287          );
   288      }
   289  
   290      /**
   291       * @dev resets all storage values to empty objects except targetBlockNumber
   292       */
   293      function reset() public onlyOwner {
   294          //reset cannot be called at Finalized status or after target block.number
   295          require(
   296              ((status != Status.Finalized) &&
   297                  (block.number < rebalanceBlockNumber)),
   298              "Contract is finalized, cannot reset values"
   299          );
   300  
   301          //`delete` keyword is used to set a storage variable or a dynamic array to its default value.
   302          delete retirees;
   303          delete newbies;
   304          delete memo;
   305          status = Status.Initialized;
   306      }
   307  
   308      //Getters
   309      /**
   310       * @dev to get retired details by retiredAddress
   311       * @param _retiredAddress is the address of the retired
   312       */
   313      function getRetired(
   314          address _retiredAddress
   315      ) public view returns (address, address[] memory) {
   316          uint256 index = getRetiredIndex(_retiredAddress);
   317          require(index != type(uint256).max, "Retired not registered");
   318          Retired memory retired = retirees[index];
   319          return (retired.retired, retired.approvers);
   320      }
   321  
   322      /**
   323       * @dev check whether retiredAddress is registered
   324       * @param _retiredAddress is the address of the retired
   325       */
   326      function retiredExists(address _retiredAddress) public view returns (bool) {
   327          require(_retiredAddress != address(0), "Invalid address");
   328          for (uint256 i = 0; i < retirees.length; i++) {
   329              if (retirees[i].retired == _retiredAddress) {
   330                  return true;
   331              }
   332          }
   333      }
   334  
   335      /**
   336       * @dev get index of the retired in the retirees array
   337       * @param _retiredAddress is the address of the retired
   338       */
   339      function getRetiredIndex(
   340          address _retiredAddress
   341      ) public view returns (uint256) {
   342          for (uint256 i = 0; i < retirees.length; i++) {
   343              if (retirees[i].retired == _retiredAddress) {
   344                  return i;
   345              }
   346          }
   347          return type(uint256).max;
   348      }
   349  
   350      /**
   351       * @dev to calculate the sum of retirees balances
   352       * @return retireesBalance the sum of balances of retireds
   353       */
   354      function sumOfRetiredBalance()
   355          public
   356          view
   357          returns (uint256 retireesBalance)
   358      {
   359          for (uint256 i = 0; i < retirees.length; i++) {
   360              retireesBalance += retirees[i].retired.balance;
   361          }
   362          return retireesBalance;
   363      }
   364  
   365      /**
   366       * @dev to get newbie details by newbieAddress
   367       * @param _newbieAddress is the address of the newbie
   368       * @return newbie is the address of the newbie
   369       * @return amount is the fund allocated to the newbie
   370       */
   371      function getNewbie(
   372          address _newbieAddress
   373      ) public view returns (address, uint256) {
   374          uint256 index = getNewbieIndex(_newbieAddress);
   375          require(index != type(uint256).max, "Newbie not registered");
   376          Newbie memory newbie = newbies[index];
   377          return (newbie.newbie, newbie.amount);
   378      }
   379  
   380      /**
   381       * @dev check whether _newbieAddress is registered
   382       * @param _newbieAddress is the address of the newbie
   383       */
   384      function newbieExists(address _newbieAddress) public view returns (bool) {
   385          require(_newbieAddress != address(0), "Invalid address");
   386          for (uint256 i = 0; i < newbies.length; i++) {
   387              if (newbies[i].newbie == _newbieAddress) {
   388                  return true;
   389              }
   390          }
   391      }
   392  
   393      /**
   394       * @dev get index of the newbie in the newbies array
   395       * @param _newbieAddress is the address of the newbie
   396       */
   397      function getNewbieIndex(
   398          address _newbieAddress
   399      ) public view returns (uint256) {
   400          for (uint256 i = 0; i < newbies.length; i++) {
   401              if (newbies[i].newbie == _newbieAddress) {
   402                  return i;
   403              }
   404          }
   405          return type(uint256).max;
   406      }
   407  
   408      /**
   409       * @dev to calculate the sum of newbie funds
   410       * @return treasuryAmount the sum of funds allocated to newbies
   411       */
   412      function getTreasuryAmount() public view returns (uint256 treasuryAmount) {
   413          for (uint256 i = 0; i < newbies.length; i++) {
   414              treasuryAmount += newbies[i].amount;
   415          }
   416          return treasuryAmount;
   417      }
   418  
   419      /**
   420       * @dev gets the length of retirees list
   421       */
   422      function getRetiredCount() public view returns (uint256) {
   423          return retirees.length;
   424      }
   425  
   426      /**
   427       * @dev gets the length of newbies list
   428       */
   429      function getNewbieCount() public view returns (uint256) {
   430          return newbies.length;
   431      }
   432  
   433      /**
   434       * @dev fallback function to revert any payments
   435       */
   436      fallback() external payable {
   437          revert("This contract does not accept any payments");
   438      }
   439  
   440      /**
   441       * @dev Helper function to check the address is contract addr or EOA
   442       */
   443      function isContractAddr(address _addr) public view returns (bool) {
   444          uint256 size;
   445          assembly {
   446              size := extcodesize(_addr)
   447          }
   448          return size > 0;
   449      }
   450  }