How to Deploy a Proxy Contract With Web3 Java

Tutorial on how to deploy a proxy smart contract with web3 java

Senudajayalath
Coinmonks
Published in
5 min readMay 29, 2023

--

In the world of blockchain development, proxy contracts play a vital role in enhancing the flexibility and upgradability of smart contracts. When it comes to deploying a proxy contract with Web3 Java, developers gain the ability to seamlessly update contract logic without disrupting existing deployments or migrating large amounts of data. This powerful feature opens up a realm of possibilities for smart contract management and maintenance. In this article, we will delve into the process of deploying a proxy contract with Web3 Java, exploring the fundamental concepts, tools, and steps required to harness this technology effectively. So, if you’re ready to unlock the potential of proxy contracts and elevate your smart contract deployments, let’s dive in!

Photo by Shubham’s Web3 on Unsplash

Step 1. Prerequisites

  1. Previous knowledge on proxy contracts and how they work.

Step 2. Understanding the smart contracts

Before we get our hands dirty, first let us look at the proxy contract and the implementation contract which we will be interacting with.

The Beacon proxy contract looks something like the following. Nothing special. Just a Beacon proxy contract that adheres to the Openzeppelin standards.

// contracts/BeaconProxy.sol
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/proxy/beacon/IBeacon.sol";
import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/proxy/Proxy.sol";
import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/proxy/ERC1967/ERC1967Upgrade.sol";

/**
* @dev This contract implements a proxy that gets the implementation address for each call from an {UpgradeableBeacon}.
*
* The beacon address is stored in storage slot `uint256(keccak256('eip1967.proxy.beacon')) - 1`, so that it doesn't
* conflict with the storage layout of the implementation behind the proxy.
*
* _Available since v3.4._
*/

contract BeaconProxy is Proxy, ERC1967Upgrade {
event BeaconProxyDeployed(address contractAddress, address beaconAddress, address creator,string assetId);
/**
* @dev Initializes the proxy with `beacon`.
*
* If `data` is nonempty, it's used as data in a delegate call to the implementation returned by the beacon. This
* will typically be an encoded function call, and allows initializing the storage of the proxy like a Solidity
* constructor.
*
* Requirements:
*
* - `beacon` must be a contract with the interface {IBeacon}.
*/
constructor(address beacon, bytes memory data,string memory assetId) payable {
assert(_BEACON_SLOT == bytes32(uint256(keccak256("eip1967.proxy.beacon")) - 1));
_upgradeBeaconToAndCall(beacon, data, false);
emit BeaconProxyDeployed(address(this), beacon, msg.sender,assetId);
}

/**
* @dev Returns the current beacon address.
*/
function _beacon() internal view virtual returns (address) {
return _getBeacon();
}

/**
* @dev Returns the current implementation address of the associated beacon.
*/
function _implementation() internal view virtual override returns (address) {
return IBeacon(_getBeacon()).implementation();
}

/**
* @dev Changes the proxy to use a new beacon. Deprecated: see {_upgradeBeaconToAndCall}.
*
* If `data` is nonempty, it's used as data in a delegate call to the implementation returned by the beacon.
*
* Requirements:
*
* - `beacon` must be a contract.
* - The implementation returned by `beacon` must be a contract.
*/
function _setBeacon(address beacon, bytes memory data) internal virtual {
_upgradeBeaconToAndCall(beacon, data, false);
}
}

In this tutorial, I have decided to utilize the ERC1400 contract as the implementation contract for the sake of clarity and simplicity. However, it’s worth noting that you have the freedom to opt for any contract that suits your requirements. The signature of the intitializer function of the ERC1400 contract is shown below for reference.

function initialize(
string memory tokenName,
uint256 tokenGranularity,
address admin,
address[] operators,
bool whiteListingEnable,
) external initializer {

Step 3. Encoding the initialize function of the implementation contract

Encoding the initialize function of the implementation contract involves transforming the function’s parameters into a hexadecimal format compatible with Ethereum’s ABI encoding. This encoding ensures seamless interaction between the proxy contract and the implementation contract during deployment. By accurately encoding the initialize function, the proxy contract can successfully initialize the implementation contract with the desired parameters, facilitating smooth contract functionality.

In web3 Java it is done as follows

Address[] addresses = new Address[] {
new Address("0x1234567890123456789012345678901234567890"),
new Address("0x0987654321098765432109876543210987654321")
};
Type operatorAddresses = new DynamicArray(Address.class, addresses);

List<Type> inputParameters = Arrays.asList(new Utf8String(tokenName), new Uint256(1), new Address(credentials.getAddress()),operatorAddresses, new Bool(false));
Function function =
new Function(
"initialize",
inputParameters,
Collections.emptyList());
String encodedFunction = FunctionEncoder.encode(function);

In the above code snippet notice how different types of variables are passed in different formats to be encoded. Also since the intialize function does not return anything, we pass in Collections.emptyList() as the third element for creating the Function.

Step 3. Encoding the constructor of the proxy contract.

In the same way the constructor of the proxy contract should also be encoded. Since the constructor requires the encoded initialize function as a parameter, we should pass that in as well.

//This is done to remove the preceding "0x" of the hex string
String encodedFunction =encodedFunction.substring(2);

byte[] hexBytes = parseHexBinary(encodedFunction);

String encodedConstructor =
FunctionEncoder.encodeConstructor(
Arrays.asList(
new Address(beaconAddress),
new DynamicBytes(hexBytes),
new Utf8String(symbol)));

Step 3. Getting the gas estimate for the transaction

Determining the gas estimate for a transaction is crucial in optimizing cost and efficiency within Ethereum. By obtaining the gas estimate, developers can estimate the amount of gas required for executing a transaction, enabling them to set appropriate gas limits and avoid unnecessary expenses. Let us see how this is done with web3 java.

// Retrieve the nonce for the relevant address
EthGetTransactionCount ethGetTransactionCount = web3.ethGetTransactionCount(credentials.getAddress(), DefaultBlockParameterName.LATEST).send();
BigInteger nonce = ethGetTransactionCount.getTransactionCount();

//Use a safe gas limit which would not be exceeded(i.e block gas limit)
BigInteger blockGasLimit = BigInteger.valueOf(3000000L);

Transaction mimicTransaction = Transaction.createContractTransaction(credentials.getAddress(),nonce, gasPrice,blockGasLimit,BigInteger.ZERO,proxy_bin +encodedConstructor);

EthEstimateGas gasEstimate = web3.ethEstimateGas(mimicTransaction).send();
if (gasEstimate.hasError()) {
log.info("Contract error mimicTransaction: {}", gasEstimate.getError().getMessage());
throw new ValidationException(gasEstimate.getError().getMessage());
} else {
log.info("Gas estimate: {}", gasEstimate.getAmountUsed());
}

As you can see in the above code the transaction is mimiced to simulate how many gas units will be used when this transaction will be actually deployed. As the last parameter of createContracttransaction function, proxy_bin + encodedConstructor is passed. This represents the binary file of the proxy contract and the encodedConstructor value which we calculated in step 3. This is the data that is passed when deploying the contract. if you remember this encodedConstructor contains the encode value of the initialize function of the implementation contract.

Step 4. Deploying the Proxy contract

Now that we have estimated the gas units which will be consumed, let us now deploy the contract. For this you can use the following code.

// Create new Transaction
RawTransaction rawTransaction = RawTransaction.createContractTransaction(nonce, gasPrice, gasEstimate.getAmountUsed().multiply(BigInteger.valueOf(2)), BigInteger.ZERO, proxy_bin + encodedConstructor );
long chainId = Long.parseLong(chain.getNetworkId());

// Sign the Transaction
byte[] signedMessage = TransactionEncoder.signMessage(rawTransaction,chainId, credentials);
String hexValue = Numeric.toHexString(signedMessage);

// Send the Transaction
org.web3j.protocol.core.methods.response.EthSendTransaction transactionResponse = web3
.ethSendRawTransaction(hexValue).sendAsync().get();
if (transactionResponse.hasError()) {
log.info("Contract error transactionResponse: {}", transactionResponse.getError().getMessage());
throw new ValidationException(transactionResponse.getError().getMessage());
} else {
log.info("Transaction hash:{}",transactionResponse.getTransactionHash());
}

The RawTransaction.createContractTransaction uses the same parameters as the Transaction.createContractTransaction function used to mimic the transaction in step 3. Just to be on the safe side I have put a margin for the amount of gas used calculated in step 3 by multiplying the value calculated by 2. After getting the chain ID and signing the raw transaction with the private key, it is sent out to the network. If everything goes well you will receive a transaction hash that would represent the contract deployment transaction.

Summary

It is evident that deploying a proxy contract with web3 java is not as simple as using web3Js but it can be done. :)

Happy Coding !!

Resources

  1. Web3 java documentation — https://docs.web3j.io/4.10.0/

--

--