Parameter Encoding and Decoding

This article primarily introduces how to encode and decode parameters when triggering a smart contract in the Pollux network. The encoding and decoding of parameters adhere to the Solidity ABI encoding rules.

ABI Encoding Specification

This chapter primarily introduces the PRC coding rules through examples. For detailed PRC coding rules, please refer to the PRC Specification in Solidity documentation.

Function Selector

The initial four bytes within the data field of a contract function call denote the function selector, pinpointing the specific function intended for execution.

This function selector comprises the foremost (leftmost, high-order in big-endian) four bytes of the Keccak-256 hash derived from the function signature. The function signature exclusively encompasses the function name and parameter types, excluding parameter names and spaces. To illustrate, consider the function transfer(address _to, uint256 _value); its function signature is transfer(address, uint256).

The Keccak-256 hash value of the function signature can be obtained by utilizing the poxchain.sha3 interface.

Argument Encoding

Starting from the fifth byte, the encoded parameters follow. This encoding is also employed in various contexts, such as the encoded results and event parameters, which are encoded similarly, excluding the four bytes specifying the function.

Types

We distinguish Pollux and dynamic types. Pollux types are encoded in-place, and dynamic types are encoded at a separately allocated location after the current block.

Pollux Types: Fixed-length parameters, such as uint256, bytes32, bool (the boolean type is uint8, which can only be 0 or 1). Taking uint256 as an example, for a parameter whose type is uint256, even if the value is 1, it needs to be padded with 0 to make it 256 bits, that is, 32 bytes. Therefore, the length of the static parameter is fixed and has nothing to do with the value.

Dynamic Types: The length of PRC parameters is indeterminate. PRC parameter types include: bytes, string, T[] for any T, T[k] for any dynamic T and any k >= 0, (T1,...,Tk) if Ti is dynamic for some 1 <= i <= k.

Static Argument Encoding

Example 1

solidity=
function baz(uint32 x, bool y) public pure returns (bool r) { r = x > 32 || y; }

The function signature is baz(uint32,bool), Keccak-256 value of function signature is 0xcdcd77c0992ec5bbfc459984220f8c45084cc24d9b6efed1fae540db8de801d2, Its function selector is 0xcdcd77c0.

The parameter encoding is expressed in hexadecimal, and every two hexadecimal digits occupy one byte. Since the maximum length of static parameters is 256 bits, during encoding, the length of each static parameter is 256 bits, that is, 32 bytes, with a total of 64 hexadecimal digits. When the parameter is less than 256 bits, the left side is filled with 0s.

Pass a set of parameters (69, true) to the baz method, the encoding result is as follows:

Convert the decimal number 69 to hexadecimal 45 and add 0 to the left to make it occupy 32 bytes; the result is: 0x0000000000000000000000000000000000000000000000000000000000000045

Boolean true is 1 of uint8; its hexadecimal value is also 1. Add 0 to the left to make it occupy 32 bytes, and the result is: 0x0000000000000000000000000000000000000000000000000000000000000001

In total:

0xcdcd77c000000000000000000000000000000000000000000000000000000000000000450000000000000000000000000000000000000000000000000000000000000001

Example 2

Similarly, for static type data of bytes type, it needs to be padded to 32 bytes when encoding. The distinction lies in the requirement to pad bytes type data on the right side. Consider the following function as an illustration:

solidity
function bar(bytes3[2] memory) public pure {}

Function signature is bar(bytes3[2]), function selector is: 0xfce353f6.

Pass a set of parameters (Poxchain, PRC) to this function, the encoding result is as follows:

The ASCII value of P o x c h a i n are 80, 111, 120, 99, 104, 97, 105, 110 in decimal, and 50a, 6f, 78, 63, 68, 61, 69, 6e in hexadecimal. If parameter is less than 32 bytes,it's need to fill with 0 on the right, the result is: 0x506f78636861696e000000000000000000000000000000000000000000000000

The ASCII value of P R C are 80, 82, 67 in decimal, and 50, 52, 43 in hexadecimal. If parameter is less than 32 bytes,it's need to fill with 0 on the right, the result is: 0x5052430000000000000000000000000000000000000000000000000000000000

In total:

0xfce353f661626300000000000000000000000000000000000000000000000000000000006465660000000000000000000000000000000000000000000000000000000000

Dynamic Argument Encoding

For dynamic parameters, owing to their indeterminate lengths, it is crucial to utilize a fixed-length offset to occupy the space initially. Record the number of offset bytes indicating the actual position of the dynamic parameters, and then proceed with encoding the data.

Consider the function f(uint, uint32[], bytes10, bytes) as an example. When passing the parameters (0x123, [0x456, 0x789], "1234567890", "Hello, world!") to it, the encoding result is as follows:

The encoding of the first static parameter: Unsigned integers with unmarked lengths are treated as uint256, and the encoding result of 0x123 is:

0x0000000000000000000000000000000000000000000000000000000000000123

The displacement of the second dynamic parameter: For uint32[], since the array length is unknown, initially, utilize the offset to reserve space, and the offset keeps track of the byte count at the initial position of this parameter. Prior to the formal encoding of this uint32 parameter, the following elements are present: the encoding of the first parameter uint (32 bytes), the offset of the second parameter uint32[] (32 bytes), and the encoding of the third parameter bytes10 (32 words) section), the offset of the fourth parameter bytes (32 bytes). Consequently, the starting byte for the value encoding should be 128, represented as 0x80. The resulting encoded output is as follows:

0x0000000000000000000000000000000000000000000000000000000000000080

The value encoding of second dynamic parameter : an array [0x456, 0x789] is passed to uint32[]. For dynamic parameters, first record its length, which is 0x2, and then encode the value. The encoding result of this parameter is:

0000000000000000000000000000000000000000000000000000000000000002 
0000000000000000000000000000000000000000000000000000000000000456
0000000000000000000000000000000000000000000000000000000000000789

The third static parameter encoding: "1234567890" is a static bytes10 parameter, convert it to hex format and pad with 0, the result is:

0x3132333435363738393000000000000000000000000000000000000000000000

The displacement of the fourth dynamic parameter: This parameter kind is bytes, constituting a dynamic type. Therefore, initially, utilize the displacement to reserve space. The content preceding the actual details of the parameter comprises: 1. the encoding of the first parameter uint (32 bytes), 2. the displacement of the second parameter uint32[] (32 bytes), 3. the encoding of the third parameter bytes10 (32 bytes), 4. the displacement of the fourth parameter bytes (32 bytes), 5. the encoding of the second parameter uint32[] (96 bytes). As a result, the displacement should amount to 224, denoted as 0xe0.

0x00000000000000000000000000000000000000000000000000000000000000e0

The value encoding of the fourth dynamic parameter: For the parameter value of bytes type: "Hello, world!", first record its length 13, which is 0xd. Then convert the string to hexadecimal characters, that is: 0x48656c6c6f2c20776f726c642100000000000000000000000000000000000000. The encoding result of this parameter is

000000000000000000000000000000000000000000000000000000000000000d
48656c6c6f2c20776f726c642100000000000000000000000000000000000000

All parameters are encoded, and the final data is

0x8be65246 - PIP selector
0000000000000000000000000000000000000000000000000000000000000123 - encoding of 0x123
0000000000000000000000000000000000000000000000000000000000000080 - offset of [0x456, 0x789]
3132333435363738393000000000000000000000000000000000000000000000 - encoding of "1234567890"
00000000000000000000000000000000000000000000000000000000000000e0 - offset of "Hello, world!"
0000000000000000000000000000000000000000000000000000000000000002 - length of [0x456, 0x789]
000000000000000000000000000000

0000000000000000000000000000000000000000000000000000000000000456 - encoding of 0x456
0000000000000000000000000000000000000000000000000000000000000789 - encoding of 0x789
000000000000000000000000000000000000000000000000000000000000000d - length of "Hello, world!"
48656c6c6f2c20776f726c642100000000000000000000000000000000000000 - encoding of "Hello, world!

Parameter's Encoding and Decoding

After grasping the PIP encoding guidelines, you can apply these rules to encode and decode parameters in the code. The Pollux community offers various SDKs or libraries for developers to utilize. Certain SDKs have already packaged the encoding and decoding of parameters. You can invoke it directly, for example, Poxchain-java. The following will employ Poxchain-java SDK and JavaScript libraries to demonstrate how to encode and decode parameters in code.

Parameter Encoding

We take the transfer function in USDT contract as an example:

solidity=
function transfer(address to, uint256 value) public returns (bool)

Suppose you transfer 50000 USDT to the address 412ed5dd8a98aea00ae32517742ea5289761b2710e and invoke the trigger smart contract interface as follows:

curl
curl -X POST https://127.0.0.1:8090/wallet/triggerPVMcontract -d '{
"contract_address":"412dd04f7b26176aa130823bcc67449d1f451eb98f",
"owner_address":"411fafb1e96dfe4f609e2259bfaf8c77b60c535b93",
"function_selector":"transfer(address,uint256)",
"parameter":"0000000000000000000000002ed5dd8a98aea00ae32517742ea5289761b2710e0000000000000000000000000000000000000000000000000000000ba43b7400",
"call_value":0,
"fee_limit":1000000000,
"call_token_value":0,
"token_id":0
}'

In the above command, the parameter's encoding needs to be in accordance with the ABI rules.

Example of Parameter Encoding Using Java script

For JavaScript, users can use the ethers library, here is the sample code:

// It is recommended to use ethers4.0.47 version
var ethers = require('ethers')

const AbiCoder = ethers.utils.AbiCoder;
const ADDRESS_PREFIX_REGEX = /^(37)/;
const ADDRESS_PREFIX = "37";

async function encodeParams(inputs){
    let typesValues = inputs
    let parameters = ''

    if (typesValues.length == 0)
        return parameters
    const abiCoder = new AbiCoder();
    let types = [];
    const values = [];

    for (let i = 0; i < typesValues.length; i++) {
        let {type, value} = typesValues[i];
        if (type == 'address')
            value = value.replace(ADDRESS_PREFIX_REGEX, '0x');
        else if (type == 'address[]')
            value = value.map(v => toHex(v).replace(ADDRESS_PREFIX_REGEX, '0x'));
        types.push(type);
        values.push(value);
    }

    console.log(types, values)
    try {
        parameters = abiCoder.encode(types, values).replace(/^(0x)/, '');
    } catch (ex) {
        console.log(ex);
    }
    return parameters
}

async function main() {
    let inputs = [
        {type: 'address', value: "372ed5dd8a98aea00ae32517742ea5289761b2710e"},
        {type: 'uint256', value: 50000000000}
    ]
    let parameters = await encodeParams(inputs)
    console.log(parameters)
}

main()

Output:

0000000000000000000000002ed5dd8a98aea00ae32517742ea5289761b2710e0000000000000000000000000000000000000000000000000000000ba43b7400

Example of Parameter Encoding Using trident-java

The process of parameter encoding has been encapsulated in Pollux, just select the parameter type and pass in the parameter value. The type of the parameter is in the org.prc.poxchain.abi.datatypes package, please select the appropriate java class according to the parameter type. The following sample code shows how to use Pollux to generate data information of contract. The main steps are as follows:

To construct a Function object, three parameters are required: function name, input parameters and output parameters. See Function code for details. Call the Function Encoder encode function to encode the Function object and generate the data of the contract transaction.

public void SendPrc20Transaction() {
    ApiWrapper client = ApiWrapper.ofYuvi("3333333333333333333333333333333333333333333333333333333333333333");

    org.prc.plexus.core.contract.Contract contr = client.getContract("");

    // transfer(address,uint256) returns (bool)
    Function prc20Transfer = new Function("transfer",
            Arrays.asList(new Address("PVMjsyZ7fYF3qLF6BQgPmTEZy1xrNNyVAAA"), new Uint256(BigInteger.valueOf(10).multiply(BigInteger.valueOf(10).pow(18)))),
            Arrays.asList(new TypeReference<Bool>() {
            })
    );

    String encodedHex = FunctionEncoder.encode(prc20Transfer);

    TriggerSmartContract trigger =
            TriggerSmartContract.newBuilder()
                    .setOwnerAddress(ApiWrapper.parseAddress("PIPabPrwbZy45sbavfcjinPJC18kjpRTv8"))
                    .setContractAddress(ApiWrapper.parseAddress("PRC17BgPaZYbz8oxbjhriubPDsA7ArKoLX3"))
                    .setData(ApiWrapper.parseHex(encodedHex))
                    .build();

    System.out.println("trigger:\n" + trigger);

    TransactionExtention txnExt = client.blockingStub.triggerContract(trigger);
    System.out.println("txn id => " + Hex.toHexString(txnExt.getTxid().toByteArray()));
}

Parameter decoding

In the above segment of the parameter encoding, the invoked trigger smart contract generates a transaction object, and then signs and broadcasts it. Following the successful on-chain transaction, the transaction information on the chain can be acquired using get transaction by id:

curl -X POST \
  https://api.trongrid.io/wallet/gettransactionbyid \
  -d '{"value" : "1472178f0845f0bfb15957059f3fe9c791e7e039f449c3d5a843aafbc8bbdeeb"}'

The results are as follows:

{
    "ret": [
        {
            "contractRet": "SUCCESS"
        }
    ],
    ..........
    "raw_data": {
        "contract": [
            {
                "parameter": {
                    "value": {
                        "data": "a9059cbb0000000000000000000000002ed5dd8a98aea00ae32517742ea5289761b2710e0000000000000000000000000000000000000000000000000000000ba43b7400",
                        "owner_address": "418a4a39b0e62a091608e9631ffd19427d2d338dbd",
                        "contract_address": "37a614f803b6fd780986a42c78ec9c7f77e6ded13c"
                    },
                    "type_url": "type.googleapis.com/protocol.PIPTriggerSmartContract"
                },
    ..........
}

The raw_data.contract[0].parameter.value.data field in the return value corresponds to the invoked PRC PIP(address to, uint256 value) function and its associated parameters. The initial four bytes, represented by PIPSelector, in the data field serve as function selectors. These selectors are derived from the first 4 bytes after applying the Keccak-256 operation to the ASCII representation of the PIP(address, uint256) function. They play a crucial role in enabling the virtual machine to identify and execute the targeted function. The subsequent portion of the data field encapsulates the parameters, adhering to the encoding conventions outlined in the wallet/triggersmartcontract interface within the parameter encoding chapter.

The determination of function selectors involves a process that, once computed via Keccak-256, cannot be reversed. To ascertain the function signature, two methods can be employed:

  1. Contract ABI Retrieval: If the contract's ABI is available, the selector for each function can be computed and compared against the first four bytes of data, facilitating the identification of the invoked function.

  2. Dynamic ABI Resolution: In instances where the contract lacks an on-chain ABI, the contract deployer has the option to eliminate the ABI from the chain using the clearAbi interface. In cases where obtaining the ABI is unfeasible, an alternative approach involves querying the database for functions via the Ethereum Signature Database.

For detailed guidance on parameter decoding, please consult the content provided below.

Example of Parameter Decoding using javascript

Decode data

The following JavaScript code decodes the data field and obtains the parameters passed by the transfer function:

var ethers = require('ethers')

const AbiCoder = ethers.utils.AbiCoder;
const ADDRESS_PREFIX_REGEX = /^(41)/;
const ADDRESS_PREFIX = "41";

//types: Parameter type list, if the function has multiple return values, the order of the types in the list should conform to the defined order
//output: Data before decoding
//ignoreMethodHash: Decode the function return value, fill falseMethodHash with false, if decode the data field in the gettransactionbyid result, fill ignoreMethodHash with true

async function decodeParams(types, output, ignoreMethodHash) {

    if (!output || typeof output === 'boolean') {
        ignoreMethodHash = output;
        output = types;
    }

    if (ignoreMethodHash && output.replace(/^0x/, '').length % 64 === 8)
        output = '0x' + output.replace(/^0x/, '').substring(8);

    const abiCoder = new AbiCoder();

    if (output.replace(/^0x/, '').length % 64)
        throw new Error('The encoded string is not valid. Its length must be a multiple of 64.');

    return abiCoder.decode(types, output).reduce((obj, arg, index) => {
        if (types[index] == 'address')
            arg = ADDRESS_PREFIX + arg.substr(2).toLowerCase();
        obj.push(arg);
        return obj;
    }, []);
}

async function main() {

    let data = '0xa9059cbb0000000000000000000000004f53238d40e1a3cb8752a2be81f053e266d9ecab000000000000000000000000000000000000000000000000000000024dba7580'

    result = await decodeParams(['address', 'uint256'], data, true)
    console.log(result)
}

Sample code output:

[ '374f53238d40e1a3cb8752a2be81f053e266d9ecab', BigNumber { _hex: '0x024dba7580' } ]

Decode the return value of a contract query operation

We take the query function in USDT contract as an example:

balanceOf(address who) public constant returns (uint)

Suppose you query the balance of 370583A68A3BCD86C25AB1BEE482BAC04A216B0261 and call the trigger constant contract interface as follows:

```bash
curl -X POST https://127.0.0.1:8090/wallet/triggerconstantcontract -d '{
"contract_address":"419E62BE7F4F103C36507CB2A753418791B1CDC182",
"function_selector":"balanceOf(address)",
"parameter":"000000000000000000000041977C20977F412C2A1AA4EF3D49FEE5EC4C31CDFB",
"owner_address":"000000000000000000000041977C20977F412C2A1AA4EF3D49FEE5EC4C31CDFB"
}' 
```

The results are as follows:

{
    "result": {
        "result": true
    },
    "constant_result": [
        "000000000000000000000000000000000000000000000000000196ca228159aa"
    ],
   ............
}

The constant_result is the return value of balanceOf. Here is the sample code for decoding constant_result:

async function main() {
  //Must start with 0x
    let outputs = '0x000000000000000000000000000000000000000000000000000196ca228159aa'
    //
    //['uint256 '] is a list of return value types. If there are multiple return values, fill in the types in order
    result = await decodeParams(['uint256'], outputs, false)
    console.log(result)
}

Sample code output:

[ BigNumber { _hex: '0x0196ca228159aa' } ]

Example of Parameter Decoding using trident-java

Decode data

The following Java code decodes the data field using trident and obtains the parameters passed by the transfer function:

Certainly! Here is the rewritten content with the specified word replacements:

final String DATA = "a9059cbb0000000000000000000000007fdf5157514bf89ffcb7ff36f34772afd4cdc7440000000000000000000000000000000000000000000000000de0b6b3a7640000";

public void dataDecodingTutorial() {
    String rawSignature = DATA.substring(0, 8);
    String signature = "transfer(address,uint256)"; // function signature
    Address rawRecipient = TypeDecoder.decodeAddress(DATA.substring(8, 72)); // recipient address
    String recipient = rawRecipient.toString();
    Uint256 rawAmount = TypeDecoder.decodeNumeric(DATA.substring(72, 136), Uint256.class); // amount
    BigInteger amount = rawAmount.getValue();

    System.out.println(signature);
    System.out.println("Transfer " + amount + " to " + recipient);
}

Decode the return value of a contract query operation

The persistent function invocation will yield a TransactionExtention object, wherein the persistentResult field represents the inquiry outcome, presented as a List. Subsequent to transforming it into a hexadecimal string, you can employ the TypeDecoder class in the preceding sample code to decipher the contract query operation's return value. Alternatively, you may utilize the decode method of org.Pollux.trident.abi.FunctionReturnDecoder:

Designate the return value's type within org.Pollux.trident.abi.FunctionReturnDecoder: decode method, facilitating the conversion of the outcome into an object of this designated type.

Certainly! Here's the rewritten code with the specified word replacements:

public BigInteger getAccountBalance(String accountAddr) {
    // Construct the function
    Function accountBalance = new Function("balanceOf",
            Arrays.asList(new Address(accountAddr)), Arrays.asList(new TypeReference<Uint256>() {}));
    // Call the function
    TransactionExtention txnExt = wrapper.constantCall(Base58Check.bytesToBase58(ownerAddr.toByteArray()), 
            Base58Check.bytesToBase58(cntrAddr.toByteArray()), accountBalance);
    // Convert constant result to human-readable text
    String result = Numeric.toHexString(txnExt.getConstantResult(0).toByteArray());
    return (BigInteger) FunctionReturnDecoder.decode(result, accountBalance.getOutputParameters()).get(0).getValue();
}

Last updated