Spaces:
Runtime error
Runtime error
File size: 6,547 Bytes
711e9c6 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 |
---
sidebar_position: 7
---
# Tutorial 7: Escrow
## Overview
In this tutorial, we will go over how to create and escrow smart contract with some advanced features, such as a requirement for multiple arbiters and a deadline, after which the buyer can get a refund.
### What is an escrow smart contract?
An escrow smart contract is a type of digital agreement that Bitcoin to facilitate transactions between parties in a secure, trustless manner.
In traditional escrow services, a trusted third party holds assets—like money, property, or goods—on behalf of the transacting parties. The assets are released only when specific conditions are met.
In the case of an escrow smart contract, the "third party" is the smart contract itself, programmed on the blockchain. The contract is written with the conditions of the transaction, and if they are met, the contract can be unlocked and the recipient(s) get payed.
### Our implementation
We will implement a specific type of escrow, called a multi-sig escrow. The participants of this contract are a buyer (Alice), a seller (Bob) and one or more arbiters.
Suppose Alice want's to buy a specific item from Bob. They don't trust each other, so they decide to use an escrow smart contract. They pick one or more arbiters, which they both trust. The job of the chosen arbiters is to verify, that the item really gets delivered in the right condition. If the conditions are met, the contract will pay the seller, Bob. In the opposite case, Alice gets a refund. Additionally, Alice is also eligible for a refund after a set period of time in the case the arbiters are not responsive.
## Contract properties
Let's declare the properties of our smart contract:
```ts
// Number of arbiters chosen.
static readonly N_ARBITERS = 3
// Buyer (Alice) address.
@prop()
readonly buyerAddr: PubKeyHash
// Seller (Bob) address.
@prop()
readonly sellerAddr: PubKeyHash
// Arbiter public keys.
@prop()
readonly arbiters: FixedArray<PubKey, typeof MultiSigEscrow.N_ARBITERS>
// Contract deadline nLocktime value.
// Either timestamp or block height.
@prop()
readonly deadline: bigint
```
## Public method - `confirmPayment`
The first method of our contract will be `confirmPayment`. This public method will be called if the item was successfully delivered in the right condition.
The method takes as inputs the buyers signature, along with her public key and the signatures of the arbiters.
```ts
// Buyer and arbiters confirm, that the item was delivered.
// Seller gets paid.
@method(SigHash.ANYONECANPAY_SINGLE)
public confirmPayment(
buyerSig: Sig,
buyerPubKey: PubKey,
arbiterSigs: FixedArray<Sig, typeof MultiSigEscrow.N_ARBITERS>
) {
// Validate buyer sig.
assert(
hash160(buyerPubKey) == this.buyerAddr,
'invalid public key for buyer'
)
assert(
this.checkSig(buyerSig, buyerPubKey),
'buyer signature check failed'
)
// Validate arbiter sigs.
assert(
this.checkMultiSig(arbiterSigs, this.arbiters),
'arbiters checkMultiSig failed'
)
// Ensure seller gets payed.
const amount = this.ctx.utxo.value
const out = Utils.buildPublicKeyHashOutput(this.sellerAddr, amount)
assert(hash256(out) == this.ctx.hashOutputs, 'hashOutputs mismatch')
}
```
The method validates all signatures are correct and ensures the **seller** receives the funds.
## Public method - `refund`
Next, we implement the public method `refund`. If the delivery wasn't successful or there is something wrong with the item and needs to be sent back, the buyer is eligible for a refund.
The method again takes as inputs the buyers signature, along with her public key and the signatures of the arbiters.
```ts
// Regular refund. Needs arbiters agreement.
@method()
public refund(
buyerSig: Sig,
buyerPubKey: PubKey,
arbiterSigs: FixedArray<Sig, typeof MultiSigEscrow.N_ARBITERS>
) {
// Validate buyer sig.
assert(
hash160(buyerPubKey) == this.buyerAddr,
'invalid public key for buyer'
)
assert(
this.checkSig(buyerSig, buyerPubKey),
'buyer signature check failed'
)
// Validate arbiter sigs.
assert(
this.checkMultiSig(arbiterSigs, this.arbiters),
'arbiters checkMultiSig failed'
)
// Ensure buyer gets refund.
const amount = this.ctx.utxo.value
const out = Utils.buildPublicKeyHashOutput(this.buyerAddr, amount)
assert(hash256(out) == this.ctx.hashOutputs, 'hashOutputs mismatch')
}
```
The method validates all signatures are correct and ensures the **buyer** receives the refund.
## Public method - `refundDeadline`
Lastly, we implement the `refundDeadline` method. This method can be called, after the specified contract deadline has been reached. After the deadline, the buyer can receive the refund, even without the arbiters agreement.
The method takes as inputs in the buyers signature, along with her public key.
```ts
// Deadline for delivery. If reached, the buyer gets refunded.
@method()
public refundDeadline(buyerSig: Sig, buyerPubKey: PubKey) {
assert(
hash160(buyerPubKey) == this.buyerAddr,
'invalid public key for buyer'
)
assert(
this.checkSig(buyerSig, buyerPubKey),
'buyer signature check failed'
)
// Require nLocktime enabled https://wiki.bitcoinsv.io/index.php/NLocktime_and_nSequence
assert(
this.ctx.sequence < UINT_MAX,
'require nLocktime enabled'
)
// Check if using block height.
if (this.deadline < LOCKTIME_BLOCK_HEIGHT_MARKER) {
// Enforce nLocktime field to also use block height.
assert(
this.ctx.locktime < LOCKTIME_BLOCK_HEIGHT_MARKER
)
}
assert(this.ctx.locktime >= this.deadline, 'deadline not yet reached')
// Ensure buyer gets refund.
const amount = this.ctx.utxo.value
const out = Utils.buildPublicKeyHashOutput(this.buyerAddr, amount)
assert(hash256(out) == this.ctx.hashOutputs, 'hashOutputs mismatch')
}
```
The method checks the buyers signature validity. It also checks the transaction nLocktime value, to ensure it can be accepted by miners only after the deadline.
## Conclusion
Congratulations! You have completed the escrow tutorial!
The full code along with [tests](https://github.com/sCrypt-Inc/boilerplate/blob/master/tests/local/multisigEscrow.test.ts) can be found in sCrypt's [boilerplate repository](https://github.com/sCrypt-Inc/boilerplate/blob/master/src/contracts/multisigEscrow.ts).
|