UNPKG

@kiroboio/fct-core

Version:

Kirobo.io FCT Core library

325 lines 14.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.FCTMulticall = void 0; const __1 = require(".."); const batchMultiSigCall_1 = require("../batchMultiSigCall"); const classes_1 = require("../batchMultiSigCall/classes"); const CallId_1 = require("../batchMultiSigCall/versions/v020201/CallId"); const constants_1 = require("../constants"); const flows_1 = require("../constants/flows"); // function multiCallFlowControlledOptimized( // CallWithFlowOptimized[] calldata calls, // uint256 first, // uint256 last, // bool dryrun // ) external returns (bytes memory returnedData) { // struct CallWithFlowOptimized { // address target; // uint256 meta; // bytes data; // } // // function multiCallFlowControlledOptimized( // CallWithFlowOptimized[] calldata calls, // uint256 first, // uint256 last, // bool dryrun // ) external returns (bytes memory returnedData) { const IMulticallV2 = new __1.ethers.utils.Interface([ "function multiCallFlowControlledOptimized(tuple(address target,uint256 meta,bytes data)[] calls,uint256 first,uint256 last,bool dryrun) external returns (bytes memory returnedData)", ]); const FCT_Lib_MultiCall_callTypes = { ACTION: 0, VIEW_ONLY: 1, }; const FCT_Lib_MultiCallV2_addresses = { 1: "0xBc0349c383bCa324c965bA854EBc90A6aDe510E9", 42161: "0xB14471893a9692658c24AA754f710CC430438683", 10: "0x64754348Aa0fb27Cce9c40214e240755bBBcb265", 8453: "0x85ac36C32014A000c6301BF69e1c56cd58db2310", 11155111: "0xB14471893a9692658c24AA754f710CC430438683", }; // Overhead for a call without variables: 631 gas before +: // - if continue on success: 1500 gas // - if continue on fail: 2500 gas // - if stop on success: 3500 gas // - if stop on fail: 4500 gas // - if revert on success: 3200 gas // - if revert on fail: 1500 gas // Check per one slot in calldata: ~ 1100 gas (actual change seems cost around 500 gas) class FCTMulticall { _FCT; constructor(FCT) { this._FCT = FCT; } /** * Compress the whole FCT into one multicall * @note Function will throw an error if it is not possible */ async compressFCTInMulticall({ multiCallV2Address, multiCallV2ENS = "@lib:multicall_v2", sender, strictGasLimits, }) { const typedData = new classes_1.EIP712(this._FCT).getTypedData(); sender = sender.toLowerCase(); const calls = this._FCT.calls; const preparedCalls = calls.map((call, i) => call.getAsMCall(typedData, i)); let totalGasLimit = 0n; const callsWithFlow = preparedCalls.map((call, i) => { const Call = calls[i]; const options = Call.options; const callType = FCT_Lib_MultiCall_callTypes[options.callType]; // Check if it is even possible to create a valid multicall if (options.usePureMethod === true) { throw new Error(`Call ${i} - pure methods are not allowed ("magic" value)`); } if (callType === undefined) { throw new Error(`Call ${i} - delegate call type is not supported`); } if (call.from.toLowerCase() !== sender) { throw new Error(`Call ${i} - there can only be one sender per multicall`); } const decodedCallId = new CallId_1.CallId_020201(this._FCT).parseWithNumbers(call.callId); const variableArgsStart = Number("0x" + decodedCallId.variableArgsStart); const variableArgsEnd = Number("0x" + decodedCallId.variableArgsEnd); const { data, slotsChanged, totalSlots } = processCallData(call.data); totalGasLimit += this.calculateGasPerCall({ call: Call, slotsChanged, totalSlots, varArgsStart: variableArgsStart, varArgsEnd: variableArgsEnd, }); return { target: call.to, callType: callType, value: call.value, method: Call.getFunctionSignature(), flow: +flows_1.flows[Call.options.flow].value, falseMeansFail: Call.options.falseMeansFail, jumpOnSuccess: decodedCallId.options.jumpOnSuccess, jumpOnFail: decodedCallId.options.jumpOnFail, varArgsStart: variableArgsStart, varArgsStop: variableArgsEnd, data, }; }); const FCTConstructor = { chainId: this._FCT.chainId, options: this._FCT.options, domain: this._FCT.domain, }; if (FCTConstructor.domain === null) { delete FCTConstructor.domain; } const NewFCT = new batchMultiSigCall_1.BatchMultiSigCall(FCTConstructor); const FCTCallData = { from: sender, to: multiCallV2Address ?? FCT_Lib_MultiCallV2_addresses[+this._FCT.chainId], toENS: multiCallV2ENS, method: "multiCallFlowControlledOptimizedExec", options: { callType: "LIBRARY", gasLimit: totalGasLimit.toString(), }, params: [ { name: "data", type: "bytes", value: buildFCTCallData(callsWithFlow, "1", calls.length.toString(), false), }, // { // name: "calls", // type: "tuple[]", // customType: true, // value: callsWithFlow.map((call) => { // return [ // { // name: "target", // type: "address", // value: call.target, // }, // { // name: "meta", // type: "uint256", // value: buildMeta(call), // }, // { // name: "data", // type: "bytes", // value: call.data, // }, // ] as Param[]; // }), // }, // { // name: "first", // type: "uint256", // value: "1", // }, // { // name: "last", // type: "uint256", // value: calls.length.toString(), // }, // { // name: "dryrun", // type: "bool", // value: false, // }, ], }; await NewFCT.add(FCTCallData); return NewFCT.export({ strictGasLimits }); } calculateGasPerCall({ call, slotsChanged, totalSlots, varArgsStart, varArgsEnd, }) { const overheadCost = 70n + 2500n; const costPerSlotCheck = 1300n; const costPerSlotChange = 650n; // Overhead for a call without variables: 631 gas before +: // - if continue on success: 1500 gas // - if continue on fail: 2500 gas // - if stop on success: 3500 gas // - if stop on fail: 4500 gas // - if revert on success: 3200 gas // - if revert on fail: 1500 gas // Check per one slot in calldata: ~ 1100 gas (actual change seems cost around 500 gas) // We start with regular overhead cost let totalGasLimit = overheadCost; // Add gas limit for the call totalGasLimit += BigInt(call.options.gasLimit === "0" ? "50000" : call.options.gasLimit); const varArgsEndInSlots = Math.floor(varArgsEnd / 64); const slotsChecked = Math.min(varArgsEndInSlots, totalSlots) - varArgsStart; if (slotsChecked > 0) { totalGasLimit += BigInt(slotsChecked) * costPerSlotCheck; } if (slotsChanged > 0) { totalGasLimit += BigInt(slotsChanged) * costPerSlotChange; } return totalGasLimit; } /** * Compress the whole FCT into one multicall * @note Function will throw an error if it is not possible */ static async compressFCTInMulticall({ FCT, sender, multiCallV2Address, multiCallV2ENS, strictGasLimits, }) { return await new FCTMulticall(FCT).compressFCTInMulticall({ sender, multiCallV2Address, multiCallV2ENS, strictGasLimits, }); } } exports.FCTMulticall = FCTMulticall; function buildFCTCallData(calls, first, last, dryrun) { const calldata = IMulticallV2.encodeFunctionData("multiCallFlowControlledOptimized", [ calls.map((call) => [call.target, buildMeta(call), call.data]), first, last, dryrun, ]); return calldata; } const OutputVariableBaseAddressBN = BigInt(constants_1.OutputVariableBaseAddress); const MaxOutputVariableAddressBN = BigInt(constants_1.MaxOutputVariableAddress); const OutputVariableBaseBytes32BN = BigInt(constants_1.OutputVariableBaseBytes32); const MaxOutputVariableBytes32BN = BigInt(constants_1.MaxOutputVariableBytes32); const BackOutputVariableBaseAddressBN = BigInt(constants_1.BackMulticallOutputVariableBaseAddress); const MaxBackOutputVariableAddressBN = BigInt(constants_1.MaxBackOutputVariableAddress); const BackOutputVariableBaseBytes32BN = BigInt(constants_1.BackMulticallOutputVariableBaseBytes32); const MaxBackOutputVariableBytes32BN = BigInt(constants_1.MaxBackOutputVariableBytes32); function processCallData(data) { let processedData = "0x"; let slotsChanged = 0; let totalSlots = 0; for (let i = 2; i < data.length; i += 64) { const chunk = data.slice(i, i + 64); const chunkBN = BigInt("0x" + chunk); const addressBN = BigInt("0x" + chunk.slice(24)); const inAddressSlot = addressBN >= OutputVariableBaseAddressBN && addressBN <= MaxOutputVariableAddressBN; const inBytes32Slot = chunkBN >= OutputVariableBaseBytes32BN && chunkBN <= MaxOutputVariableBytes32BN; const inAddressSlotBack = addressBN >= BackOutputVariableBaseAddressBN && addressBN <= MaxBackOutputVariableAddressBN; const inBytes32SlotBack = chunkBN >= BackOutputVariableBaseBytes32BN && chunkBN <= MaxBackOutputVariableBytes32BN; if (inAddressSlot || inBytes32Slot) { if (inAddressSlot) { // 0x000000000000000000000000FD00000000000000000000000000000000000000 const address = chunk.slice(24); processedData += (constants_1.MulticallOutputVariableBaseAddress.slice(2, 4) + address.slice(2)).padStart(64, "0"); } else { // 0xFD00000000000000000000000000000000000000000000000000010000000000 processedData += constants_1.MulticallOutputVariableBaseBytes32.slice(2, 4) + chunk.slice(2); } slotsChanged++; } else if (inAddressSlotBack || inBytes32SlotBack) { if (inAddressSlotBack) { // 0x000000000000000000000000FDB0000000000000000000000000000000000000 const address = chunk.slice(24); processedData += (constants_1.BackMulticallOutputVariableBaseAddress.slice(2, 4) + address.slice(2)).padStart(64, "0"); } else { // 0xFDB0000000000000000000000000000000000000000000000000010000000000 processedData += constants_1.BackMulticallOutputVariableBaseBytes32.slice(2, 4) + chunk.slice(2); } slotsChanged++; } else { processedData += chunk; } totalSlots++; } return { data: processedData, slotsChanged, totalSlots, }; } function buildMeta(call) { // /** @dev we used a parameter called callId to hold the following info: */ // uint256 constant CALL_TYPE_BIT = 0; // 0-8 8bit call type // uint256 constant VALUE_BIT = 8; // 8-104 96bit gas limit // uint256 constant OK_JUMP_BIT = 104; // 104-120 16bit jump on success // uint256 constant FAIL_JUMP_BIT = 120; // 120-136 16bit jump on fail // uint256 constant FLOW_BIT = 136; // 136-144 8bit flow // uint256 constant VAR_ARGS_START_BIT = 144; // 144-176 32bit permissions // uint256 constant VAR_ARGS_END_BIT = 176; // 176-208 32bit permissions // uint256 constant FALSE_MEANS_FAIL_BIT = 208; // 208-216 8bit false means fail // uint256 constant METHOD_SIG_BIT = 224; // 224-256 32bit method signature // Value max = 79228162514.26434 // Call type - 2 // Value - 24 // Ok Jump - 4 // Fail Jump - 4 // Flow - 2 // Var args start - 8 // Var args end - 8 // False means fail - 2 // Method signature - 8 // Method / EMPTY / False = fail / Var args end / Var args start / Flow / Fail Jump / Success Jump / Value / Call Type // 0x00000000 / 00 / 00 / 00000000 / 00000000 / 00 / 0000 / 0000 / 000000000000000000000000 / 00 // 0x70a08231 / 00 / 00 / 00000000 / 00000000 / 00 / 0000 / 0000 / 000000000000000000000000 / 01 const start = "0x"; const method = call.method.slice(2, 10); const EMPTY = "00"; const falseMeansFail = call.falseMeansFail ? "01" : "00"; const varArgsEnd = call.varArgsStop.toString(16).padStart(8, "0"); const varArgsStart = call.varArgsStart.toString(16).padStart(8, "0"); const flow = call.flow.toString(16).padStart(2, "0"); const failJump = call.jumpOnFail.toString(16).padStart(4, "0"); const successJump = call.jumpOnSuccess.toString(16).padStart(4, "0"); const value = BigInt(call.value).toString(16).padStart(24, "0"); const callType = call.callType.toString(16).padStart(2, "0"); return [ start, method, EMPTY, falseMeansFail, varArgsEnd, varArgsStart, flow, failJump, successJump, value, callType, ].join(""); } //# sourceMappingURL=index.js.map