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

Commit

Permalink
Merge pull request #6137 from trufflesuite/multicall3
Browse files Browse the repository at this point in the history
Add support for multicall3 to decoder interpretations
  • Loading branch information
haltman-at committed Jul 11, 2023
2 parents 967ad43 + d3b6e0a commit 81c5ec2
Show file tree
Hide file tree
Showing 6 changed files with 341 additions and 68 deletions.
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

0 comments on commit 81c5ec2

Please sign in to comment.