Skip to main content

Protocol limit estimation

Certain actions in the protocol require looping over dynamic size arrays. To avoid hitting the block gas limit, special protocol limits were introduced which revert the transaction before the loop even starts. Values for these limits are then determined through estimation process, described here.

Identifying the limits

First we identify which limits we use at the moment and which functions (or chain of functions) use them. The goal is to identify the external functions which can be called during the estimation.

List of limits

limitused inremarks
maxExchangesPerBatchcompleteExchangeBatch
maxOffersPerGroupcreateGroupInternal -> createGroup
maxOffersPerGroupcreateGroupInternal -> createOfferWithConditionnot a problem, always length 1
maxOffersPerGrouppreUpdateChecks -> addOffersToGroupInternal -> addOffersToGroup
maxOffersPerGrouppreUpdateChecks -> addOffersToGroupInternal -> createOfferAddToGroupnot a problem, always length 1
maxOffersPerGrouppreUpdateChecks -> removeOffersFromGroup
maxOffersPerBundlecreateBundleInternal -> createBundle
maxOffersPerBundlecreateBundleInternal -> createTwinAndBundleAfterOffernot a problem, always length 1
maxTwinsPerBundlecreateBundleInternal -> createBundle
maxTwinsPerBundlecreateBundleInternal -> createTwinAndBundleAfterOffernot a problem, always length 1
maxOffersPerBatchcreateOfferBatch
maxOffersPerBatchvoidOfferBatch
maxOffersPerBatchextendOfferBatch
maxTokensPerWithdrawalwithdrawFundsInternal -> withdrawFunds
maxTokensPerWithdrawalwithdrawFundsInternal -> withdrawProtocolFees
maxFeesPerDisputeResolvercreateDisputeResolver
maxFeesPerDisputeResolveraddFeesToDisputeResolver
maxFeesPerDisputeResolverremoveFeesFromDisputeResolver
maxDisputesPerBatchexpireDisputeBatch
maxAllowedSellerscreateDisputeResolver
maxAllowedSellersaddSellersToAllowList
maxAllowedSellersremoveSellersFromAllowList

Estimation config

Config file is placed in scripts/config/limit-estimation.js. It has the following fields:

  • blockGasLimit: block gas limit against which you want to make the estimate
  • safeGasLimitPercent: percent of total gas block limit that you consider safe for a transaction to actually be included in the block. For example if blockGasLimit is 30M and you don't want your transaction to exceed 15M, set safeGasLimitPercent to 50.
  • maxArrayLength: maximum length of the array used during the estimation. This value is typically smaller than actual limits calculated at the end. Increasing this value makes estimation more precise, however it also takes more time. Improvement in the estimate is increasing slower than run time, so setting this to 100 should be more than enough. If you want to speed up the process, setting this to 10 will still give you very good results.
  • limits: list of limits you want to estimate. Each limit is an object with fields:
    • name: name of the limit
    • methods: object of pairs "methodName":"handlerName" where methodName is the name of the external function that uses the limit and handlerName is the name of the handler where this function is implemented. Example for limit maxOffersPerGroup:
      {
      name: "maxOffersPerGroup",
      methods: {
      createGroup: "IBosonGroupHandler",
      addOffersToGroup: "IBosonGroupHandler",
      removeOffersFromGroup: "IBosonGroupHandler",
      },
      },

Setting up the environment

For each of the limits you must prepare an evironment, before it can be tested. For example, before maxOffersPerGroup can be tested, protocol contracts must be deployed and enough offers must be created so the limit can actually be tested. A similar setup is needed for all other methods.

This is done in file scripts/util/estimate-limits.js. Each of the limits must have a setup function which accepts maxArrayLength, prepares the environment and returns the invocation details that can be then used when invoking the methods during the estimation.

Invocation details contain

  • account: account that calls the method (important if access is restricted)
  • args: array of arguments that needs to be passed into method
  • arrayIndex: index that tells which parameter's length should be varied during the estimation
  • structField: if array is part of a struct, specify the field name

The returned object must be in form { methodName_1: invocationDetails_1, methodName_2: invocationDetails_2, ..., methodName_n: invocationDetails_2} with details for all methods specified in estimation config.

Running the script

Scrip is run by calling

npm run estimate-limits

During the estimation it outputs the information about the method it is estimating. At the end it stores the estimation details into two files:

  • logs/limit_estimates.json Data in JSON format
  • logs/limit_estimates.md Data in MD table

Results

The results for parameters

  • blockGasLimit: 30,000,000 (current ethereum mainnet block gas limit)
  • safeGasLimitPercent: 60
limitmax valuesafe value
maxExchangesPerBatch557333
maxOffersPerGroup388232
maxOffersPerBundle508303
maxTwinsPerBundle510304
maxOffersPerBatch5131
maxTokensPerWithdrawal491293
maxFeesPerDisputeResolver305181
maxDisputesPerBatch302181
maxAllowedSellers597352

max value is determined based on blockGasLimit, while safe value also applies safeGasLimitPercent.

Gas spent for different sizes of arrays

#maxExchangesPerBatchmaxOffersPerGroupmaxOffersPerGroupmaxOffersPerGroupmaxOffersPerBundlemaxTwinsPerBundlemaxOffersPerBatchmaxOffersPerBatchmaxOffersPerBatchmaxTokensPerWithdrawalmaxTokensPerWithdrawalmaxFeesPerDisputeResolvermaxFeesPerDisputeResolvermaxFeesPerDisputeResolvermaxDisputesPerBatchmaxAllowedSellersmaxAllowedSellersmaxAllowedSellers
completeExchangeBatchcreateGroupaddOffersToGroupremoveOffersFromGroupcreateBundlecreateBundlecreateOfferBatchvoidOfferBatchextendOfferBatchwithdrawFundswithdrawProtocolFeescreateDisputeResolveraddFeesToDisputeResolverremoveFeesFromDisputeResolverexpireDisputeBatchcreateDisputeResolveraddSellersToAllowListremoveSellersFromAllowList
11843572161791668133399502663392663396454107636963841982461242634567271680149932522364372035512061176720
223794529282424389936291732437332423712208831112038565014488019342455274026445514534932206176946316911799602
32906923692873198113859563826853824141796358145208107442191521262643649449360736191089420849817790217760122522
43442714460103968024096874407894403692371848179853129223238345330958745985457657236552519034866852266659145744
53975065224384734994327434986564981022947330213982150777284285399900842313553655282583617217915916315181168453
64509335991455499014557245568305561423522863248993172476330931468759939354650340327624716072964978364020191603
7504557675705626011478855615481614658409840028274019432237739853802110383897468513735268143501015980412672214102
8557350752116703168502121673490672532467395131749321631342368760677011356428431904187879126291065136462015237182
96107048291747795815253837313867302955289900351745237643470697675733123288294023246462110137461114293510385260262
1066415790536685584754864878992878870058699223866812598535167207445611325047103925751045411123021163448559101283342
201199674167203116228687800971371424136885411585194730484476329981216143550522938942003996966566209789916487041050125511799
3017315952440464239156410123871955241195132617350904107874369389214468382126474325656129670751424971308356521384111535807740419
4022718363202792315424612446802539116253385623112733142382791145419134502817507424805039341001882668406930126281262025507969085
502800719396983539215501476974311706531104722891162817689631135500238013035032265220856492941623403815055106311188125152181199241
6033337934764194471589515421053699938369200221141521353506284137739839436192222590216024093136040982360068029991841220202
7038669275535734548769716072354307529429819524593951566348330725944715697137316685394124782337030675411303834879771241164
8044241686302543625613716723694893873488318828046901783781377318049531268125585779901125471838046950460467239767811262132
9049603387052123700598517375665480275546823931444912001293426355654347289068993878589826161149014128509631744913101283105
100549656978018147755932180282660667356053349348928422188824732249591638710051836972925826850599977691558797249829501304081
max55738838917335085105186813756414913053099323025976101906
safe3332322321031303304315208243842931811855571813523651141

Methodology

As seen from the results above, some limits are relatively high and to actually hit the limit, already the setup would take a lot of time (e.g. for making >1700 offers to hit limit in removeOffersFromGroup). To get the estimates we therefore use the following approach:

  • get the actual estimates for relatively small number of different array lengths
  • given how gas is determined, there exist approximate linear relation, which can be written as gasSpent = intrinsicGas + arrayLength*costPerLoop. Intrinsic costs here contains all costs that are fixed regardless of the array size.
  • use linear regression to estimate intrinsicGas and costPerLoop
  • use these estimates to calculate the biggest arrayLength where gasSpent <= blockGasLimit which gives the maximum value. To get the safe value we find the biggest arrayLength where gasSpent <= safeGasLimitPercent*blockGasLimit