Parameter encoding and decoding
Encode function selectors and arguments for raw HTTP API calls; decode return values and event data using the Solidity ABI specification.
Prerequisites
This article mainly introduces how to encode and decode parameters when triggering a smart contract in the Tron network. The encoding and decoding of parameters follows the Solidity ABI encoding rules.
ABI encoding specification
This chapter mainly introduces the ABI coding rules through examples. For detailed ABI coding rules, please refer to the ABI Specification in Solidity documentation.
Function selector
The first four bytes of the data field of a contract function call are the function selector, specifying the function to be called.
The function selector is the first (left, high-order in big-endian) four bytes of the Keccak-256 hash of the function signature. The function signature contains only the function name and parameter types, without parameter names and spaces. Take tranfer(address to, uint256 value) as an example, its function signature is transfer(address, uint256).
You can get the Keccak-256 hash value of the function signature by calling the tronweb.sha3 interface.
Argument encoding
Starting from the fifth byte, the encoded arguments follow. This encoding is also used in other places, e.g. the return values and also event arguments are encoded in the same way, without the four bytes specifying the function.
Types
We distinguish static and dynamic types. Static types are encoded in-place and dynamic types are encoded at a separately allocated location after the current block.
- Static 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, so the length of the static parameter is fixed and has nothing to do with the value.
- Dynamic Types:The length of dynamic parameters is indeterminate. Dynamic 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.
TRON address encoding — two equivalent formsFor the
addresstype, the canonical EVM ABI encoding is the 20-byte address hash padded to 32 bytes with 12 leading zero bytes (no0x41prefix). This is what TronWeb, Trident, and the examples on this page produce.Some TRON HTTP API examples and legacy tooling emit a TRON-flavored form instead: 11 leading zero bytes + the
0x41network prefix byte + the 20-byte address hash (still 32 bytes total). The two forms are functionally equivalent — the EVM reads the last 20 bytes of the 32-byte word as the address value, so the leading padding (whether all-zero or with a0x41inside) does not change the resulting address. If you see...004115...in an example where you expected...0015..., both decode to the same on-chain address.
Static argument encoding
Example 1
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 occupies 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, and add 0 to the left to make it occupy 32 bytes, and the result is:
0x0000000000000000000000000000000000000000000000000000000000000001
In total:
0xcdcd77c000000000000000000000000000000000000000000000000000000000000000450000000000000000000000000000000000000000000000000000000000000001Example 2
Similarly, for static type data of bytes type, it needs to be padded to 32 bytes when encoding. The difference is that bytes type data needs to be padded on the right. Take the following function as an example
function bar(bytes3[2] memory) public pure {}Function signature is bar(bytes3[2]), function selector is: 0xfce353f6.
Pass a set of parameters (abc, def) to this function, the encoding result is as follows:
- The ASCII value of a b c are 97, 98, 99 in decimal, and 61, 62, 63 in hexadecimal. If parameter is less than 32 bytes,it's need to fill with 0 on the right, the result is: 0x6162630000000000000000000000000000000000000000000000000000000000
- The ASCII value of d e f are 100, 101, 102 in decimal, and 64, 65, 66 in hexadecimal. If parameter is less than 32 bytes,it's need to fill with 0 on the right, the result is: 0x6465660000000000000000000000000000000000000000000000000000000000
In total:
0xfce353f661626300000000000000000000000000000000000000000000000000000000006465660000000000000000000000000000000000000000000000000000000000Dynamic argument encoding
For dynamic parameters, due to their indeterminate lengths, it is necessary to use a fixed-length offset to occupy the space first, and record the number of offset bytes of the actual position of the dynamic parameters, and then encode the data.
Take the function f(uint,uint32[],bytes10,bytes) as an example, when passing the parameters (0x123, [0x456, 0x789], "1234567890", "Hello, world!") to it, the encoding The result is as follows:
-
The first static parameter encoding: uints with unmarked lengths are regarded as uint256, and the encoding result of 0x123 is:
0x0000000000000000000000000000000000000000000000000000000000000123 -
The offset of the second dynamic parameter: For uint32[], since the array length is unknown,first use the offset to occupy the place, and the offset records the number of bytes at the starting position of this parameter. Before the formal encoding of this uint32 parameter, there are: 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), therefore, the start bytes of the value encoding should be 128, which is 0x80, and the encoding result is:
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 offset of the fourth dynamic parameter: This parameter type is
bytes, it is a dynamic type, so first use the offset to occupy the place. The content before the actual content of the parameter is: 1. the encoding of the first parameter uint (32 bytes), 2. the offset of the second parameter uint32[] (32 bytes), 3. the encoding of the third parameter bytes10 (32 bytes) ), 4. the offset of the fourth parameter bytes (32 bytes), 5. the encoding of the second parameter uint32[] (96 bytes). So the offset should be 224 which is 0xe0.0x00000000000000000000000000000000000000000000000000000000000000e0 -
The value encoding of fourth dynamic parameter : For the parameter value of
bytestype :"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 48656c6c6f2c20776f726c642100000000000000000000000000000000000000All parameters are encoded, and the final data is
0x8be65246 - function selector 0000000000000000000000000000000000000000000000000000000000000123 - encoding of 0x123 0000000000000000000000000000000000000000000000000000000000000080 - offset of [0x456, 0x789] 3132333435363738393000000000000000000000000000000000000000000000 - encoding of "1234567890" 00000000000000000000000000000000000000000000000000000000000000e0 - offset of "Hello, world!" 0000000000000000000000000000000000000000000000000000000000000002 - length of [0x456, 0x789] 0000000000000000000000000000000000000000000000000000000000000456 - encoding of 0x456 0000000000000000000000000000000000000000000000000000000000000789 - encoding of 0x789 000000000000000000000000000000000000000000000000000000000000000d - length of "Hello, world!" 48656c6c6f2c20776f726c642100000000000000000000000000000000000000 - encoding of "Hello, world!"
Parameter's encoding and decoding
After understanding the ABI encoding rules, you can encode and decode parameters in application code. The following examples use TronWeb for JavaScript and Trident for Java.
Parameter encoding
We take the transfer function in USDT contract as an example:
function transfer(address to, uint256 value) public returns (bool);Suppose you transfer 50000 USDT to the address 412ed5dd8a98aea00ae32517742ea5289761b2710e, and call the triggersmartcontract interface as follows:
BASE_URL=https://api.shasta.trongrid.io # example — replace with any TRON node (TronGrid, third-party, or self-hosted)
curl -X POST ${BASE_URL}/wallet/triggersmartcontract -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 javascript
For JavaScript, use TronWeb's ABI utilities. Install TronWeb first:
npm install tronwebencodeParams() converts a TRON Base58Check address or a hex address beginning with 41 to the address format required by ABI encoding:
const { utils } = require('tronweb');
const parameters = utils.abi.encodeParams(
['address', 'uint256'],
['412ed5dd8a98aea00ae32517742ea5289761b2710e', '50000000000']
).replace(/^0x/, '');
console.log(parameters);Output:
0000000000000000000000002ed5dd8a98aea00ae32517742ea5289761b2710e0000000000000000000000000000000000000000000000000000000ba43b7400Example of parameter encoding using trident-java
The process of parameter encoding has been encapsulated in trident, just select the parameter type and pass in the parameter value. The type of the parameter is in the org.tron.trident.abi.datatypes package, please select the appropriate java class according to the parameter type. The following sample code shows how to use trident to generate data information of contract. The main steps are as follows:
- To construct a
Functionobject, three parameters are required: function name, input parameters and output parameters. See Function code for details. - Call the
FunctionEncoder.encodefunction to encode theFunctionobject and generate thedataof the contract transaction.
Before running the example, set the private key of a Nile test account and the address of a TRC-20 contract deployed on Nile:
export NILE_PRIVATE_KEY="<64-character-hex-private-key>"
export NILE_TRC20_CONTRACT_ADDRESS="<deployed-Nile-TRC20-contract-address>"
Private key safetyUse only a Nile test account that holds no real assets. Never store a mainnet private key in source code, shell history, or a version-control repository. Use a secret-management service in production.
import java.math.BigInteger;
import java.util.Arrays;
import org.bouncycastle.util.encoders.Hex;
import org.tron.trident.abi.FunctionEncoder;
import org.tron.trident.abi.TypeReference;
import org.tron.trident.abi.datatypes.Address;
import org.tron.trident.abi.datatypes.Bool;
import org.tron.trident.abi.datatypes.Function;
import org.tron.trident.abi.datatypes.generated.Uint256;
import org.tron.trident.core.ApiWrapper;
import org.tron.trident.proto.Contract.TriggerSmartContract;
import org.tron.trident.proto.Response.TransactionExtention;
public class EncodeTrc20Parameters {
public static void main(String[] args) {
String privateKey = requirePrivateKey("NILE_PRIVATE_KEY");
String contractAddress = requireTronAddress("NILE_TRC20_CONTRACT_ADDRESS");
ApiWrapper client = ApiWrapper.ofNile(privateKey);
try {
// transfer(address,uint256) returns (bool)
Function trc20Transfer = new Function(
"transfer",
Arrays.asList(
new Address("TVjsyZ7fYF3qLF6BQgPmTEZy1xrNNyVAAA"),
new Uint256(BigInteger.TEN.multiply(BigInteger.TEN.pow(6)))),
Arrays.asList(new TypeReference<Bool>() {}));
String encodedHex = FunctionEncoder.encode(trc20Transfer);
String ownerAddress = client.keyPair.toBase58CheckAddress();
TriggerSmartContract trigger = TriggerSmartContract.newBuilder()
.setOwnerAddress(ApiWrapper.parseAddress(ownerAddress))
.setContractAddress(ApiWrapper.parseAddress(contractAddress))
.setData(ApiWrapper.parseHex(encodedHex))
.build();
TransactionExtention txnExt = client.blockingStub.triggerContract(trigger);
if (!txnExt.getResult().getResult()) {
throw new IllegalStateException(
"triggerContract failed: "
+ txnExt.getResult().getMessage().toStringUtf8());
}
System.out.println("encoded data => " + encodedHex);
System.out.println("txn id => " + Hex.toHexString(txnExt.getTxid().toByteArray()));
} finally {
client.close();
}
}
private static String requirePrivateKey(String name) {
String value = requireEnv(name);
if (!value.matches("(?i)^[0-9a-f]{64}$")) {
throw new IllegalArgumentException(name + " must be a 64-character hex private key");
}
return value;
}
private static String requireTronAddress(String name) {
String value = requireEnv(name);
try {
if (!value.startsWith("T") || ApiWrapper.parseAddress(value).size() != 21) {
throw new IllegalArgumentException();
}
} catch (RuntimeException e) {
throw new IllegalArgumentException(name + " must be a valid Base58Check TRON address", e);
}
return value;
}
private static String requireEnv(String name) {
String value = System.getenv(name);
if (value == null || value.trim().isEmpty()) {
throw new IllegalStateException("Missing required environment variable: " + name);
}
return value.trim();
}
}Parameter decoding
After calling /wallet/triggersmartcontract, sign the returned transaction and broadcast it. Once the transaction is on-chain, query its details through /wallet/gettransactionbyid:
BASE_URL=https://api.trongrid.io # example — replace with any TRON node (TronGrid, third-party, or self-hosted)
curl -X POST \
${BASE_URL}/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": "41a614f803b6fd780986a42c78ec9c7f77e6ded13c"
},
"type_url": "type.googleapis.com/protocol.TriggerSmartContract"
},
..........
}The raw_data.contract[0].parameter.value.data field in the return value is the called transfer(address to, uint256 value) function and its parameters. The first four bytes a9059cbb of the data field are function selectors, which come from the first 4 bytes after Keccak-256 operation of transfer(address, uint256) in ASCII format, which is used for the virtual machine to address the function. The latter part is the parameter, which is the same as the parameter in the wallet/triggersmartcontract interface in the parameter encoding chapter.
Function selector,the first four bytes of data, obtained by Keccak-256, cannot be reversed. The function signature can be obtained in two ways:
- If the contract ABI can be obtained, the selector of each contract function can be calculated and compared with the first four bytes of data to judge the function
- The contract generated by the contract may not have ABI on the chain. The contract deployer can also clear the ABI on the chain through the
clearAbiinterface. When the ABI cannot be obtained, you can try to query the functions in the database throughEthereum Signature Database.
For parameters decode please refer to below content.
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:
const { utils } = require('tronweb');
const data = '0xa9059cbb0000000000000000000000004f53238d40e1a3cb8752a2be81f053e266d9ecab000000000000000000000000000000000000000000000000000000024dba7580';
const result = utils.abi.decodeParams(
[],
['address', 'uint256'],
data,
true // data includes the four-byte function selector
);
console.log(result[0], result[1].toString());Sample code output:
414f53238d40e1a3cb8752a2be81f053e266d9ecab 9894000000Decode 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 410583A68A3BCD86C25AB1BEE482BAC04A216B0261 and call the triggerconstantcontract interface as follows:
BASE_URL=https://api.shasta.trongrid.io # example — replace with any TRON node (TronGrid, third-party, or self-hosted)
curl -X POST ${BASE_URL}/wallet/triggerconstantcontract -d '{
"contract_address":"419E62BE7F4F103C36507CB2A753418791B1CDC182",
"function_selector":"balanceOf(address)",
"parameter":"0000000000000000000000000583A68A3BCD86C25AB1BEE482BAC04A216B0261",
"owner_address":"41977C20977F412C2A1AA4EF3D49FEE5EC4C31CDFB"
}'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:
const { utils } = require('tronweb');
const output = '0x000000000000000000000000000000000000000000000000000196ca228159aa';
const [balance] = utils.abi.decodeParams([], ['uint256'], output);
console.log(balance.toString());Sample code output:
447269883173290Example 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:
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 constant function call will return a TransactionExtention object, in which the constantResult field is the query result, which is a List<ByteString>. After converting it to a hex string, you can use the TypeDecoder class in the above sample code to decode the return value of the contract query operation. Or you can also use the decode method of org.tron.trident.abi.FunctionReturnDecoder:
Specify the type of the return value in the org.tron.trident.abi.FunctionReturnDecoder: decode method, and it can convert the result to an object of this type.
public BigInteger balanceOf(String accountAddr) {
//construct the funtion
Function balanceOf = 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()), balanceOf);
//Convert constant result to human readable text
String result = Numeric.toHexString(txnExt.getConstantResult(0).toByteArray());
return (BigInteger)FunctionReturnDecoder.decode(result, balanceOf.getOutputParameters()).get(0).getValue();
}Related resources
- Upgrading — Smart contracts are automated code segments that run on a blockchain
- Contract To Contract Calls — This document outlines how to perform contract-to-contract calls using Solidity...
Updated 12 days ago