Boosting top-1000 contracts through fee reduction

Motivation

It seems natural that contracts that are touched infrequently should cost more to execute than the contracts that are touched frequently. In the worst case scenario, if contract is called once every few years it seem unfair and unnecessary if we attempt to guarantee it the same privileges as to the contract that is getting executed many times a second.

On the other hand, it is clear that contract execution performance can be greatly increased if the contract was always pre-loaded into memory (because it avoids unpacking and loading Wasmer module, and disk costs).

Additionally, dApps that are frequently used are only the ones that care about high TPS.

Proposal

I suggest we consider modifying the protocol to track the top-1000 contracts for each epoch, based on the amount of burnt gas. The list of the hashes of these contracts would be published together with list of new validators at the end of the epoch. Clients then would store these contracts in memory and charge a reduced fee for their execution.

Additionally, we would identify these contracts by their hash of their code, as opposed to account id. So if there are multiple NEP-141 accounts, their total burnt gas will be added up together.

Problems to consider

If we include the cost of loading the contract into the total burnt gas, when computing top-1000 contracts then we might end up with certain “borderline” contracts leaving and reentering top-1000 contracts every epoch in oscillation fashion. This might not be a big problem though.

2 Likes

I like this idea, because it also mitigates the effect of an issue that we currently overestimate function call fee by about 10 times (more details). If we actualize these fees right now, then we would have to pay 367 Tgas for each Aurora contract call which is not an option, so reduced fees help here.

Potential angle of attack here is that someone could boost usage of small contracts, so that actually used contracts will be kicked out of the top list. It seems that cost will not be very high:

NEAR Explorer | Stats - top-3 contract relayer.bridge.near is touched by 60K txs in 14 days, which is 60K / 14 / 2 ~= 2200 txs per epoch in average.

I assume that when we call small contract, the most significant part of paid gas is function call base fee, which is going to be ~= 0.5 Tgas as we computed here.

Then, to boost 1000 contracts to top-3 level, we have to pay 1000 * 2200 * 0.5 * 0.0001 (gas price) = 110 NEAR.

To avoid this, we could:

  • Store 10000 or more contracts.
  • Set a lower bound for contract size to be put to the top list. Assuming function call bytes fee = 207 Ggas per Kb in new estimation, 250 Kb limit will require to pay at least 50 Tgas for transaction instead of 0.5 Tgas, thus attack will cost 11000 NEAR which is less comfortable.

Also when a small contract is boosted, it will be easier to intentionally keep it in the top list because of lower fees, so reduced fees should be set high enough to avoid it.

1 Like

How is top 3 relevant here? My understanding is that this is binary – the cost is solely dependent on whether the contract is in top 1000.

Looks like this doesn’t work very well with challenges. We can store the gas usage of a contract in an epoch in the state to make it work, but that would be a more invasive change

By mentioning top 3, I’ve meant that it takes 110 NEAR to move relayer.bridge.near from 3rd place to 1003th place. So it will be kicked out of top 1000 and we will charge full fee instead of reduced one.

We discussed this approach today and found that this approach would make our devx worse:

  • For an application that pays for user transactions, it is hard to estimate the cost of transactions when the cost can change nontrivially between two epochs. If we go with this approach, whether a contract is in the top 1000 has a large impact on the fees of transactions calling this contract. As a result, it may be hard for developers to understand the cost, which may have other implications from a business perspective. For example, if the application uses some freemium model or charges users in some other ways, it is not clear how to price things properly.
  • For cross-contract calls, the unpredictability may pose a bigger problem. For example, a cross-contract call may succeed if all the touched contracts are in top 1000 but may fail if one of them falls out of the top 1000. Due to the big difference in gas cost depending on whether the contract is in top 1000, even if we get rid of hardcoded gas amount for cross-contract calls, it could still be quite possible for developers to mess up and write some method that will fail when one of the contracts in the chain of promises is not in top 1000. This could create quite some confusion for developers and even security risks. For defi applications where cross-contract calls are used often, it may be possible to craft some attack to evict a certain contract from the top 1000 to break some other contract (DEX for example).

In general I think we should avoid degrading our devx as much as possible when considering new protocol changes.

1 Like

Couple of ways to mitigate “discontinuity” of the top-1000:

  • make the fee a linear interpolation depending on a position in the list: actual_fee = (large_fee * (1000 - pos) * small_fee * pos) / 1000
  • rather than computing top-1000 positions at a point in time (which might be volatile), compute position changes: in every epoch, we move each contract up or down by at most, say, 10 positions. So you need many epoch to climb to the top or to fall from it.
1 Like