Skip to content
This repository has been archived by the owner on Feb 26, 2024. It is now read-only.

Add support for multicall3 to decoder interpretations #6137

Merged
merged 2 commits into from
Jul 11, 2023
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
50 changes: 39 additions & 11 deletions packages/codec/lib/export.ts
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,18 @@ export class CalldataDecodingInspector {
options,
this.options
);
} else if (this.decoding.interpretations.tryAggregate) {
const { requireSuccess, calls } =
this.decoding.interpretations.tryAggregate;
return formatAggregate(
fullName,
calls,
options,
this.options,
"requireSuccess",
options.stylize(requireSuccess.toString(), "number"),
true //including try here would be redundant, so suppress it
);
} else if (this.decoding.interpretations.tryAggregate) {
const { requireSuccess, calls } =
this.decoding.interpretations.tryAggregate;
Expand Down Expand Up @@ -692,24 +704,40 @@ function formatAggregate(
options: InspectOptions,
inspectorOptions?: ResultInspectorOptions,
additionalParameterName?: string,
additionalParameterValue?: string
additionalParameterValue?: string,
suppressTry: boolean = false
): string {
if (calls.length === 0) {
return `${fullName}()`;
}
const indent = 2;
let formattedCalls = calls.map(({ address, decoding }, index) => {
const formattedCall =
decoding === null
? "<decoding error>"
: util
.inspect(
let formattedCalls = calls.map(
({ address, allowFailure, value, decoding }, index) => {
let formattedCall =
decoding === null
? "<decoding error>"
: util.inspect(
new CalldataDecodingInspector(decoding, inspectorOptions),
options
)
.replace(".", `(${options.stylize(address, "number")}).`); //HACK: splice in the address
return formattedCall + (index < calls.length - 1 ? "," : "");
});
);
if (value !== null && value.gtn(0)) {
formattedCall = formattedCall.replace(
".",
`{value: ${options.stylize(value.toString(), "number")}}.` //HACK: splice in the value
);
}
if (address !== null) {
formattedCall = formattedCall.replace(
".",
`(${options.stylize(address, "number")}).`
); //HACK: splice in the address
}
if (allowFailure && !suppressTry && decoding !== null) {
formattedCall = "[try] " + formattedCall;
}
return formattedCall + (index < calls.length - 1 ? "," : "");
}
);
if (additionalParameterName) {
formattedCalls.unshift(
`${additionalParameterName}: ${additionalParameterValue},`
Expand Down
12 changes: 10 additions & 2 deletions packages/codec/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,8 @@ export interface FunctionDecoding {
/**
* If this interpretation is present, indicates that the transaction can be
* understood as an `aggregate` (from multicallv2). This also includes
* `blockAndAggregate`. See [[CallInterpretationInfo]] for more
* information.
* `blockAndAggregate`, as well as `aggregate3` (from multicallv3), and
* `aggregate3Value`. See [[CallInterpretationInfo]] for more information.
*/
aggregate?: CallInterpretationInfo[];
/**
Expand Down Expand Up @@ -952,6 +952,14 @@ export interface CallInterpretationInfo {
* The decoding of the call; may be null in case of error.
*/
decoding: CalldataDecoding | null;
/**
* Whether failure was allowed; may be null in case of error.
*/
allowFailure: boolean | null;
/**
* The value sent with the call, in Wei. May be null in case of error.
*/
value: BN | null;
}

/**
Expand Down
174 changes: 127 additions & 47 deletions packages/decoder/lib/decoders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1096,6 +1096,63 @@ export class ProjectDecoder {
)
)
);
} else if (
decoding.kind === "function" &&
decoding.abi.name === "aggregate3" &&
decoding.abi.inputs.length === 1 &&
decoding.abi.inputs[0].type === "tuple[]" &&
decoding.abi.inputs[0].components.length === 3 &&
decoding.abi.inputs[0].components[0].type === "address" &&
decoding.abi.inputs[0].components[1].type === "bool" &&
decoding.abi.inputs[0].components[2].type === "bytes" &&
decoding.arguments[0].value.kind === "value"
) {
//Identical to above, just split out for clarity
const decodedArray = decoding.arguments[0]
.value as Format.Values.ArrayValue;
return await Promise.all(
decodedArray.value.map(
async callResult =>
await this.interpretCallInAggregate(
callResult as
| Format.Values.StructResult
| Format.Values.TupleResult,
transaction,
additionalContexts,
additionalAllocations,
overrideContext
)
)
);
} else if (
decoding.kind === "function" &&
decoding.abi.name === "aggregate3Value" &&
decoding.abi.inputs.length === 1 &&
decoding.abi.inputs[0].type === "tuple[]" &&
decoding.abi.inputs[0].components.length === 4 &&
decoding.abi.inputs[0].components[0].type === "address" &&
decoding.abi.inputs[0].components[1].type === "bool" &&
decoding.abi.inputs[0].components[2].type === "uint256" &&
decoding.abi.inputs[0].components[3].type === "bytes" &&
decoding.arguments[0].value.kind === "value"
) {
//Identical to above, just split out for clarity
const decodedArray = decoding.arguments[0]
.value as Format.Values.ArrayValue;
return await Promise.all(
decodedArray.value.map(
async callResult =>
await this.interpretCallInAggregate(
callResult as
| Format.Values.StructResult
| Format.Values.TupleResult,
transaction,
additionalContexts,
additionalAllocations,
overrideContext
)
)
);
} else {
return undefined;
}
Expand All @@ -1112,41 +1169,62 @@ export class ProjectDecoder {
},
overrideContext?: Contexts.Context
): Promise<CallInterpretationInfo> {
switch (callResult.kind) {
case "value":
const addressResult = callResult.value[0]
.value as Codec.Format.Values.AddressResult;
const bytesResult = callResult.value[1]
.value as Codec.Format.Values.BytesResult;
let address: string;
switch (addressResult.kind) {
case "value":
address = addressResult.value.asAddress;
break;
case "error":
//can't decode in this case
return { address: null, decoding: null };
}
switch (bytesResult.kind) {
case "value":
const subDecoding =
await this.decodeTransactionWithAdditionalContexts(
{
...transaction,
input: bytesResult.value.asHex,
to: address
},
additionalContexts,
additionalAllocations,
overrideContext
);
return { address, decoding: subDecoding };
case "error":
return { address, decoding: null };
}
case "error":
return { address: null, decoding: null };
if (callResult.kind === "error") {
return {
address: null,
allowFailure: null,
value: null,
decoding: null
};
}
let address: string;
let subDecoding: Codec.CalldataDecoding;
let value: BN = new BN(0);
let allowFailure: boolean = false;
for (const { value: subResult } of callResult.value) {
switch (subResult.type.typeClass) {
case "address":
if (subResult.kind === "error") {
address = null;
} else {
address = (subResult as Format.Values.AddressValue).value.asAddress;
}
break;
case "bool":
if (subResult.kind === "error") {
allowFailure = null;
} else {
allowFailure = (subResult as Format.Values.BoolValue).value
.asBoolean;
}
break;
case "uint":
if (subResult.kind === "error") {
value = null;
} else {
value = (subResult as Format.Values.UintValue).value.asBN.clone();
}
break;
case "bytes":
if (subResult.kind === "error") {
subDecoding = null;
} else {
const asHex = (subResult as Format.Values.BytesValue).value.asHex;
subDecoding = await this.decodeTransactionWithAdditionalContexts(
{
...transaction,
input: asHex,
to: address
},
additionalContexts,
additionalAllocations,
overrideContext
);
}
break;
}
}
return { address, allowFailure, value, decoding: subDecoding };
}

private async interpretTryAggregate(
Expand Down Expand Up @@ -1179,20 +1257,22 @@ export class ProjectDecoder {
const decodedArray = decoding.arguments[1]
.value as Format.Values.ArrayValue;
const requireSuccess: boolean = decodedBool.value.asBoolean;
const calls = await Promise.all(
decodedArray.value.map(
async callResult =>
await this.interpretCallInAggregate(
callResult as
| Format.Values.StructResult
| Format.Values.TupleResult,
transaction,
additionalContexts,
additionalAllocations,
overrideContext
)
const calls = (
await Promise.all(
decodedArray.value.map(
async callResult =>
await this.interpretCallInAggregate(
callResult as
| Format.Values.StructResult
| Format.Values.TupleResult,
transaction,
additionalContexts,
additionalAllocations,
overrideContext
)
)
)
);
).map(call => ({ ...call, allowFailure: !requireSuccess }));
return { requireSuccess, calls };
} else {
return undefined;
Expand Down
35 changes: 30 additions & 5 deletions packages/decoder/test/current/contracts/WireTest.sol
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ contract WireTest is WireTestParent, WireTestAbstract {
emit Done();
} //just a dummy function, not

constructor(bool status, bytes memory info, Ternary whoknows) {
constructor(bool status, bytes memory info, Ternary whoknows) payable {
deepStruct["blornst"].push();
deepStruct["blornst"].push();
deepString.push();
Expand Down Expand Up @@ -98,7 +98,7 @@ contract WireTest is WireTestParent, WireTestAbstract {

event HasIndices(uint, uint indexed, string, string indexed, uint);

function indexTest(uint a, uint b, string memory c, string memory d, uint e) public {
function indexTest(uint a, uint b, string memory c, string memory d, uint e) public payable {
emit HasIndices(a, b, c, d, e);
}

Expand Down Expand Up @@ -241,15 +241,40 @@ contract WireTest is WireTestParent, WireTestAbstract {
}
}

event Result(bytes);

function tryAggregate(bool requireSuccess, Call[] calldata calls) external {
for (uint i = 0; i < calls.length; i++) {
(bool success, bytes memory result) = calls[i].target.call(calls[i].data);
if (requireSuccess) require(success);
}
}

struct Call3 {
address target;
bool allowFailure;
bytes data;
}

function aggregate3(Call3[] calldata calls) external {
for (uint i = 0; i < calls.length; i++) {
(bool success, bytes memory result) = calls[i].target.call(calls[i].data);
if (!calls[i].allowFailure) require(success);
}
}

struct Call3Value {
address target;
bool allowFailure;
uint value;
bytes data;
}

function aggregate3Value(Call3Value[] calldata calls) external {
for (uint i = 0; i < calls.length; i++) {
(bool success, bytes memory result) = calls[i].target.call{value: calls[i].value}(calls[i].data);
if (!calls[i].allowFailure) require(success);
}
}

function multicall(uint deadline, bytes[] calldata datas) external {
require(block.timestamp <= deadline);
for (uint i = 0; i < datas.length; i++) {
Expand Down Expand Up @@ -297,7 +322,7 @@ contract WireTestRedHerring {
emit NonAmbiguousEvent();
}

function otherMethod(uint k) public {
function otherMethod(uint k) public payable {
emit SemiAmbiguousEvent(k, 0);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ module.exports = function (deployer) {
deployer.link(DecoyLibrary, DowngradeTest);
deployer.deploy(WireTestLibrary);
deployer.link(WireTestLibrary, WireTest);
deployer.deploy(WireTest, false, "0x", 0);
deployer.deploy(WireTest, false, "0x", 0, { value: 100 });
deployer.deploy(ReceiveTest);
deployer.deploy(FallbackTest);
deployer.deploy(DecodingSample);
Expand Down