Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/tuple array fix #102

Merged
merged 5 commits into from
Jun 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 43 additions & 5 deletions src/main/java/net/osslabz/evm/abi/decoder/AbiDecoder.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package net.osslabz.evm.abi.decoder;

import lombok.Getter;
import net.osslabz.evm.abi.definition.AbiDefinition;
import org.bouncycastle.util.encoders.Hex;

Expand All @@ -14,11 +15,11 @@
import java.util.List;
import java.util.Map;

@Getter
public class AbiDecoder {

private final AbiDefinition abi;

Map<String, AbiDefinition.Entry> methodSignatures = new HashMap<>();
protected final AbiDefinition abi;
protected final Map<String, AbiDefinition.Entry> methodSignatures = new HashMap<>();

public AbiDecoder(String abiFilePath) throws IOException {
this.abi = AbiDefinition.fromJson(new String(Files.readAllBytes(Paths.get(abiFilePath)), StandardCharsets.UTF_8));
Expand All @@ -37,12 +38,11 @@ private void init() {
}
}


public DecodedFunctionCall decodeFunctionCall(String inputData) {
if (inputData == null || (inputData.startsWith("0x") && inputData.length() < 10) || inputData.length() < 8) {
throw new IllegalArgumentException("Can't decode invalid input '" + inputData + "'.");
}
String inputNoPrefix = inputData.startsWith("0x") ? inputData.substring(2) : inputData;
String inputNoPrefix = cleanup(inputData);

String methodBytes = inputNoPrefix.substring(0, 8);

Expand Down Expand Up @@ -112,4 +112,42 @@ public List<DecodedFunctionCall> decodeFunctionsCalls(String inputData) {
}
return resolvedCalls;
}


public DecodedFunctionCall decodeLogEvent(List<String> topics, String data) {
if (topics.isEmpty()) {
throw new IllegalArgumentException("Log.topics is empty");
}
String funcSignature = cleanup(topics.get(0));
AbiDefinition.Entry abiEntry = methodSignatures.get(funcSignature);
if (abiEntry == null) {
throw new IllegalStateException("Couldn't find method with signature " + funcSignature);
} else {
if (abiEntry instanceof AbiDefinition.Event) {
AbiDefinition.Event abiEvent = (AbiDefinition.Event) abiEntry;
List<?> decoded = abiEvent.decode(hexBytes(data), topics
.stream()
.map(AbiDecoder::hexBytes)
.toArray(byte[][]::new));
List<DecodedFunctionCall.Param> params = new ArrayList<>(abiEvent.inputs.size());
for (int i = 0; i < decoded.size(); i++) {
AbiDefinition.Entry.Param paramDefinition = abiEvent.inputs.get(i);
DecodedFunctionCall.Param param = new DecodedFunctionCall.Param(paramDefinition.getName(), paramDefinition.getType()
.getName(), decoded.get(i));
params.add(param);
}
return new DecodedFunctionCall(abiEvent.name, params);
} else {
throw new IllegalArgumentException("Input data is not a event, it's of type '" + abiEntry.type + "'.");
}
}
}

private static String cleanup(String hex) {
return hex.startsWith("0x") ? hex.substring(2) : hex;
}

private static byte[] hexBytes(String hex) {
return Hex.decode(cleanup(hex));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

@Data
public class DecodedFunctionCall {

private String name;
private Map<String, Param> params;

Expand All @@ -28,6 +29,10 @@ public Param getParam(String paramName) {
return this.params.get(paramName.toLowerCase());
}

public Map<String, Param> params() {
return this.params;
}

public Collection<Param> getParams() {
return this.params.values();
}
Expand Down Expand Up @@ -69,4 +74,4 @@ public String toString() {
return this.getClass().getName() + "(name=" + this.name + ", type=" + this.getType() + ", value=" + valueString + ")";
}
}
}
}
36 changes: 22 additions & 14 deletions src/main/java/net/osslabz/evm/abi/definition/AbiDefinition.java
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,15 @@ public ParamSanitizer() {
@Override
public Entry.Param convert(Entry.Param param) {
if (param.type instanceof SolidityType.TupleType) {
for (Entry.Component c : param.components) {
((SolidityType.TupleType) param.type).types.add(c.getType());
for (Entry.Param c : param.components) {
((SolidityType.TupleType) param.type).getTypes().add(c.getType());
}
} else if (param.type instanceof SolidityType.ArrayType) {
SolidityType.ArrayType arrayType = (SolidityType.ArrayType) param.type;
if (arrayType.elementType instanceof SolidityType.TupleType) {
for (AbiDefinition.Entry.Param c : param.components) {
((SolidityType.TupleType) arrayType.elementType).getTypes().add(c.getType());
}
}
}
return param;
Expand Down Expand Up @@ -164,17 +171,24 @@ public String formatSignature() {
StringBuilder paramsTypes = new StringBuilder();
if (inputs != null) {
for (Param param : inputs) {
String type = param.type.getCanonicalName();
if (param.type instanceof SolidityType.TupleType) {
type = "(" + StringUtils.join(param.getComponents().stream().map(Component::getType).collect(Collectors.toList()), ",") + ")";
}
String type = formatParamSignature(param);
paramsTypes.append(type).append(",");
}
}

return format("%s(%s)", name, stripEnd(paramsTypes.toString(), ","));
}

public String formatParamSignature(Param param) {
String type = param.type.getCanonicalName();
if (param.type instanceof SolidityType.TupleType) {
type = "(" + StringUtils.join(param.getComponents().stream().map(this::formatParamSignature).collect(Collectors.toList()), ",") + ")";
} else if (param.type instanceof SolidityType.ArrayType && ((SolidityType.ArrayType)param.type).elementType instanceof SolidityType.TupleType) {
type = "(" + StringUtils.join(param.getComponents().stream().map(this::formatParamSignature).collect(Collectors.toList()), ",") + ")[]";
}
return type;
}

public byte[] fingerprintSignature() {
return HashUtil.hashAsKeccak(formatSignature().getBytes());
}
Expand All @@ -192,12 +206,6 @@ public enum Type {
error
}

@Data
public static class Component {
private String name;
private SolidityType type;
}

@Data
@JsonInclude(Include.NON_NULL)
@JsonDeserialize(converter = ParamSanitizer.class) // invoked after class is fully deserialized
Expand All @@ -206,7 +214,7 @@ public static class Param {
private String name;
private SolidityType type;

private List<Component> components;
private List<Param> components;

public static List<?> decodeList(List<Param> params, byte[] encoded) {
List<Object> result = new ArrayList<>(params.size());
Expand Down Expand Up @@ -381,4 +389,4 @@ public String toString() {
return format("error %s(%s);", name, join(inputs, ", "));
}
}
}
}
19 changes: 17 additions & 2 deletions src/main/java/net/osslabz/evm/abi/definition/SolidityType.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonValue;
import lombok.Getter;
import net.osslabz.evm.abi.util.ByteUtil;

import java.lang.reflect.Array;
Expand All @@ -14,6 +15,10 @@

public abstract class SolidityType {
private final static int Int32Size = 32;
/**
* -- GETTER --
* The type name as it was specified in the interface description
*/
protected String name;

public SolidityType(String name) {
Expand Down Expand Up @@ -463,13 +468,14 @@ public byte[] encode(Object value) {

@Override
public Object decode(byte[] encoded, int offset) {
return Boolean.valueOf(((Number) super.decode(encoded, offset)).intValue() != 0);
return ((Number) super.decode(encoded, offset)).intValue() != 0;
}
}

@Getter
public static class TupleType extends SolidityType {

List<SolidityType> types = new ArrayList<>();
private final List<SolidityType> types = new ArrayList<>();

public TupleType() {
super("tuple");
Expand All @@ -480,6 +486,15 @@ public boolean isDynamicType() {
return containsDynamicTypes();
}

@Override
public int getFixedSize() {
if (isDynamicType()) {
return super.getFixedSize();
} else {
return types.stream().mapToInt(SolidityType::getFixedSize).sum();
}
}

private boolean containsDynamicTypes(){
return types.stream().anyMatch(SolidityType::isDynamicType);
}
Expand Down
20 changes: 20 additions & 0 deletions src/main/java/net/osslabz/evm/abi/util/FileUtil.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package net.osslabz.evm.abi.util;

import lombok.experimental.UtilityClass;

import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Objects;

@UtilityClass
public class FileUtil {

public String readFileIntoString(String path) throws URISyntaxException, IOException {
URL resource = Objects.requireNonNull(FileUtil.class.getClassLoader().getResource(path));
return new String(Files.readAllBytes(Paths.get(resource.toURI())), StandardCharsets.UTF_8);
}
}
118 changes: 117 additions & 1 deletion src/test/java/net/osslabz/evm/abi/AbiDecoderTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,16 @@
import lombok.extern.slf4j.Slf4j;
import net.osslabz.evm.abi.decoder.AbiDecoder;
import net.osslabz.evm.abi.decoder.DecodedFunctionCall;
import net.osslabz.evm.abi.definition.AbiDefinition;
import net.osslabz.evm.abi.util.FileUtil;
import org.bouncycastle.util.encoders.Hex;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

import java.io.File;
import java.io.IOException;
import java.math.BigInteger;
import java.net.URISyntaxException;
import java.util.Arrays;
import java.util.List;

Expand Down Expand Up @@ -162,4 +166,116 @@ public void testDecodeFunctionCallTupleContainingDynamicTypes() throws IOExcepti
log.debug("-------------------------");
}
}
}

@Test
public void testTupleArrayParamsSignature() {
String funcName = "commitBlocks";
AbiDecoder decoder = new AbiDecoder(this.getClass()
.getClassLoader()
.getResourceAsStream("abiFiles/ZkSync.json"));
AbiDefinition.Entry func = decoder.getAbi()
.stream()
.filter(e -> funcName.equals(e.name))
.findAny()
.orElse(null);
Assertions.assertNotNull(func);
Assertions.assertEquals("commitBlocks((uint32,uint64,bytes32,uint256,bytes32,bytes32),(bytes32,bytes,uint256,(bytes,uint32)[],uint32,uint32)[])", func.formatSignature());
Assertions.assertEquals("45269298", Hex.toHexString(func.encodeSignature()));
}

@Test
public void testTupleArrayParamsDecode() throws URISyntaxException, IOException {
String funcName = "commitBlocks";
AbiDecoder decoder = new AbiDecoder(this.getClass()
.getClassLoader()
.getResourceAsStream("abiFiles/ZkSync.json"));
DecodedFunctionCall decode = decoder.decodeFunctionCall(FileUtil.readFileIntoString("abiFiles/zkSync-input/input_0xe35a7dceb1536dfbd819ab6f756e4dcb19ea09541df54abf0f40064ba1163981"));
Assertions.assertNotNull(decode);
Assertions.assertEquals(funcName, decode.getName());
Assertions.assertEquals(2, decode.getParams().size());
for (DecodedFunctionCall.Param param : decode.getParams()) {
if ("_lastCommittedBlockData".equals(param.getName())) {
Object[] value = Assertions.assertInstanceOf(Object[].class, param.getValue());
Assertions.assertEquals(6, value.length);
Assertions.assertArrayEquals(new Object[]{
new BigInteger("432775"),
new BigInteger("0"),
"0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470",
new BigInteger("1710501302"),
"0x071035b8b917294d5b81ae4c5124ecc2e4d5782c50cb9cfc7d037f7f8af7d791",
"0x316e6378abf242e3494c50d6afc3e929d9be0ad9cfdc5ebaa34ae7b8456dd3f6"
}, value);
} else if ("_newBlocksData".equals(param.getName())) {
Object[] value = Assertions.assertInstanceOf(Object[].class, param.getValue());
Assertions.assertEquals(10, value.length);
}
}
}

@Test
public void testTupleParamsSignature() {
String funcName = "exactInput";
AbiDecoder decoder = new AbiDecoder(this.getClass()
.getClassLoader()
.getResourceAsStream("abiFiles/UniswapV3Router.json"));
AbiDefinition.Entry func = decoder.getAbi()
.stream()
.filter(e -> funcName.equals(e.name))
.findAny()
.orElse(null);
Assertions.assertNotNull(func);
Assertions.assertEquals("exactInput((bytes,address,uint256,uint256,uint256))", func.formatSignature());
Assertions.assertEquals("c04b8d59", Hex.toHexString(func.encodeSignature()));
}

@Test
public void testUniswapV3Router() throws URISyntaxException, IOException {
String funcName = "exactInput";
AbiDecoder decoder = new AbiDecoder(this.getClass()
.getClassLoader()
.getResourceAsStream("abiFiles/UniswapV3Router.json"));
DecodedFunctionCall decode = decoder.decodeFunctionCall(FileUtil.readFileIntoString("abiFiles/uniswapV3Router-input/input_0xeb154fb38972106bfc0e9bce28130379c44d80be292de775e0f43e2c861e0f48"));
Assertions.assertNotNull(decode);
Assertions.assertEquals(funcName, decode.getName());
Assertions.assertEquals(1, decode.getParams().size());
DecodedFunctionCall.Param param = decode.getParams().stream().findFirst().orElse(null);
Assertions.assertNotNull(param);
Object[] value = Assertions.assertInstanceOf(Object[].class, param.getValue());
Assertions.assertEquals(5, value.length);
Assertions.assertArrayEquals(new Object[]{
"0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000bb8514910771af9ca656af840dff83e8264ecf986ca000bb8dac17f958d2ee523a2206206994597c13d831ec7",
"0x9e3df1cd92386519734558178e535e0460b10ab6",
new BigInteger("1692606069"),
new BigInteger("500000000000000"),
new BigInteger("843698")
}, value);
}

@Test
public void testUSDTTransferLog() {
AbiDecoder decoder = new AbiDecoder(this.getClass()
.getClassLoader()
.getResourceAsStream("abiFiles/TetherToken.json"));
DecodedFunctionCall log = decoder.decodeLogEvent(Arrays.asList(
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
"0x000000000000000000000000abea9132b05a70803a4e85094fd0e1800777fbef",
"0x00000000000000000000000047c27dea4d3625169a3dcad8c1fc4375e1c0a8fc"),
"0x000000000000000000000000000000000000000000000000000000000edc4c64");
Assertions.assertEquals("Transfer", log.getName());
Assertions.assertEquals(3, log.getParams().size());
Assertions.assertEquals("0xabea9132b05a70803a4e85094fd0e1800777fbef", log.params().get("from").getValue());
Assertions.assertEquals("0x47c27dea4d3625169a3dcad8c1fc4375e1c0a8fc", log.params().get("to").getValue());
Assertions.assertEquals(BigInteger.valueOf(249318500), log.params().get("value").getValue());
}

@Test
public void testLogWrongInput() {
AbiDecoder decoder = new AbiDecoder(this.getClass()
.getClassLoader()
.getResourceAsStream("abiFiles/TetherToken.json"));
Assertions.assertThrows(IllegalStateException.class, () -> decoder.decodeLogEvent(Arrays.asList("0xefef619ae4a542a2b8810b4efeccd8478bd683e985354ee31dd2d644aff6d0ca",
"0x000000000000000000000000a5ece9bab9a0e56ad63ad0734033c944eeb00e1a",
"0x0000000000000000000000000000000000000000000000000000000000000000"),
"0x0000000000000000000000000000000000000000000000000020affce72f5800"));
}
}
Loading
Loading