Skip links
money on chain logo

Money on Chain Gas Cost

This is the second of a series of four audits we performed for MOC: Previous Audit, Next Audit

Purpose Of This Document

The purpose of this document is to offer different alternatives to reduce gas cost in certain functions of the Money on Chain project. These functions when called in a transaction may reach the block gas limit, rendering them unable to be called.

Current implementation

We are going to take the function executeBurnout as an example. This function looks like this currently:

/**
  @dev Iterate over the burnout address book and redeem docs
  for all users sending the RBTC to the corresponding burnout address
*/
function executeBurnout() public onlyWhitelisted(msg.sender) {
  for (uint i = 0; i < numElements; i++) {
	address account = burnoutQueue[i];
	address burnout = burnoutBook[account];
	uint256 btcTotal = mocExchange.redeemAllDoc(account, burnout);
	emit BurnoutAddressProcessed(account, burnout, btcTotal);
  }
  emit BurnoutExecuted(numElements);
  clearBook();
}

This function is called at liquidation, it refunds the DoC tokens of any user who registered itself in the burnoutQueue array and burnoutBook mapping using a function called pushBurnoutAddress. The storage variable numElements, is the amount of elements in the queue currently. The internal function clearBook called at the end, sets this variable to 0. 

This function has a problem, the variable numElements can grow unexpectedly large, in which case the amount of iterations executed will consume more gas than the current block limit allows. If users push too many addresses, and since there is no other way than calling this function to reduce numElements, this function will become “uncallable” unless the gas limit increases. In this case “uncallable” is used between quotes as the function will be callable, will consume gas, but will never be able to finish. On each call it will try to run, but fail and return to the original state.  This is effectively a denial of service.

In the next section we’ll be discussing different alternatives that do not have this problem, pointing out the pros and cons for each case.

Alternatives

User Withdrawal

This is the fairest one and more in line to the philosophy of a decentralized exchange. Instead of batching the liquidation in a queue, you expect your users to execute the transactions themselves. That is, if a user wants to liquidate their tokens, each one of them will need to call the function themselves when the liquidation comes.

Pros

  • Reduces the maximum gas cost in any single transaction to the minimum
  • It’s the fairest one and less prone to attacks.

Cons

  • It requires users to pay for the transaction (but currently they do need to pay gas to be in the book to begin with).
  • It requires users to be attentive at the time of liquidation, so it’s possible to consider it a loss of functionality. But you could generate a new off-chain component that will listen to the event and send notifications.

Queue cap

This is the easiest one to implement. This implementation puts a cap on the queue size. You may only add further elements to the queue once it is cleared when executeBurnout is called. This is effectively a single require in the function pushBurnoutAddress.

Pros

  • Easiest to implement, a single line of code addition

Cons

  • If the queue is full, users need to wait for someone to clear the queue.
  • If the queue is full, users will experience a failed transaction, losing a bit of currency in gas and may cause confusion.
  • It’s prone to another type of DoS, an attacker may fill the queue by spamming transactions as soon as the queue is emptied. This is more complicated than the original attack and can be dampened by requiring the user to hold tokens, but is still possible to execute.

Multiple queues

This doesn’t have the disadvantages of the previous two, but implementation is more involved. Instead of having a single large queue, we can keep track of multiple capped ones. We index these queues starting from zero, and save them in a mapping using these indexes as keys and the queues as values. We save the last queue added in a counter. If this last queue is filled, the counter is incremented and a new one is created with this counter as key. We also keep track of the next queue we have yet to liquidate, starting from zero up to the last queue added. If the last queue gets liquidated, a new queue is not added, instead new addresses are pushed onto that queue, exactly the same as how it works now for a single queue.

Pros

  • Solves the problem without adding constraints, the functionality stays the same.

Cons

  • It’s the most complicated to implement of the three alternatives.
  • Adds complexity to the contract.
  • Adding a mapping may slightly increase gas costs by a constant amount.

This audit is subject to a Creative Commons License.