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 }