DEV Community

Cover image for How To Write Smart Contract With Scrypt.
Bravolakmedia
Bravolakmedia

Posted on • Edited on

How To Write Smart Contract With Scrypt.

WHAT IS A SMART CONTRACT

A smart contract is a self-executing contract that automatically enforces the rules and terms of an agreement between parties. It is a computer program that runs on a blockchain network and can facilitate, verify, or enforce the negotiation or performance of a contract. A smart contract is like a written agreement, but instead of being on paper and handled by a person, it's a piece of computer code on a blockchain.

How Smart Contract Works.

  1. Digital Agreement: A smart contract is a digital agreement written in code. It has all the terms and conditions of the agreement clearly stated.
  2. Automatic Execution: Once the smart contract is on the blockchain, it automatically enforces the terms. No one can change it or cheat.
  3. No Middleman Needed: Unlike the written agreement where you need a trusted third party to ensure fairness, the smart contract itself ensures fairness by automatically carrying out the agreement based on the pre-set rules.
  4. Security and Trust: The blockchain technology behind the smart contract ensures security. It’s decentralized, meaning it’s not controlled by any single person or organization, making it almost impossible to tamper with. So, in essence, a smart contract is a self-executing contract where the terms are directly written into lines of code. The code and the agreements it contains exist across a distributed, decentralized blockchain network. This ensures that the contract is transparent, secure, and executed exactly as agreed without the need for intermediaries.

How do Bitcoin Smart Contracts work?

Smart contracts on Bitcoin are based on the UTXO model, which is very different from an account model like Ethereum used.

A Bitcoin transaction

Each bitcoin transaction consists of some inputs and outputs. A single bitcoin is divisible into 100,000,000 satoshis, similar to how a dollar is divisible into 100 cents or pennies.

An output contains:
a. The amount of bitcoins (satoshis) it contains.
b. bytecodes (the locking script).
While an input contains:
a. A reference to the previous transaction output.
b. bytecodes (the unlocking script).

UTXO model

An Unspent Transaction Output (UTXO) is an output not consumed in any transaction yet. The low-level bytecode/opcode is called Bitcoin Script, which is interpreted by the Bitcoin Virtual Machine (BVM).

An image of a lock and key with a boolean function illustrating<br>
 UTXO model
In the example above, we have two transactions, each having one input (in green) and one output (in red). And the transaction on the right spends the one on the left. The locking script can be regarded as a boolean function f that specifies conditions to spend the bitcoins in the UTXO, acting as a lock (thus the name "locking"). The unlocking script in turns provides the function arguments that makes f evaluates to true, i.e., the "key" (also called witness) needed to unlock. Only when the “key” in an input matches previous output’s “lock”, it can spend bitcoins contained in the output.

In a regular Bitcoin payment to a Bitcoin address, the locking script is Pay To Pubkey Hash (P2PKH). It checks the spender has the right private key corresponding to the address so she can produce a valid signature in the unlocking script. The expressive Script enables the locking script to specify arbitrarily more complex spending conditions than simple P2PKH, i.e., Bitcoin smart contracts.

Components of A Smart Contract Written On Scrypt.

In case you are new to Scrypt you can follow the previous article on scrypt syntax and data types here.
A smart contract is a class that extends the SmartContract base class. It has the following components:

1. Properties

A smart contract can have two kinds of properties:

a. With @prop decorator: these properties are only allowed to have types specified below and they shall only be initialized in the constructor.
b. Without @prop decorator: these properties are regular TypeScript properties without any special requirement, meaning they can use any types. Accessing these properties is prohibited in methods decorated with the @method decorator.

2. @prop decorator

This decorator is used to mark any property that intends to be stored on chain. It takes a boolean parameter. By default, it is set to false, meaning the property cannot be changed after the contract is deployed.
If the value is true, the property is a so-called stateful property and its value can be updated in subsequent contract calls.

// good, `a` is stored on chain, and it's **readonly** after the contract is deployed
@prop()
readonly a: bigint

// valid, but not good enough, `a` cannot be changed after the contract is deployed
@prop()
a: bigint

// good, `b` is stored on chain, and its value can be updated in subsequent contract calls
@prop(true)
b: bigint

// invalid, `b` is a stateful property that cannot be readonly
@prop(true)
readonly b: bigint

// good
@prop()
static c: bigint = 1n

// invalid, static property must be initialized when declared
@prop()
static c: bigint

// invalid, stateful property cannot be static
@prop(true)
static c: bigint = 1n

// good, `UINT_MAX` is a compile-time constant (CTC), and doesn't need to be typed explicitly
static readonly UINT_MAX = 0xffffffffn

// valid, but not good enough, `@prop()` is not necessary for a CTC
@prop()
static readonly UINT_MAX = 0xffffffffn

// invalid
@prop(true)
static readonly UINT_MAX = 0xffffffffn
Enter fullscreen mode Exit fullscreen mode

3. Constructor

A smart contract must have an explicit constructor() if it has at least one @prop that is not static. The super method must be called in the constructor and all the arguments of the constructor should be passed to super in the same order as they are passed into the constructor. For example,

class A extends SmartContract {
  readonly p0: bigint

  @prop()
  readonly p1: bigint

  @prop()
  readonly p2: boolean

  constructor(p0: bigint, p1: bigint, p2: boolean) {
    super(...arguments) // same as super(p0, p1, p2)
    this.p0 = p0
    this.p1 = p1
    this.p2 = p2
  }
}
Enter fullscreen mode Exit fullscreen mode

arguments is an array containing the values of the arguments passed to that function. ... is the spread syntax.

4. Methods

Like properties, a smart contract can also have two kinds of methods:
a. With @method decorator: these methods can only call methods also decorated by @method or functions specified below. Also, only the properties decorated by @prop can be accessed.

b. Without @method decorator: these methods are just regular TypeScript class methods.

a. @method decorator
This decorator is used to mark any method that intends to run on chain. It takes a sighash flag as a parameter.

Public @methods

Each contract must have at least one public @method. It is denoted with the public modifier and does not return any value. It is visible outside the contract and acts as the main method into the contract (like main in C and Java).
A public @method can be called from an external transaction. The call succeeds if it runs to completion without violating any conditions in assert(). An example is shown below.

@method()
public unlock(x: bigint, y: bigint) {
  assert(x + y == this.sum, 'incorrect sum')
  assert(x - y == this.diff, 'incorrect diff')
}
Enter fullscreen mode Exit fullscreen mode

Ending rule
A public @method method must end with assert() in all reachable code paths. A detailed example is shown below.

class PublicMethodDemo extends SmartContract {

  @method()
  public foo() {
    // valid, last statement is `assert()` statement
    assert(true);
  }

  @method()
  public foo() {
    // valid, `console.log` calls will be ignored when verifying the last `assert()` statement
    assert(true); //
    console.log();
    console.log();
  }

  @method()
  public foo() {
    // valid, last statement is `for` statement
    for (let index = 0; index < 3; index++) {
        assert(true);
    }
  }

  @method()
  public foo(z: bigint) {
    // valid, last statement is `if-else` statement
    if(z > 3n) {
        assert(true)
    } else {
        assert(true)
    }
  }

  @method()
  public foo() {
    // invalid, the last statement of every public method should be an `assert()` statement
  }

  @method()
  public foo() {
    assert(true);
    return 1n;  // invalid, because a public method cannot return any value
  }

  @method()
  public foo() {
    // invalid, the last statement in the `for` statement body doesn't end with `assert()`
    for (let index = 0; index < 3; index++) {
        assert(true);
        z + 3n;
    }
  }

  @method()
  public foo() {
    // invalid, because each conditional branch does not end with `assert()`
    if(z > 3n) {
      assert(true)
    } else {

    }
  }

  @method()
  public foo() {
    // invalid, because each conditional branch does not end with `assert()`
    if(z > 3n) {
      assert(true)
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Non-public @methods

Without a public modifier, a @method is internal and cannot be directly called from an external transaction.

@method()
xyDiff(): bigint {
  return this.x - this.y
}

// static method
@method()
static add(a: bigint, b: bigint): bigint {
  return a + b;
}
Enter fullscreen mode Exit fullscreen mode

NOTE
Recursion is disallowed. A @method, whether public or not, cannot call itself either directly in its own body, nor indirectly call another method that transitively calls itself. A more detailed example is shown below.

class MethodsDemo extends SmartContract {
  @prop()
  readonly x: bigint;
  @prop()
  readonly y: bigint;

  constructor(x: bigint, y: bigint) {
    super(...arguments);
    this.x = x;
    this.y = y;
  }

  // good, non-public static method without access `@prop` properties
  @method()
  static add(a: bigint, b: bigint): bigint {
    return a + b;
  }

  // good, non-public method
  @method()
  xyDiff(): bigint {
    return this.x - this.y
  }

  // good, public method
  @method()
  public checkSum(z: bigint) {
    // good, call `sum` with the class name
    assert(z == MethodsDemo.add(this.x, this.y), 'check sum failed');
  }

  // good, another public method
  @method()
  public sub(z: bigint) {
    // good, call `xyDiff` with the class instance
    assert(z == this.xyDiff(), 'sub check failed');
  }

  // valid but bad, public static method
  @method()
  public static alwaysPass() {
    assert(true)
  }
}
Enter fullscreen mode Exit fullscreen mode

A SIMPLE SUMMATION SMART CONTRACT

A smart contract is a class that extends the SmartContract base class. A simple example is shown below.

import { SmartContract, method, prop, assert } from "scrypt-ts"

class Equations extends SmartContract {

  @prop()
  sum: bigint

  @prop()
  diff: bigint

  constructor(sum: bigint, diff: bigint) {
    super(...arguments)
    this.sum = sum
    this.diff = diff
  }

  @method()
  public unlock(x: bigint, y: bigint) {
    assert(x + y == this.sum, 'incorrect sum')
    assert(x - y == this.diff, 'incorrect diff')
  }

}
Enter fullscreen mode Exit fullscreen mode

The smart contract above adds up two unknown variables, x and y.
Class members decorated with @prop and @method will end up on the blockchain and thus must be a strict subset of TypeScript. Everywhere decorated with them can be regarded in the on-chain context. Members decorated with neither are regular TypeScript and are kept off chain. The significant benefit of sCrypt is that both on-chain and off-chain code are written in the same language: TypeScript.
For further reading on how to write smart contracts on scrypt you can visit the scrypt doc.

Top comments (0)