Skip to content

Commit

Permalink
Merge pull request #102 from osslabz/feat/tuple-array-fix
Browse files Browse the repository at this point in the history
Feat/tuple array fix
  • Loading branch information
rvullriede authored Jun 21, 2024
2 parents 1c53715 + 49fc6dc commit 8065c2f
Show file tree
Hide file tree
Showing 11 changed files with 2,771 additions and 23 deletions.
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

0 comments on commit 8065c2f

Please sign in to comment.