Skip to content
Iuri Matias edited this page Dec 19, 2019 · 1 revision

Teller Network - Smart contracts specification

Sellers and Arbitrators License Acquisition

Required for being able to publish offers and to act as an arbitrator. Initial step for both of these roles.

  • SNT amount determined by License.methods.price().call() is required for acquiring the license
  • License can only be bought once

Via approveAndCall

Method used in the UI so the user can do the operation in a single step.

  • Input parameters of receiveApproval are validated: Only SNT token can be used, with amount == price, and data containing abi encoded buy()
participant User
participant SNT
participant Proxy(*License)
User-->Proxy(*License): price()
Proxy(*License)-->User: price
User->SNT: approveAndCall(..., buy())
SNT-->User: event - Approval
SNT->Proxy(*License): receiveApproval()
Proxy(*License)-->Proxy(License): _abiDecodeBuy()
Proxy(*License)->Proxy(License): _buyFrom()
Proxy(*License)-->User: event - Bought
SNT->SNT: transferFrom(user, burnAddress)
SNT-->User: event - Transfer
SNT-->User: true

Separate transactions

Not used on the frontend

participant User
participant SNT
participant Proxy(*License)
User-->Proxy(*License): price()
Proxy(*License)-->User: price
User->SNT: approve(Proxy(*License), price)
SNT-->User: event - Approval
SNT-->User: true
User->Proxy(*License): buy()
Proxy(*License)->Proxy(*License): _buyFrom()
Proxy(*License)-->User: event - Bought
SNT->SNT: transferFrom(user, burnAddress)
SNT-->User: event - Transfer
SNT-->User: true

Arbitrator setting "Accept all sellers" option

Arbitrators need to either approve sellers individually, or accept every seller automatically.

  • Requires the user to have acquired a arbitrator license before
User(Arbitrator)->Proxy(ArbitratorLicense): changeAcceptAny()
Proxy(ArbitratorLicense)-->Proxy(ArbitratorLicense): isLicenseOwner()

Seller requests Arbitrator's approval

Sellers require the approval from arbitrators to use them in their offers. They need to individually request arbitrators that don't accept all sellers.

  • Requires the selected arbitrator to be valid (have acquired a license before)
  • The arbitrator must not accept all sellers.
  • Arbitrator must not have accepted the seller before or received requests from them.
  • If the arbitrator have received a request before, this request should have been rejected (REJECTED), or canceled by the seller (CLOSED), and 3 days should have passed since last request.
participant User(Seller)
participant Proxy(ArbitratorLicense)
User(Seller)->Proxy(ArbitratorLicense): requestArbitrator()
Proxy(ArbitratorLicense)-->Proxy(ArbitratorLicense): isLicenseOwner()
Proxy(ArbitratorLicense)-->User(Seller): event - ArbitratorRequested

Seller cancels or rejects Arbitrator's approval

A seller may cancel a request sent to an arbitrator, or stop working with an arbitrator at any time.

  • A request Id is required. These are made via keccak256(arbitrator, seller)
  • Cancelling an arbitrator that had previously accepted the seller will disable published offers with that arbitrator. Previously created escrows will not be affected.
  • Request must be pending (AWAIT) or accepted (ACCEPTED)
participant User(Seller)
participant Proxy(ArbitratorLicense)
Note over User(Seller): Request arbitrator's approval
User(Seller)->Proxy(ArbitratorLicense): cancelRequest()
Proxy(ArbitratorLicense)-->User(Seller): event - RequestCanceled

Arbitrator approving / rejecting a seller

Arbitrators must evaluate if they want to work with a seller or not. Acceptance criteria is individual per arbitrator. Once a decision is made, they can use either acceptRequest or rejectRequest, using the request id.

  • Request must be pending (AWAIT)
  • Arbitrator must not accept all sellers already
  • Only the arbitrator can approve their own received requests
participant User(Seller)
participant User(Arbitrator)
participant Proxy(ArbitratorLicense)
Note over User(Seller): Request arbitrator's approval
Note left of User(Arbitrator): Accepts request
User(Arbitrator)->Proxy(ArbitratorLicense): acceptRequest()
Proxy(ArbitratorLicense)-->Proxy(ArbitratorLicense): isLicenseOwner()
Proxy(ArbitratorLicense)-->User(Arbitrator): event - RequestAccepted
Note left of User(Arbitrator): Rejects request
User(Arbitrator)->Proxy(ArbitratorLicense): rejectRequest()
Proxy(ArbitratorLicense)-->Proxy(ArbitratorLicense): isLicenseOwner()
Proxy(ArbitratorLicense)-->User(Arbitrator): event - RequestRejected

Publishing an offer

Used by the sellers after they acquire a license, and also are approved by arbitrators.

  • Seller must have a license
  • Arbitrator must have approved the seller
  • Margin must be between -100% and 100%
  • The seller cannot be the arbitrator for their offers
  • The username, contact code and location of the seller will be updated with the info from the latest offer they create.
User(Seller)->Proxy(MetadataStore): addOffer(..., arbitrator)
Proxy(MetadataStore)-->Proxy(SellerLicense): isLicenseOwner()
Proxy(SellerLicense)-->Proxy(MetadataStore): true
Proxy(MetadataStore)-->Proxy(ArbitratorLicense): isAllowed(arbitrator, seller)
Proxy(ArbitratorLicense)-->Proxy(MetadataStore): true
Proxy(MetadataStore)->Proxy(MetadataStore): _addOrUpdateUser(seller)
Proxy(MetadataStore)-->User(Seller): event - OfferAdded

Deleting an offer

A seller might remove an offer after they don't want to sell anymore, if they create their offer by mistake or if the arbitrator rejected the seller.

  • The offer and user must exist to remove the offer
Participant User(Seller)
Participant Proxy(MetadataStore)
Note over User(Seller): Publishes an offer
User(Seller)->Proxy(MetadataStore): removeOffer()
Proxy(MetadataStore)-->User(Seller): event - OfferRemoved

Add or update user information

Used after the user buys an arbitrator license, or when the user edit their contact information via the [Profile] screen. This information will be used to display the user details for all the participants involved in a trade

User->Proxy(MetadataStore): addOrUpdateUser(bytes,string,string)

Creating a trade

Trades can be created by anyone assuming they have the buyer's signature. The seller opinion in this case is not taken inaccount (a seller is interested in selling anyway, so if they don't want to receive trades, they can remove the offer). Future versions might have some integration with Tribute to Talk SNT usecase in order for sellers to block receiving offers from buyers they don't want to receive trades from.

  • Buyer needs to sign a hash composed by their username, status contact code and a unique nonce
  • The offer must not have been deleted
  • The seller must be valid
  • Arbitrator, Buyer and Seller should all be different
  • Prices and amount of tokens being bought must all be different
  • Prices are an input value, and is a value calculated in the UI using the seller's margin and the cryptocompare price. Both the seller and the buyer must be aware of any difference between the asset price and the trade information (A warning is presented in the UI if the prices deviate too much from current asset price)
  • A paused escrow contract will not allow the creation and funding of new trades
Participant User(Buyer)
Participant User(Seller)
Participant Proxy(Escrow)
Participant Proxy(MetadataStore)
Participant Proxy(SellerLicense)
User(Buyer)->Proxy(Escrow): createEscrow(...)
Proxy(Escrow)->Proxy(MetadataStore): addOrUpdateUser(signature, ...)
Proxy(MetadataStore)-->Proxy(Escrow): buyer address
Proxy(Escrow)->Proxy(Escrow): _createTransaction(...)
Proxy(Escrow)-->Proxy(MetadataStore): offer(...)
Proxy(MetadataStore)-->Proxy(Escrow): offer
Proxy(Escrow)-->Proxy(SellerLicense): isLicenseOwner(seller)
Proxy(SellerLicense)-->Proxy(Escrow): true
Note over Proxy(Escrow): CREATED
Proxy(Escrow)-->User(Buyer): event - Created
Proxy(Escrow)-->User(Buyer): escrow id

Escrow flow after creation

A trade will change status during the interactions the participants will have, being the sequence for a successfull trade: CREATED -> FUNDED -> PAID -> RELEASED, with PAID being optional since the seller can release the funds at any moment after the escrow has been funded.

  • A paused escrow contract will not allow the funding of escrows. Escrows that have moved forward from this status can continue normally
  • Funding an escrow requires the trade status to be CREATED. Funding can only be done by a valid seller. (Valid in this case means to have a license, and being the owner of the offer). A fee is deducted in addition to funding and sent to the burn address. After funding the status changes to FUNDED. The trade needs to be paid before 5 days have passed.
  • createAndFund is an alternative function that can be used by the sellers to create an escrow and fund it in a single step. No UI is available for this function.
  • Buyers need to optionally mark the transaction as PAID, to notify the seller that they sent their FIAT payment. This step is required for buyers to be able to open a dispute. A transaction can be marked as paid before the expiration time of 5 days. Only FUNDED escrows can be marked as paid.
  • After the seller verifies they have received their payment, they proceed to release the funds. Only PAID or FUNDED trades can be released, and they must not be under a dispute. The release process will transfer the funds to the buyer and a fee to the arbitrator. This arbitrator fee will be less than if the arbitrator had resolved a dispute for the same trade. The status will change to RELEASED.
  • Buyer can then optionally rate the transaction in order to increase / decrease the seller's reputation score.
Participant User(Buyer)
Participant User(Seller)
Participant Proxy(Escrow)
Participant Proxy(SellerLicense)
Participant ERC20
Note over User(Buyer): creates a trade
Note over Proxy(Escrow): CREATED
User(Seller)-->Proxy(Escrow): filter Created events
Proxy(Escrow)-->User(Seller): events - Created 
User(Seller)->Proxy(Escrow): fund(escrowId)
Proxy(Escrow)->Proxy(Escrow): _fund(...)
Proxy(Escrow)-->Proxy(SellerLicense): isLicenseOwner(seller)
Proxy(SellerLicense)-->Proxy(Escrow): true
Proxy(Escrow)->Proxy(Escrow): _payFee()
Proxy(Escrow)-->Proxy(Escrow): _getValueOffMillipercent()
Proxy(Escrow)->ERC20: transferFrom(seller, Proxy(Escrow))
ERC20-->User(Seller): event - Transfer
Note over Proxy(Escrow): FUNDED
Proxy(Escrow)-->User(Seller): event - Funded
Note over User(Buyer): Pays the seller (optional)
User(Buyer)->Proxy(Escrow): pay(escrowId)
Proxy(Escrow)->Proxy(Escrow): _pay(...)
Note over Proxy(Escrow): PAID
Proxy(Escrow)-->User(Buyer): event - Paid
Note over User(Seller): Verifies payment
User(Seller)->Proxy(Escrow): release(escrowId)
Proxy(Escrow)->Proxy(Escrow): _release(...)
Proxy(Escrow)->ERC20: transfer(buyer, ...)
Proxy(Escrow)->Proxy(Escrow): _releaseFee(...)
Proxy(Escrow)-->Proxy(Escrow): _getValueOffMillipercent()
Proxy(Escrow)-->Proxy(Escrow): _getValueOffMillipercent()
Proxy(Escrow)->ERC20: transfer(arbitrator, ...)
ERC20-->User(Seller): event - Transfer
Proxy(Escrow)->ERC20: transfer(burnAddress, ...)
ERC20-->User(Seller): event - Transfer
Note over Proxy(Escrow): RELEASED
Proxy(Escrow)-->User(Seller): event - Released
Note over User(Seller): Rates transaction
User(Seller)->Proxy(Escrow): rateTransaction(escrowId, ...)
Proxy(Escrow)-->User(Seller): event - Rating

Buyer/Seller cancels a non funded trade

Escrows which are in the CREATED state can be cancelled by both trade participants but not by a third party.

Participant User
Participant Proxy(Escrow)
Note over User: creates a trade
User->Proxy(Escrow): cancel(escrowId)
Proxy(Escrow)->Proxy(Escrow): _cancel()
Note over Proxy(Escrow): CANCELED
Proxy(Escrow)-->User: event - Canceled

Seller cancels a funded escrow

Escrows which are in the FUNDED state can be cancelled by the buyer at any time. Sellers however, need to wait until the expiration time of 5 days pass before being able to retrieve the funds. After this expiration time passes, Sellers can cancel the escrow. Funds will be then transfered back to the seller.

  • This is a possible vector of attack for griefing the sellers.
Participant User(Buyer)
Participant User(Seller)
Participant Proxy(Escrow)
Participant ERC20
Note over User(Buyer): creates a trade
Note over User(Seller): funds the trade
Note over Proxy(Escrow): FUNDED
Note over User(Seller): waits for expiration
User(Seller)->Proxy(Escrow): cancel(escrowId)
Proxy(Escrow)->Proxy(Escrow): _cancel()
Proxy(Escrow)->ERC20: transfer(seller, ...)
ERC20-->User(Seller): event - Transfer
Note over Proxy(Escrow): CANCELED
Proxy(Escrow)-->User(Seller): event - Canceled

Participant opens a dispute

Any participant can open a dispute after the trade has been marked as paid (PAID status).

  • A dispute should not have been opened before
  • No third party is allowed to open a dispute, unless it's on behalf of the user via the openCase function that receives a signature
Participant User
Participant Proxy(Escrow)
Note over User: Buyer creates a trade
Note over User: Seller funds the trade
Note over User: Buyer marks transaction as paid
Note over Proxy(Escrow): PAID
User->Proxy(Escrow): openCase(...)
Proxy(Escrow)-->Proxy(Escrow): isDisputed()
Proxy(Escrow)->Proxy(Escrow): _openDispute(...)
Proxy(Escrow)-->Proxy(Escrow): getArbitrator()
Proxy(Escrow)-->User: event - ArbitrationRequired

Participant cancels a dispute

The participant that opened a dispute, can cancel it, if an arbitrator hasn't resoved the dispute before.

Participant User(Participant)
Participant User(Arbitrator)
Participant Proxy(Escrow)
Note over User(Participant): Buyer creates a trade
Note over User(Participant): Seller funds the trade
Note over User(Participant): Buyer marks transaction as paid
Note over Proxy(Escrow): PAID
Note over User(Participant): Participant opens dispute
User(Participant)->Proxy(Escrow): cancelArbitration(escrowId)
Proxy(Escrow)-->User(Participant): event - ArbitrationCanceled

Resolving a dispute

After a trade participant opens a dispute, the selected arbitrator will proceed to make a decision based on their findings. Disputes will be solved in favor of the buyer or the seller.

  • Disputes can only be solved once, and need to be open in order for the arbitrator to work on it.
  • Arbitrator must have a valid license, and only the selected arbitrator for the offer can work disputed created with that offer.
  • If the dispute is solved in favor of the buyer, the flow ill be similar to the release process, except that a fee percentage will be deducted for the arbitrator services and sent to the arbitrator address.
Participant User(Participant)
Participant User(Arbitrator)
Participant Proxy(Escrow)
Note over User(Participant): Buyer creates a trade
Note over User(Participant): Seller funds the trade
Note over User(Participant): Buyer marks transaction as paid
Note over Proxy(Escrow): PAID
Note over User(Participant): Participant opens dispute
User(Arbitrator)-->Proxy(Escrow): filter ArbitrationRequired events
Proxy(Escrow)-->User(Arbitrator): events - ArbitrationRequired 
User(Arbitrator)->Proxy(Escrow): setArbitrationResult(...)
Proxy(Escrow)-->Proxy(ArbitratorLicense): isLicenseOwner
Proxy(ArbitratorLicense)-->Proxy(Escrow): true
Proxy(Escrow)-->User(Arbitrator): event - ArbitrationResolved
Proxy(Escrow)->Proxy(Escrow): _solveDispute(...)
Note left of Proxy(Escrow): If solved in favor of buyer
Proxy(Escrow)->Proxy(Escrow): _release(...)
Note left of Proxy(Escrow): If solved in favor of seller
Proxy(Escrow)->Proxy(Escrow): _cancel(...)

Relaying a trade operation

Considering most buyers probably wont have ether in order to perform the required transactions, we will use Gas Station Network's contracts to relay the escrow creation, cancel, marking an operation as paid and opening a dispute for ETH and SNT offers.

  • These operations are transparent for the user since we change the contract address dinamically in the provider so transactions go to EscrowRelay instead of EscrowProxy.
  • Buyers' balance should be < 600000gas * gas price
  • No extra operations are allowed
  • Creating and canceling an escrow can be done once every 15 minutes. The rest of the operations do not have this restriction
  • Only operations related to ETH and SNT are allowed
  • This is a possible vector of attack. The funds for relaying transactions can be depleted by malicious users by creating many transactions with different accounts.
Participant User(Buyer)
Participant Relay
Participant RelayHub
Participant EscrowRelay
Participant Proxy(Escrow)
User(Buyer)->User(Buyer): sign operation to relay
User(Buyer)->Relay: send signature
Relay->RelayHub: relay(...)
RelayHub-->EscrowRelay: accept_relayed_call
EscrowRelay-->Proxy(Escrow): getBasicTradeData
Proxy(Escrow)-->EscrowRelay: trade data
EscrowRelay-->Proxy(MetadataStore): getAsset
Proxy(MetadataStore)-->EscrowRelay: asset
EscrowRelay-->RelayHub: 0
RelayHub->EscrowRelay: operation to execute
EscrowRelay->Proxy(Escrow): operation to execute
Proxy(Escrow)-->Relay: operation result
RelayHub->EscrowRelay: post_relayed_call(...)
Relay-->User(Buyer): operation result

Upgrading contracts

MetadataStore, SellerLicense, ArbitratorLicense, and Escrow are base contracts for their Proxy counterparts. The proxy instances are upgradable by the owner of the contract, and this is inteded to be used in case of adding new functionality or error fixes. Upgrades can be simple or require an init function. This uses the unstructured storage upgrade pattern from ZeppelinOS: https://github.com/zeppelinos/labs/tree/master/upgradeability_using_unstructured_storage

Participant User(Owner)
Participant Base Contract
Participant OwnedUpgradabilityProxy
User(Owner)->BaseContract: deploys
BaseContract-->User(Owner): address
User(Owner)->OwnedUpgradabilityProxy: deploys
OwnedUpgradabilityProxy-->User(Owner): address
Note left of User(Owner): If base contract has an init method
User(Owner)-->User(Owner): abi encode the init method and parameters
User(Owner)->OwnedUpgradabilityProxy: upgrade / upgradeToAndCall(Base Contract Address, init parameters)
OwnedUpgradabilityProxy->OwnedUpgradabilityProxy: _setImplementation(Base Contract Address)
OwnedUpgradabilityProxy-->User(Owner): event - Upgraded
OwnedUpgradabilityProxy-->OwnedUpgradabilityProxy: delegateCall

Burning fees with Kyber

All fees received in teller network, (from license acquisitions and escrows), are sent to a burn address, which in this case, it will be the KyberFeeBurner contract address. The idea is to create a token sink which will reduce the circulating SNT supply. Since fees can be obtained in diverse tokens, we use Kyber to perform the exchange from any Token or Ether to SNT (this also puts additional buy pressure).

  • Anyone can call the swap function.
  • Owners of the contract could call escape(address) in case they want to remove the tokens from the contract
  • Having this process separate from the escrow reduces costs for the users as well as possibilities of errors during the normal teller operation.
  • The minimum conversion rate comes from Kyber's expected rate
Participant TellerContracts
Participant User
Participant KyberFeeBurner
Participant KyberNetworkProxy
Participant ERC20Token
TellerContracts->KyberFeeBurner: transfer asset
User->KyberFeeBurner: swap
KyberFeeBurner-->KyberNetworkProxy: getExpectedRate
KyberNetworkProxy-->KyberFeeBurner: minConversionRate
Note left of KyberFeeBurner: if asset != SNT
KyberFeeBurner->ERC20Token: approve(kyberNetworkProxy)
KyberFeeBurner->KyberNetworkProxy: trade()
Note left of KyberFeeBurner: if asset == SNT
KyberFeeBurner->ERC20Token: transfer(burnAddress)
KyberFeeBurner-->User: event - Swap