async fn prepare_upgrade_call(
    proposed_upgrade: &abi::ProposedUpgrade,
    chain_specific: Option<ZkChainSpecificUpgradeData>,
) -> anyhow::Result<Vec<u8>> {
    // No upgrade
    if proposed_upgrade.l2_protocol_upgrade_tx.tx_type == U256::zero() {
        return Ok(vec![]);
    }

    let minor_version = proposed_upgrade.l2_protocol_upgrade_tx.nonce;
    if ProtocolVersionId::try_from(minor_version.as_u32() as u16).unwrap()
        != ProtocolVersionId::gateway_upgrade()
    {
        // We'll just keep it the same for non-Gateway upgrades
        return Ok(proposed_upgrade.l2_protocol_upgrade_tx.data.clone());
    }

    // For gateway upgrade, things are bit more complex.
    // The source of truth for the code below is the one that is present in
    // `GatewayUpgrade.sol`.
    let mut encoded_input = GatewayUpgradeEncodedInput::decode(
        ethabi::decode(
            &[GatewayUpgradeEncodedInput::schema()],
            &proposed_upgrade.post_upgrade_calldata,
        )?[0]
            .clone(),
    )?;

    let gateway_upgrade_calldata = ethabi::encode(&[
        Token::Address(encoded_input.ctm_deployer),
        Token::Bytes(encoded_input.fixed_force_deployments_data),
        Token::Bytes(chain_specific.context("chain_specific")?.encode_bytes()),
    ]);

    // May not be very idiomatic, but we do it in the same way as it was done in Solidity
    // for easier review
    encoded_input.force_deployments[encoded_input.l2_gateway_upgrade_position].input =
        gateway_upgrade_calldata;

    let force_deployments_as_tokens: Vec<_> = encoded_input
        .force_deployments
        .iter()
        .map(ForceDeployment::encode)
        .collect();

    let full_data = zksync_contracts::deployer_contract()
        .function("forceDeployOnAddresses")
        .unwrap()
        .encode_input(&[Token::Array(force_deployments_as_tokens)])
        .unwrap();

    Ok(full_data)
}
Some upgrades have chain-dependent calldata that has to be prepared properly.
core/lib/types/src/protocol_upgrade.rs