Memo Data In Transactions (Draft)

Greetings, Cadence smart contract authors.

Some of you have asked about Flow’s support for “Memo” fields in transactions. We have drafted the following proposal and would love to hear your feedback regarding it. We are particularly interested to know whether this fits your desired use cases for Memo data.

Thank you!

– R.


“Memo” Data In Flow

“Memos” are a common pattern used to include informational data included in a blockchain transaction which is not otherwise processed by code on the blockchain. This data can be used to track attributes of the transaction off-chain. Common uses of Memo data include specifying which off-chain account to credit with a token deposit, identifying an invoice that is being paid, indicating an item that the transaction is intended to purchase, or a handling a cross-chain swap involving other blockchain systems.

This document describes several ways that Memo functionality can be implemented on the Flow blockchain in Cadence.

Memo Events

Flow transactions can combine multiple calls to many different contracts during their execution. Rather than including a specific Memo field in each transaction, we can emit Memo events in transactions that require them by calling into a standard Memo contract:

// MemoEmitter.cdc
// A contract that allows anyone to emit a Memo event

pub contract MemoEmitter {
    pub event Memo(memo: String)

    pub fun emitMemo(memo: String) {
        emit Memo(memo: memo)
    }
}

// transfer_token_with_memo.cdc
// A token transfer transaction that uses MemoEmitter to emit a Memo event.

import FungibleToken from 0xFUNGIBLETOKENADDRESS
import ExampleToken from 0xTOKENADDRESS
import MemoEmitter from 0xMEMOEMITTER

transaction(amount: UFix64, to: Address, memo: String) {

    // The Vault resource that holds the tokens that are being transferred
    let sentVault: @FungibleToken.Vault

    prepare(signer: AuthAccount) {

        // Get a reference to the signer's stored vault
        let vaultRef = signer.borrow<&ExampleToken.Vault>(from: /storage/exampleTokenVault)
			?? panic("Could not borrow reference to the owner's Vault!")

        // Withdraw tokens from the signer's stored vault
        self.sentVault <- vaultRef.withdraw(amount: amount)
    }

    execute {

        // Get the recipient's public account object
        let recipient = getAccount(to)

        // Get a reference to the recipient's Receiver
        let receiverRef = recipient.getCapability(/public/exampleTokenReceiver)!.borrow<&{FungibleToken.Receiver}>()
			?? panic("Could not borrow receiver reference to the recipient's Vault")

        // Deposit the withdrawn tokens in the recipient's receiver
        receiverRef.deposit(from: <-self.sentVault)

        // Emit a Memo event containing the memo
        MemoEmitter.emitMemo(memo: memo)
    }
}

The payment recipient can watch for deposit events to their address and then recover the sender’s address from the withdrawal event and the extra data included in the Memo events emitted in the same transaction. This provides all the information needed to associate the transaction with the memo information.

Advantages:

  • You can add calls to the MemoEmitter in any transaction.
  • Any number of Memo events can be attached to a single transaction, in case multiple Memo use cases coincide.
  • Closely matches the Memo functionality available on other chains.

Disadvantages:

  • Flow Wallet software is not aware of the Memo functionality, and would need to be updated to be aware.
  • Memo information is unstructured and not visible to the receiving account during the transaction.

Receiver Proxy

A token recipient who wishes to associate received transfers with Memo data can replace the standard Flow FT standard Receiver capability with an alternative implementation that takes both a Vault and an additional argument (or arguments) tagging the transfer appropriately.

The receiving object could then emit a contract-specific event, or even divert the funds based on the value of that tag as part of the same transaction.

// MemoTokenProxy.cdc
// This provides an implementation of FungibleToken.Receiver that wraps
// an existing reciver and emits a Memo event when tokens are deposited.

import FungibleToken from 0xFUNGIBLETOKENADDRESS

pub contract MemoTokenProxy {

    pub event Memo(memo: String)

    pub resource MemoReceiverProxy {

        // This is where the deposited tokens will be sent.
        // The type indicates that it is a reference to a receiver
        //
        access(self) var recipient: Capability

        // deposit
        //
        // Function that takes a Vault object as an argument and forwards
        // it to the recipient's Vault using the stored reference
        //
        pub fun deposit(from: @FungibleToken.Vault, memo: String) {
            let receiverRef = self.recipient.borrow<&{FungibleToken.Receiver}>()!

            let balance = from.balance
          
            receiverRef.deposit(from: <-from)

            emit Memo(memo: memo)
        }

    }

    // createNewMemoReceiverProxy creates a new FMemoReceiverProxy reference with the provided recipient
    //
    pub fun createNewMemoReceiverProxy(recipient: Capability): @MemoReceiverProxy {
        return <-create MemoReceiverProxy(recipient: recipient)
    }
}

// install_memo_receiver_proxy.cdc
// This wraps the existing Receiver in a proxy that takes a memo.

import FungibleToken from 0xFUNGIBLETOKENADDRESS
import MemoTokenProxy from 0xFORWARDINGADDRESS

transaction(receiver: Address) {

    prepare(acct: AuthAccount) {
        let recipient = getAccount(receiver).getCapability<&{FungibleToken.Receiver}>(/public/exampleTokenReceiver)!

        let vault <- MemoTokenProxy.createNewMemoReceiverProxy(recipient: recipient)
        acct.save(<-vault, to: /storage/exampleTokenMemoReceiverProxy)

        if acct.getCapability(/public/exampleTokenReceiver)!.borrow<&{FungibleToken.Receiver}>() != nil {
            acct.unlink(/public/exampleTokenReceiver)
        }
        acct.link<&MemoReceiverProxy>(/public/exampleTokenReceiver, target: /storage/exampleTokenMemoReceiverProxy)
    }
}

// transfer_tokens_with_memo.cdc
// This sends tokens to the Receiver proxy along with a memo string.

import FungibleToken from 0xFUNGIBLETOKENADDRESS
import MemoTokenProxy from 0xTOKENADDRESS

transaction(amount: UFix64, to: Address, memo: String) {

    // The Vault resource that holds the tokens that are being transferred
    let sentVault: @FungibleToken.Vault

    prepare(signer: AuthAccount) {

        // Get a reference to the signer's stored vault
        let vaultRef = signer.borrow<&MemoTokenProxy.Vault>(from: /storage/exampleTokenVault)
			?? panic("Could not borrow reference to the owner's Vault!")

        // Withdraw tokens from the signer's stored vault
        self.sentVault <- vaultRef.withdraw(amount: amount)
    }

    execute {

        // Get the recipient's public account object
        let recipient = getAccount(to)

        // Get a reference to the recipient's Receiver
        let receiverRef = recipient.getCapability(/public/exampleTokenMemoReceiverProxy)!.borrow<&{FungibleToken.Receiver}>()
			?? panic("Could not borrow receiver reference to the recipient's Vault")

        // Deposit the withdrawn tokens in the recipient's receiver, with a memo
        receiverRef.deposit(from: <-self.sentVault, memo: memo)
    }
}

Advantages:

  • Users are prevented from accidentally sending funds to the receiver without a tag. (If the receiver did additional validation on the tag, you could even prevent transfers with an invalid tag.)
  • A single transaction can include multiple transfers with different tags.
  • The receiver can act on the value of the tag, possible diverting the funds directly to different accounts depending on that value.
  • The tag doesn’t need to be a string. Different use-cases could replace the string with any kind of appropriate data structure.

Disadvantages:

  • The sender of the transaction needs to understand the format of the tag. This scheme probably works best when the receiver has a web portal that uses FCL to propose the correct transaction format directly to the user’s wallet software.
  • This scheme only works for token transfers; other use cases (e.g. NFT transfers) could easily adopt this strategy, but this scheme doesn’t support adding arbitrary memos to transactions that interact with contracts/accounts that don’t expect tagging.

Updated Token Standards

We have considered updating the Flow Fungible Token standard to include a memo field, similar to the deposit method shown in the second example above. This would allow any token transfer to include a memo, without any additional support from other wallets or smart contracts.

However, we decided against this because it would add unnecessary baggage to the majority of token transfers that don’t require a Memo to be attached, and doesn’t actually solve the general memo problem outside of fungible token transfers.

Recommendations

We recommend that, for most use cases, the Receiver Proxy pattern provides the most flexibility and power to handle the most common use cases of attaching a Memo to a token transfer. Additionally, we would encourage teams that would find Memo functionality useful for non-token transfer use cases to consider how the Proxy pattern might be adapted to their scenario. We expect that most teams will find that the clarity, power, and flexibility of this pattern is superior to the traditional transaction memo field most of the time.

However, since this pattern can’t be adapted to all cases, we will be making a standard MemoEmitter resource, as described above, available on Flow in the near future. We will be sure to announce the account address where it can be found when it goes live.

3 Likes