diff --git a/contracts/interchain-token-service/InterchainTokenService.sol b/contracts/interchain-token-service/InterchainTokenService.sol index 05e00566..57320451 100644 --- a/contracts/interchain-token-service/InterchainTokenService.sol +++ b/contracts/interchain-token-service/InterchainTokenService.sol @@ -495,7 +495,7 @@ contract InterchainTokenService is * @param tokenIds an array of the token Ids of the tokenManagers to set the flow limit of. * @param flowLimits the flowLimits to set */ - function setFlowLimits(bytes32[] calldata tokenIds, uint256[] calldata flowLimits) external onlyOperator { + function setFlowLimits(bytes32[] calldata tokenIds, uint256[] calldata flowLimits) external onlyRole(uint8(Roles.OPERATOR)) { uint256 length = tokenIds.length; if (length != flowLimits.length) revert LengthMismatch(); for (uint256 i; i < length; ++i) { @@ -518,7 +518,7 @@ contract InterchainTokenService is \****************/ function _setup(bytes calldata params) internal override { - _setOperator(params.toAddress()); + _addOperator(params.toAddress()); } function _sanitizeTokenManagerImplementation( diff --git a/contracts/interfaces/IDistributable.sol b/contracts/interfaces/IDistributable.sol index cfe446dc..66406480 100644 --- a/contracts/interfaces/IDistributable.sol +++ b/contracts/interfaces/IDistributable.sol @@ -3,18 +3,6 @@ pragma solidity ^0.8.0; interface IDistributable { - error NotDistributor(); - error NotProposedDistributor(); - - event DistributorshipTransferred(address indexed distributor); - event DistributorshipTransferStarted(address indexed distributor); - - /** - * @notice Get the address of the distributor - * @return distributor_ of the distributor - */ - function distributor() external view returns (address distributor_); - /** * @notice Change the distributor of the contract * @dev Can only be called by the current distributor @@ -33,5 +21,11 @@ interface IDistributable { * @notice Accept a change of the distributor of the contract * @dev Can only be called by the proposed distributor */ - function acceptDistributorship() external; + function acceptDistributorship(address fromDistributor) external; + + /** + * @notice Query if an address is a distributor + * @param addr the address to query for + */ + function isDistributor(address addr) external view returns (bool); } diff --git a/contracts/interfaces/IOperatable.sol b/contracts/interfaces/IOperatable.sol index 861a84b6..186ffe4b 100644 --- a/contracts/interfaces/IOperatable.sol +++ b/contracts/interfaces/IOperatable.sol @@ -3,18 +3,6 @@ pragma solidity ^0.8.0; interface IOperatable { - error NotOperator(); - error NotProposedOperator(); - - event OperatorshipTransferred(address indexed operator); - event OperatorChangeProposed(address indexed operator); - - /** - * @notice Get the address of the operator - * @return operator_ of the operator - */ - function operator() external view returns (address operator_); - /** * @notice Change the operator of the contract * @dev Can only be called by the current operator @@ -33,5 +21,11 @@ interface IOperatable { * @notice Accept a proposed change of operatorship * @dev Can only be called by the proposed operator */ - function acceptOperatorship() external; + function acceptOperatorship(address fromOperator) external; + + /** + * @notice Query if an address is a operator + * @param addr the address to query for + */ + function isOperator(address addr) external view returns (bool); } diff --git a/contracts/interfaces/ITokenManager.sol b/contracts/interfaces/ITokenManager.sol index 37fc5ed0..6658645c 100644 --- a/contracts/interfaces/ITokenManager.sol +++ b/contracts/interfaces/ITokenManager.sol @@ -17,6 +17,9 @@ interface ITokenManager is ITokenManagerType, IOperatable, IFlowLimit, IImplemen error TakeTokenFailed(); error GiveTokenFailed(); error NotToken(); + error ZeroAddress(); + error AlreadyFlowLimiter(address flowLimiter); + error NotFlowLimiter(address flowLimiter); /** * @notice A function that returns the token id. diff --git a/contracts/test/FeeOnTransferTokenTest.sol b/contracts/test/FeeOnTransferTokenTest.sol index 05664232..7715ed44 100644 --- a/contracts/test/FeeOnTransferTokenTest.sol +++ b/contracts/test/FeeOnTransferTokenTest.sol @@ -19,7 +19,7 @@ contract FeeOnTransferTokenTest is InterchainToken, Distributable, IERC20Mintabl name = name_; symbol = symbol_; decimals = decimals_; - _setDistributor(msg.sender); + _addDistributor(msg.sender); tokenManager_ = ITokenManager(tokenManagerAddress); } @@ -50,11 +50,11 @@ contract FeeOnTransferTokenTest is InterchainToken, Distributable, IERC20Mintabl tokenManagerRequiresApproval_ = requiresApproval; } - function mint(address account, uint256 amount) external onlyDistributor { + function mint(address account, uint256 amount) external onlyRole(uint8(Roles.DISTRIBUTOR)) { _mint(account, amount); } - function burn(address account, uint256 amount) external onlyDistributor { + function burn(address account, uint256 amount) external onlyRole(uint8(Roles.DISTRIBUTOR)) { _burn(account, amount); } diff --git a/contracts/test/HardCodedConstantsTest.sol b/contracts/test/HardCodedConstantsTest.sol index 2fcb7226..2ff0e1e8 100644 --- a/contracts/test/HardCodedConstantsTest.sol +++ b/contracts/test/HardCodedConstantsTest.sol @@ -21,10 +21,7 @@ contract TestTokenManager is TokenManagerLiquidityPool { contract TestDistributable is Distributable { string public constant NAME = 'TestDistributable'; - constructor() { - require(DISTRIBUTOR_SLOT == uint256(keccak256('distributor')) - 1, 'invalid constant'); - require(PROPOSED_DISTRIBUTOR_SLOT == uint256(keccak256('proposed-distributor')) - 1, 'invalid constant'); - } + constructor() {} } contract TestFlowLimit is FlowLimit { @@ -46,10 +43,7 @@ contract TestNoReEntrancy is NoReEntrancy { contract TestOperatable is Operatable { string public constant NAME = 'TestOperatable'; - constructor() { - require(OPERATOR_SLOT == uint256(keccak256('operator')) - 1, 'invalid constant'); - require(PROPOSED_OPERATOR_SLOT == uint256(keccak256('proposed-operator')) - 1, 'invalid constant'); - } + constructor() {} } contract TestPausable is Pausable { diff --git a/contracts/test/InterchainTokenTest.sol b/contracts/test/InterchainTokenTest.sol index 8a44f732..f6739c1c 100644 --- a/contracts/test/InterchainTokenTest.sol +++ b/contracts/test/InterchainTokenTest.sol @@ -18,7 +18,7 @@ contract InterchainTokenTest is InterchainToken, Distributable, IERC20MintableBu name = name_; symbol = symbol_; decimals = decimals_; - _setDistributor(msg.sender); + _addDistributor(msg.sender); tokenManager_ = ITokenManager(tokenManagerAddress); } @@ -49,11 +49,11 @@ contract InterchainTokenTest is InterchainToken, Distributable, IERC20MintableBu tokenManagerRequiresApproval_ = requiresApproval; } - function mint(address account, uint256 amount) external onlyDistributor { + function mint(address account, uint256 amount) external onlyRole(uint8(Roles.DISTRIBUTOR)) { _mint(account, amount); } - function burn(address account, uint256 amount) external onlyDistributor { + function burn(address account, uint256 amount) external onlyRole(uint8(Roles.DISTRIBUTOR)) { _burn(account, amount); } diff --git a/contracts/test/InvalidStandardizedToken.sol b/contracts/test/InvalidStandardizedToken.sol index 088a3ddf..56a2a60d 100644 --- a/contracts/test/InvalidStandardizedToken.sol +++ b/contracts/test/InvalidStandardizedToken.sol @@ -21,14 +21,6 @@ contract InvalidStandardizedToken is IERC20MintableBurnable, InterchainToken, ER bytes32 private constant CONTRACT_ID = keccak256('invalid-standardized-token'); - modifier onlyDistributorOrTokenManager() { - if (msg.sender != tokenManager_) { - if (msg.sender != distributor()) revert NotDistributor(); - } - - _; - } - /** * @notice Getter for the contract id. */ @@ -62,7 +54,8 @@ contract InvalidStandardizedToken is IERC20MintableBurnable, InterchainToken, ER tokenManager_ = tokenManagerAddress; name = tokenName; - _setDistributor(distributor_); + _addDistributor(distributor_); + _addDistributor(tokenManagerAddress); _setNameHash(tokenName); } { @@ -82,7 +75,7 @@ contract InvalidStandardizedToken is IERC20MintableBurnable, InterchainToken, ER * @param account The address that will receive the minted tokens * @param amount The amount of tokens to mint */ - function mint(address account, uint256 amount) external onlyDistributorOrTokenManager { + function mint(address account, uint256 amount) external onlyRole(uint8(Roles.DISTRIBUTOR)) { _mint(account, amount); } @@ -92,7 +85,7 @@ contract InvalidStandardizedToken is IERC20MintableBurnable, InterchainToken, ER * @param account The address that will have its tokens burnt * @param amount The amount of tokens to burn */ - function burn(address account, uint256 amount) external onlyDistributorOrTokenManager { + function burn(address account, uint256 amount) external onlyRole(uint8(Roles.DISTRIBUTOR)) { _burn(account, amount); } } diff --git a/contracts/test/utils/DistributableTest.sol b/contracts/test/utils/DistributableTest.sol index d7e8b819..ccb1d9e6 100644 --- a/contracts/test/utils/DistributableTest.sol +++ b/contracts/test/utils/DistributableTest.sol @@ -8,10 +8,14 @@ contract DistributableTest is Distributable { uint256 public nonce; constructor(address distributor) { - _setDistributor(distributor); + _addDistributor(distributor); } - function testDistributable() external onlyDistributor { + function testDistributable() external onlyRole(uint8(Roles.DISTRIBUTOR)) { nonce++; } + + function getDistributorRole() external pure returns (uint8) { + return uint8(Roles.DISTRIBUTOR); + } } diff --git a/contracts/test/utils/AdminableTest.sol b/contracts/test/utils/OperatableTest.sol similarity index 54% rename from contracts/test/utils/AdminableTest.sol rename to contracts/test/utils/OperatableTest.sol index a3c9cedf..34b07258 100644 --- a/contracts/test/utils/AdminableTest.sol +++ b/contracts/test/utils/OperatableTest.sol @@ -8,10 +8,14 @@ contract OperatorableTest is Operatable { uint256 public nonce; constructor(address operator) { - _setOperator(operator); + _addOperator(operator); } - function testOperatorable() external onlyOperator { + function testOperatorable() external onlyRole(uint8(Roles.OPERATOR)) { nonce++; } + + function getOperatorRole() external pure returns (uint8) { + return uint8(Roles.OPERATOR); + } } diff --git a/contracts/token-implementations/StandardizedToken.sol b/contracts/token-implementations/StandardizedToken.sol index 80e1dbad..fd3b040e 100644 --- a/contracts/token-implementations/StandardizedToken.sol +++ b/contracts/token-implementations/StandardizedToken.sol @@ -27,14 +27,6 @@ contract StandardizedToken is InterchainToken, ERC20Permit, Implementation, Dist bytes32 private constant CONTRACT_ID = keccak256('standardized-token'); - modifier onlyDistributorOrTokenManager() { - if (msg.sender != tokenManager_) { - if (msg.sender != distributor()) revert NotDistributor(); - } - - _; - } - /** * @notice Getter for the contract id. */ @@ -71,7 +63,8 @@ contract StandardizedToken is InterchainToken, ERC20Permit, Implementation, Dist tokenManager_ = tokenManagerAddress; name = tokenName; - _setDistributor(distributor_); + _addDistributor(distributor_); + _addDistributor(tokenManagerAddress); _setNameHash(tokenName); } { @@ -91,7 +84,7 @@ contract StandardizedToken is InterchainToken, ERC20Permit, Implementation, Dist * @param account The address that will receive the minted tokens * @param amount The amount of tokens to mint */ - function mint(address account, uint256 amount) external onlyDistributorOrTokenManager { + function mint(address account, uint256 amount) external onlyRole(uint8(Roles.DISTRIBUTOR)) { _mint(account, amount); } @@ -101,7 +94,7 @@ contract StandardizedToken is InterchainToken, ERC20Permit, Implementation, Dist * @param account The address that will have its tokens burnt * @param amount The amount of tokens to burn */ - function burn(address account, uint256 amount) external onlyDistributorOrTokenManager { + function burn(address account, uint256 amount) external onlyRole(uint8(Roles.DISTRIBUTOR)) { _burn(account, amount); } } diff --git a/contracts/token-manager/TokenManager.sol b/contracts/token-manager/TokenManager.sol index 507e7c5e..063b67ba 100644 --- a/contracts/token-manager/TokenManager.sol +++ b/contracts/token-manager/TokenManager.sol @@ -86,7 +86,7 @@ abstract contract TokenManager is ITokenManager, Operatable, FlowLimit, Implemen operator_ = operatorBytes.toAddress(); } - _setOperator(operator_); + _addAccountRoles(operator_, (1 << uint8(Roles.FLOW_LIMITER)) | (1 << uint8(Roles.OPERATOR))); _setup(params); } @@ -197,10 +197,34 @@ abstract contract TokenManager is ITokenManager, Operatable, FlowLimit, Implemen } /** - * @notice This function sets the flow limit for this TokenManager. Can only be called by the operator. + * @notice This function adds a flow limiter for this TokenManager. Can only be called by the operator. + * @param flowLimiter the address of the new flow limiter. + */ + function addFlowLimiter(address flowLimiter) external onlyRole(uint8(Roles.OPERATOR)) { + if (flowLimiter == address(0)) revert ZeroAddress(); + + if (hasRole(flowLimiter, uint8(Roles.FLOW_LIMITER))) revert AlreadyFlowLimiter(flowLimiter); + + _addRole(flowLimiter, uint8(Roles.FLOW_LIMITER)); + } + + /** + * @notice This function adds a flow limiter for this TokenManager. Can only be called by the operator. + * @param flowLimiter the address of the new flow limiter. + */ + function removeFlowLimiter(address flowLimiter) external onlyRole(uint8(Roles.OPERATOR)) { + if (flowLimiter == address(0)) revert ZeroAddress(); + + if (!hasRole(flowLimiter, uint8(Roles.FLOW_LIMITER))) revert NotFlowLimiter(flowLimiter); + + _removeRole(flowLimiter, uint8(Roles.FLOW_LIMITER)); + } + + /** + * @notice This function sets the flow limit for this TokenManager. Can only be called by the flow limiters. * @param flowLimit the maximum difference between the tokens flowing in and/or out at any given interval of time (6h) */ - function setFlowLimit(uint256 flowLimit) external onlyOperator { + function setFlowLimit(uint256 flowLimit) external onlyRole(uint8(Roles.FLOW_LIMITER)) { _setFlowLimit(flowLimit); } diff --git a/contracts/token-manager/implementations/TokenManagerLiquidityPool.sol b/contracts/token-manager/implementations/TokenManagerLiquidityPool.sol index 381b1ab2..9ac153b1 100644 --- a/contracts/token-manager/implementations/TokenManagerLiquidityPool.sol +++ b/contracts/token-manager/implementations/TokenManagerLiquidityPool.sol @@ -68,7 +68,7 @@ contract TokenManagerLiquidityPool is TokenManager, NoReEntrancy, ITokenManagerL * @dev Updates the address of the liquidity pool. Can only be called by the operator. * @param newLiquidityPool The new address of the liquidity pool */ - function setLiquidityPool(address newLiquidityPool) external onlyOperator { + function setLiquidityPool(address newLiquidityPool) external onlyRole(uint8(Roles.OPERATOR)) { _setLiquidityPool(newLiquidityPool); } diff --git a/contracts/utils/Distributable.sol b/contracts/utils/Distributable.sol index efdeda77..f73b8ce0 100644 --- a/contracts/utils/Distributable.sol +++ b/contracts/utils/Distributable.sol @@ -4,46 +4,22 @@ pragma solidity ^0.8.0; import { IDistributable } from '../interfaces/IDistributable.sol'; +import { RolesBase } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/utils/RolesBase.sol'; +import { RolesConstants } from './RolesConstants.sol'; + /** * @title Distributable Contract * @dev A contract module which provides a basic access control mechanism, where * there is an account (a distributor) that can be granted exclusive access to * specific functions. This module is used through inheritance. */ -contract Distributable is IDistributable { - // uint256(keccak256('distributor')) - 1 - uint256 internal constant DISTRIBUTOR_SLOT = 0x71c5a35e45a25c49e8f747acd4bcb869814b3d104c492d2554f4c46e12371f56; - - // uint256(keccak256('proposed-distributor')) - 1 - uint256 internal constant PROPOSED_DISTRIBUTOR_SLOT = 0xbb1aa7d30971a97896e14e460c5ace030e39b624cf8f7c1ce200eeb378d7dcf1; - - /** - * @dev Throws a NotDistributor custom error if called by any account other than the distributor. - */ - modifier onlyDistributor() { - if (distributor() != msg.sender) revert NotDistributor(); - _; - } - - /** - * @notice Get the address of the distributor - * @return distributor_ of the distributor - */ - function distributor() public view returns (address distributor_) { - assembly { - distributor_ := sload(DISTRIBUTOR_SLOT) - } - } - +contract Distributable is IDistributable, RolesBase, RolesConstants { /** * @dev Internal function that stores the new distributor address in the correct storage slot * @param distributor_ The address of the new distributor */ - function _setDistributor(address distributor_) internal { - assembly { - sstore(DISTRIBUTOR_SLOT, distributor_) - } - emit DistributorshipTransferred(distributor_); + function _addDistributor(address distributor_) internal { + _addRole(distributor_, uint8(Roles.DISTRIBUTOR)); } /** @@ -51,8 +27,8 @@ contract Distributable is IDistributable { * @dev Can only be called by the current distributor * @param distributor_ The address of the new distributor */ - function transferDistributorship(address distributor_) external onlyDistributor { - _setDistributor(distributor_); + function transferDistributorship(address distributor_) external onlyRole(uint8(Roles.DISTRIBUTOR)) { + _transferRole(msg.sender, distributor_, uint8(Roles.DISTRIBUTOR)); } /** @@ -60,24 +36,23 @@ contract Distributable is IDistributable { * @dev Can only be called by the current distributor * @param distributor_ The address of the new distributor */ - function proposeDistributorship(address distributor_) external onlyDistributor { - assembly { - sstore(PROPOSED_DISTRIBUTOR_SLOT, distributor_) - } - emit DistributorshipTransferStarted(distributor_); + function proposeDistributorship(address distributor_) external onlyRole(uint8(Roles.DISTRIBUTOR)) { + _proposeRole(msg.sender, distributor_, uint8(Roles.DISTRIBUTOR)); } /** * @notice Accept a change of the distributor of the contract * @dev Can only be called by the proposed distributor */ - function acceptDistributorship() external { - address proposedDistributor; - assembly { - proposedDistributor := sload(PROPOSED_DISTRIBUTOR_SLOT) - sstore(PROPOSED_DISTRIBUTOR_SLOT, 0) - } - if (msg.sender != proposedDistributor) revert NotProposedDistributor(); - _setDistributor(proposedDistributor); + function acceptDistributorship(address fromDistributor) external { + _acceptRole(fromDistributor, msg.sender, uint8(Roles.DISTRIBUTOR)); + } + + /** + * @notice Query if an address is a distributor + * @param addr the address to query for + */ + function isDistributor(address addr) external view returns (bool) { + return hasRole(addr, uint8(Roles.DISTRIBUTOR)); } } diff --git a/contracts/utils/Operatable.sol b/contracts/utils/Operatable.sol index 203b9957..9cfbb9cb 100644 --- a/contracts/utils/Operatable.sol +++ b/contracts/utils/Operatable.sol @@ -4,45 +4,22 @@ pragma solidity ^0.8.0; import { IOperatable } from '../interfaces/IOperatable.sol'; +import { RolesBase } from '@axelar-network/axelar-gmp-sdk-solidity/contracts/utils/RolesBase.sol'; +import { RolesConstants } from './RolesConstants.sol'; + /** * @title Operatable Contract * @dev A contract module which provides a basic access control mechanism, where - * there is an account (an operator) that can be granted exclusive access to + * there is an account (a operator) that can be granted exclusive access to * specific functions. This module is used through inheritance. */ -contract Operatable is IOperatable { - // uint256(keccak256('operator')) - 1 - uint256 internal constant OPERATOR_SLOT = 0x46a52cf33029de9f84853745a87af28464c80bf0346df1b32e205fc73319f621; - // uint256(keccak256('proposed-operator')) - 1 - uint256 internal constant PROPOSED_OPERATOR_SLOT = 0x18dd7104fe20f6107b1523000995e8f87ac02b734a65cf0a45fafa7635a2c526; - - /** - * @dev Throws a NotOperator custom error if called by any account other than the operator. - */ - modifier onlyOperator() { - if (operator() != msg.sender) revert NotOperator(); - _; - } - - /** - * @notice Get the address of the operator - * @return operator_ of the operator - */ - function operator() public view returns (address operator_) { - assembly { - operator_ := sload(OPERATOR_SLOT) - } - } - +contract Operatable is IOperatable, RolesBase, RolesConstants { /** - * @dev Internal function that stores the new operator address in the operator storage slot + * @dev Internal function that stores the new operator address in the correct storage slot * @param operator_ The address of the new operator */ - function _setOperator(address operator_) internal { - assembly { - sstore(OPERATOR_SLOT, operator_) - } - emit OperatorshipTransferred(operator_); + function _addOperator(address operator_) internal { + _addRole(operator_, uint8(Roles.OPERATOR)); } /** @@ -50,8 +27,8 @@ contract Operatable is IOperatable { * @dev Can only be called by the current operator * @param operator_ The address of the new operator */ - function transferOperatorship(address operator_) external onlyOperator { - _setOperator(operator_); + function transferOperatorship(address operator_) external onlyRole(uint8(Roles.OPERATOR)) { + _transferRole(msg.sender, operator_, uint8(Roles.OPERATOR)); } /** @@ -59,24 +36,23 @@ contract Operatable is IOperatable { * @dev Can only be called by the current operator * @param operator_ The address of the new operator */ - function proposeOperatorship(address operator_) external onlyOperator { - assembly { - sstore(PROPOSED_OPERATOR_SLOT, operator_) - } - emit OperatorChangeProposed(operator_); + function proposeOperatorship(address operator_) external onlyRole(uint8(Roles.OPERATOR)) { + _proposeRole(msg.sender, operator_, uint8(Roles.OPERATOR)); } /** - * @notice Accept a proposed change of operatorship + * @notice Accept a change of the operator of the contract * @dev Can only be called by the proposed operator */ - function acceptOperatorship() external { - address proposedOperator; - assembly { - proposedOperator := sload(PROPOSED_OPERATOR_SLOT) - sstore(PROPOSED_OPERATOR_SLOT, 0) - } - if (msg.sender != proposedOperator) revert NotProposedOperator(); - _setOperator(proposedOperator); + function acceptOperatorship(address fromOperator) external { + _acceptRole(fromOperator, msg.sender, uint8(Roles.OPERATOR)); + } + + /** + * @notice Query if an address is an operator + * @param addr the address to query for + */ + function isOperator(address addr) external view returns (bool) { + return hasRole(addr, uint8(Roles.OPERATOR)); } } diff --git a/contracts/utils/RolesConstants.sol b/contracts/utils/RolesConstants.sol new file mode 100644 index 00000000..8f5ddba9 --- /dev/null +++ b/contracts/utils/RolesConstants.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +contract RolesConstants { + enum Roles { + DISTRIBUTOR, + OPERATOR, + FLOW_LIMITER + } +} diff --git a/package-lock.json b/package-lock.json index bd52f159..37c1f028 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "MIT", "dependencies": { "@axelar-network/axelar-cgp-solidity": "6.1.2", - "@axelar-network/axelar-gmp-sdk-solidity": "5.4.0" + "@axelar-network/axelar-gmp-sdk-solidity": "5.6.0" }, "devDependencies": { "@axelar-network/axelar-chains-config": "~0.1.2", @@ -63,9 +63,9 @@ } }, "node_modules/@axelar-network/axelar-gmp-sdk-solidity": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/@axelar-network/axelar-gmp-sdk-solidity/-/axelar-gmp-sdk-solidity-5.4.0.tgz", - "integrity": "sha512-iPTJZq13ARDXJ5nCjKohq43bC45w0z0E8Dc5zAbQ0evKXXHi/XSNWqs7zvVFQzAHbUTAmEl19431lGCKPRuMcw==", + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@axelar-network/axelar-gmp-sdk-solidity/-/axelar-gmp-sdk-solidity-5.6.0.tgz", + "integrity": "sha512-Fzy1ev+VzuqcBVV5em7Dnhk4gPG42NpT2mHeI1piwanFRu3YGafSyZIcY0A/DYMr3nJAAnhxHbIkgvZu9vB30g==", "engines": { "node": ">=16" } diff --git a/package.json b/package.json index 30691469..110ed7a6 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ }, "dependencies": { "@axelar-network/axelar-cgp-solidity": "6.1.2", - "@axelar-network/axelar-gmp-sdk-solidity": "5.4.0" + "@axelar-network/axelar-gmp-sdk-solidity": "5.6.0" }, "devDependencies": { "@axelar-network/axelar-chains-config": "~0.1.2", diff --git a/test/TokenManager.js b/test/TokenManager.js index 7e8936af..d467cafe 100644 --- a/test/TokenManager.js +++ b/test/TokenManager.js @@ -90,7 +90,7 @@ describe('Token Manager', () => { await expectRevert( (gasOptions) => tokenManagerLockUnlock.setFlowLimit(flowLimit, gasOptions), tokenManagerLockUnlock, - 'NotOperator', + 'MissingRole', ); }); diff --git a/test/TokenService.js b/test/TokenService.js index 5857e8d5..693bd8ca 100644 --- a/test/TokenService.js +++ b/test/TokenService.js @@ -33,6 +33,10 @@ const MINT_BURN_FROM = 1; const LOCK_UNLOCK = 2; const LOCK_UNLOCK_FEE_ON_TRANSFER = 3; +// const DISTRIBUTOR_ROLE = 0; +const OPERATOR_ROLE = 1; +const FLOW_LIMITER_ROLE = 2; + describe('Interchain Token Service', () => { let wallet, otherWallet; let service, gateway, gasService; @@ -403,7 +407,7 @@ describe('Interchain Token Service', () => { expect(tokenManagerAddress).to.not.equal(AddressZero); const tokenManager = new Contract(tokenManagerAddress, TokenManager.abi, wallet); - expect(await tokenManager.operator()).to.equal(service.address); + expect(await tokenManager.hasRole(service.address, OPERATOR_ROLE)).to.be.true; }); it('Should revert if canonical token has already been registered', async () => { @@ -541,7 +545,7 @@ describe('Interchain Token Service', () => { expect(tokenManagerAddress).to.not.equal(AddressZero); const tokenManager = new Contract(tokenManagerAddress, TokenManager.abi, wallet); - expect(await tokenManager.operator()).to.equal(wallet.address); + expect(await tokenManager.hasRole(wallet.address, OPERATOR_ROLE)).to.be.true; }); it('Should revert when registering a standardized token when service is paused', async () => { @@ -583,7 +587,7 @@ describe('Interchain Token Service', () => { expect(tokenManagerAddress).to.not.equal(AddressZero); const tokenManager = new Contract(tokenManagerAddress, TokenManager.abi, wallet); - expect(await tokenManager.operator()).to.equal(wallet.address); + expect(await tokenManager.hasRole(wallet.address, OPERATOR_ROLE)).to.be.true; // Register the same token again await expectRevert( @@ -778,7 +782,7 @@ describe('Interchain Token Service', () => { .withArgs(tokenId, MINT_BURN, params); const tokenManager = new Contract(tokenManagerAddress, TokenManager.abi, wallet); expect(await tokenManager.tokenAddress()).to.equal(tokenAddress); - expect(await tokenManager.operator()).to.equal(wallet.address); + expect(await tokenManager.hasRole(wallet.address, OPERATOR_ROLE)).to.be.true; }); it('Should be able to receive a remote standardized token depoloyment with a mint/burn token manager', async () => { @@ -813,7 +817,7 @@ describe('Interchain Token Service', () => { .withArgs(tokenId, MINT_BURN, params); const tokenManager = new Contract(tokenManagerAddress, TokenManager.abi, wallet); expect(await tokenManager.tokenAddress()).to.equal(tokenAddress); - expect(await tokenManager.operator()).to.equal(operator); + expect(await tokenManager.hasRole(operator, OPERATOR_ROLE)).to.be.true; }); it('Should be able to receive a remote standardized token depoloyment with a mint/burn token manager with empty distributor and operator', async () => { @@ -851,7 +855,7 @@ describe('Interchain Token Service', () => { .withArgs(tokenId, MINT_BURN, params); const tokenManager = new Contract(tokenManagerAddress, TokenManager.abi, wallet); expect(await tokenManager.tokenAddress()).to.equal(tokenAddress); - expect(await tokenManager.operator()).to.equal(service.address); + expect(await tokenManager.hasRole(service.address, OPERATOR_ROLE)).to.be.true; }); it('Should be able to receive a remote standardized token depoloyment with a mint/burn token manager with non-empty mintTo bytes', async () => { @@ -889,7 +893,7 @@ describe('Interchain Token Service', () => { .withArgs(tokenId, MINT_BURN, params); const tokenManager = new Contract(tokenManagerAddress, TokenManager.abi, wallet); expect(await tokenManager.tokenAddress()).to.equal(tokenAddress); - expect(await tokenManager.operator()).to.equal(service.address); + expect(await tokenManager.hasRole(service.address, OPERATOR_ROLE)).to.be.true; }); it('Should revert on execute with token', async () => { @@ -930,7 +934,7 @@ describe('Interchain Token Service', () => { expect(tokenManagerAddress).to.not.equal(AddressZero); const tokenManager = new Contract(tokenManagerAddress, TokenManager.abi, wallet); - expect(await tokenManager.operator()).to.equal(wallet.address); + expect(await tokenManager.hasRole(wallet.address, OPERATOR_ROLE)).to.be.true; const tokenAddress = await service.getTokenAddress(tokenId); expect(tokenAddress).to.eq(token.address); @@ -952,7 +956,7 @@ describe('Interchain Token Service', () => { expect(tokenManagerAddress).to.not.equal(AddressZero); const tokenManager = new Contract(tokenManagerAddress, TokenManager.abi, wallet); - expect(await tokenManager.operator()).to.equal(wallet.address); + expect(await tokenManager.hasRole(wallet.address, OPERATOR_ROLE)).to.be.true; const tokenAddress = await service.getTokenAddress(tokenId); expect(tokenAddress).to.eq(token.address); @@ -974,7 +978,7 @@ describe('Interchain Token Service', () => { expect(tokenManagerAddress).to.not.equal(AddressZero); const tokenManager = new Contract(tokenManagerAddress, TokenManager.abi, wallet); - expect(await tokenManager.operator()).to.equal(wallet.address); + expect(await tokenManager.hasRole(wallet.address, OPERATOR_ROLE)).to.be.true; const tokenAddress = await service.getTokenAddress(tokenId); expect(tokenAddress).to.eq(token.address); @@ -1001,7 +1005,7 @@ describe('Interchain Token Service', () => { expect(tokenManagerAddress).to.not.equal(AddressZero); const tokenManager = new Contract(tokenManagerAddress, TokenManager.abi, wallet); - expect(await tokenManager.operator()).to.equal(wallet.address); + expect(await tokenManager.hasRole(wallet.address, OPERATOR_ROLE)).to.be.true; const tokenAddress = await service.getTokenAddress(tokenId); expect(tokenAddress).to.eq(token.address); @@ -1176,7 +1180,7 @@ describe('Interchain Token Service', () => { .withArgs(tokenId, LOCK_UNLOCK, params); const tokenManager = new Contract(tokenManagerAddress, TokenManager.abi, wallet); expect(await tokenManager.tokenAddress()).to.equal(token.address); - expect(await tokenManager.operator()).to.equal(wallet.address); + expect(await tokenManager.hasRole(wallet.address, OPERATOR_ROLE)).to.be.true; }); it('Should be able to receive a remote mint/burn token manager depoloyment', async () => { @@ -1199,7 +1203,7 @@ describe('Interchain Token Service', () => { .withArgs(tokenId, MINT_BURN, params); const tokenManager = new Contract(tokenManagerAddress, TokenManager.abi, wallet); expect(await tokenManager.tokenAddress()).to.equal(token.address); - expect(await tokenManager.operator()).to.equal(wallet.address); + expect(await tokenManager.hasRole(wallet.address, OPERATOR_ROLE)).to.be.true; }); }); @@ -2028,7 +2032,7 @@ describe('Interchain Token Service', () => { tokenIds.push(tokenId); tokenManagers.push(tokenManager); - await tokenManager.transferOperatorship(service.address).then((tx) => tx.wait()); + await tokenManager.addFlowLimiter(service.address).then((tx) => tx.wait()); } const flowLimits = new Array(tokenManagers.length).fill(flowLimit); @@ -2036,7 +2040,7 @@ describe('Interchain Token Service', () => { await expectRevert( (gasOptions) => service.connect(otherWallet).setFlowLimits(tokenIds, flowLimits, gasOptions), service, - 'NotOperator', + 'MissingRole', ); await expect(service.setFlowLimits(tokenIds, flowLimits)) @@ -2052,4 +2056,66 @@ describe('Interchain Token Service', () => { await expectRevert((gasOptions) => service.setFlowLimits(tokenIds, flowLimits, gasOptions), service, 'LengthMismatch'); }); }); + + describe('Flow Limiters', () => { + let tokenManager; + const sendAmount = 1234; + const flowLimit = (sendAmount * 3) / 2; + const mintAmount = flowLimit * 3; + + beforeEach(async () => { + [, tokenManager] = await deployFunctions.mintBurn(`Test Token Lock Unlock`, 'TT', 12, mintAmount); + }); + + it('Should have only the owner be a flow limiter', async () => { + expect(await tokenManager.hasRole(wallet.address, FLOW_LIMITER_ROLE)).to.equal(true); + expect(await tokenManager.hasRole(otherWallet.address, FLOW_LIMITER_ROLE)).to.equal(false); + }); + + it('Should be able to add a flow limiter', async () => { + await expect(tokenManager.addFlowLimiter(otherWallet.address)) + .to.emit(tokenManager, 'RolesAdded') + .withArgs(otherWallet.address, 1 << FLOW_LIMITER_ROLE); + + expect(await tokenManager.hasRole(wallet.address, FLOW_LIMITER_ROLE)).to.equal(true); + expect(await tokenManager.hasRole(otherWallet.address, FLOW_LIMITER_ROLE)).to.equal(true); + }); + + it('Should be able to remove a flow limiter', async () => { + await expect(tokenManager.removeFlowLimiter(wallet.address)) + .to.emit(tokenManager, 'RolesRemoved') + .withArgs(wallet.address, 1 << FLOW_LIMITER_ROLE); + + expect(await tokenManager.hasRole(wallet.address, FLOW_LIMITER_ROLE)).to.equal(false); + expect(await tokenManager.hasRole(otherWallet.address, FLOW_LIMITER_ROLE)).to.equal(false); + }); + + it('Should revert if trying to add a flow limiter as not the operator', async () => { + await expectRevert( + (gasOptions) => tokenManager.connect(otherWallet).addFlowLimiter(otherWallet.address, gasOptions), + tokenManager, + 'MissingRole', + ); + }); + + it('Should revert if trying to add a flow limiter as not the operator', async () => { + await expectRevert( + (gasOptions) => tokenManager.connect(otherWallet).removeFlowLimiter(wallet.address, gasOptions), + tokenManager, + 'MissingRole', + ); + }); + + it('Should revert if trying to add an existing flow limiter', async () => { + await expectRevert((gasOptions) => tokenManager.addFlowLimiter(wallet.address, gasOptions), tokenManager, 'AlreadyFlowLimiter'); + }); + + it('Should revert if trying to add a flow limiter as not the operator', async () => { + await expectRevert( + (gasOptions) => tokenManager.removeFlowLimiter(otherWallet.address, gasOptions), + tokenManager, + 'NotFlowLimiter', + ); + }); + }); }); diff --git a/test/TokenServiceFullFlow.js b/test/TokenServiceFullFlow.js index 05028287..a02bc4ce 100644 --- a/test/TokenServiceFullFlow.js +++ b/test/TokenServiceFullFlow.js @@ -8,7 +8,7 @@ const { AddressZero } = ethers.constants; const { defaultAbiCoder, keccak256 } = ethers.utils; const { Contract, Wallet } = ethers; -const IStandardizedToken = require('../artifacts/contracts/interfaces/IStandardizedToken.sol/IStandardizedToken.json'); +const StandardizedToken = require('../artifacts/contracts/token-implementations/StandardizedToken.sol/StandardizedToken.json'); const ITokenManager = require('../artifacts/contracts/interfaces/ITokenManager.sol/ITokenManager.json'); const ITokenManagerMintBurn = require('../artifacts/contracts/interfaces/ITokenManagerMintBurn.sol/ITokenManagerMintBurn.json'); @@ -19,11 +19,10 @@ const SELECTOR_SEND_TOKEN = 1; const SELECTOR_DEPLOY_TOKEN_MANAGER = 3; const SELECTOR_DEPLOY_AND_REGISTER_STANDARDIZED_TOKEN = 4; +const DISTRIBUTOR_ROLE = 0; + const MINT_BURN = 0; -// const MINT_BURN_FROM = 1; const LOCK_UNLOCK = 2; -// const LOCK_UNLOCK_FEE_ON_TRANSFER = 3; -// const LIQUIDITY_POOL = 4; describe('Interchain Token Service Full Flow', () => { let wallet; @@ -122,10 +121,14 @@ describe('Interchain Token Service Full Flow', () => { await expect(token.mint(newAddress, amount)).to.emit(token, 'Transfer').withArgs(AddressZero, newAddress, amount); await expect(token.burn(newAddress, amount)).to.emit(token, 'Transfer').withArgs(newAddress, AddressZero, amount); - await expect(token.transferDistributorship(newAddress)).to.emit(token, 'DistributorshipTransferred').withArgs(newAddress); + await expect(token.transferDistributorship(newAddress)) + .to.emit(token, 'RolesRemoved') + .withArgs(wallet.address, 1 << DISTRIBUTOR_ROLE) + .to.emit(token, 'RolesAdded') + .withArgs(newAddress, 1 << DISTRIBUTOR_ROLE); - await expectRevert((gasOptions) => token.mint(newAddress, amount, gasOptions), token, 'NotDistributor'); - await expectRevert((gasOptions) => token.burn(newAddress, amount, gasOptions), token, 'NotDistributor'); + await expectRevert((gasOptions) => token.mint(newAddress, amount, gasOptions), token, 'MissingRole'); + await expectRevert((gasOptions) => token.burn(newAddress, amount, gasOptions), token, 'MissingRole'); }); }); @@ -139,7 +142,7 @@ describe('Interchain Token Service Full Flow', () => { before(async () => { tokenId = await service.getCustomTokenId(wallet.address, salt); const tokenAddress = await service.getStandardizedTokenAddress(tokenId); - token = new Contract(tokenAddress, IStandardizedToken.abi, wallet); + token = new Contract(tokenAddress, StandardizedToken.abi, wallet); const tokenManagerAddress = await service.getTokenManagerAddress(tokenId); tokenManager = new Contract(tokenManagerAddress, ITokenManager.abi, wallet); }); @@ -230,10 +233,14 @@ describe('Interchain Token Service Full Flow', () => { await expect(token.mint(newAddress, amount)).to.emit(token, 'Transfer').withArgs(AddressZero, newAddress, amount); await expect(token.burn(newAddress, amount)).to.emit(token, 'Transfer').withArgs(newAddress, AddressZero, amount); - await expect(token.transferDistributorship(newAddress)).to.emit(token, 'DistributorshipTransferred').withArgs(newAddress); + await expect(token.transferDistributorship(newAddress)) + .to.emit(token, 'RolesRemoved') + .withArgs(wallet.address, 1 << DISTRIBUTOR_ROLE) + .to.emit(token, 'RolesAdded') + .withArgs(newAddress, 1 << DISTRIBUTOR_ROLE); - await expectRevert((gasOptions) => token.mint(newAddress, amount, gasOptions), token, 'NotDistributor'); - await expectRevert((gasOptions) => token.burn(newAddress, amount, gasOptions), token, 'NotDistributor'); + await expectRevert((gasOptions) => token.mint(newAddress, amount, gasOptions), token, 'MissingRole'); + await expectRevert((gasOptions) => token.burn(newAddress, amount, gasOptions), token, 'MissingRole'); }); }); @@ -305,11 +312,13 @@ describe('Interchain Token Service Full Flow', () => { await expect(token.burn(newAddress, amount)).to.emit(token, 'Transfer').withArgs(newAddress, AddressZero, amount); await expect(token.transferDistributorship(tokenManager.address)) - .to.emit(token, 'DistributorshipTransferred') - .withArgs(tokenManager.address); + .to.emit(token, 'RolesRemoved') + .withArgs(wallet.address, 1 << DISTRIBUTOR_ROLE) + .to.emit(token, 'RolesAdded') + .withArgs(tokenManager.address, 1 << DISTRIBUTOR_ROLE); - await expectRevert((gasOptions) => token.mint(newAddress, amount, gasOptions), token, 'NotDistributor'); - await expectRevert((gasOptions) => token.burn(newAddress, amount, gasOptions), token, 'NotDistributor'); + await expectRevert((gasOptions) => token.mint(newAddress, amount, gasOptions), token, 'MissingRole'); + await expectRevert((gasOptions) => token.burn(newAddress, amount, gasOptions), token, 'MissingRole'); }); // In order to be able to receive tokens the distributorship should be changed on other chains as well. diff --git a/test/UtilsTest.js b/test/UtilsTest.js index 528d7936..ac366763 100644 --- a/test/UtilsTest.js +++ b/test/UtilsTest.js @@ -25,8 +25,11 @@ before(async () => { describe('Operatable', () => { let test; + let operatorRole; + before(async () => { test = await deployContract(ownerWallet, 'OperatorableTest', [ownerWallet.address]); + operatorRole = await test.getOperatorRole(); }); it('Should calculate hardcoded constants correctly', async () => { @@ -40,40 +43,57 @@ describe('Operatable', () => { }); it('Should not be able to run the onlyOperatorable function as not the operator', async () => { - await expectRevert((gasOptions) => test.connect(otherWallet).testOperatorable(gasOptions), test, 'NotOperator'); + await expectRevert((gasOptions) => test.connect(otherWallet).testOperatorable(gasOptions), test, 'MissingRole'); }); it('Should be able to change the operator only as the operator', async () => { - expect(await test.operator()).to.equal(ownerWallet.address); + expect(await test.hasRole(ownerWallet.address, operatorRole)).to.be.true; - await expect(test.transferOperatorship(otherWallet.address)).to.emit(test, 'OperatorshipTransferred').withArgs(otherWallet.address); + await expect(test.transferOperatorship(otherWallet.address)) + .to.emit(test, 'RolesRemoved') + .withArgs(ownerWallet.address, 1 << operatorRole) + .to.emit(test, 'RolesAdded') + .withArgs(otherWallet.address, 1 << operatorRole); - expect(await test.operator()).to.equal(otherWallet.address); + expect(await test.hasRole(otherWallet.address, operatorRole)).to.be.true; - await expectRevert((gasOptions) => test.transferOperatorship(otherWallet.address, gasOptions), test, 'NotOperator'); + await expectRevert((gasOptions) => test.transferOperatorship(otherWallet.address, gasOptions), test, 'MissingRole'); }); it('Should be able to propose operator only as the operator', async () => { - expect(await test.operator()).to.equal(otherWallet.address); + expect(await test.hasRole(otherWallet.address, operatorRole)).to.be.true; + + await expectRevert((gasOptions) => test.proposeOperatorship(ownerWallet.address, gasOptions), test, 'MissingRole'); - await expectRevert((gasOptions) => test.proposeOperatorship(ownerWallet.address, gasOptions), test, 'NotOperator'); await expect(test.connect(otherWallet).proposeOperatorship(ownerWallet.address)) - .to.emit(test, 'OperatorChangeProposed') - .withArgs(ownerWallet.address); + .to.emit(test, 'RolesProposed') + .withArgs(otherWallet.address, ownerWallet.address, 1 << operatorRole); }); it('Should be able to accept operatorship only as proposed operator', async () => { - expect(await test.operator()).to.equal(otherWallet.address); + expect(await test.hasRole(otherWallet.address, operatorRole)).to.be.true; + + await expectRevert( + (gasOptions) => test.connect(otherWallet).acceptOperatorship(otherWallet.address, gasOptions), + test, + 'InvalidProposedRoles', + ); - await expectRevert((gasOptions) => test.connect(otherWallet).acceptOperatorship(gasOptions), test, 'NotProposedOperator'); - await expect(test.acceptOperatorship()).to.emit(test, 'OperatorshipTransferred').withArgs(ownerWallet.address); + await expect(test.connect(ownerWallet).acceptOperatorship(otherWallet.address)) + .to.emit(test, 'RolesRemoved') + .withArgs(otherWallet.address, 1 << operatorRole) + .to.emit(test, 'RolesAdded') + .withArgs(ownerWallet.address, 1 << operatorRole); }); }); describe('Distributable', () => { let test; + let distributorRole; + before(async () => { test = await deployContract(ownerWallet, 'DistributableTest', [ownerWallet.address]); + distributorRole = await test.getDistributorRole(); }); it('Should calculate hardcoded constants correctly', async () => { @@ -87,41 +107,51 @@ describe('Distributable', () => { }); it('Should not be able to run the onlyDistributor function as not the distributor', async () => { - await expectRevert((gasOptions) => test.connect(otherWallet).testDistributable(gasOptions), test, 'NotDistributor'); + await expectRevert((gasOptions) => test.connect(otherWallet).testDistributable(gasOptions), test, 'MissingRole'); }); it('Should be able to change the distributor only as the distributor', async () => { - expect(await test.distributor()).to.equal(ownerWallet.address); + expect(await test.hasRole(ownerWallet.address, distributorRole)).to.be.true; await expect(test.transferDistributorship(otherWallet.address)) - .to.emit(test, 'DistributorshipTransferred') - .withArgs(otherWallet.address); + .to.emit(test, 'RolesRemoved') + .withArgs(ownerWallet.address, 1 << distributorRole) + .to.emit(test, 'RolesAdded') + .withArgs(otherWallet.address, 1 << distributorRole); - expect(await test.distributor()).to.equal(otherWallet.address); + expect(await test.hasRole(otherWallet.address, distributorRole)).to.be.true; - await expectRevert((gasOptions) => test.transferDistributorship(otherWallet.address, gasOptions), test, 'NotDistributor'); + await expectRevert((gasOptions) => test.transferDistributorship(otherWallet.address, gasOptions), test, 'MissingRole'); }); it('Should be able to propose a new distributor only as distributor', async () => { - expect(await test.distributor()).to.equal(otherWallet.address); + expect(await test.hasRole(otherWallet.address, distributorRole)).to.be.true; await expectRevert( (gasOptions) => test.connect(ownerWallet).proposeDistributorship(ownerWallet.address, gasOptions), test, - 'NotDistributor', + 'MissingRole', ); + await expect(test.connect(otherWallet).proposeDistributorship(ownerWallet.address)) - .to.emit(test, 'DistributorshipTransferStarted') - .withArgs(ownerWallet.address); + .to.emit(test, 'RolesProposed') + .withArgs(otherWallet.address, ownerWallet.address, 1 << distributorRole); }); it('Should be able to accept distributorship only as the proposed distributor', async () => { - expect(await test.distributor()).to.equal(otherWallet.address); + expect(await test.hasRole(otherWallet.address, distributorRole)).to.be.true; + + await expectRevert( + (gasOptions) => test.connect(otherWallet).acceptDistributorship(otherWallet.address, gasOptions), + test, + 'InvalidProposedRoles', + ); - await expectRevert((gasOptions) => test.connect(otherWallet).acceptDistributorship(gasOptions), test, 'NotProposedDistributor'); - await expect(test.connect(ownerWallet).acceptDistributorship()) - .to.emit(test, 'DistributorshipTransferred') - .withArgs(ownerWallet.address); + await expect(test.connect(ownerWallet).acceptDistributorship(otherWallet.address)) + .to.emit(test, 'RolesRemoved') + .withArgs(otherWallet.address, 1 << distributorRole) + .to.emit(test, 'RolesAdded') + .withArgs(ownerWallet.address, 1 << distributorRole); }); }); @@ -360,6 +390,7 @@ describe('StandardizedTokenDeployer', () => { const symbol = 'tokenSymbol'; const decimals = 18; const mintAmount = 123; + const DISTRIBUTOR_ROLE = 0; before(async () => { standardizedToken = await deployContract(ownerWallet, 'StandardizedToken'); @@ -387,15 +418,17 @@ describe('StandardizedTokenDeployer', () => { ) .to.emit(token, 'Transfer') .withArgs(AddressZero, mintTo, mintAmount) - .and.to.emit(token, 'DistributorshipTransferred') - .withArgs(tokenManager); + .and.to.emit(token, 'RolesAdded') + .withArgs(tokenManager, 1 << DISTRIBUTOR_ROLE) + .to.emit(token, 'RolesAdded') + .withArgs(tokenManager, 1 << DISTRIBUTOR_ROLE); expect(await tokenProxy.implementation()).to.equal(standardizedToken.address); expect(await token.name()).to.equal(name); expect(await token.symbol()).to.equal(symbol); expect(await token.decimals()).to.equal(decimals); expect(await token.balanceOf(mintTo)).to.equal(mintAmount); - expect(await token.distributor()).to.equal(tokenManager); + expect(await token.hasRole(tokenManager, DISTRIBUTOR_ROLE)).to.be.true; expect(await token.tokenManager()).to.equal(tokenManager); await expectRevert(