DeFi on bitcoin: alternative token and token exchange

Posted by lucidpc on Sun, 30 Jan 2022 00:42:01 +0100

Decentralized finance or DeFi has recently experienced rapid growth because it claims to solve the problems inherent in the traditional financial system.

Since the general view is that bitcoin cannot support DeFi, it has not developed rapidly on bitcoin.

In this series, we will show that DeFi is not only feasible on bitcoin, but also in terms of cost, security, composability and scalability. Running DeFi on bitcoin is actually more advantageous than on other blockchains. We will introduce the DeFi infrastructure and principles, which can be combined into Lego blocks to build various DeFi applications, such as exchanges, lending platforms and NFT markets.

In the first part of this series, we showed how to implement alternative tokens and exchange them atomically.

Fungible Tokens

In essence, an alternative token contract is an account table. Each account contains a user (identified here as bitcoin public key) and the number of tokens she owns. Alternatively, the issuer may MINT NEW tokens. The following contract implements such a basic token, similar to that in Ethereum ERC20 Token standard.

// a basic ERC20-like fungible token
contract ERC20 {
    PubKey minter;

    @state
    HashedMap<PubKey, int> balances;

    // mint new tokens to receiver
    public function mint(PubKey receiver, Sig minterSig, int balance, int amount, int keyIndex, SigHashPreimage preimage) {
        // authenticate
        require(checkSig(minterSig, this.minter));

        require(this.balances.canGet(receiver, balance, keyIndex));
        require(this.balances.set(receiver, balance + amount, keyIndex));

        require(this.propagateState(preimage));
    }

    // transfer tokens from sender to receiver
    public function transferFrom(PubKey sender, PubKey receiver, int amount, Sig senderSig, int senderBalance, int senderKeyIndex, int receiverBalance, int receiverKeyIndex, SigHashPreimage preimage) {
        // authenticate
        require(checkSig(senderSig, sender));

        require(this.balances.canGet(sender, senderBalance, senderKeyIndex));
        require(senderBalance >= amount);
        require(this.balances.set(sender, senderBalance - amount, senderKeyIndex));
        require(this.balances.canGet(receiver, receiverBalance, receiverKeyIndex));
        require(this.balances.set(receiver, receiverBalance + amount, receiverKeyIndex));
    
        require(this.propagateState(preimage));
    }

    // propagate state to the next UTXO
    function propagateState(SigHashPreimage preimage): bool {
        require(Tx.checkPreimageSigHashType(preimage, SigHash.SINGLE | SigHash.FORKID));
        bytes outputScript = this.getStateScript();
        bytes output = Utils.buildOutput(outputScript, SigHash.value(preimage));
        return hash256(output) == SigHash.hashOutputs(preimage);
    }
}

It's a Stateful contract (line 5), which contains a table with addresses mapped to account token balances (line 6).

  • Mint: mint a new token and distribute it to the recipient. Only the issuer (the Minter on line 3) can issue coins. Check on line 11. Line 13 checks the recipient's old balance ¹, Add it on line 14. Line 16 is used as usual OP_PUSH_TX Keep the state.

  • Transfer from: transfers tokens from sender to receiver. Line 22 verifies the sender. Line 24 checks her balance, line 23 makes sure she has enough tokens, then her balance is deducted and the recipient's balance is increased by the same amount. Line 30 remains the same as before.

Notice that line 35 uses SigHash.SINGLE , ensure that the contract is always in the same index output as the input that invokes it.

Token exchange

Now that we have tokens, let's exchange them.

In A typical token exchange, Alice trades x amount of A tokens with y amount of B tokens from Bob. In the following example, x is 10 and Y is 20, which means that the value of each A token is equivalent to two B tokens.

Alice exchanged 10 A tokens for Bob's 20 B Tokens: left / right before / after

We want to exchange them atomically: either Alice receives 20 b tokens, Bob receives 10 A tokens, or none at all. To achieve this, we use bitcoin's UTXO model: each transaction can have multiple inputs and outputs. Alice or Bob (or A third party like an exchange) creates A transaction tx2. The first and second inputs of tx2 come from tx0 and tx1, and their outputs contain the contracts of A and B tokens in the latest state, respectively. The first and second outputs of tx2 contain updated A and B token contracts, respectively.

Alice can verify that everything (such as exchange rate and amount) is correct before signing. Bob can do the same. It is worth noting that after one party signs (Alice or Bob can sign first), the other party cannot unilaterally change tx2 without invalidating the existing signature. Since the transaction can only be accepted or rejected by the whole miner, the exchange is atomic.

Compared with Ethereum

Token exchange in Ethereum is usually carried out in two steps:

  1. Alice and Bob authorize the exchange contract to spend a certain amount of tokens, which is called approving the quota in ERC20.
  2. The Swap contract performs the actual exchange.

The Swap contract essentially acts as a trusted third party. After step 1, rogue contracts can transfer tokens and steal them at will. Or if there is a BUG in the exchange contract, the approved tokens may be burned or stolen. In our token exchange, users can fully control the exchange rate and amount at any time without trusting a third party. Therefore, it is unmanaged and more secure.