From b64ba1c38cea3add0d3e2e9b2d539d3f88da6cbc Mon Sep 17 00:00:00 2001 From: Foivos Date: Tue, 31 Oct 2023 14:44:38 +0200 Subject: [PATCH] feat: added Interchain Router (#105) * Added the eternal storage here as well and using it for the InterchainRouter. * Removed Eternal Storage * Added a few tests * Simplified storage and added more tests * made lint happy. * Fixed a warnong * addressed some comments * made slither happy * Added string storage library and a test file to make slither happy. * fix testInterchainRouterProxy * Added delete in string storage, using it in the interchain router and added tests * Rename interchain router to interchain address tracker * changed names of StringStorage functions * refactor names * address comments * bump npm version * addressed comments --------- Co-authored-by: Milap Sheth --- .../interfaces/IInterchainAddressTracker.sol | 59 ++++++ contracts/libs/StringStorage.sol | 27 +++ contracts/test/libs/TestStringStorage.sol | 19 ++ .../utils/TestInterchainAddressTracker.sol | 16 ++ .../TestInterchainAddressTrackerProxy.sol | 15 ++ contracts/utils/InterchainAddressTracker.sol | 156 +++++++++++++++ .../utils/InterchainAddressTrackerProxy.sol | 33 ++++ package-lock.json | 4 +- package.json | 2 +- test/utils/InterchainAddressTracker.js | 182 ++++++++++++++++++ test/utils/StringStorage.js | 45 +++++ 11 files changed, 555 insertions(+), 3 deletions(-) create mode 100644 contracts/interfaces/IInterchainAddressTracker.sol create mode 100644 contracts/libs/StringStorage.sol create mode 100644 contracts/test/libs/TestStringStorage.sol create mode 100644 contracts/test/utils/TestInterchainAddressTracker.sol create mode 100644 contracts/test/utils/TestInterchainAddressTrackerProxy.sol create mode 100644 contracts/utils/InterchainAddressTracker.sol create mode 100644 contracts/utils/InterchainAddressTrackerProxy.sol create mode 100644 test/utils/InterchainAddressTracker.js create mode 100644 test/utils/StringStorage.js diff --git a/contracts/interfaces/IInterchainAddressTracker.sol b/contracts/interfaces/IInterchainAddressTracker.sol new file mode 100644 index 00000000..fa88007b --- /dev/null +++ b/contracts/interfaces/IInterchainAddressTracker.sol @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import { IContractIdentifier } from './IContractIdentifier.sol'; + +/** + * @title IInterchainAddressTracker + * @dev Manages trusted addresses by chain, keeps track of addresses supported by the Axelar gateway contract + */ +interface IInterchainAddressTracker is IContractIdentifier { + error ZeroAddress(); + error LengthMismatch(); + error ZeroStringLength(); + error UntrustedChain(); + + event TrustedAddressSet(string chain, string address_); + event TrustedAddressRemoved(string chain); + + /** + * @dev Gets the name of the chain this is deployed at + */ + function chainName() external view returns (string memory); + + /** + * @dev Gets the trusted address at a remote chain + * @param chain Chain name of the remote chain + * @return trustedAddress_ The trusted address for the chain. Returns '' if the chain is untrusted + */ + function trustedAddress(string memory chain) external view returns (string memory trustedAddress_); + + /** + * @dev Gets the trusted address hash for a chain + * @param chain Chain name + * @return trustedAddressHash_ the hash of the trusted address for that chain + */ + function trustedAddressHash(string memory chain) external view returns (bytes32 trustedAddressHash_); + + /** + * @dev Checks whether the interchain sender is a trusted address + * @param chain Chain name of the sender + * @param address_ Address of the sender + * @return bool true if the sender chain/address are trusted, false otherwise + */ + function isTrustedAddress(string calldata chain, string calldata address_) external view returns (bool); + + /** + * @dev Sets the trusted address for the specified chain + * @param chain Chain name to be trusted + * @param address_ Trusted address to be added for the chain + */ + function setTrustedAddress(string memory chain, string memory address_) external; + + /** + * @dev Remove the trusted address of the chain. + * @param chain Chain name that should be made untrusted + */ + function removeTrustedAddress(string calldata chain) external; +} diff --git a/contracts/libs/StringStorage.sol b/contracts/libs/StringStorage.sol new file mode 100644 index 00000000..1375dad7 --- /dev/null +++ b/contracts/libs/StringStorage.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +library StringStorage { + struct Wrapper { + string value; + } + + function set(bytes32 slot, string memory value) internal { + _getStorageStruct(slot).value = value; + } + + function get(bytes32 slot) internal view returns (string memory value) { + value = _getStorageStruct(slot).value; + } + + function clear(bytes32 slot) internal { + delete _getStorageStruct(slot).value; + } + + function _getStorageStruct(bytes32 slot) internal pure returns (Wrapper storage wrapper) { + assembly { + wrapper.slot := slot + } + } +} diff --git a/contracts/test/libs/TestStringStorage.sol b/contracts/test/libs/TestStringStorage.sol new file mode 100644 index 00000000..11667750 --- /dev/null +++ b/contracts/test/libs/TestStringStorage.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import { StringStorage } from '../../libs/StringStorage.sol'; + +contract TestStringStorage { + function set(bytes32 slot, string calldata value) external { + StringStorage.set(slot, value); + } + + function get(bytes32 slot) external view returns (string memory value) { + value = StringStorage.get(slot); + } + + function clear(bytes32 slot) external { + StringStorage.clear(slot); + } +} diff --git a/contracts/test/utils/TestInterchainAddressTracker.sol b/contracts/test/utils/TestInterchainAddressTracker.sol new file mode 100644 index 00000000..3052c7a3 --- /dev/null +++ b/contracts/test/utils/TestInterchainAddressTracker.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import { InterchainAddressTracker } from '../../utils/InterchainAddressTracker.sol'; + +contract TestInterchainAddressTracker is InterchainAddressTracker { + string public name = 'Test'; // Dummy var for a different bytecode + + error Invalid(); + + constructor(string memory chainName_) InterchainAddressTracker(chainName_) { + if (_CHAIN_NAME_SLOT != bytes32(uint256(keccak256('interchain-address-tracker-chain-name')) - 1)) + revert Invalid(); + } +} diff --git a/contracts/test/utils/TestInterchainAddressTrackerProxy.sol b/contracts/test/utils/TestInterchainAddressTrackerProxy.sol new file mode 100644 index 00000000..2cac7f6f --- /dev/null +++ b/contracts/test/utils/TestInterchainAddressTrackerProxy.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import { InterchainAddressTrackerProxy } from '../../utils/InterchainAddressTrackerProxy.sol'; + +contract TestInterchainAddressTrackerProxy is InterchainAddressTrackerProxy { + constructor( + address implementationAddress, + address owner, + bytes memory params + ) InterchainAddressTrackerProxy(implementationAddress, owner, params) { + contractId(); + } +} diff --git a/contracts/utils/InterchainAddressTracker.sol b/contracts/utils/InterchainAddressTracker.sol new file mode 100644 index 00000000..47a0a9f9 --- /dev/null +++ b/contracts/utils/InterchainAddressTracker.sol @@ -0,0 +1,156 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import { IInterchainAddressTracker } from '../interfaces/IInterchainAddressTracker.sol'; +import { StringStorage } from '../libs/StringStorage.sol'; +import { Upgradable } from '../upgradable/Upgradable.sol'; + +/** + * @title InterchainAddressTracker + * @dev Manages and validates trusted interchain addresses of an application. + */ +contract InterchainAddressTracker is IInterchainAddressTracker, Upgradable { + bytes32 internal constant PREFIX_ADDRESS_MAPPING = keccak256('interchain-address-tracker-address-mapping'); + bytes32 internal constant PREFIX_ADDRESS_HASH_MAPPING = + keccak256('interchain-address-tracker-address-hash-mapping'); + // bytes32(uint256(keccak256('interchain-address-tracker-chain-name')) - 1) + bytes32 internal constant _CHAIN_NAME_SLOT = 0x0e2c162a1f4b5cff9fdbd6b34678a9bcb9898a0b9fbca695b112d61688d8b2ac; + + bytes32 private constant CONTRACT_ID = keccak256('interchain-address-tracker'); + + /** + * @dev Constructs the InterchainAddressTracker contract, both array parameters must be equal in length. + * @param chainName_ The name of the current chain. + */ + constructor(string memory chainName_) { + if (bytes(chainName_).length == 0) revert ZeroStringLength(); + + StringStorage.set(_CHAIN_NAME_SLOT, chainName_); + } + + /** + * @notice Getter for the contract id. + */ + function contractId() external pure returns (bytes32) { + return CONTRACT_ID; + } + + function _setup(bytes calldata params) internal override { + (string[] memory trustedChainNames, string[] memory trustedAddresses) = abi.decode( + params, + (string[], string[]) + ); + uint256 length = trustedChainNames.length; + + if (length != trustedAddresses.length) revert LengthMismatch(); + + for (uint256 i; i < length; ++i) { + setTrustedAddress(trustedChainNames[i], trustedAddresses[i]); + } + } + + /** + * @dev Gets the name of the chain this is deployed at + */ + function chainName() external view returns (string memory chainName_) { + chainName_ = StringStorage.get(_CHAIN_NAME_SLOT); + } + + /** + * @dev Gets the key for the trusted address at a remote chain + * @param chain Chain name of the remote chain + * @return slot the slot to store the trusted address in + */ + function _getTrustedAddressSlot(string memory chain) internal pure returns (bytes32 slot) { + slot = keccak256(abi.encode(PREFIX_ADDRESS_MAPPING, chain)); + } + + /** + * @dev Gets the key for the trusted address at a remote chain + * @param chain Chain name of the remote chain + * @return slot the slot to store the trusted address hash in + */ + function _getTrustedAddressHashSlot(string memory chain) internal pure returns (bytes32 slot) { + slot = keccak256(abi.encode(PREFIX_ADDRESS_HASH_MAPPING, chain)); + } + + /** + * @dev Sets the trusted address and its hash for a remote chain + * @param chain Chain name of the remote chain + * @param trustedAddress_ the string representation of the trusted address + */ + function _setTrustedAddress(string memory chain, string memory trustedAddress_) internal { + StringStorage.set(_getTrustedAddressSlot(chain), trustedAddress_); + + bytes32 slot = _getTrustedAddressHashSlot(chain); + bytes32 addressHash = keccak256(bytes(trustedAddress_)); + assembly { + sstore(slot, addressHash) + } + } + + /** + * @dev Gets the trusted address at a remote chain + * @param chain Chain name of the remote chain + * @return trustedAddress_ The trusted address for the chain. Returns '' if the chain is untrusted + */ + function trustedAddress(string memory chain) public view returns (string memory trustedAddress_) { + trustedAddress_ = StringStorage.get(_getTrustedAddressSlot(chain)); + } + + /** + * @dev Gets the trusted address hash for a chain + * @param chain Chain name + * @return trustedAddressHash_ the hash of the trusted address for that chain + */ + function trustedAddressHash(string memory chain) public view returns (bytes32 trustedAddressHash_) { + bytes32 slot = _getTrustedAddressHashSlot(chain); + assembly { + trustedAddressHash_ := sload(slot) + } + } + + /** + * @dev Checks whether the interchain sender is a trusted address + * @param chain Chain name of the sender + * @param address_ Address of the sender + * @return bool true if the sender chain/address are trusted, false otherwise + */ + function isTrustedAddress(string calldata chain, string calldata address_) external view returns (bool) { + bytes32 addressHash = keccak256(bytes(address_)); + + return addressHash == trustedAddressHash(chain); + } + + /** + * @dev Sets the trusted address for the specified chain + * @param chain Chain name to be trusted + * @param address_ Trusted address to be added for the chain + */ + function setTrustedAddress(string memory chain, string memory address_) public onlyOwner { + if (bytes(chain).length == 0) revert ZeroStringLength(); + if (bytes(address_).length == 0) revert ZeroStringLength(); + + _setTrustedAddress(chain, address_); + + emit TrustedAddressSet(chain, address_); + } + + /** + * @dev Remove the trusted address of the chain. + * @param chain Chain name that should be made untrusted + */ + function removeTrustedAddress(string calldata chain) external onlyOwner { + if (bytes(chain).length == 0) revert ZeroStringLength(); + + StringStorage.clear(_getTrustedAddressSlot(chain)); + + bytes32 slot = _getTrustedAddressHashSlot(chain); + assembly { + sstore(slot, 0) + } + + emit TrustedAddressRemoved(chain); + } +} diff --git a/contracts/utils/InterchainAddressTrackerProxy.sol b/contracts/utils/InterchainAddressTrackerProxy.sol new file mode 100644 index 00000000..01713ab0 --- /dev/null +++ b/contracts/utils/InterchainAddressTrackerProxy.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import { Proxy } from '../upgradable/Proxy.sol'; + +/** + * @title InterchainAddressTrackerProxy + * @dev Proxy contract for the InterchainAddressTracker contract. Inherits from the Proxy contract. + */ +contract InterchainAddressTrackerProxy is Proxy { + bytes32 private constant CONTRACT_ID = keccak256('interchain-address-tracker'); + + /** + * @dev Constructs the InterchainAddressTrackerProxy contract. + * @param implementationAddress Address of the InterchainAddressTracker implementation + * @param owner Address of the owner of the proxy + * @param params The params to be passed to the _setup function of the implementation. + */ + constructor( + address implementationAddress, + address owner, + bytes memory params + ) Proxy(implementationAddress, owner, params) {} + + /** + * @dev Override for the `contractId` function in Proxy. Returns a unique identifier for this contract. + * @return bytes32 Identifier for this contract. + */ + function contractId() internal pure override returns (bytes32) { + return CONTRACT_ID; + } +} diff --git a/package-lock.json b/package-lock.json index 9686be96..e361cc31 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@axelar-network/axelar-gmp-sdk-solidity", - "version": "5.5.0", + "version": "5.6.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@axelar-network/axelar-gmp-sdk-solidity", - "version": "5.5.0", + "version": "5.6.0", "license": "MIT", "devDependencies": { "@axelar-network/axelar-chains-config": "^0.1.2", diff --git a/package.json b/package.json index 7ef0fd63..5a73358a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@axelar-network/axelar-gmp-sdk-solidity", - "version": "5.5.0", + "version": "5.6.0", "description": "Solidity GMP SDK and utilities provided by Axelar for cross-chain development", "main": "index.js", "scripts": { diff --git a/test/utils/InterchainAddressTracker.js b/test/utils/InterchainAddressTracker.js new file mode 100644 index 00000000..f6599ad7 --- /dev/null +++ b/test/utils/InterchainAddressTracker.js @@ -0,0 +1,182 @@ +'use strict'; + +const chai = require('chai'); +const { ethers } = require('hardhat'); +const { + utils: { defaultAbiCoder }, +} = ethers; +const { expect } = chai; +const { deployContract } = require('../utils.js'); + +describe('InterchainAddressTracker', () => { + let ownerWallet, + otherWallet, + interchainAddressTracker, + interchainAddressTrackerFactory; + + const otherRemoteAddress = 'any string as an address'; + const otherChain = 'Other Name'; + const chainName = 'Chain Name'; + + const defaultChains = ['Chain1', 'Chain2']; + const defaultAddresses = []; + + before(async () => { + const wallets = await ethers.getSigners(); + ownerWallet = wallets[0]; + otherWallet = wallets[1]; + defaultAddresses.push(wallets[2].address); + defaultAddresses.push('another address format'); + const implementation = await deployContract( + ownerWallet, + 'InterchainAddressTracker', + [chainName], + ); + const params = defaultAbiCoder.encode( + ['string[]', 'string[]'], + [defaultChains, defaultAddresses], + ); + interchainAddressTracker = await deployContract( + ownerWallet, + 'InterchainAddressTrackerProxy', + [implementation.address, ownerWallet.address, params], + ); + + interchainAddressTrackerFactory = await ethers.getContractFactory( + 'InterchainAddressTracker', + ); + interchainAddressTracker = interchainAddressTrackerFactory + .attach(interchainAddressTracker.address) + .connect(ownerWallet); + }); + + it('check internal constants', async () => { + const interchainAddressTracker = await deployContract( + ownerWallet, + 'TestInterchainAddressTracker', + [chainName], + ); + + expect(await interchainAddressTracker.chainName()).to.equal(chainName); + }); + + it('Should revert on interchainAddressTracker deployment with invalid chain name', async () => { + await expect( + interchainAddressTrackerFactory.deploy(''), + ).to.be.revertedWithCustomError( + interchainAddressTracker, + 'ZeroStringLength', + ); + }); + + it('Should revert on interchainAddressTracker deployment with length mismatch between chains and trusted addresses arrays', async () => { + const interchainAddressTrackerImpl = await deployContract( + ownerWallet, + 'InterchainAddressTracker', + [chainName], + ); + const interchainAddressTrackerProxyFactory = + await ethers.getContractFactory('InterchainAddressTrackerProxy'); + const params = defaultAbiCoder.encode( + ['string[]', 'string[]'], + [['Chain A'], []], + ); + await expect( + interchainAddressTrackerProxyFactory.deploy( + interchainAddressTrackerImpl.address, + ownerWallet.address, + params, + ), + ).to.be.revertedWithCustomError(interchainAddressTracker, 'SetupFailed'); + }); + + it('Should get empty strings for the trusted address for unregistered chains', async () => { + expect(await interchainAddressTracker.trustedAddress(otherChain)).to.equal( + '', + ); + }); + + it('Should be able to check trusted addresses properly', async () => { + expect( + await interchainAddressTracker.isTrustedAddress( + otherChain, + otherRemoteAddress, + ), + ).to.equal(false); + }); + + it('Should not be able to add a trusted address as not the owner', async () => { + await expect( + interchainAddressTracker + .connect(otherWallet) + .setTrustedAddress(otherChain, otherRemoteAddress), + ).to.be.revertedWithCustomError(interchainAddressTracker, 'NotOwner'); + }); + + it('Should be able to add a trusted address as the owner', async () => { + await expect( + interchainAddressTracker.setTrustedAddress( + otherChain, + otherRemoteAddress, + ), + ) + .to.emit(interchainAddressTracker, 'TrustedAddressSet') + .withArgs(otherChain, otherRemoteAddress); + expect(await interchainAddressTracker.trustedAddress(otherChain)).to.equal( + otherRemoteAddress, + ); + }); + + it('Should revert on setting a trusted address with an empty chain name', async () => { + await expect( + interchainAddressTracker.setTrustedAddress('', otherRemoteAddress), + ).to.be.revertedWithCustomError( + interchainAddressTracker, + 'ZeroStringLength', + ); + }); + + it('Should revert on setting a trusted address with an invalid remote address', async () => { + await expect( + interchainAddressTracker.setTrustedAddress(otherChain, ''), + ).to.be.revertedWithCustomError( + interchainAddressTracker, + 'ZeroStringLength', + ); + }); + + it('Should be able to check trusted addresses properly.', async () => { + expect( + await interchainAddressTracker.isTrustedAddress( + otherChain, + otherRemoteAddress, + ), + ).to.equal(true); + }); + + it('Should not be able to remove a trusted address as not the owner', async () => { + await expect( + interchainAddressTracker + .connect(otherWallet) + .removeTrustedAddress(otherChain), + ).to.be.revertedWithCustomError(interchainAddressTracker, 'NotOwner'); + }); + + it('Should be able to remove a trusted address as the owner', async () => { + await expect(interchainAddressTracker.removeTrustedAddress(otherChain)) + .to.emit(interchainAddressTracker, 'TrustedAddressRemoved') + .withArgs(otherChain); + expect(await interchainAddressTracker.trustedAddress(otherChain)).to.equal( + '', + ); + }); + + it('Should revert on removing a custom remote address with an empty chain name', async () => { + await expect( + interchainAddressTracker.removeTrustedAddress(''), + ).to.be.revertedWithCustomError( + interchainAddressTracker, + 'ZeroStringLength', + ); + }); +}); diff --git a/test/utils/StringStorage.js b/test/utils/StringStorage.js new file mode 100644 index 00000000..062b69c7 --- /dev/null +++ b/test/utils/StringStorage.js @@ -0,0 +1,45 @@ +'use strict'; + +const { ethers } = require('hardhat'); +const chai = require('chai'); +const { deployContract } = require('../utils'); +const { keccak256 } = ethers.utils; +const { expect } = chai; + +describe('StringStorage', () => { + let stringStorage; + let ownerWallet; + + before(async () => { + const wallets = await ethers.getSigners(); + ownerWallet = wallets[0]; + + stringStorage = await deployContract(ownerWallet, 'TestStringStorage'); + }); + + it('Should store, load and delete a short string properly', async () => { + const str = 'hello'; + const slot = keccak256('0x1234'); + + await stringStorage.set(slot, str).then((tx) => tx.wait()); + + expect(await stringStorage.get(slot)).to.equal(str); + + await stringStorage.clear(slot).then((tx) => tx.wait()); + + expect(await stringStorage.get(slot)).to.equal(''); + }); + + it('Should store, load and delete a long string properly', async () => { + const str = keccak256('0x1234') + keccak256('0x5678'); + const slot = keccak256('0x1234'); + + await stringStorage.set(slot, str).then((tx) => tx.wait()); + + expect(await stringStorage.get(slot)).to.equal(str); + + await stringStorage.clear(slot).then((tx) => tx.wait()); + + expect(await stringStorage.get(slot)).to.equal(''); + }); +});