📑MTVSwap AMM

  • PAIRS CONTRACTS

MTVSwap allows liquidity providers to create pair contracts for any two tokens. The AMM calls the pair contract through a “router” contract, this calculates the amounts and transfers funds to the pair contract. The sheer number of pairs between any 2 tokens could make it slow and difficult to find the best value path for a particular pair, but the router streamlines this process.

  • PRICE ORACLE

The price offered by MTVSwap (not including fees) at time t can simplistically be calculated by dividing the amount of asset a in a liquidity pool by the amount of asset b:

If the price offered on MTVSwap is sufficiently different to the wider market then arbitrageurs would step in; therefore the price offered by MTVSwap is expected to track the relative market price. This process is described by Angeris et al [Guillermo Angeris et al. An analysis of Uniswap markets. 2019. arXiv: 1911.03380 (q-fin.TR)]. This expectation means the calculation can be used as an approximate price oracle.

MTVSwap provides this oracle functionality by measuring and recording the price before the first trade of each block. Not performing this check can make a contract vulnerable to an attack [see samczsun. Taking undercollateralized loans for fun and for profit. Sept. 2019]. Using this method makes it more difficult to manipulate than if prices within a block were allowed. In short, if an attacker submits a transaction that attempts to manipulate the price at the end of a block, some other arbitrageur may be able to submit another transaction to trade back immediately in the same block. A miner (or an attacker who uses enough gas to fill an entire block) could try to manipulate the price at the end of a block, but unless they successfully mine the next block as well, they will not gain any particular advantage in arbitraging the trade. To further mitigate this risk, MTVSwap actually accumulates the price, keeping track of the cumulative sum of prices at the beginning of each block. Each price is weighted by the amount of time that has passed since the last block in which it was updated, according to the block timestamp. This means that the accumulator value at any given time should be the sum of the spot price at each second in the history of the contract.

To estimate the TWAP from time t1 to t2, you can checkpoint the accumulator’s value at t1 and then again at t2, subtract the first value from the second, and divide by the number of seconds between the two (Note that the contract itself does not store historical values for this accumulator—the caller has to call the contract at the beginning of the period to read and store this value.)

Users of the oracle can choose when to start and end this period. Choosing a longer period makes it more expensive for an attacker to manipulate the TWAP, but you must trade that against a less up-to-date price.

One complication is whether we should measure the price of asset A in terms of asset B, or the price of asset B in terms of asset A? While the spot price of A in terms of B is always the reciprocal of the spot price of B in terms of A, the mean price of asset A in terms of asset B over a particular period of time is not equal to the reciprocal of the mean price of asset B in terms of asset A. For example, if the USD/MTV price is 100 in block 1 and 300 in block 2, the average USD/MTV price will be 200 USD/MTV, but the average MTV/USD price will be 1/150 MTV/USD. Since the contract cannot know which of the two assets users would want to use as the unit of account, MTVSwap must track both prices.

Another complication is that it is possible for someone to send assets to the pair contract—and thus change its balances and marginal price—without triggering an oracle update. If the contract simply checked its own balances and updated the oracle based on the current price, an attacker could manipulate the oracle by sending an asset to the contract immediately before calling it for the first time in a block. If the last trade was in a block whose timestamp was X seconds ago, the contract would incorrectly multiply the new price by X before accumulating it, even though nobody has had an opportunity to trade at that price.

To prevent this, the core contract caches its reserves after each interaction, and updates the oracle using the price derived from the cached reserves rather than the current reserves. In addition to protecting the oracle from manipulation, this change enables the contract re-architecture described below in the Contract Architecture section.

  • PRECISION

Because Solidity does not have first-class support for non-integer numeric data types, the MTVSwap uses a simple binary fixed point format to encode and manipulate prices. Specifically, prices at a given moment are stored as UQ112.112 numbers, meaning that 112 fractional bits of precision are specified on either side of the decimal point, with no sign. These numbers have a range of [0, 2112 − 1] and a precision of 1 / 2112 .

The UQ112.112 format was chosen for a pragmatic reason — because these numbers can be stored in a uint224, this leaves 32 bits of a 256 bit storage slot free. It also happens that the reserves, each stored in a uint112, also leave 32 bits free in a (packed) 256 bit storage slot.

These free spaces are used for the accumulation process described above. Specifically, the reserves are stored alongside the timestamp of the most recent block with at least one trade, modded with 232 so that it fits into 32 bits. Additionally, although the price at any given moment (stored as a UQ112.112 number) is guaranteed to fit in 224 bits, the accumulation of this price over an interval is not. The extra 32 bits on the end of the storage slots for the accumulated price of A/B and B/A are used to store overflow bits resulting from repeated summations of prices. This design means that the price oracle only adds an additional three SSTORE operations to the first trade in each block.

The primary downside is that 32 bits isn’t quite enough to store timestamp values that will reasonably never overflow. In fact, the date when the Unix timestamp overflows a uint32 is 02/07/2106. To ensure that this system continued to function properly after this date, and every multiple of 232 − 1 seconds thereafter, oracles are simply required to check prices at least once per interval (approximately 136 years). This is because the core method of accumulation (and modding of timestamp), is actually overflow-safe, meaning that trades across overflow intervals can be appropriately accounted for given that oracles are using the proper (simple) overflow arithmetic to compute deltas.

  • FLASH SWAPS

MTVSwap technically allows a user to receive and use an asset before paying for it, as long as they make the payment within the same atomic transaction. The swap function makes a call to an optional user-specified callback contract in between transferring out the tokens requested by the user and enforcing the invariant. Once the callback is complete, the contract checks the new balances and confirms that the invariant is satisfied (after adjusting for fees on the amounts paid in). If the contract does not have sufficient funds, it reverts the entire transaction.

A user can also repay the MTVSwap pool using the same token, rather than completing the swap. This is effectively the same as letting anyone flash-borrow any of the assets stored in a MTVSwap pool (for the same fee as MTVSwap charges for trading).

Last updated