How to find a counterparty address?

I’m building a token bridge with Polymer and wanted to use deterministic deployments to have the same addresses for bridge accounts deployed cross-chain. In RecvPacket and Acknowledgement handlers I want to check the counterparty address to confirm that is similar to the current contract’s address. Is it possible to do?

There is access to the counterparty’s port id but to extract the address I need to know the counterparty’s port prefix which is not available.

2 Likes

Hey @IbcFan great question! I believe this is possible depending on where you would like to check the counterparty address to confirm they are the same. Some possible options:

Embedding and Validating Contract Addresses

One strategy to validate the counterparty’s address without direct access to the port prefix is to embed address information within the data being sent over the bridge:

  • Embed Counterparty Address in the Payload: When sending a packet, include the sender’s contract address within the payload. This allows the receiving contract to extract and compare this embedded address with its predetermined counterpart address.
  • Use a Registry Contract: Implement a registry contract on each chain that maps counterpart contracts to each other. Both the sending and receiving contracts can refer to this registry to validate if they are interacting with the correct counterpart. This registry would need to be populated with the correct mappings post-deployment.

Implicitly Determining Port ID

Construct PortID and Counterparty Port ID : Given the channel type (universal or custom) and the proof usage (e.g., optimism-sim or base-sim ), construct the PortID and Counterparty Port ID dynamically within your smart contracts. This involves concatenating the portPrefix with the known contract address or the UniversalChannelHandler address.

For Example:

A contract on Optimism with address 0xa9e65230ee6c57267846ae546c2bfc5bb10016b4 wanting to communicate with its counterpart on Base over a custom sim (non-proof) channel:

  • On Optimism, construct the PortID as polyibc.optimism-sim.0xa9e65230ee6c57267846ae546c2bfc5bb10016b4.
  • On Base, knowing the counterpart’s address, you construct the expected Counterparty Port ID as polyibc.base-sim.19E2aCe726Dd152C0C4646209DaDDf49911951CC.

By understanding the contract’s environment (universal vs. custom channel, with or without sim proofs), you can accurately predict and verify PortIDs and Counterparty Port IDs, facilitating secure cross-chain communications without direct access to the counterparty’s port prefix. This method leverages both deterministic deployment and the specific structure of port IDs to ensure that contracts can confirm their counterpart’s identity and maintain consistent interactions across chains.

Note: If you are using Universal Channels the contractAddress in the PortID will be the UniversalChannelHandler address.

Hope this helps!

1 Like

Allow me to propose a slightly different solution:

I’d say the answer to your question really depends on whether you’re using a Universal channel to send your packets over or a custom IBC channel.

Custom IBC channel

In fact, if you’re using a custom IBC channel, there really is no need to do this check, because:

  • The IBC protocol has designed the “port” concept for the very reason that it handles authentication of modules (here contracts) that would attempt to send packets. See our docs on ports
  • This means that the only thing you’d need to make sure is that the channel is established using the expected contracts on both counterparties. And the channel handshake is triggered by the dApp developer who can ensure the right parameters are used…

So in conclusion, an extra authentication check in the case of custom IBC channels is not required (and given cost of compute in blockchains, should be avoided)…

Universal channels

For universal channels, the situation is different. As you can read in the docs, applications do not own the port for the universal channel (Polymer’s Universal Channel Middleware does!). Which means it does make sense for you to handle authentication!

However, when we look at the structure of a UniversalPacket:

We see that it contains both application Adresses on source and destination (without prefix!) + you have access to the UniversalPacket in the callbacks…

See for example: ibc-app-solidity-template/contracts/base/UniversalChanIbcApp.sol at main · open-ibc/ibc-app-solidity-template · GitHub

So in fact, your check should be very cleverly feasible!

@kenobi @tmsdkeys thank you for the responses.

@tmsdkeys for a custom channel what prevents anyone from calling the dispatcher’s openIbcChannel method with a malicious counterparty’s bridge address? I don’t see any prevention done in the contract’s implementation

Hey @IbcFan can you sketch out a little better what situation you’re envisioning? I believe you’re asking a valuable question here so want to make sure we’re talking about the same thing

Do I understand correctly you’re considering a scenario where you have contract “MyIbcContract” on Optimism for example and you’re asking:

  1. What if someone on Base (example, could be other rollups) with a malicious contract “EvilContract” attempts to create a channel handshake with “MyIbcContract” and what are the risks or recommendations?

  2. What if someone calls the OpenIbcChannel method on the dispatcher on Optimism with the info for “MyIbcContract” starting the channel handshake with “EvilContract” on Base?

  3. Some other situation? …

Let us know

@tmsdkeys I was asking about the second scenario but interested to know about the first scenario too!

All right! Let’s reason through it…

After some consideration, I think the answer between both situations does not meaningfully change.

The first “solution” could be to simply have your app communicate a canonical channel which all users should send packets over. That way a malicious actor could try to open an additional channel with a malicious counterparty contract, but it would not gather any traffic if users can only interact with the dApp through a frontend which is controlled by the developer to pick the canonical channel…

The second (better IMO) solution would be to use custom logic in the channel handshake steps to ensure the channel creation only succeeds if both port addresses are valid.

This would work as follows:

  • define an allowList storage variable that could be an address or an array of addresses that are whitelisted to create channels to your contract

  • override the channel handshake callbacks from CustomChanIbcApp.sol in your contract and add custom logic to check if the input addresses (potentially malicious) match any in the allowlist.

    in scenario 1. above you’d check on the onChanOpenTry step, in scenario 2. you’d check in the onChanOpenInit step (note that right now those are combined in onOpenIbcChannel callback but this will soon change to be more conform the IBC spec)

    If the counterparty port address is not in the allowlist, you stop channel creation

Note that in this case you would again run into the issue of knowing the portId, not the address. Given that the portId always ends with the portaddress I think you could slice the portId for its last 20 chars (and add “0x”) to get the address

Once the channel is established, what I said above holds and you don’t have to check additionally for each packet

I ended up doing an override in the channel handshake callback.

Cool! I also got the idea to add a sample project (and perhaps even another base contract to inherit from) that implements these additional security best practices.

If you intend to open source this code, maybe one thing would be to not just edit the functions in CustomChanIbcApp.sol but override them in the application contract that inherits from CustomChanIbcApp