The Curse of Flash Loan In Cardano
By: Amirhossein Khajehpour
Tags: Uniswap, Flash Loan, Plutus, Cardano, Defi

eUTxO based Blockchains

Cardano blockchain uses the Extended UTxO (eUTxO) model that is an extension to Bitcoin’s ledger model. Unlike Ethereum ledger that every user is modeled as an account in the current network state, in this model, we only have a large tree of transactions, and those located at the leaves of the tree are still unspent. The difference that makes the Cardano model extended to what Bitcoin has is the presence of a Turing complete scripting language in the validation process. Cardano uses Plutus, a language written in Haskell.

Plutus contracts consist of parts that run on the blockchain (on-chain code) and parts that run on a user’s machine (off-chain or client code). Both the on-chain and off-chain code are written in Haskell, and Plutus smart contracts are effectively Haskell programs.

Inside a Plutus contract, you only have access to the current UTxO and not the entire blockchain, meaning that we are isolated from the world outside of the UTxO. Contracts in Cardano are written in Haskell, compiled to Plutus. Each contract has its own address, which is the hash of its Plutus code. Then, in order to use the contract, we only need to send funds to the contract address. In fact, there is no contract deployment in Cardano, unlike what we have in the Ethereum blockchain.

Now, let’s see how a simple Uniswap DEX can be made using Plutus. We saw that for each trading pair in our DEX, we need to have a separate reserve that contains involving assets. So the most straightforward idea would be to create a UTxO for each reserve. Then, we need to know how Plutus contracts control funds when they are being used to make sure that our AMM is working correctly. The answer is simple. You only need the amount of funds locked in the previous state of the contract (the input UTxO) and the newly updated amount of funds that is going to be stored in this contract (the output UTxO). You have all you need inside your isolated Plutus environment. For being more precise, below a summarized Plutus version of AMM contracts is provided:

validateSwap LiquidityPool{..} c ctx = checkSwap oldA oldB newA newB

where

info :: TxInfo

info = scriptContextTxInfo ctx -- inVal : the value associated with the input UTxO

-- outVal : the value associated with the output UTxO oldA = amountA inVal

oldB = amountB inVal

newA = amountA outVal

newB = amountB outVal checkSwap oldA oldB newA newB =

traceIfFalse "expected positive oldA" (oldA > 0) &&

traceIfFalse "expected positive oldB" (oldB > 0) &&

traceIfFalse "expected positive-newA" (newA > 0) &&

traceIfFalse "expected positive-newB" (newB > 0) &&

traceIfFalse "expected product to increase" ((((newA * feeDen) - (inA * feeNum)) * ((newB * feeDen) - (inB * feeNum)))

>= (feeDen * feeDen * oldA * oldB))

where inA = max 0 $ newA - oldA

inB = max 0 $ newB - oldB

feeNum, feeDen :: Integer

feeNum = 3

feeDen = 1000

When a transaction is sent to our script, the validateSwap function is executed which in turn checks the AMM algorithm using the checkSwap function. Eventually, the below line checks whether the liquidity provider fee is paid and the swap parameters are all set correctly.

traceIfFalse "expected product to increase" ((((newA * feeDen) - (inA * feeNum)) * ((newB * feeDen) - (inB * feeNum)))

>= (feeDen * feeDen * oldA * oldB))

Exactly like the Ethereum model, if anything goes wrong during the process, nothing will change on the ledger state, and everything will be reverted.

This way of transaction validation and transactions with multiple inputs and outputs play a prominent role in helping the arbitrageur to exercise arbitrage opportunities. Let’s see what happens.

Assume we have the same initial states in our reserves R1, R2, R3, and R4. Construct the below transaction that has multiple inputs and outputs:

Capital-free arbitrage transaction

Constraints of each input UTxO are as follows:

Check if there is an output UTxO that sends 3000 A to R1 address then spend 4604 B from R1, otherwise revert everything. Check if there is an output UTxO that sends 4604 B to R2 address, then spend 5600 C from R2, otherwise revert everything. Check if there is an output UTxO that sends 5600 C to R3 address, then spend 6276 D from R3, otherwise revert everything. Check if there is an output UTxO that sends 6276 D to R4 address, then spend 10821 A from R4, otherwise revert everything.

What happens ?!! Magically we no longer need any initial funds. It’s because the transaction validation mechanism in Cardano does not depend on the order of execution. The constraints are all checked together, and their sequence doesn’t matter.

The implementation of this example can be located in https://github.com/RadNi/uniswap-plutus/blob/main/src/Uniswap/Trace.hs