Smart contracts on the blockchain are immutable, i.e., they cannot be replaced or updated once deployed. However, there are a few upgradeability frameworks that use a proxy contract to delegate your calls to the actual implementation contracts. Those implementation contracts can then be upgraded with some limitations.

The Router cross-chain bridge contracts use Openzeppelin’s implementation of the UUPS proxy pattern. Why do we use upgradeable contracts?

This thread by @eth_sensai explains it.

Problem Link to heading

While working on the project, I faced an issue when testing BNB transfers on the mainnet. The contract could not unwrap WBNB and execute the further logic.

We did some debugging in Tenderly and found that WBNB on the BSC chain used the transfer method to transfer BNB.

Line #34 Funds are transferred using .transfer function
Line #34 Funds are transferred using .transfer function

This meant that there was a limit of 2300 gas on this transfer function call, which is usually enough to receive BNB. However, as we were using proxy contracts, there was an additional cost of the delegate call from the proxy to the implementation’s receive function. This additional cost surpassed the 2300 gas limit, and the transaction failed.

So at that time, we thought of two solutions to this problem :

  • Add a receive function in the proxy contract so that it does not need to delegate the call to the implementation contract’s receive function.

  • Add a middleman contract that would receive WBNB from our contract, unwrap them, and transfer BNB to the proxy contract through the call method, which has no such gas limit.

As we were using Openzeppelin’s upgradeable contracts and plugins, we already had our proxy contracts deployed, which couldn’t be replaced. So we chose to deploy a middleman contract that would receive the BNB and then send them again to the proxy contract without gas restrictions.

So I deployed an ETHHandler contract.


Post that, I upgraded our contracts to send the WBNB to ETH Handler and call the withdraw function on ETHHandler to receive unwrapped BNB.

This solved the issue. The BNB transfers are now routed through the ETHHandler contract.

What I learned Link to heading

If you plan to receive ETH in your proxy contracts make sure you have a receive function in the proxy itself.

Twitter @pranav__garg_