diff --git a/contracts/interfaces/root/IRootERC20Predicate.sol b/contracts/interfaces/root/IRootERC20Predicate.sol index 8706fe78..34710799 100644 --- a/contracts/interfaces/root/IRootERC20Predicate.sol +++ b/contracts/interfaces/root/IRootERC20Predicate.sol @@ -49,4 +49,10 @@ interface IRootERC20Predicate is IL2StateReceiver { * @return address Address of the child token */ function mapToken(IERC20Metadata rootToken) external returns (address); + + /** + * @notice Function that retrieves rootchain token that represents Supernets native token + * @return address Address of rootchain token (mapped to Supernets native token) + */ + function nativeTokenRoot() external returns (address); } diff --git a/contracts/interfaces/root/staking/ICustomSupernetManager.sol b/contracts/interfaces/root/staking/ICustomSupernetManager.sol index 4b0e35b1..a56e5e4a 100644 --- a/contracts/interfaces/root/staking/ICustomSupernetManager.sol +++ b/contracts/interfaces/root/staking/ICustomSupernetManager.sol @@ -21,6 +21,7 @@ interface ICustomSupernetManager { event RemovedFromWhitelist(address indexed validator); event ValidatorRegistered(address indexed validator, uint256[4] blsKey); event ValidatorDeactivated(address indexed validator); + event GenesisBalanceAdded(address indexed account, uint256 indexed amount); event GenesisFinalized(uint256 amountValidators); event StakingEnabled(); @@ -51,4 +52,9 @@ interface ICustomSupernetManager { /// @notice returns validator instance based on provided address function getValidator(address validator_) external view returns (Validator memory); + + /// @notice addGenesisBalance is used to specify genesis balance information for genesis accounts on the Supernets. + /// It is applicable only in case Supernets native contract is mapped to a pre-existing rootchain ERC20 token. + /// @param amount represents the amount to be premined in the genesis. + function addGenesisBalance(uint256 amount) external; } diff --git a/contracts/lib/GenesisLib.sol b/contracts/lib/GenesisLib.sol index a011ac9b..7bf093b4 100644 --- a/contracts/lib/GenesisLib.sol +++ b/contracts/lib/GenesisLib.sol @@ -13,7 +13,7 @@ enum GenesisStatus { } struct GenesisValidator { - address validator; + address addr; uint256 initialStake; } @@ -41,7 +41,8 @@ library GenesisLib { self.genesisValidators.push(GenesisValidator(validator, stake)); } else { // update values - GenesisValidator storage genesisValidator = self.genesisValidators[_indexOf(self, validator)]; + uint256 idx = _indexOf(self, validator); + GenesisValidator storage genesisValidator = self.genesisValidators[idx]; genesisValidator.initialStake += stake; } } diff --git a/contracts/root/RootERC20Predicate.sol b/contracts/root/RootERC20Predicate.sol index b9ab0ba1..286ff2cc 100644 --- a/contracts/root/RootERC20Predicate.sol +++ b/contracts/root/RootERC20Predicate.sol @@ -19,6 +19,7 @@ contract RootERC20Predicate is Initializable, IRootERC20Predicate { bytes32 public constant WITHDRAW_SIG = keccak256("WITHDRAW"); bytes32 public constant MAP_TOKEN_SIG = keccak256("MAP_TOKEN"); mapping(address => address) public rootTokenToChildToken; + address public nativeTokenRoot; /** * @notice Initialization function for RootERC20Predicate @@ -32,7 +33,7 @@ contract RootERC20Predicate is Initializable, IRootERC20Predicate { address newExitHelper, address newChildERC20Predicate, address newChildTokenTemplate, - address nativeTokenRootAddress + address newNativeTokenRoot ) external initializer { require( newStateSender != address(0) && @@ -45,9 +46,10 @@ contract RootERC20Predicate is Initializable, IRootERC20Predicate { exitHelper = newExitHelper; childERC20Predicate = newChildERC20Predicate; childTokenTemplate = newChildTokenTemplate; - if (nativeTokenRootAddress != address(0)) { - rootTokenToChildToken[nativeTokenRootAddress] = 0x0000000000000000000000000000000000001010; - emit TokenMapped(nativeTokenRootAddress, 0x0000000000000000000000000000000000001010); + if (newNativeTokenRoot != address(0)) { + nativeTokenRoot = newNativeTokenRoot; + rootTokenToChildToken[nativeTokenRoot] = 0x0000000000000000000000000000000000001010; + emit TokenMapped(nativeTokenRoot, 0x0000000000000000000000000000000000001010); } } @@ -138,5 +140,5 @@ contract RootERC20Predicate is Initializable, IRootERC20Predicate { } // slither-disable-next-line unused-state,naming-convention - uint256[50] private __gap; + uint256[49] private __gap; } diff --git a/contracts/root/staking/CustomSupernetManager.sol b/contracts/root/staking/CustomSupernetManager.sol index 402fb563..a0503d03 100644 --- a/contracts/root/staking/CustomSupernetManager.sol +++ b/contracts/root/staking/CustomSupernetManager.sol @@ -9,6 +9,7 @@ import "../../interfaces/common/IBLS.sol"; import "../../interfaces/IStateSender.sol"; import "../../interfaces/root/staking/ICustomSupernetManager.sol"; import "../../interfaces/root/IExitHelper.sol"; +import "../../interfaces/root/IRootERC20Predicate.sol"; contract CustomSupernetManager is ICustomSupernetManager, Ownable2StepUpgradeable, SupernetManager { using SafeERC20 for IERC20; @@ -27,6 +28,8 @@ contract CustomSupernetManager is ICustomSupernetManager, Ownable2StepUpgradeabl GenesisSet private _genesis; mapping(address => Validator) public validators; + IRootERC20Predicate private _rootERC20Predicate; + mapping(address => uint256) public genesisBalances; modifier onlyValidator(address validator) { if (!validators[validator].isActive) revert Unauthorized("VALIDATOR"); @@ -40,6 +43,7 @@ contract CustomSupernetManager is ICustomSupernetManager, Ownable2StepUpgradeabl address newMatic, address newChildValidatorSet, address newExitHelper, + address newRootERC20Predicate, string memory newDomain ) public initializer { require( @@ -52,12 +56,14 @@ contract CustomSupernetManager is ICustomSupernetManager, Ownable2StepUpgradeabl bytes(newDomain).length != 0, "INVALID_INPUT" ); + __SupernetManager_init(newStakeManager); _bls = IBLS(newBls); _stateSender = IStateSender(newStateSender); _matic = IERC20(newMatic); _childValidatorSet = newChildValidatorSet; _exitHelper = newExitHelper; + _rootERC20Predicate = IRootERC20Predicate(newRootERC20Predicate); domain = keccak256(abi.encodePacked(newDomain)); __Ownable2Step_init(); } @@ -129,6 +135,33 @@ contract CustomSupernetManager is ICustomSupernetManager, Ownable2StepUpgradeabl return validators[validator_]; } + /** + * + * @inheritdoc ICustomSupernetManager + */ + function addGenesisBalance(uint256 amount) external { + require(amount > 0, "CustomSupernetManager: INVALID_AMOUNT"); + if (address(_rootERC20Predicate) == address(0)) { + revert Unauthorized("CustomSupernetManager: UNDEFINED_ROOT_ERC20_PREDICATE"); + } + + IERC20 nativeTokenRoot = IERC20(_rootERC20Predicate.nativeTokenRoot()); + if (address(nativeTokenRoot) == address(0)) { + revert Unauthorized("CustomSupernetManager: UNDEFINED_NATIVE_TOKEN_ROOT"); + } + require(!_genesis.completed(), "CustomSupernetManager: GENESIS_SET_IS_ALREADY_FINALIZED"); + + // we need to track EOAs as well in the genesis set, in order to be able to query genesisBalances mapping + _genesis.insert(msg.sender, 0); + genesisBalances[msg.sender] += amount; + + // lock native tokens on the root erc20 predicate + nativeTokenRoot.safeTransferFrom(msg.sender, address(_rootERC20Predicate), amount); + + // slither-disable-next-line reentrancy-events + emit GenesisBalanceAdded(msg.sender, amount); + } + function _onStake(address validator, uint256 amount) internal override onlyValidator(validator) { if (_genesis.gatheringGenesisValidators()) { _genesis.insert(validator, amount); @@ -181,5 +214,5 @@ contract CustomSupernetManager is ICustomSupernetManager, Ownable2StepUpgradeabl } // slither-disable-next-line unused-state,naming-convention - uint256[50] private __gap; + uint256[48] private __gap; } diff --git a/docs/interfaces/root/IRootERC20Predicate.md b/docs/interfaces/root/IRootERC20Predicate.md index c11982d6..2037f675 100644 --- a/docs/interfaces/root/IRootERC20Predicate.md +++ b/docs/interfaces/root/IRootERC20Predicate.md @@ -67,6 +67,23 @@ Function to be used for token mapping |---|---|---| | _0 | address | address Address of the child token | +### nativeTokenRoot + +```solidity +function nativeTokenRoot() external nonpayable returns (address) +``` + +Function that retrieves rootchain token that represents Supernets native token + + + + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | address | address Address of rootchain token (mapped to Supernets native token) | + ### onL2StateReceive ```solidity diff --git a/docs/interfaces/root/staking/ICustomSupernetManager.md b/docs/interfaces/root/staking/ICustomSupernetManager.md index beb2d3af..3a5b474a 100644 --- a/docs/interfaces/root/staking/ICustomSupernetManager.md +++ b/docs/interfaces/root/staking/ICustomSupernetManager.md @@ -10,6 +10,22 @@ Manages validator access and syncs voting power between the stake manager and va ## Methods +### addGenesisBalance + +```solidity +function addGenesisBalance(uint256 amount) external nonpayable +``` + +addGenesisBalance is used to specify genesis balance information for genesis accounts on the Supernets. It is applicable only in case Supernets native contract is mapped to a pre-existing rootchain ERC20 token. + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| amount | uint256 | represents the amount to be premined in the genesis. | + ### enableStaking ```solidity @@ -142,6 +158,23 @@ event AddedToWhitelist(address indexed validator) |---|---|---| | validator `indexed` | address | undefined | +### GenesisBalanceAdded + +```solidity +event GenesisBalanceAdded(address indexed account, uint256 indexed amount) +``` + + + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| account `indexed` | address | undefined | +| amount `indexed` | uint256 | undefined | + ### GenesisFinalized ```solidity diff --git a/docs/root/RootERC20Predicate.md b/docs/root/RootERC20Predicate.md index a1116f71..15170349 100644 --- a/docs/root/RootERC20Predicate.md +++ b/docs/root/RootERC20Predicate.md @@ -150,7 +150,7 @@ function exitHelper() external view returns (address) ### initialize ```solidity -function initialize(address newStateSender, address newExitHelper, address newChildERC20Predicate, address newChildTokenTemplate, address nativeTokenRootAddress) external nonpayable +function initialize(address newStateSender, address newExitHelper, address newChildERC20Predicate, address newChildTokenTemplate, address newNativeTokenRoot) external nonpayable ``` Initialization function for RootERC20Predicate @@ -165,7 +165,7 @@ Initialization function for RootERC20Predicate | newExitHelper | address | Address of ExitHelper to receive withdrawal information from | | newChildERC20Predicate | address | Address of child ERC20 predicate to communicate with | | newChildTokenTemplate | address | undefined | -| nativeTokenRootAddress | address | undefined | +| newNativeTokenRoot | address | undefined | ### mapToken @@ -189,6 +189,23 @@ Function to be used for token mapping |---|---|---| | _0 | address | address Address of the child token | +### nativeTokenRoot + +```solidity +function nativeTokenRoot() external view returns (address) +``` + +Function that retrieves rootchain token that represents Supernets native token + + + + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | address | address Address of rootchain token (mapped to Supernets native token) | + ### onL2StateReceive ```solidity diff --git a/docs/root/staking/CustomSupernetManager.md b/docs/root/staking/CustomSupernetManager.md index 3490284e..eacd2065 100644 --- a/docs/root/staking/CustomSupernetManager.md +++ b/docs/root/staking/CustomSupernetManager.md @@ -21,6 +21,22 @@ function acceptOwnership() external nonpayable *The new owner accepts the ownership transfer.* +### addGenesisBalance + +```solidity +function addGenesisBalance(uint256 amount) external nonpayable +``` + +addGenesisBalance is used to specify genesis balance information for genesis accounts on the Supernets. It is applicable only in case Supernets native contract is mapped to a pre-existing rootchain ERC20 token. + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| amount | uint256 | represents the amount to be premined in the genesis. | + ### domain ```solidity @@ -60,6 +76,28 @@ finalizes initial genesis validator set *only callable by owner* +### genesisBalances + +```solidity +function genesisBalances(address) external view returns (uint256) +``` + + + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| _0 | address | undefined | + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | uint256 | undefined | + ### genesisSet ```solidity @@ -119,7 +157,7 @@ function id() external view returns (uint256) ### initialize ```solidity -function initialize(address newStakeManager, address newBls, address newStateSender, address newMatic, address newChildValidatorSet, address newExitHelper, string newDomain) external nonpayable +function initialize(address newStakeManager, address newBls, address newStateSender, address newMatic, address newChildValidatorSet, address newExitHelper, address newRootERC20Predicate, string newDomain) external nonpayable ``` @@ -136,6 +174,7 @@ function initialize(address newStakeManager, address newBls, address newStateSen | newMatic | address | undefined | | newChildValidatorSet | address | undefined | | newExitHelper | address | undefined | +| newRootERC20Predicate | address | undefined | | newDomain | string | undefined | ### onInit @@ -327,6 +366,23 @@ event AddedToWhitelist(address indexed validator) |---|---|---| | validator `indexed` | address | undefined | +### GenesisBalanceAdded + +```solidity +event GenesisBalanceAdded(address indexed account, uint256 indexed amount) +``` + + + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| account `indexed` | address | undefined | +| amount `indexed` | uint256 | undefined | + ### GenesisFinalized ```solidity diff --git a/script/deployment/DeployNewRootContractSet.s.sol b/script/deployment/DeployNewRootContractSet.s.sol index 0f5d1c43..9f79a1c5 100644 --- a/script/deployment/DeployNewRootContractSet.s.sol +++ b/script/deployment/DeployNewRootContractSet.s.sol @@ -61,6 +61,7 @@ contract DeployNewRootContractSet is config.readAddress('["CustomSupernetManager"].newMatic'), config.readAddress('["CustomSupernetManager"].newChildValidatorSet'), exitHelperProxy, + config.readAddress('["CustomSupernetManager"].newRootERC20Predicate'), config.readString('["CustomSupernetManager"].newDomain') ); } diff --git a/script/deployment/root/staking/DeployCustomSupernetManager.s.sol b/script/deployment/root/staking/DeployCustomSupernetManager.s.sol index f68ab3ab..8f4e6c9c 100644 --- a/script/deployment/root/staking/DeployCustomSupernetManager.s.sol +++ b/script/deployment/root/staking/DeployCustomSupernetManager.s.sol @@ -16,11 +16,21 @@ abstract contract CustomSupernetManagerDeployer is Script { address newMatic, address newChildValidatorSet, address newExitHelper, + address newRootERC20Predicate, string memory newDomain ) internal returns (address logicAddr, address proxyAddr) { bytes memory initData = abi.encodeCall( CustomSupernetManager.initialize, - (newStakeManager, newBls, newStateSender, newMatic, newChildValidatorSet, newExitHelper, newDomain) + ( + newStakeManager, + newBls, + newStateSender, + newMatic, + newChildValidatorSet, + newExitHelper, + newRootERC20Predicate, + newDomain + ) ); vm.startBroadcast(); @@ -49,6 +59,7 @@ contract DeployCustomSupernetManager is CustomSupernetManagerDeployer { address newMatic, address newChildValidatorSet, address newExitHelper, + address newRootERC20Predicate, string memory newDomain ) external returns (address logicAddr, address proxyAddr) { return @@ -60,6 +71,7 @@ contract DeployCustomSupernetManager is CustomSupernetManagerDeployer { newMatic, newChildValidatorSet, newExitHelper, + newRootERC20Predicate, newDomain ); } diff --git a/script/deployment/rootContractSetConfig.json b/script/deployment/rootContractSetConfig.json index a8c42f64..543b0142 100644 --- a/script/deployment/rootContractSetConfig.json +++ b/script/deployment/rootContractSetConfig.json @@ -12,6 +12,7 @@ "newBls": "", "newMatic": "", "newChildValidatorSet": "", + "newRootERC20Predicate": "", "newDomain": "" } } diff --git a/test/forge/root/staking/CustomSupernetManager.t.sol b/test/forge/root/staking/CustomSupernetManager.t.sol index c6fa7327..65f483cc 100644 --- a/test/forge/root/staking/CustomSupernetManager.t.sol +++ b/test/forge/root/staking/CustomSupernetManager.t.sol @@ -8,6 +8,7 @@ import {ExitHelper} from "contracts/root/ExitHelper.sol"; import {StakeManager} from "contracts/root/staking/StakeManager.sol"; import {CustomSupernetManager, Validator, GenesisValidator} from "contracts/root/staking/CustomSupernetManager.sol"; import {MockERC20} from "contracts/mocks/MockERC20.sol"; +import {RootERC20Predicate} from "contracts/root/RootERC20Predicate.sol"; import "contracts/interfaces/Errors.sol"; abstract contract Uninitialized is Test { @@ -20,6 +21,7 @@ abstract contract Uninitialized is Test { MockERC20 token; StakeManager stakeManager; CustomSupernetManager supernetManager; + RootERC20Predicate rootERC20Predicate; function setUp() public virtual { bls = new BLS(); @@ -30,6 +32,7 @@ abstract contract Uninitialized is Test { stakeManager = new StakeManager(); supernetManager = new CustomSupernetManager(); stakeManager.initialize(address(token)); + rootERC20Predicate = new RootERC20Predicate(); } } @@ -43,6 +46,7 @@ abstract contract Initialized is Uninitialized { address(token), childValidatorSet, exitHelper, + address(rootERC20Predicate), DOMAIN ); } @@ -170,6 +174,7 @@ contract CustomSupernetManager_Initialize is Uninitialized { address(token), childValidatorSet, exitHelper, + address(rootERC20Predicate), DOMAIN ); assertEq(supernetManager.owner(), address(this), "should set owner"); @@ -267,7 +272,7 @@ contract CustomSupernetManager_StakeGenesis is ValidatorsRegistered { GenesisValidator[] memory genesisValidators = supernetManager.genesisSet(); assertEq(genesisValidators.length, 1, "should set genesisSet"); GenesisValidator memory validator = genesisValidators[0]; - assertEq(validator.validator, address(this), "should set validator address"); + assertEq(validator.addr, address(this), "should set validator address"); assertEq(validator.initialStake, amount, "should set amount"); } @@ -277,7 +282,7 @@ contract CustomSupernetManager_StakeGenesis is ValidatorsRegistered { GenesisValidator[] memory genesisValidators = supernetManager.genesisSet(); assertEq(genesisValidators.length, 1, "should set genesisSet"); GenesisValidator memory validator = genesisValidators[0]; - assertEq(validator.validator, address(this), "should set validator address"); + assertEq(validator.addr, address(this), "should set validator address"); assertEq(validator.initialStake, amount, "should set amount"); } } @@ -382,3 +387,87 @@ contract CustomSupernetManager_Unstake is EnabledStaking { assertEq(supernetManager.getValidator(address(this)).isActive, true, "should not deactivate"); } } + +contract CustomSupernetManager_PremineInitialized is Initialized { + uint256 balance = 100 ether; + event GenesisBalanceAdded(address indexed account, uint256 indexed amount); + + address childERC20Predicate; + address childTokenTemplate; + address bob = makeAddr("bob"); + + function setUp() public virtual override { + super.setUp(); + token.mint(bob, balance); + childERC20Predicate = makeAddr("childERC20Predicate"); + childTokenTemplate = makeAddr("childTokenTemplate"); + rootERC20Predicate.initialize( + address(stateSender), + exitHelper, + childERC20Predicate, + childTokenTemplate, + address(token) + ); + } + + function test_addGenesisBalance_successful() public { + vm.startPrank(bob); + token.approve(address(supernetManager), balance); + vm.expectEmit(true, true, true, true); + emit GenesisBalanceAdded(bob, balance); + supernetManager.addGenesisBalance(balance); + + GenesisValidator[] memory genesisAccounts = supernetManager.genesisSet(); + assertEq(genesisAccounts.length, 1, "should set genesisSet"); + GenesisValidator memory account = genesisAccounts[0]; + assertEq(account.addr, bob, "should set validator address"); + assertEq(account.initialStake, 0, "should set initial stake to 0"); + + uint256 actualBalance = supernetManager.genesisBalances(account.addr); + assertEq(actualBalance, balance, "should set genesis balance"); + } + + function test_addGenesisBalance_genesisSetFinalizedRevert() public { + supernetManager.finalizeGenesis(); + supernetManager.enableStaking(); + vm.expectRevert("CustomSupernetManager: GENESIS_SET_IS_ALREADY_FINALIZED"); + supernetManager.addGenesisBalance(balance); + } + + function test_addGenesisBalance_invalidAmountRevert() public { + vm.expectRevert("CustomSupernetManager: INVALID_AMOUNT"); + supernetManager.addGenesisBalance(0); + } +} + +contract CustomSupernetManager_UndefinedRootERC20Predicate is Uninitialized { + function setUp() public virtual override { + super.setUp(); + supernetManager.initialize( + address(stakeManager), + address(bls), + address(stateSender), + address(token), + childValidatorSet, + exitHelper, + address(0), + DOMAIN + ); + } + + function test_addGenesisBalance_revertUndefinedRootERC20Predicate() public { + vm.expectRevert( + abi.encodeWithSelector(Unauthorized.selector, "CustomSupernetManager: UNDEFINED_ROOT_ERC20_PREDICATE") + ); + supernetManager.addGenesisBalance(100 ether); + } +} + +contract CustomSupernetManager_UndefinedNativeTokenRoot is Initialized { + function test_addGenesisBalance_revertUndefinedNativeTokenRoot() public { + vm.expectRevert( + abi.encodeWithSelector(Unauthorized.selector, "CustomSupernetManager: UNDEFINED_NATIVE_TOKEN_ROOT") + ); + supernetManager.addGenesisBalance(100 ether); + } +} diff --git a/test/forge/root/staking/deployment/DeployCustomSupernetManager.t.sol b/test/forge/root/staking/deployment/DeployCustomSupernetManager.t.sol index 41137dc1..c544a93d 100644 --- a/test/forge/root/staking/deployment/DeployCustomSupernetManager.t.sol +++ b/test/forge/root/staking/deployment/DeployCustomSupernetManager.t.sol @@ -25,6 +25,7 @@ contract DeployCustomSupernetManagerTest is Test { address newMatic; address newChildValidatorSet; address newExitHelper; + address newRootERC20Predicate; string newDomain; function setUp() public { @@ -37,6 +38,7 @@ contract DeployCustomSupernetManagerTest is Test { newMatic = makeAddr("newMatic"); newChildValidatorSet = makeAddr("newChildValidatorSet"); newExitHelper = makeAddr("newExitHelper"); + newRootERC20Predicate = makeAddr("newRootERC20Predicate"); newDomain = "newDomain"; (logicAddr, proxyAddr) = deployer.run( @@ -47,6 +49,7 @@ contract DeployCustomSupernetManagerTest is Test { newMatic, newChildValidatorSet, newExitHelper, + newRootERC20Predicate, newDomain ); _recordProxy(proxyAddr); @@ -70,6 +73,7 @@ contract DeployCustomSupernetManagerTest is Test { newMatic, newChildValidatorSet, newExitHelper, + newRootERC20Predicate, newDomain );