Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FORTA-798: Chain Settings contract #213

Draft
wants to merge 6 commits into
base: release/1.2.4
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions contracts/components/Roles.sol
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ bytes32 constant SCANNER_POOL_ADMIN_ROLE = keccak256("SCANNER_POOL_ADMIN_ROLE");
bytes32 constant SCANNER_2_SCANNER_POOL_MIGRATOR_ROLE = keccak256("SCANNER_2_SCANNER_POOL_MIGRATOR_ROLE");
bytes32 constant DISPATCHER_ROLE = keccak256("DISPATCHER_ROLE");
bytes32 constant MIGRATION_EXECUTOR_ROLE = keccak256("MIGRATION_EXECUTOR_ROLE");
bytes32 constant CHAIN_SETTINGS_ROLE = keccak256("CHAIN_SETTINGS_ROLE");

// Staking
bytes32 constant SLASHER_ROLE = keccak256("SLASHER_ROLE");
Expand Down
135 changes: 135 additions & 0 deletions contracts/components/chain_settings/ChainSettingsRegistry.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
// SPDX-License-Identifier: UNLICENSED
// See Forta Network License: https://github.com/forta-network/forta-contracts/blob/master/LICENSE.md

pragma solidity ^0.8.9;

import "../BaseComponentUpgradeable.sol";
import "../../errors/GeneralErrors.sol";

contract ChainSettingsRegistry is BaseComponentUpgradeable {
string public constant version = "0.1.0";
uint8 constant MAX_CHAIN_IDS_PER_UPDATE = 5;

uint256 private _supportedChainIdsAmount;
mapping(uint256 => bool) private _chainIdSupported;
mapping(uint256 => string) private _chainIdMetadata;
// chainId => metadata => uniqueness
mapping(uint256 => mapping(bytes32 => bool)) private _chainIdMetadataUniqueness;

error ChainIdsAmountExceeded(uint256 exceedingAmount);
error ChainIdAlreadySupported(uint256 chainId);
error ChainIdUnsupported(uint256 chainId);
error MetadataNotUnique(bytes32 hash);

event ChainSettingsUpdated(uint256 indexed chainId, string metadata);
event ChainIdSupported(uint256 indexed chainId);

/// @custom:oz-upgrades-unsafe-allow constructor
constructor(address forwarder) initializer ForwardedContext(forwarder) {}

/**
* @notice Initializer method, access point to initialize inheritance tree.
* @param __manager address of AccessManager.
*/
function initialize(
address __manager
) public initializer {
__BaseComponentUpgradeable_init(__manager);
}

/**
* @notice Method to update which chains are supported by the network.
* @dev Method implements a cap to how many chain ids can be updated
* at once, to prevent looping through too many chain ids.
* The cap is also a lower number, since it is expected that the
* supported chains will not change often.
* @param chainIds Array of chain ids that are to be supported.
* @param metadata IPFS pointer to chain id's metadata JSON.
*/
function updateSupportedChains(uint256[] calldata chainIds, string calldata metadata) external onlyRole(CHAIN_SETTINGS_ROLE) {
if (chainIds.length == 0) revert EmptyArray("chainIds");
if (chainIds.length > MAX_CHAIN_IDS_PER_UPDATE) revert ChainIdsAmountExceeded(chainIds.length - MAX_CHAIN_IDS_PER_UPDATE);

for(uint256 i = 0; i < chainIds.length; i++) {
if (_chainIdSupported[chainIds[i]]) revert ChainIdAlreadySupported(chainIds[i]);
_updateSupportedChainIds(chainIds[i]);
_chainSettingsUpdate(chainIds[i], metadata);
}

_supportedChainIdsAmount += chainIds.length;
}

/**
* @notice Method to update a chain's settings/metadata.
* @dev Checks to confirm there aren't more chains attempting to be updated
* than there are supported chains. Also checks to confirm that the chains
* attempting to be updated are supported.
* @param chainIds Array of chain ids that are to have their settings updated.
* @param metadata IPFS pointer to chain id's metadata JSON.
*/
function updateChainSettings(uint256[] calldata chainIds, string calldata metadata) external onlyRole(CHAIN_SETTINGS_ROLE) {
if (chainIds.length == 0) revert EmptyArray("chainIds");
if (chainIds.length > _supportedChainIdsAmount) revert ChainIdsAmountExceeded(chainIds.length - _supportedChainIdsAmount);

for(uint256 i = 0; i < chainIds.length; i++) {
if (!_chainIdSupported[chainIds[i]]) revert ChainIdUnsupported(chainIds[i]);
_chainSettingsUpdate(chainIds[i], metadata);
}
}

/**
* @notice Logic for chain metadata update.
* @dev Checks chain id's metadata uniqueness and updates chain's metadata.
* @param chainId Chain id that is to have its settings updated.
* @param metadata IPFS pointer to chain id's metadata JSON.
*/
function _chainSettingsUpdate(uint256 chainId, string calldata metadata) private {
bytes32 newHash = keccak256(bytes(metadata));
if (_chainIdMetadataUniqueness[chainId][newHash]) revert MetadataNotUnique(newHash);
bytes32 oldHash = keccak256(bytes(_chainIdMetadata[chainId]));
_chainIdMetadataUniqueness[chainId][newHash] = true;
_chainIdMetadataUniqueness[chainId][oldHash] = false;

_chainIdMetadata[chainId] = metadata;
emit ChainSettingsUpdated(chainId, metadata);
}

function _updateSupportedChainIds(uint256 chainId) private {
_chainIdSupported[chainId] = true;
emit ChainIdSupported(chainId);
}

/**
* @notice Getter for metadata for the `chainId`.
*/
function getChainIdSettings(uint256 chainId) public view returns (string memory) {
return _chainIdMetadata[chainId];
}

/**
* @notice Getter for the current amount of supported chains.
*/
function getSupportedChainIdsAmount() public view returns (uint256) {
return _supportedChainIdsAmount;
}

/**
* @notice Checks if chainId is currently supported.
* @param chainId Chain id of the specific chain.
* @return true if chain is supported, false otherwise.
*/
function isChainIdSupported(uint256 chainId) public view returns (bool) {
return _chainIdSupported[chainId];
}

/**
* 50
* - 1 _supportedChainIdsAmount;
* - 1 _chainIdSupported;
* - 1 _chainIdMetadata;
* - 1 _chainIdMetadataUniqueness;
* --------------------------
* 46 __gap
*/
uint256[46] private __gap;
}
14 changes: 14 additions & 0 deletions scripts/deployments/platform.js
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,20 @@ async function migrate(config = {}) {
CACHE
);
DEBUG(`[${Object.keys(contracts).length}] dispatch: ${contracts.dispatch.address}`);

DEBUG(`Deploying Chain Setting Registry...`);
contracts.chainSettings = await contractHelpers.tryFetchProxy(
hre,
'ChainSettingsRegistry',
'uups',
[contracts.access.address],
{
constructorArgs: [contracts.forwarder.address],
unsafeAllow: 'delegatecall',
},
CACHE
);
DEBUG(`[${Object.keys(contracts).length}] chainSettings: ${contracts.dispatch.address}`);
}

// Roles dictionary
Expand Down
120 changes: 120 additions & 0 deletions test/components/chainsettings.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
const { ethers } = require('hardhat');
const { expect } = require('chai');
const { prepare } = require('../fixture');

describe('Chain Settings Registry', function () {
prepare();

const MAX_CHAIN_IDS_PER_UPDATE = 5;
const supportedChainIds = [1, 29, 387, 4654, 53219];
beforeEach(async function () {
await this.chainSettings.connect(this.accounts.manager).updateSupportedChains(supportedChainIds, 'Metadata1');
});
const unsupportedChainIds = [8, 23, 3500, 90059];

describe('Adding supported chains', function () {
it('should allow the amount of supported chains to be updated', async function () {
await this.chainSettings.connect(this.accounts.manager).updateSupportedChains([...unsupportedChainIds], 'Metadata1');

unsupportedChainIds.forEach(async (chainId) => {
expect(await this.chainSettings.connect(this.accounts.manager).isChainIdSupported(chainId)).to.be.equal(true);
});

expect(await this.chainSettings.connect(this.accounts.manager).getSupportedChainIdsAmount()).to.be.equal(supportedChainIds.length + unsupportedChainIds.length);
});

it('should not allow account that was not granted access to update supported chains', async function () {
await expect(this.chainSettings.connect(this.accounts.user3).updateSupportedChains([...unsupportedChainIds], 'Metadata1')).to.be.revertedWith(
`MissingRole("${this.roles.CHAIN_SETTINGS}", "${this.accounts.user3.address}")`
);
});

it('should not allow to update supported chains when attempting to add too many chains', async function () {
const additionalUnsupportedChainIds = [37, 98, 444];
await expect(this.chainSettings.connect(this.accounts.manager).updateSupportedChains(
[...unsupportedChainIds, ...additionalUnsupportedChainIds],
'Metadata1'
)).to.be.revertedWith(
`ChainIdsAmountExceeded(${(unsupportedChainIds.length + additionalUnsupportedChainIds.length) - MAX_CHAIN_IDS_PER_UPDATE})`
);
});

it('should not allow to add a chain to be supported that is already supported', async function () {
await expect(this.chainSettings.connect(this.accounts.manager).updateSupportedChains([supportedChainIds[1]], 'Metadata2')).to.be.revertedWith(
`ChainIdAlreadySupported(${supportedChainIds[1]})`
);
});

it('should not add support for chain ids if passed chain ids contain a chain that is already supported', async function () {
await expect(this.chainSettings.connect(this.accounts.manager).updateSupportedChains(
[unsupportedChainIds[0], unsupportedChainIds[1], supportedChainIds[1], unsupportedChainIds[2]],
'Metadata2'
)).to.be.revertedWith(
`ChainIdAlreadySupported(${supportedChainIds[1]})`
);

unsupportedChainIds.forEach(async (chainId) => {
expect(await this.chainSettings.connect(this.accounts.manager).isChainIdSupported(chainId)).to.be.equal(false);
});

expect(await this.chainSettings.connect(this.accounts.manager).getSupportedChainIdsAmount()).to.be.equal(supportedChainIds.length);
});

it('should not allow to pass an empty array of chains ids to be supported', async function () {
await expect(this.chainSettings.connect(this.accounts.manager).updateSupportedChains([], 'Metadata1')).to.be.revertedWith(
`EmptyArray("chainIds")`
);
});
});

describe('Updating chain settings', function () {
it('Updates the chain settings', async function () {
await this.chainSettings.connect(this.accounts.manager).updateChainSettings(supportedChainIds, 'Metadata2');
supportedChainIds.forEach(async (chainId) => {
expect(await this.chainSettings.connect(this.accounts.manager).getChainIdSettings(chainId)).to.be.equal('Metadata2');
});

await this.chainSettings.connect(this.accounts.manager).updateChainSettings(supportedChainIds, 'Metadata3');
supportedChainIds.forEach(async (chainId) => {
expect(await this.chainSettings.connect(this.accounts.manager).getChainIdSettings(chainId)).to.be.equal('Metadata3');
});
});

it('should not allow accounts that were not granted access to update chain settings', async function () {
await expect(this.chainSettings.connect(this.accounts.user3).updateChainSettings(supportedChainIds, 'Metadata1')).to.be.revertedWith(
`MissingRole("${this.roles.CHAIN_SETTINGS}", "${this.accounts.user3.address}")`
);
});

it('should not allow settings to be updated when it is the same as current settings', async function () {
supportedChainIds.forEach(async (chainId) => {
expect(await this.chainSettings.connect(this.accounts.manager).updateChainSettings([chainId], 'Metadata1')).to.be.revertedWith(
`MetadataNotUnique("${ethers.utils.id('Metadata1')}")`
);
});
});

it('should not allow to update more chains than are supported', async function () {
const additionalChainIds = [23, 37];
// Including the supported chains, but should fail because passing too many chain ids
await expect(this.chainSettings.connect(this.accounts.manager).updateChainSettings(
[...supportedChainIds, ...additionalChainIds],
'Metadata1'
)).to.be.revertedWith(
`ChainIdsAmountExceeded(${additionalChainIds.length})`
);
});

it('should not allow to update chain settings for an unsupported chain', async function () {
await expect(this.chainSettings.connect(this.accounts.manager).updateChainSettings([unsupportedChainIds[0]], 'Metadata1')).to.be.revertedWith(
`ChainIdUnsupported(${unsupportedChainIds[0]})`
);
});

it('should not allow to pass an empty array of chains ids to update chain settings', async function () {
await expect(this.chainSettings.connect(this.accounts.manager).updateChainSettings([], 'Metadata2')).to.be.revertedWith(
`EmptyArray("chainIds")`
);
});
});
});
1 change: 1 addition & 0 deletions test/fixture.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ function prepare(config = {}) {
this.access.connect(this.accounts.admin).grantRole(this.roles.STAKING_CONTRACT, this.contracts.staking.address),
this.access.connect(this.accounts.admin).grantRole(this.roles.ALLOCATOR_CONTRACT, this.contracts.stakeAllocator.address),
this.access.connect(this.accounts.admin).grantRole(this.roles.MIGRATION_EXECUTOR, this.accounts.manager.address),
this.access.connect(this.accounts.admin).grantRole(this.roles.CHAIN_SETTINGS, this.accounts.manager.address),
this.token.connect(this.accounts.admin).grantRole(this.roles.MINTER, this.accounts.minter.address),
this.otherToken.connect(this.accounts.admin).grantRole(this.roles.MINTER, this.accounts.minter.address),
].map((txPromise) => txPromise.then((tx) => tx.wait()).catch(() => {}))
Expand Down