Skip to content

Basic Implementation of a Local Exchange Trading System (LETS)#

Overview#

The basic blueprint of our system encompasses two contracts: an administrative contract and a trade contract. Before delving into the details, we recommend acquainting yourself with the foundational aspects of Ergo by reviewing this ICO article as well as ErgoScript tutorials.

Despite the aforementioned recommendations, we will elucidate a few novel terms in the upcoming sections.

Singleton Tokens and Boxes#

In Ergo, when a token is minted with a value of one, it is termed a singleton token. Similarly, a box containing a singleton token is known as a singleton box.

Administrative Contract#

Purpose#

  • The administrative contract oversees a singleton box that encompasses the members of the LETS system.
  • This contract facilitates the addition of new members, at a rate of one member per transaction.
  • Instead of storing members, the box merely holds a succinct digest of an authenticated data structure based on the members' directory.
  • Each member is linked with a singleton token minted during a transaction that adds the member to the directory.
  • This transaction generates a new member's box, housing the member's singleton token.
  • The trade contract safeguards the member's box.
  • Moreover, the freshly generated member's box records the initial balance in the R4 register, which in this case is zero.
  • The transaction that adds a new member is obligated to validate the correctness of the directory transformation.

Committee Management#

A committee generally manages the administrative contract box, and the composition of this committee may change over time. To accommodate this, we allow the committee's logic to reside in the R5 register. For instance, if a new member is added to both the committee and the LETS system, the incoming administrative contract box would require signatures from two out of three members, while the outgoing box would require three out of four signatures. Consequently, the data within the R5 register of the input and output boxes would vary.

Below, we provide the administrative contract's code written in ErgoScript, complete with comments. Note that userContractHash corresponds to the hash of the trade contract.

    val selfOut = OUTPUTS(0)

    // Administrative script
    val adminScript = selfOut.R5[SigmaProp].get

    // Confirming that the script replicates itself and the administrative script is satisfied
    val scriptIsCorrect = (selfOut.propositionBytes == SELF.propositionBytes) && adminScript

    // A spending transaction creates boxes for the directory, user, and fee
    val isOutputSizeCorrect = OUTPUTS.size == 3

    // Verifies the replication of the administrative label token 
    val isTokenOutputCorrect = (selfOut.tokens.size == 1) && (selfOut.tokens(0)._1 == letsToken)

    // Validates the issuance of a new token and its amount
    // OUTPUTS(0) tokens are already checked via isTokenOutputCorrect
    val issuedTokenId = INPUTS(0).id
    val userOut = OUTPUTS(1)
    val areTokenAmountsCorrect =
      (userOut.tokens.size == 1 &&
        userOut.tokens(0)._1 == issuedTokenId &&
        userOut.tokens(0)._2 == 1 &&
        OUTPUTS(2).tokens.size == 0 &&
        isTokenOutputCorrect)

    // Verifies that the new user is created with a zero balance
    val isUserBalanceZero  = userOut.R4[Long

].get == 0

    val isUserScriptProper = blake2b256(userOut.propositionBytes) == userContractHash

    // Validates the addition of the new token identifier to the directory
    val selfTree = SELF.R4[AvlTree].get
    val toAdd: Coll[(Coll[Byte], Coll[Byte])] = Coll((issuedTokenId, Coll[Byte]()))
    val proof = getVar[Coll[Byte]](1).get
    val updatedTree = selfTree.insert(toAdd, proof).get
    val expectedTree = selfOut.R4[AvlTree].get
    val isTreeCorrect = updatedTree == expectedTree

    areTokenAmountsCorrect && scriptIsCorrect && isTreeCorrect && isUserBalanceZero && isUserScriptProper      

The trade contract script, presented below with explanatory comments, is fairly straightforward. The contract requires that a transaction spending an exchange contract box should have at least two inputs. The first two inputs must be protected by the exchange contract script and contain LETS member tokens. To validate the legitimacy of singleton member tokens in the inputs, the transaction provides the administrative contract box as the first read-only data input and also supplies proof that the member tokens indeed belong to the directory authenticated via the R4 register of the administrative contract box. The "letsToken" in the script refers to the singleton token of the administrative box.

  // Minimum balance allowed for a LETS trader
  val minBalance = -20000

  val lookupProof = getVar[Coll[Byte]](1).get

  // Read-only box containing the directory of LETS members
  val treeHolderBox = CONTEXT.dataInputs(0)
  val isLetsTokenProper = treeHolderBox.tokens(0)._1 == letsToken
  val membersTree = treeHolderBox.R4[AvlTree].get

  // A spending transaction takes two LETS members' boxes willing to make a trade, 
  // and returns boxes with updated balances.
  val participant0 = INPUTS(0)
  val participant1 = INPUTS(1)
  val participantOut0 = OUTPUTS(0)
  val participantOut1 = OUTPUTS(1)

  // Check that members do indeed belong to the LETS
  val token0 = participant0.tokens(0)._1
  val token1 = participant1.tokens(0)._1
  val memberTokens = Coll(token0, token1)
  val doMembersExist = membersTree.getMany(memberTokens, lookupProof).forall({ (o: Option[Coll[Byte]]) => o.isDefined })

  // Verify that changes in LETS member balances during the transaction are correct
  val initialBalance0 = participant0.R4[Long].get
  val initialBalance1 = participant1.R4[Long].get
  val finalBalance0  = participantOut0.R4[Long].get
  val finalBalance1  = participantOut1.R4[Long].get
  val balanceDifference0 = finalBalance0 - initialBalance0
  val balanceDifference1 = finalBalance1 - initialBalance1
  val areBalanceDifferencesCorrect = balanceDifference0 == -balanceDifference1
  val areBalancesCorrect = (finalBalance0 > minBalance) && (finalBalance1 > minBalance) && areBalanceDifferencesCorrect

  // Check that member boxes retain their scripts.
  val isScript0Preserved = participantOut0.propositionBytes == participant0.propositionBytes
  val isScript1Preserved = participantOut1.propositionBytes == participant1.propositionBytes
  val areScriptsPreserved = isScript0Preserved && isScript1Pres

erved

  // Protection specific to member boxes
  val selfPubKey = SELF.R5[SigmaProp].get

  selfPubKey && isLetsTokenProper && doMembersExist && areBalanceDifferencesCorrect && areScriptsPreserved

Please note that both the administrative and trade contracts can be modified in various ways to create new systems with different characteristics.