diff --git a/app/upgrades.go b/app/upgrades.go index bdfa657135..54d66a4de7 100644 --- a/app/upgrades.go +++ b/app/upgrades.go @@ -30,6 +30,13 @@ var ContractMigrations = map[string][]contractMigration{ Value: common.HexToHash("0x000000000000000000000000730CbB94480d50788481373B43d83133e171367e"), }, }, + "cronos_777-1": { + { + Contract: common.HexToAddress("0x28838c2e6db87977e0cae0e218f1929e440d1598"), + Slot: common.BigToHash(big.NewInt(0)), + Value: common.HexToHash("0x730CbB94480d50788481373B43d83133e171367e"), + }, + }, } func (app *App) RegisterUpgradeHandlers(cdc codec.BinaryCodec, clientKeeper clientkeeper.Keeper) { diff --git a/integration_tests/contracts/contracts/IMintedToken.sol b/integration_tests/contracts/contracts/IMintedToken.sol new file mode 100644 index 0000000000..025b90b345 --- /dev/null +++ b/integration_tests/contracts/contracts/IMintedToken.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +interface IMintedToken is IERC20 { + function SUPPLY_CAP() external view returns (uint256); + + function mint(address account, uint256 amount) external returns (bool); +} \ No newline at end of file diff --git a/integration_tests/contracts/contracts/TokenDistributor.sol b/integration_tests/contracts/contracts/TokenDistributor.sol new file mode 100644 index 0000000000..91528a6026 --- /dev/null +++ b/integration_tests/contracts/contracts/TokenDistributor.sol @@ -0,0 +1,276 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; +import {ReentrancyGuard} from "@openzeppelin/contracts/security/ReentrancyGuard.sol"; +import {IERC20, SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "./IMintedToken.sol"; + +import "@openzeppelin/contracts/access/Ownable.sol"; + +/** + * @title TokenDistributor + * @notice It handles the distribution of MTD token. + * It auto-adjusts block rewards over a set number of periods. + */ +contract TokenDistributor is Ownable, ReentrancyGuard { + using SafeERC20 for IERC20; + using SafeERC20 for IMintedToken; + uint256 public constant PRECISION_FACTOR = 10**18; + struct StakingPeriod { + uint256 rewardPerBlockForStaking; + uint256 rewardPerBlockForOthers; + uint256 periodLengthInBlock; + } + + IMintedToken public immutable mintedToken; + + address public immutable tokenSplitter; + address public stakingAddr; + + // Number of reward periods + uint256 public immutable NUMBER_PERIODS; + + // Block number when rewards start + uint256 public immutable START_BLOCK; + + // Current phase for rewards + uint256 public currentPhase; + + // Block number when rewards end + uint256 public endBlock; + + // Block number of the last update + uint256 public lastRewardBlock; + + // Tokens distributed per block for other purposes (team + treasury + trading rewards) + uint256 public rewardPerBlockForOthers; + + // Tokens distributed per block for staking + uint256 public rewardPerBlockForStaking; + + mapping(uint256 => StakingPeriod) public stakingPeriod; + + event NewRewardsPerBlock( + uint256 indexed currentPhase, + uint256 startBlock, + uint256 rewardPerBlockForStaking, + uint256 rewardPerBlockForOthers + ); + event SetupStakingAddress(address stakingAddr); + + /** + * @notice Constructor + * @param _mintedToken MTD token address + * @param _tokenSplitter token splitter contract address (for team and trading rewards) + * @param _startBlock start block for reward program + * @param _rewardsPerBlockForStaking array of rewards per block for staking + * @param _rewardsPerBlockForOthers array of rewards per block for other purposes (team + treasury + trading rewards) + * @param _periodLengthesInBlocks array of period lengthes + * @param _numberPeriods number of periods with different rewards/lengthes (e.g., if 3 changes --> 4 periods) + */ + constructor( + address _mintedToken, + address _tokenSplitter, + uint256 _startBlock, + uint256[] memory _rewardsPerBlockForStaking, + uint256[] memory _rewardsPerBlockForOthers, + uint256[] memory _periodLengthesInBlocks, + uint256 _numberPeriods, + uint256 slippage + ) { + require(_mintedToken != address(0), "Distributor: mintedToken must not be address(0)"); + require( + (_periodLengthesInBlocks.length == _numberPeriods) && + (_rewardsPerBlockForStaking.length == _numberPeriods) && + (_rewardsPerBlockForOthers.length == _numberPeriods), + "Distributor: lengths must match numberPeriods" + ); + require(_tokenSplitter != address(0), "Distributor: tokenSplitter must not be address(0)"); + + // 1. Operational checks for supply + uint256 nonCirculatingSupply = IMintedToken(_mintedToken).SUPPLY_CAP() - + IMintedToken(_mintedToken).totalSupply(); + + uint256 amountTokensToBeMinted; + + for (uint256 i = 0; i < _numberPeriods; i++) { + amountTokensToBeMinted += + (_rewardsPerBlockForStaking[i] * _periodLengthesInBlocks[i]) + + (_rewardsPerBlockForOthers[i] * _periodLengthesInBlocks[i]); + + stakingPeriod[i] = StakingPeriod({ + rewardPerBlockForStaking: _rewardsPerBlockForStaking[i], + rewardPerBlockForOthers: _rewardsPerBlockForOthers[i], + periodLengthInBlock: _periodLengthesInBlocks[i] + }); + } + // require(amountTokensToBeMinted <= nonCirculatingSupply, "Distributor: rewards exceeds supply"); + // uint256 residueAmt = nonCirculatingSupply - amountTokensToBeMinted; + // uint256 rewardSlippage = (residueAmt * 100 * PRECISION_FACTOR) / nonCirculatingSupply; + // require(rewardSlippage <= slippage, "Distributor: slippage exceeds"); + // // 2. Store values + // mintedToken = IMintedToken(_mintedToken); + // tokenSplitter = _tokenSplitter; + // rewardPerBlockForStaking = _rewardsPerBlockForStaking[0]; + // rewardPerBlockForOthers = _rewardsPerBlockForOthers[0]; + + // START_BLOCK = _startBlock; + // endBlock = _startBlock + _periodLengthesInBlocks[0]; + + // NUMBER_PERIODS = _numberPeriods; + + // // Set the lastRewardBlock as the startBlock + // lastRewardBlock = _startBlock; + } + + /** + * @dev updates the staking adddress as a mintedBoost contract address once it is deployed. + */ + + function setupStakingAddress(address _stakingAddr) external onlyOwner { + require(_stakingAddr != address(0), "invalid address"); + stakingAddr = _stakingAddr; + emit SetupStakingAddress(stakingAddr); + } + + /** + * @notice Update pool rewards + */ + function updatePool() external nonReentrant { + _updatePool(); + } + + /** + * @notice Update reward variables of the pool + */ + function _updatePool() internal { + require(stakingAddr != address(0), "staking address not setup"); + if (block.number <= lastRewardBlock) { + return; + } + (uint256 tokenRewardForStaking, uint256 tokenRewardForOthers) = _calculatePendingRewards(); + // mint tokens only if token rewards for staking are not null + if (tokenRewardForStaking > 0) { + // It allows protection against potential issues to prevent funds from being locked + mintedToken.mint(stakingAddr, tokenRewardForStaking); + mintedToken.mint(tokenSplitter, tokenRewardForOthers); + } + + // Update last reward block only if it wasn't updated after or at the end block + if (lastRewardBlock <= endBlock) { + lastRewardBlock = block.number; + } + } + + function _calculatePendingRewards() internal returns (uint256, uint256) { + if (block.number <= lastRewardBlock) { + return (0, 0); + } + // Calculate multiplier + uint256 multiplier = _getMultiplier(lastRewardBlock, block.number, endBlock); + // Calculate rewards for staking and others + uint256 tokenRewardForStaking = multiplier * rewardPerBlockForStaking; + uint256 tokenRewardForOthers = multiplier * rewardPerBlockForOthers; + + // Check whether to adjust multipliers and reward per block + while ((block.number > endBlock) && (currentPhase < (NUMBER_PERIODS - 1))) { + // Update rewards per block + _updateRewardsPerBlock(endBlock); + + uint256 previousEndBlock = endBlock; + + // Adjust the end block + endBlock += stakingPeriod[currentPhase].periodLengthInBlock; + + // Adjust multiplier to cover the missing periods with other lower inflation schedule + uint256 newMultiplier = _getMultiplier(previousEndBlock, block.number, endBlock); + + // Adjust token rewards + tokenRewardForStaking += (newMultiplier * rewardPerBlockForStaking); + tokenRewardForOthers += (newMultiplier * rewardPerBlockForOthers); + } + return (tokenRewardForStaking, tokenRewardForOthers); + } + + function getPendingRewards() external view returns (uint256, uint256) { + if (block.number <= lastRewardBlock) { + return (0, 0); + } + // shadow state vars to avoid updates + uint256 tEndBlock = endBlock; + uint256 tCurrentPhase = currentPhase; + uint256 tRewardPerBlockForStaking = rewardPerBlockForStaking; + uint256 tRewardPerBlockForOthers = rewardPerBlockForOthers; + // Calculate multiplier + uint256 multiplier = _getMultiplier(lastRewardBlock, block.number, tEndBlock); + // Calculate rewards for staking and others + uint256 tokenRewardForStaking = multiplier * tRewardPerBlockForStaking; + uint256 tokenRewardForOthers = multiplier * tRewardPerBlockForOthers; + // Check whether to adjust multipliers and reward per block + while ((block.number > tEndBlock) && (tCurrentPhase < (NUMBER_PERIODS - 1))) { + // Update rewards per block + tCurrentPhase++; + tRewardPerBlockForStaking = stakingPeriod[tCurrentPhase].rewardPerBlockForStaking; + tRewardPerBlockForOthers = stakingPeriod[tCurrentPhase].rewardPerBlockForOthers; + uint256 previousEndBlock = tEndBlock; + + // Adjust the end block + tEndBlock += stakingPeriod[tCurrentPhase].periodLengthInBlock; + + // Adjust multiplier to cover the missing periods with other lower inflation schedule + uint256 newMultiplier = _getMultiplier(previousEndBlock, block.number, tEndBlock); + + // Adjust token rewards + tokenRewardForStaking += (newMultiplier * tRewardPerBlockForStaking); + tokenRewardForOthers += (newMultiplier * tRewardPerBlockForOthers); + } + return (tokenRewardForStaking, tokenRewardForOthers); + } + + function getPendingStakingRewards() external view returns (uint256) { + if (block.number <= lastRewardBlock) { + return 0; + } + uint256 multiplier = block.number - lastRewardBlock; + return multiplier * rewardPerBlockForStaking; + } + + /** + * @notice Update rewards per block + * @dev Rewards are halved by 2 (for staking + others) + */ + function _updateRewardsPerBlock(uint256 _newStartBlock) internal { + // Update current phase + currentPhase++; + + // Update rewards per block + rewardPerBlockForStaking = stakingPeriod[currentPhase].rewardPerBlockForStaking; + rewardPerBlockForOthers = stakingPeriod[currentPhase].rewardPerBlockForOthers; + + emit NewRewardsPerBlock( + currentPhase, + _newStartBlock, + rewardPerBlockForStaking, + rewardPerBlockForOthers + ); + } + + /** + * @notice Return reward multiplier over the given "from" to "to" block. + * @param from block to start calculating reward + * @param to block to finish calculating reward + * @return the multiplier for the period + */ + function _getMultiplier( + uint256 from, + uint256 to, + uint256 tEndBlock + ) internal pure returns (uint256) { + if (to <= tEndBlock) { + return to - from; + } else if (from >= tEndBlock) { + return 0; + } else { + return tEndBlock - from; + } + } +} \ No newline at end of file diff --git a/integration_tests/test_basic.py b/integration_tests/test_basic.py index d558016bfa..299b47c617 100644 --- a/integration_tests/test_basic.py +++ b/integration_tests/test_basic.py @@ -26,6 +26,8 @@ contract_address, contract_path, deploy_contract, + derive_new_account, + fund_acc, get_receipts_by_block, modify_command_in_supervisor_config, send_transaction, @@ -79,6 +81,23 @@ def approve_proposal_legacy(node, rsp): return amount + +def test_sc(cronos): + w3 = cronos.w3 + acc = derive_new_account() + fund_acc(w3, acc) + addr = "0xa16226396d79dc7B3Bc70DE0daCa4Eef11742a9E" + sc = deploy_contract( + w3, + CONTRACTS["TokenDistributor"], + (addr, addr, 1, [0], [0], [0], 1, 1), + key=acc.key, + ) + owner = sc.functions.owner().call() + # 0xeBF80fF512D5aF394c2F86B39Aa92670d6D3B15f 0x28838c2E6Db87977e0CaE0E218f1929e440d1598 + print("mm-owner", owner, sc.address) + + def test_ica_enabled(cronos): cli = cronos.cosmos_cli() p = cli.query_icacontroller_params() diff --git a/integration_tests/test_upgrade.py b/integration_tests/test_upgrade.py index 6dc61269dc..a85e2d7eb2 100644 --- a/integration_tests/test_upgrade.py +++ b/integration_tests/test_upgrade.py @@ -17,7 +17,9 @@ KEYS, approve_proposal, deploy_contract, + derive_new_account, edit_ini_sections, + fund_acc, get_consensus_params, get_send_enable, send_transaction, @@ -229,6 +231,17 @@ def do_upgrade(plan_name, target, mode=None, method="submit-legacy-proposal"): ) print("old values", old_height, old_balance, old_base_fee) + acc = derive_new_account() + fund_acc(w3, acc) + addr = "0xa16226396d79dc7B3Bc70DE0daCa4Eef11742a9E" + sc = deploy_contract( + w3, + CONTRACTS["TokenDistributor"], + (addr, addr, 1, [0], [0], [0], 1, 1), + key=acc.key, + ) + owner = sc.functions.owner().call() + print("mm-owner-bf", owner, sc.address) do_upgrade("v1.3", target_height1) cli = c.cosmos_cli() @@ -244,6 +257,8 @@ def do_upgrade(plan_name, target, mode=None, method="submit-legacy-proposal"): }, ) assert receipt.status == 1 + owner = sc.functions.owner().call() + print("mm-owner-af", owner, sc.address) # deploy contract should still work deploy_contract(w3, CONTRACTS["Greeter"]) diff --git a/integration_tests/utils.py b/integration_tests/utils.py index 2afe6dc1c2..bf05653980 100644 --- a/integration_tests/utils.py +++ b/integration_tests/utils.py @@ -60,6 +60,7 @@ "TestBank": "TestBank.sol", "TestICA": "TestICA.sol", "Random": "Random.sol", + "TokenDistributor": "TokenDistributor.sol", } @@ -732,3 +733,18 @@ def get_send_enable(port): url = f"http://127.0.0.1:{port}/cosmos/bank/v1beta1/params" raw = requests.get(url).json() return raw["params"]["send_enabled"] + + +def derive_new_account(n=1): + # derive a new address + account_path = f"m/44'/60'/0'/0/{n}" + mnemonic = os.getenv("COMMUNITY_MNEMONIC") + return Account.from_mnemonic(mnemonic, account_path=account_path) + +def fund_acc(w3, acc): + fund = 3000000000000000000 + addr = acc.address + if w3.eth.get_balance(addr, "latest") == 0: + tx = {"to": addr, "value": fund, "gasPrice": w3.eth.gas_price} + send_transaction(w3, tx) + assert w3.eth.get_balance(addr, "latest") == fund \ No newline at end of file