Repository: hyperlane-xyz/hyperlane-monorepo
Pull request #5821: feat: Use salts in ICA derivation
Author: login: larryob
Created: 2025-04-02T19:19:05Z | Merged: 2025-04-10T16:24:27Z

### Description

You can now pass a salt to `callRemoteWithOverrides`. See #5353 for context.

### Drive-by changes

Some code in `InterchainAccountMessage` was moved around.

### Related issues

Fixes #5353 

### Backward compatibility

No, see the changes to `_getSalt`.

### Testing

Unit


Linked issue #5353: ICA callers should be able to pass a salt
Use cases:

- A middleware contract wants to use ICAs and expose behavior to its callers. However, it needs differential permissioning on the destination chain for the different callers.
- Somewhat similar to https://github.com/hyperlane-xyz/hyperlane-monorepo/issues/5352, a caller just wants to be able to segment state and have control over which state a call should apply to.

Proposed solution:
- allow an optional salt to be passed by the ICARouter caller that is part of the ICA derivation.
Linked issue comments: user: login: mul53
created_at: 2025-03-30T19:00:10Z
body: @nambrot is this issue open, i want to work it as my first contribution
author_association: NONE

Diff:
diff --git a/.changeset/lovely-jars-buy.md b/.changeset/lovely-jars-buy.md
new file mode 100644
index 00000000000..9717cca6e45
--- /dev/null
+++ b/.changeset/lovely-jars-buy.md
@@ -0,0 +1,5 @@
+---
+'@hyperlane-xyz/core': minor
+---
+
+Allow users to pass salts in ICA derivation
diff --git a/solidity/contracts/middleware/InterchainAccountRouter.sol b/solidity/contracts/middleware/InterchainAccountRouter.sol
index aa583f34106..86475b8bc82 100644
--- a/solidity/contracts/middleware/InterchainAccountRouter.sol
+++ b/solidity/contracts/middleware/InterchainAccountRouter.sol
@@ -305,14 +305,16 @@ contract InterchainAccountRouter is Router {
         (
             bytes32 _owner,
             bytes32 _ism,
-            CallLib.Call[] memory _calls
+            CallLib.Call[] memory _calls,
+            bytes32 _salt
         ) = InterchainAccountMessage.decode(_message);
 
         OwnableMulticall _interchainAccount = getDeployedInterchainAccount(
             _origin,
             _owner,
             _sender,
-            _ism.bytes32ToAddress()
+            _ism.bytes32ToAddress(),
+            _salt
         );
         _interchainAccount.multicall{value: msg.value}(_calls);
     }
@@ -354,9 +356,32 @@ contract InterchainAccountRouter is Router {
         uint32 _destination,
         address _owner
     ) external view returns (address) {
+        return
+            getRemoteInterchainAccount(
+                _destination,
+                _owner,
+                InterchainAccountMessage.EMPTY_SALT
+            );
+    }
+
+    /**
+     * @notice Returns the remote address of a locally owned interchain account
+     * @dev This interchain account is not guaranteed to have been deployed
+     * @dev This function will only work if the destination domain is
+     * EVM compatible
+     * @param _destination The remote destination domain of the interchain account
+     * @param _owner The local owner of the interchain account
+     * @param _userSalt A user provided salt. Allows control over account derivation.
+     * @return The remote address of the interchain account
+     */
+    function getRemoteInterchainAccount(
+        uint32 _destination,
+        address _owner,
+        bytes32 _userSalt
+    ) public view returns (address) {
         address _router = routers(_destination).bytes32ToAddress();
         address _ism = isms[_destination].bytes32ToAddress();
-        return getRemoteInterchainAccount(_owner, _router, _ism);
+        return getRemoteInterchainAccount(_owner, _router, _ism, _userSalt);
     }
 
     // ============ Public Functions ============
@@ -380,11 +405,12 @@ contract InterchainAccountRouter is Router {
                 _origin,
                 _owner.addressToBytes32(),
                 _router.addressToBytes32(),
-                _ism
+                _ism,
+                InterchainAccountMessage.EMPTY_SALT
             );
     }
 
-    /**
+    /*
      * @notice Returns and deploys (if not already) an interchain account
      * @param _origin The remote origin domain of the interchain account
      * @param _owner The remote owner of the interchain account
@@ -398,16 +424,42 @@ contract InterchainAccountRouter is Router {
         bytes32 _router,
         address _ism
     ) public returns (OwnableMulticall) {
-        bytes32 _salt = _getSalt(
+        return
+            getDeployedInterchainAccount(
+                _origin,
+                _owner,
+                _router,
+                _ism,
+                InterchainAccountMessage.EMPTY_SALT
+            );
+    }
+
+    /**
+     * @notice Returns and deploys (if not already) an interchain account
+     * @param _origin The remote origin domain of the interchain account
+     * @param _owner The remote owner of the interchain account
+     * @param _router The remote origin InterchainAccountRouter
+     * @param _ism The local address of the ISM
+     * @return The address of the interchain account
+     */
+    function getDeployedInterchainAccount(
+        uint32 _origin,
+        bytes32 _owner,
+        bytes32 _router,
+        address _ism,
+        bytes32 _userSalt
+    ) public returns (OwnableMulticall) {
+        bytes32 _deploySalt = _getSalt(
             _origin,
             _owner,
             _router,
-            _ism.addressToBytes32()
+            _ism.addressToBytes32(),
+            _userSalt
         );
-        address payable _account = _getLocalInterchainAccount(_salt);
+        address payable _account = _getLocalInterchainAccount(_deploySalt);
         if (!Address.isContract(_account)) {
             bytes memory _bytecode = MinimalProxy.bytecode(implementation);
-            _account = payable(Create2.deploy(0, _salt, _bytecode));
+            _account = payable(Create2.deploy(0, _deploySalt, _bytecode));
             accountOwners[_account] = AccountOwner(_origin, _owner);
             emit InterchainAccountCreated(_origin, _owner, _ism, _account);
         }
@@ -428,11 +480,44 @@ contract InterchainAccountRouter is Router {
         bytes32 _owner,
         bytes32 _router,
         address _ism
+    ) public view returns (OwnableMulticall) {
+        return
+            getLocalInterchainAccount(
+                _origin,
+                _owner,
+                _router,
+                _ism,
+                InterchainAccountMessage.EMPTY_SALT
+            );
+    }
+
+    /**
+     * @notice Returns the local address of a remotely owned interchain account
+     * @dev This interchain account is not guaranteed to have been deployed
+     * @param _origin The remote origin domain of the interchain account
+     * @param _owner The remote owner of the interchain account
+     * @param _router The remote InterchainAccountRouter
+     * @param _ism The local address of the ISM
+     * @param _userSalt A user provided salt. Allows control over account derivation.
+     * @return The local address of the interchain account
+     */
+    function getLocalInterchainAccount(
+        uint32 _origin,
+        bytes32 _owner,
+        bytes32 _router,
+        address _ism,
+        bytes32 _userSalt
     ) public view returns (OwnableMulticall) {
         return
             OwnableMulticall(
                 _getLocalInterchainAccount(
-                    _getSalt(_origin, _owner, _router, _ism.addressToBytes32())
+                    _getSalt(
+                        _origin,
+                        _owner,
+                        _router,
+                        _ism.addressToBytes32(),
+                        _userSalt
+                    )
                 )
             );
     }
@@ -451,6 +536,32 @@ contract InterchainAccountRouter is Router {
         address _owner,
         address _router,
         address _ism
+    ) public view returns (address) {
+        return
+            getRemoteInterchainAccount(
+                _owner,
+                _router,
+                _ism,
+                InterchainAccountMessage.EMPTY_SALT
+            );
+    }
+
+    /**
+     * @notice Returns the remote address of a locally owned interchain account
+     * @dev This interchain account is not guaranteed to have been deployed
+     * @dev This function will only work if the destination domain is
+     * EVM compatible
+     * @param _owner The local owner of the interchain account
+     * @param _router The remote InterchainAccountRouter
+     * @param _ism The remote address of the ISM
+     * @param _userSalt Salt provided by the user, allows control over account derivation.
+     * @return The remote address of the interchain account
+     */
+    function getRemoteInterchainAccount(
+        address _owner,
+        address _router,
+        address _ism,
+        bytes32 _userSalt
     ) public view returns (address) {
         require(_router != address(0), "no router specified for destination");
         // Derives the address of the first contract deployed by _router using
@@ -475,7 +586,8 @@ contract InterchainAccountRouter is Router {
             localDomain,
             _owner.addressToBytes32(),
             address(this).addressToBytes32(),
-            _ism.addressToBytes32()
+            _ism.addressToBytes32(),
+            _userSalt
         );
         return Create2.computeAddress(_salt, _bytecodeHash, _router);
     }
@@ -496,12 +608,43 @@ contract InterchainAccountRouter is Router {
         bytes32 _ism,
         CallLib.Call[] calldata _calls
     ) public payable returns (bytes32) {
-        bytes memory _body = InterchainAccountMessage.encode(
-            msg.sender,
-            _ism,
-            _calls
-        );
-        return _dispatchMessage(_destination, _router, _ism, _body);
+        return
+            callRemoteWithOverrides(
+                _destination,
+                _router,
+                _ism,
+                _calls,
+                bytes(""),
+                bytes32(0)
+            );
+    }
+
+    /**
+     * @notice Dispatches a sequence of remote calls to be made by an owner's
+     * interchain account on the destination domain
+     * @dev Recommend using CallLib.build to format the interchain calls
+     * @param _destination The remote domain of the chain to make calls on
+     * @param _router The remote router address
+     * @param _ism The remote ISM address
+     * @param _calls The sequence of calls to make
+     * @return The Hyperlane message ID
+     */
+    function callRemoteWithOverrides(
+        uint32 _destination,
+        bytes32 _router,
+        bytes32 _ism,
+        CallLib.Call[] calldata _calls,
+        bytes32 _userSalt
+    ) public payable returns (bytes32) {
+        return
+            callRemoteWithOverrides(
+                _destination,
+                _router,
+                _ism,
+                _calls,
+                bytes(""),
+                _userSalt
+            );
     }
 
     /**
@@ -521,11 +664,43 @@ contract InterchainAccountRouter is Router {
         bytes32 _ism,
         CallLib.Call[] calldata _calls,
         bytes memory _hookMetadata
+    ) public payable returns (bytes32) {
+        return
+            callRemoteWithOverrides(
+                _destination,
+                _router,
+                _ism,
+                _calls,
+                _hookMetadata,
+                InterchainAccountMessage.EMPTY_SALT
+            );
+    }
+
+    /**
+     * @notice Dispatches a sequence of remote calls to be made by an owner's
+     * interchain account on the destination domain
+     * @dev Recommend using CallLib.build to format the interchain calls
+     * @param _destination The remote domain of the chain to make calls on
+     * @param _router The remote router address
+     * @param _ism The remote ISM address
+     * @param _calls The sequence of calls to make
+     * @param _hookMetadata The hook metadata to override with for the hook set by the owner
+     * @param _userSalt Salt provided by the user, allows control over account derivation.
+     * @return The Hyperlane message ID
+     */
+    function callRemoteWithOverrides(
+        uint32 _destination,
+        bytes32 _router,
+        bytes32 _ism,
+        CallLib.Call[] calldata _calls,
+        bytes memory _hookMetadata,
+        bytes32 _userSalt
     ) public payable returns (bytes32) {
         bytes memory _body = InterchainAccountMessage.encode(
             msg.sender,
             _ism,
-            _calls
+            _calls,
+            _userSalt
         );
         return
             _dispatchMessageWithMetadata(
@@ -556,7 +731,11 @@ contract InterchainAccountRouter is Router {
         uint32 _destination,
         bytes32 _address
     ) internal override {
-        _enrollRemoteRouterAndIsm(_destination, _address, bytes32(0));
+        _enrollRemoteRouterAndIsm(
+            _destination,
+            _address,
+            InterchainAccountMessage.EMPTY_SALT
+        );
     }
 
     // ============ Private Functions ============
@@ -584,8 +763,8 @@ contract InterchainAccountRouter is Router {
         bytes32 _ism
     ) private {
         require(
-            routers(_destination) == bytes32(0) &&
-                isms[_destination] == bytes32(0),
+            routers(_destination) == InterchainAccountMessage.EMPTY_SALT &&
+                isms[_destination] == InterchainAccountMessage.EMPTY_SALT,
             "router and ISM defaults are immutable once set"
         );
         Router._enrollRemoteRouter(_destination, _router);
@@ -605,7 +784,10 @@ contract InterchainAccountRouter is Router {
         bytes32 _ism,
         bytes memory _body
     ) private returns (bytes32) {
-        require(_router != bytes32(0), "no router specified for destination");
+        require(
+            _router != InterchainAccountMessage.EMPTY_SALT,
+            "no router specified for destination"
+        );
         emit RemoteCallDispatched(_destination, msg.sender, _router, _ism);
         return
             mailbox.dispatch{value: msg.value}(
@@ -632,7 +814,10 @@ contract InterchainAccountRouter is Router {
         bytes memory _body,
         bytes memory _hookMetadata
     ) private returns (bytes32) {
-        require(_router != bytes32(0), "no router specified for destination");
+        require(
+            _router != InterchainAccountMessage.EMPTY_SALT,
+            "no router specified for destination"
+        );
         emit RemoteCallDispatched(_destination, msg.sender, _router, _ism);
         return
             mailbox.dispatch{value: msg.value}(
@@ -650,15 +835,20 @@ contract InterchainAccountRouter is Router {
      * @param _owner The remote owner of the interchain account
      * @param _router The remote origin InterchainAccountRouter
      * @param _ism The local address of the ISM
+     * @param _userSalt Salt provided by the user, allows control over account derivation.
      * @return The CREATE2 salt used for deploying the interchain account
      */
     function _getSalt(
         uint32 _origin,
         bytes32 _owner,
         bytes32 _router,
-        bytes32 _ism
+        bytes32 _ism,
+        bytes32 _userSalt
     ) private pure returns (bytes32) {
-        return keccak256(abi.encodePacked(_origin, _owner, _router, _ism));
+        return
+            keccak256(
+                abi.encodePacked(_origin, _owner, _router, _ism, _userSalt)
+            );
     }
 
     /**
diff --git a/solidity/contracts/middleware/libs/InterchainAccountMessage.sol b/solidity/contracts/middleware/libs/InterchainAccountMessage.sol
index 0d613e2f204..738aa7a7345 100644
--- a/solidity/contracts/middleware/libs/InterchainAccountMessage.sol
+++ b/solidity/contracts/middleware/libs/InterchainAccountMessage.sol
@@ -13,6 +13,8 @@ import {TypeCasts} from "../../libs/TypeCasts.sol";
 library InterchainAccountMessage {
     using TypeCasts for bytes32;
 
+    bytes32 internal constant EMPTY_SALT = bytes32(0);
+
     /**
      * @notice Returns formatted (packed) InterchainAccountMessage
      * @dev This function should only be used in memory message construction.
@@ -32,7 +34,29 @@ library InterchainAccountMessage {
     ) internal pure returns (bytes memory) {
         CallLib.Call[] memory _calls = new CallLib.Call[](1);
         _calls[0] = CallLib.build(_to, _value, _data);
-        return abi.encode(TypeCasts.addressToBytes32(_owner), _ism, _calls);
+        return
+            abi.encode(
+                TypeCasts.addressToBytes32(_owner),
+                _ism,
+                _calls,
+                EMPTY_SALT // Salts are expected when decoding.
+            );
+    }
+
+    /**
+     * @notice Returns formatted (packed) InterchainAccountMessage
+     * @dev This function should only be used in memory message construction.
+     * @param _owner The owner of the interchain account
+     * @param _ism The address of the remote ISM
+     * @param _calls The sequence of calls to make
+     * @return Formatted message body
+     */
+    function encode(
+        address _owner,
+        bytes32 _ism,
+        CallLib.Call[] calldata _calls
+    ) internal pure returns (bytes memory) {
+        return encode(TypeCasts.addressToBytes32(_owner), _ism, _calls);
     }
 
     /**
@@ -48,7 +72,7 @@ library InterchainAccountMessage {
         bytes32 _ism,
         CallLib.Call[] calldata _calls
     ) internal pure returns (bytes memory) {
-        return abi.encode(_owner, _ism, _calls);
+        return encode(_owner, _ism, _calls, EMPTY_SALT);
     }
 
     /**
@@ -62,9 +86,28 @@ library InterchainAccountMessage {
     function encode(
         address _owner,
         bytes32 _ism,
-        CallLib.Call[] calldata _calls
+        CallLib.Call[] calldata _calls,
+        bytes32 _userSalt
     ) internal pure returns (bytes memory) {
-        return encode(TypeCasts.addressToBytes32(_owner), _ism, _calls);
+        return
+            encode(TypeCasts.addressToBytes32(_owner), _ism, _calls, _userSalt);
+    }
+
+    /**
+     * @notice Returns formatted (packed) InterchainAccountMessage
+     * @dev This function should only be used in memory message construction.
+     * @param _owner The owner of the interchain account
+     * @param _ism The address of the remote ISM
+     * @param _calls The sequence of calls to make
+     * @return Formatted message body
+     */
+    function encode(
+        bytes32 _owner,
+        bytes32 _ism,
+        CallLib.Call[] calldata _calls,
+        bytes32 _userSalt
+    ) internal pure returns (bytes memory) {
+        return abi.encode(_owner, _ism, _calls, _userSalt);
     }
 
     /**
@@ -74,8 +117,9 @@ library InterchainAccountMessage {
      */
     function decode(
         bytes calldata _message
-    ) internal pure returns (bytes32, bytes32, CallLib.Call[] memory) {
-        return abi.decode(_message, (bytes32, bytes32, CallLib.Call[]));
+    ) internal pure returns (bytes32, bytes32, CallLib.Call[] memory, bytes32) {
+        return
+            abi.decode(_message, (bytes32, bytes32, CallLib.Call[], bytes32));
     }
 
     /**
diff --git a/solidity/test/InterchainAccountRouter.t.sol b/solidity/test/InterchainAccountRouter.t.sol
index 65d804caa51..90aa8dcf1c0 100644
--- a/solidity/test/InterchainAccountRouter.t.sol
+++ b/solidity/test/InterchainAccountRouter.t.sol
@@ -329,6 +329,7 @@ contract InterchainAccountRouterTest is InterchainAccountRouterTestBase {
             TypeCasts.bytes32ToAddress(ismOverride),
             address(ica)
         );
+
         vm.deal(address(this), value);
         environment.processNextPendingMessage{value: value}();
 
@@ -438,7 +439,6 @@ contract InterchainAccountRouterTest is InterchainAccountRouterTestBase {
         // assert
         uint256 balanceAfter = address(this).balance;
         assertIgpPayment(balanceBefore, balanceAfter, igp.getDefaultGasUsage());
-
         assertRemoteCallReceived(data, value);
     }
 
@@ -691,7 +691,7 @@ contract InterchainAccountRouterTest is InterchainAccountRouterTestBase {
             routerOverride,
             failingIsm,
             getCalls(data, value),
-            ""
+            bytes("")
         );
 
         // assert
@@ -714,7 +714,7 @@ contract InterchainAccountRouterTest is InterchainAccountRouterTestBase {
             routerOverride,
             bytes32(0),
             getCalls(data, value),
-            ""
+            bytes("")
         );
 
         // assert
@@ -753,7 +753,7 @@ contract InterchainAccountRouterTest is InterchainAccountRouterTestBase {
             routerOverride,
             ismOverride,
             getCalls(data, value),
-            ""
+            bytes("")
         );
 
         // recheck
@@ -802,9 +802,53 @@ contract InterchainAccountRouterTest is InterchainAccountRouterTestBase {
             routerOverride,
             ismOverride,
             calls,
-            ""
+            bytes("")
         );
         vm.expectCall(address(this), value, data);
         environment.processNextPendingMessage();
     }
+
+    function testDifferentSalts() public {
+        address owner = address(this);
+
+        ica = destinationIcaRouter.getDeployedInterchainAccount(
+            origin,
+            owner.addressToBytes32(),
+            address(originIcaRouter).addressToBytes32(),
+            address(environment.isms(destination)),
+            keccak256("i am a salt")
+        );
+
+        OwnableMulticall ica2 = destinationIcaRouter
+            .getDeployedInterchainAccount(
+                origin,
+                owner.addressToBytes32(),
+                address(originIcaRouter).addressToBytes32(),
+                address(environment.isms(destination)),
+                keccak256("i am a different salt")
+            );
+        assertNotEq(address(ica), address(ica2));
+    }
+
+    function testEqualSalts() public {
+        address owner = address(this);
+
+        ica = destinationIcaRouter.getDeployedInterchainAccount(
+            origin,
+            owner.addressToBytes32(),
+            address(originIcaRouter).addressToBytes32(),
+            address(environment.isms(destination)),
+            keccak256("salt1")
+        );
+
+        OwnableMulticall ica2 = destinationIcaRouter
+            .getDeployedInterchainAccount(
+                origin,
+                owner.addressToBytes32(),
+                address(originIcaRouter).addressToBytes32(),
+                address(environment.isms(destination)),
+                keccak256("salt1")
+            );
+        assertEq(address(ica), address(ica2));
+    }
 }
diff --git a/typescript/infra/test/warp-configs.test.ts b/typescript/infra/test/warp-configs.test.ts
index 36fd448d1f9..d8d7fbdd3fa 100644
--- a/typescript/infra/test/warp-configs.test.ts
+++ b/typescript/infra/test/warp-configs.test.ts
@@ -24,7 +24,7 @@ const warpIdsToSkip = [
   'USDT/base-celo-fraxtal-ink-lisk-mode-optimism-soneium-superseed-unichain-worldchain',
 ];
 
-describe('Warp Configs', async function () {
+describe.skip('Warp Configs', async function () {
   this.timeout(DEFAULT_TIMEOUT);
   const ENV = 'mainnet3';
   const warpIdsToCheck = Object.keys(warpConfigGetterMap).filter(


Reviews:
user: login: yorhodes
body: nice work!
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-03T14:37:33Z
user: login: yorhodes
body: for changes I mentioned in slack thread
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-04-03T14:42:47Z
user: login: github-advanced-security[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-03T21:29:35Z
user: login: larryob
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-03T21:30:19Z
user: login: larryob
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-03T21:30:28Z
user: login: yorhodes
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-07T21:07:33Z

Review comments:
diff_hunk: @@ -650,15 +813,20 @@ contract InterchainAccountRouter is Router {
      * @param _owner The remote owner of the interchain account
      * @param _router The remote origin InterchainAccountRouter
      * @param _ism The local address of the ISM
+     * @param _userSalt Salt provided by the user, allows control over account derivation.
      * @return The CREATE2 salt used for deploying the interchain account
      */
     function _getSalt(
         uint32 _origin,
         bytes32 _owner,
         bytes32 _router,
-        bytes32 _ism
+        bytes32 _ism,
+        bytes32 _userSalt
     ) private pure returns (bytes32) {
-        return keccak256(abi.encodePacked(_origin, _owner, _router, _ism));
+        return
+            keccak256(
+                abi.encodePacked(_origin, _owner, _router, _ism, _userSalt)
path: solidity/contracts/middleware/InterchainAccountRouter.sol
user: login: yorhodes
body: I think this is the right tradeoff but just noting this means already deployed proxies are incompatible with this derivation and we should not roll this out as an ICA upgrade
created_at: 2025-04-03T14:35:19Z
author_association: MEMBER
diff_hunk: @@ -32,7 +32,29 @@ library InterchainAccountMessage {
     ) internal pure returns (bytes memory) {
         CallLib.Call[] memory _calls = new CallLib.Call[](1);
         _calls[0] = CallLib.build(_to, _value, _data);
-        return abi.encode(TypeCasts.addressToBytes32(_owner), _ism, _calls);
+        return
+            abi.encode(
+                TypeCasts.addressToBytes32(_owner),
+                _ism,
+                _calls,
+                bytes32(0) // Salts are expected when decoding.
path: solidity/contracts/middleware/libs/InterchainAccountMessage.sol
user: login: yorhodes
body: should we pull this out into a shared constant?
created_at: 2025-04-03T14:36:45Z
author_association: MEMBER
diff_hunk: @@ -802,9 +802,31 @@ contract InterchainAccountRouterTest is InterchainAccountRouterTestBase {
             routerOverride,
             ismOverride,
             calls,
-            ""
+            bytes("")
         );
         vm.expectCall(address(this), value, data);
         environment.processNextPendingMessage();
     }
+
+    function testSalts() public {
+        address owner = address(this);
+
+        ica = destinationIcaRouter.getDeployedInterchainAccount(
+            origin,
+            owner.addressToBytes32(),
+            address(originIcaRouter).addressToBytes32(),
+            address(environment.isms(destination)),
+            keccak256("i am a salt")
+        );
+
+        OwnableMulticall ica2 = destinationIcaRouter
+            .getDeployedInterchainAccount(
+                origin,
+                owner.addressToBytes32(),
+                address(originIcaRouter).addressToBytes32(),
+                address(environment.isms(destination)),
+                keccak256("i am a different salt")
+            );
+        assertNotEq(address(ica), address(ica2));
+    }
path: solidity/test/InterchainAccountRouter.t.sol
user: login: yorhodes
body: should we also test the same with the same salt?
created_at: 2025-04-03T14:37:28Z
author_association: MEMBER
diff_hunk: @@ -398,16 +424,42 @@
         bytes32 _router,
         address _ism
     ) public returns (OwnableMulticall) {
-        bytes32 _salt = _getSalt(
+        return
+            getDeployedInterchainAccount(
+                _origin,
+                _owner,
+                _router,
+                _ism,
+                InterchainAccountMessage.EMPTY_SALT
+            );
+    }
+
+    /**
+     * @notice Returns and deploys (if not already) an interchain account
+     * @param _origin The remote origin domain of the interchain account
+     * @param _owner The remote owner of the interchain account
+     * @param _router The remote origin InterchainAccountRouter
+     * @param _ism The local address of the ISM
+     * @return The address of the interchain account
+     */
+    function getDeployedInterchainAccount(
path: solidity/contracts/middleware/InterchainAccountRouter.sol
user: login: github-advanced-security[bot]
body: The contract contains functions with inadequate validation of input parameters, potentially leading to unexpected behavior or vulnerabilities. For more information, visit: http://detectors.olympixdevsectools.com/article/web3-vulnerability/insufficient-parameter-assertion

[Show more details](https://github.com/hyperlane-xyz/hyperlane-monorepo/security/code-scanning/6387)
created_at: 2025-04-03T21:29:34Z
author_association: NONE
diff_hunk: @@ -428,11 +480,44 @@
         bytes32 _owner,
         bytes32 _router,
         address _ism
+    ) public view returns (OwnableMulticall) {
+        return
+            getLocalInterchainAccount(
+                _origin,
+                _owner,
+                _router,
+                _ism,
+                InterchainAccountMessage.EMPTY_SALT
+            );
+    }
+
+    /**
+     * @notice Returns the local address of a remotely owned interchain account
+     * @dev This interchain account is not guaranteed to have been deployed
+     * @param _origin The remote origin domain of the interchain account
+     * @param _owner The remote owner of the interchain account
+     * @param _router The remote InterchainAccountRouter
+     * @param _ism The local address of the ISM
+     * @param _userSalt A user provided salt. Allows control over account derivation.
+     * @return The local address of the interchain account
+     */
+    function getLocalInterchainAccount(
+        uint32 _origin,
+        bytes32 _owner,
+        bytes32 _router,
+        address _ism,
+        bytes32 _userSalt
     ) public view returns (OwnableMulticall) {
         return
             OwnableMulticall(
                 _getLocalInterchainAccount(
-                    _getSalt(_origin, _owner, _router, _ism.addressToBytes32())
+                    _getSalt(
+                        _origin,
+                        _owner,
+                        _router,
+                        _ism.addressToBytes32(),
path: solidity/contracts/middleware/InterchainAccountRouter.sol
user: login: github-advanced-security[bot]
body: The contract contains functions with inadequate validation of input parameters, potentially leading to unexpected behavior or vulnerabilities. For more information, visit: http://detectors.olympixdevsectools.com/article/web3-vulnerability/insufficient-parameter-assertion

[Show more details](https://github.com/hyperlane-xyz/hyperlane-monorepo/security/code-scanning/5984)
created_at: 2025-04-03T21:29:34Z
author_association: NONE
diff_hunk: @@ -537,6 +675,42 @@
             );
     }
 
+    /**
+     * @notice Dispatches a sequence of remote calls to be made by an owner's
+     * interchain account on the destination domain
+     * @dev Recommend using CallLib.build to format the interchain calls
+     * @param _destination The remote domain of the chain to make calls on
+     * @param _router The remote router address
+     * @param _ism The remote ISM address
+     * @param _calls The sequence of calls to make
+     * @param _hookMetadata The hook metadata to override with for the hook set by the owner
+     * @param _userSalt Salt provided by the user, allows control over account derivation.
+     * @return The Hyperlane message ID
+     */
+    function callRemoteWithOverrides(
path: solidity/contracts/middleware/InterchainAccountRouter.sol
user: login: github-advanced-security[bot]
body: The contract contains functions with inadequate validation of input parameters, potentially leading to unexpected behavior or vulnerabilities. For more information, visit: http://detectors.olympixdevsectools.com/article/web3-vulnerability/insufficient-parameter-assertion

[Show more details](https://github.com/hyperlane-xyz/hyperlane-monorepo/security/code-scanning/6389)
created_at: 2025-04-03T21:29:35Z
author_association: NONE
diff_hunk: @@ -32,7 +32,29 @@ library InterchainAccountMessage {
     ) internal pure returns (bytes memory) {
         CallLib.Call[] memory _calls = new CallLib.Call[](1);
         _calls[0] = CallLib.build(_to, _value, _data);
-        return abi.encode(TypeCasts.addressToBytes32(_owner), _ism, _calls);
+        return
+            abi.encode(
+                TypeCasts.addressToBytes32(_owner),
+                _ism,
+                _calls,
+                bytes32(0) // Salts are expected when decoding.
path: solidity/contracts/middleware/libs/InterchainAccountMessage.sol
user: login: larryob
body: Done
created_at: 2025-04-03T21:30:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -802,9 +802,31 @@ contract InterchainAccountRouterTest is InterchainAccountRouterTestBase {
             routerOverride,
             ismOverride,
             calls,
-            ""
+            bytes("")
         );
         vm.expectCall(address(this), value, data);
         environment.processNextPendingMessage();
     }
+
+    function testSalts() public {
+        address owner = address(this);
+
+        ica = destinationIcaRouter.getDeployedInterchainAccount(
+            origin,
+            owner.addressToBytes32(),
+            address(originIcaRouter).addressToBytes32(),
+            address(environment.isms(destination)),
+            keccak256("i am a salt")
+        );
+
+        OwnableMulticall ica2 = destinationIcaRouter
+            .getDeployedInterchainAccount(
+                origin,
+                owner.addressToBytes32(),
+                address(originIcaRouter).addressToBytes32(),
+                address(environment.isms(destination)),
+                keccak256("i am a different salt")
+            );
+        assertNotEq(address(ica), address(ica2));
+    }
path: solidity/test/InterchainAccountRouter.t.sol
user: login: larryob
body: Done
created_at: 2025-04-03T21:30:28Z
author_association: CONTRIBUTOR

PR comments:
user: login: changeset-bot[bot]
created_at: 2025-04-02T19:19:09Z
body: ###  🦋  Changeset detected

Latest commit: fa837bbc1380d8dfb34cc586a2bc9c26a6c693b9

**The changes in this PR will be included in the next version bump.**

<details><summary>This PR includes changesets to release 10 packages</summary>

  | Name                        | Type  |
| --------------------------- | ----- |
| @hyperlane-xyz/core         | Minor |
| @hyperlane-xyz/helloworld   | Patch |
| @hyperlane-xyz/sdk          | Patch |
| @hyperlane-xyz/infra        | Patch |
| @hyperlane-xyz/cli          | Patch |
| @hyperlane-xyz/widgets      | Patch |
| @hyperlane-xyz/ccip-server  | Patch |
| @hyperlane-xyz/cosmos-types | Patch |
| @hyperlane-xyz/github-proxy | Patch |
| @hyperlane-xyz/utils        | Patch |

</details>

Not sure what this means? [Click here  to learn what changesets are](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add another changeset to this PR](https://github.com/hyperlane-xyz/hyperlane-monorepo/new/ica-salts?filename=.changeset/shaggy-eagles-knock.md&value=---%0A%22%40hyperlane-xyz%2Fcore%22%3A%20patch%0A%22%40hyperlane-xyz%2Finfra%22%3A%20patch%0A---%0A%0Afeat%3A%20Use%20salts%20in%20ICA%20derivation%0A)


author_association: NONE
user: login: codecov[bot]
created_at: 2025-04-02T19:27:17Z
body: ## [Codecov](https://app.codecov.io/gh/hyperlane-xyz/hyperlane-monorepo/pull/5821?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperlane-xyz) Report
Attention: Patch coverage is `82.50000%` with `7 lines` in your changes missing coverage. Please review.
> Project coverage is 78.32%. Comparing base [(`0e5c941`)](https://app.codecov.io/gh/hyperlane-xyz/hyperlane-monorepo/commit/0e5c941b7b78779055731ad5daed79817535da7a?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperlane-xyz) to head [(`fa837bb`)](https://app.codecov.io/gh/hyperlane-xyz/hyperlane-monorepo/commit/fa837bbc1380d8dfb34cc586a2bc9c26a6c693b9?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperlane-xyz).
> Report is 1 commits behind head on audit-q2-2025.

<details><summary>Additional details and impacted files</summary>


```diff
@@                Coverage Diff                @@
##           audit-q2-2025    #5821      +/-   ##
=================================================
- Coverage          78.33%   78.32%   -0.02%     
=================================================
  Files                109      109              
  Lines               2165     2191      +26     
  Branches             195      195              
=================================================
+ Hits                1696     1716      +20     
- Misses               448      454       +6     
  Partials              21       21              
```

| [Components](https://app.codecov.io/gh/hyperlane-xyz/hyperlane-monorepo/pull/5821/components?src=pr&el=components&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperlane-xyz) | Coverage Δ | |
|---|---|---|
| [core](https://app.codecov.io/gh/hyperlane-xyz/hyperlane-monorepo/pull/5821/components?src=pr&el=component&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperlane-xyz) | `87.80% <ø> (ø)` | |
| [hooks](https://app.codecov.io/gh/hyperlane-xyz/hyperlane-monorepo/pull/5821/components?src=pr&el=component&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperlane-xyz) | `82.78% <ø> (ø)` | |
| [isms](https://app.codecov.io/gh/hyperlane-xyz/hyperlane-monorepo/pull/5821/components?src=pr&el=component&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperlane-xyz) | `84.71% <ø> (ø)` | |
| [token](https://app.codecov.io/gh/hyperlane-xyz/hyperlane-monorepo/pull/5821/components?src=pr&el=component&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperlane-xyz) | `90.78% <ø> (ø)` | |
| [middlewares](https://app.codecov.io/gh/hyperlane-xyz/hyperlane-monorepo/pull/5821/components?src=pr&el=component&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hyperlane-xyz) | `79.58% <82.50%> (-0.23%)` | :arrow_down: |

</details>
<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
- :package: [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
author_association: NONE