A Multi-bridge Implementation for Uniswap Governance is Now Available
This post includes the implementation of a multi-bridge Uniswap governance solution that is vendor-lock-in-free for Uniswap community to consider.
This solution is strictly better than choosing a single bridge in terms of security and availability. In addition, it also retains the strongest bargaining power in the hands of Uniswap community to always receive the best services. See this for more context.
The implementation combines both Celer and Wormhole and is open-sourced here
All other bridges/cross-chain solutions such as LayerZero can be added easily via adapters.
A testnet deployment is done.
Setup:
- Source Chain: Avalanche Fuji testnet
- Destination Chain: BSC testnet
The reason we did not use Ethereum Goeril is that there is no available generic relayer from @Wormhole on Goeril and Wormhole team has not responded to us in assisting things.
- Cross-chain solutions used: Celer, Wormhole
- Contract addresses:
– MultiBridgeSender Fuji 0x5a5F095183241f4B23a7Aa7f8949fd02D06CaeCf
– CelerSenderAdapter fuji 0x243b40e96c6bF21511E53d85c86F6Ec982f9a879
– WormholeSenderAdapter fuji
0x0cb93b5c42437FE78b3697740D15b46707f97361
– MultiBridgeReceiver bsctest 0x207fF5290Bb518D963CF0a62B371B5B2dB6943A2
– CelerReceiverAdapter bsctest 0x37D2F4DCb5986ce2639bd66B022dcb9e3879156c
– WormholeReceiverAdapter bsctest
0xa5711D74780527484Be167bA3052eD3700d52B1D
– UniswapV3Factory bsctest
0x0bEC2a9E08658eAA15935C25cfF953caB2934C85
Test results:
- Source chain transaction: Avalanche Transaction Hash (Txhash) Details | SnowTrace
- Destination Chain tx1: message from wormhole:
Binance Transaction Hash (Txhash) Details | BscScan - Destination Chain tx2: message from celer and execute the governance call:
Binance Transaction Hash (Txhash) Details | BscScan
Now let’s get to the solution specification.
Send Cross-Chain Messages through Multiple Bridges
This is a solution for cross-chain message passing without vendor lock-in and with enhanced security beyond any single bridge. A message with multiple copies are sent through different bridges to the destination chains, and will only be executed at the destination chain when the same message has been delivered by a quorum of different bridges.
The current solution are designed for messages being sent from one source chain to multiple destination chains. It also requires that there is only one permitted sender on the source chain. This would be the use case for Uniswap governance contract on Ethereum
calling remote functions of contracts on other EVM chains.
Workflow
Send message on source chain
To send a message to execute a remote call on the destintion chain, sender on the source chain should call remoteCall()
of MultiBridgeSender
, which invokes sendMessage()
of every bridge sender apdater to send messages via different message bridges.
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Source chain │
│ │
│ ┌─────────────────┐ ┌───────────────────┐ │
│ ┌──►│ Bridge1 Adapter ├──►│ Bridge1 Contracts │ │
│ │ └─────────────────┘ └───────────────────┘ │
│ │ │
│ ┌────────┐remoteCall()┌───────────────────┐sendMessage()│ ┌─────────────────┐ ┌───────────────────┐ │
│ │ Caller ├───────────►│ MultiBridgeSender ├─────────────┼──►│ Bridge2 Adapter ├──►│ Bridge2 Contracts │ │
│ └────────┘ └───────────────────┘ │ └─────────────────┘ └───────────────────┘ │
│ │ │
│ │ ┌─────────────────┐ ┌───────────────────┐ │
│ └──►│ Bridge3 Adapter ├──►│ Bridge3 Contracts │ │
│ └─────────────────┘ └───────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────┘
Receive message on destination chain
On the destination chain, MultiBridgeReceiver receives messages from every bridge receiver adapter. Each receiver adapter gets encoded message data from its bridge contracts, and then decode the message and call receiveMessage()
of MultiBrideReceiver
.
MultiBridgeReceiver
maintains a map from bridge adapter address to its power. Only adapter with non-zero power has access to receiveMessage()
function. If the accumulated power of a message has reached the a threshold, which means enough number of different bridges have delivered a same message, the message will be executed by the MultiBrideReceiver
contract.
The message execution will invoke a function call according to the message content, which will either call functions of other contracts, or call the param adjustment functions of the MultiBridgeReceiver
itself. Note that the only legit message sender is the trusted dApp contract on the source chain, which means only that single dApp contract has the ability to execute functions calls through the MultiBridgeReceiver
contracts on different other chains.
┌────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Destination chain │
│ │
│ ┌───────────────────┐ ┌─────────────────┐ │
│ │ Bridge1 Contracts ├──►│ Bridge1 Adapter ├──┐ │
│ └───────────────────┘ └─────────────────┘ │ │
│ │ │
│ ┌───────────────────┐ ┌─────────────────┐ │receiveMessage()┌─────────────────────┐ call() ┌──────────┐ │
│ │ Bridge1 Contracts ├──►│ Bridge2 Adapter ├──┼───────────────►│ MultiBridgeReceiver ├────────►│ Receiver │ │
│ └───────────────────┘ └─────────────────┘ │ └─────────────────────┘ └──────────┘ │
│ │ │
│ ┌───────────────────┐ ┌─────────────────┐ │ │
│ │ Bridge2 Contracts ├──►│ Bridge3 Adapter ├──┘ │
│ └───────────────────┘ └─────────────────┘ │
│ │
└────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
Add new bridge and update threshold
Below are steps to add a new bridge (e.g. Bridge4) by the dApp community.
- Bridge4 provider should implement and deploy Bridge4 adapters on source chain and all destination chains. The adapter contracts should meet the following requirements.
- On the source chain, the sender adapter should only accept
sendMessage()
call fromMultiBridgeSender
. - On the destination chain, the receiver adapter should only accept messages sent from the Bridge4 sender adapter on the source chain, and then call
receiveMessage()
ofMultiBridgeReceiver
for each valid message. - Renounce any ownership or special roles of the adapter contracts after inital parameter setup.
- On the source chain, the sender adapter should only accept
- Bridge4 provider deploy and open source the adapter contracts. dApp community should review the code and check if the requirements above are met.
- dApp contract (
Caller
) on the source chain adds the new Bridge4 sender adapter toMultiBridgeSender
on the source chain by calling theaddSenderAdapters()
function ofMultiBridgeSender
. - dApp contract (
Caller
) on the source chain adds the new Bridge4 receiver adapter toMultiBridgeReceiver
on the destination chain by calling theremoteCall()
function ofMultiBridgeSender
, with arguments to callupdateReceiverAdapter()
of theMultiBridgeReceiver
on the destination chain.
Updating quorum threshold is similar to configure a new bridge receiver adapter on destination chain. It requires a remoteCall()
from the source chain Caller
with calldata calling updateQuorumThreshold()
of the MultiBridgeReceiver
on the destination chain.
Example
Use case: contract A on Goerli send message to contract B on BSC Testnet in order to call enableFeeAmount()
for state change. Apply a 2-of-3 messages governance model with message bridge C, D and E.
Deployment and initialization
- Deploy
MultiBridgeSender
on Goerli, set address of A as allowed caller. - Deploy
MultiBridgeReceiver
on BSC Testnet. - Each message bridge provider prepare their own
SenderAdapter
andReceiverAdapter
, named with a prefix of their bridge name. Take preparation ofCSenderAdapter
andCReceiverAdapter
as an example.- Deploy
CSenderAdapter
on Goerli, set address ofMultiBridgeSender
as multiBridgeSender. - Deploy
CReceiverAdapter
on BSC Testnet, set address ofMultiBridgeReceiver
as multiBridgeReceiver. - Call updateReceiverAdapter() of
CSenderAdapter
, set address ofCReceiverAdapter
on BSC Testnet(chain id 97) as a valid ReceiverAdapter. - Call updateSenderAdapter() of
CReceiverAdapter
, set address ofCSenderAdapter
on Goerli(chain id 5) as a valid SenderAdapter. - Transfer ownership of
CSenderAdapter
andCReceiverAdapter
to address(0).
- Deploy
- Once all message bridges are ready, somehow let contract A call addSenderAdapters() of
MultiBridgeSender
with an address array ofCSenderAdapter
,DSenderAdapter
andESenderAdapter
. - Call
initialize()
ofMultiBridgeReceiver
, with an address array ofCReceiverAdapter
,DReceiverAdapter
andEReceiverAdapter
, and power threshold 2.
Sending your message
Prepare a calldata for contract B for calling enableFeeAmount()
, then somehow let contract A call remoteCall()
of MultiBridgeSender
with _dstChainId = 97
, _target = <address of contract B>
and _callData = <calldata you prepared>
.
Result
Imagine that the messages sent via C, D and E received by MultiBridgeReceiver
on BSC Testnet in an order of 1.C 2.D 3.E
. During receiving message sent via D, accumulated power reaches power threshold 2, which result in message execution(the calldata will be sent to contract B).
Notes to the community
We are more than happy to answer any questions, help with tests, collaborate with similar-minded builders like @AlexSmirnov, iterate based on feedbacks and provide audits from community selected firms.
Although Celer, being a 2018-vintage project, does not have the backing of large UNI-holding investors at this moment, we do trust the integrity and intelligence of all the Uniswap delegates to choose this positive-sum path that ensures the highest level of security and service quality for Uniswap’s multi-blockchain expansion.