Simple notarised documents on the Ethereum blockchain - Notes.eth

It is interesting to me how life experiences often provide inspiration for ideas.

I previously purchased the Ethereum Name Service (ENS) name Notes.eth. I had had an idea for an interesting blockchain use case, namely notarisation of documents.

The idea is fairly simple - you use blockchain to provably verify that a particular version of a document was uploaded on a particular date by a particular person.

The obvious use cases of notarisation services are legal (I am reminded of the early days of Ethereum where people were getting married on the Blockchain) but my personal use case is somewhat more simple - verifiably dated notes - the ability to prove that I said/felt/thought something at a particular point in time.

There are further development opportunities for developing this into a full blown journalling solution or divergent ideas based on similar premises such as on chain Twitter. I'm going for a MVP, keep it as simple as possible kind of approach.

It was also a fun opportunity to have another play with Solidity and the current development tooling ecosystem.

Photo by Kelly Sikkema / Unsplash

IPFS

Noting that storing data on chain is expensive it makes little sense (and reduces flexibility) to store actual files on-chain. Instead I opted to use IPFS.

IPFS is essentially decentralised file storage and its usage is in keeping with the concept more generally. If I were to store files on a centralised service (like S3) the authenticity guarantees associated with your document are predicated on the continued existance of the service. You would be relying on me. Whilst I intend to continually support the product.. sometimes things happen.

We can then simply store the content identifer (CID) of the file on chain within our smart contract. We can do this because the CID is not a location identifier but rather identifies the specific contents of a file. If you edit your file the IPFS CID will inherently change.

Rather than reinvent the wheel and maintain my own IPFS infrastructure I opted to use web3.storage. I had previously tried to use Filebase but the S3 compatible API was not fully compatible and the support was non-existant. web3.storage worked out the box and had clean, clear, accurate documentation.

Contract

The smart contract for this is fairly simple. It needs to store the CID of the note/document, somehow associate it with a user, and keep track of the submission date/time.

I pieced together a very basic contract to do this:

// SPDX-License-Identifier: MIT
pragma solidity >=0.4.22 <0.9.0;

import "@openzeppelin/contracts/access/Ownable.sol";

contract NotesProxy is Ownable {

  struct NoteStruct {
      bytes32 hash;
      uint8 hashFunction;
      uint8 size;
      uint insertionDate;
      string noteName;
   }

   NoteStruct[] public notes;
   uint public noteCount;


  function addNote(bytes32 hash) onlyOwner public {
      
      addNote(hash, 0x12, 0x20);
  }


  function addNote(bytes32 hash, uint8 hashFunction, uint8 size) onlyOwner public {
      
      noteCount++;
      NoteStruct memory newNote = NoteStruct(hash, hashFunction, size, block.timestamp, "name");

      notes.push(newNote);
  }
}

In this initial implementation I stored the CID on the basis of some information on efficient storage that I garnered from this answer. I suspect there will be more nuance to my final implementation, but I need to read more about IPFS.

An observant individual will note that this stores notes generally and doesn't resolve the issue of user association.

I had a play with various ideas - mapping user addresses to arrays of notes for example. Then I stumbled upon a post about patterns used in the Gnosis safe contracts and was introduced to EIP1167 - Minimal Proxy Contract.

I did some further research and ended up using the OpenZeppelin contracts for the proxy and ownership elements of the code. There is no point reinventing the wheel especially when it comes to battle tested code that touches assembly and has significant security implications.

Whilst the product specific contracts in question are not particularly heavy, and the deployment costs (I suspect) are not that significant such a design approach does open the doors for extensibility. The premise is essentially to maintain the contract as written but deploy a clone contract for each user. User association would come from maintaining a mapping of user addresses to the contract address for that specific user's notes in the proxy deployment factory.

Factory

My initial first pass at the proxy factory looks like this:

// SPDX-License-Identifier: MIT
pragma solidity >=0.4.22 <0.9.0;

import "@openzeppelin/contracts/proxy/Clones.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract NotesProxyFactory is Ownable {

	address public implementationContract;
	address[] public allClones;
	mapping(address => address) public noteClones;

	event NewClone(address _clone);

	constructor(address _implementation) {
		implementationContract = _implementation;
	}

	function createNew() payable external returns(address instance) {
		instance = Clones.clone(implementationContract);
		(bool success, ) = instance.call{value: msg.value}(abi.encodeWithSignature("initialize()"));
		allClones.push(instance);
		noteClones[msg.sender] = instance;
		emit NewClone(instance);
		return instance;
	}
}

Truffle and Ganache

I use the combination of Truffle and Ganache for my smart contract development.

I wrote my contracts, and created a migration script that both deploys and tests the basic premise/functionality.

const NotesProxy        = artifacts.require("NotesProxy");
const NotesProxyFactory = artifacts.require("NotesProxyFactory");

module.exports = async function (deployer, network, accounts) {

  //Deploy the template Proxy contract
  await deployer.deploy(NotesProxy);

  const proxy = await NotesProxy.deployed();
  console.log("NotesProxy: " + proxy.address);

  //Deploy the proxy factory, specifying the template implementation address
  await deployer.deploy(NotesProxyFactory, proxy.address);
  const factory = await NotesProxyFactory.deployed();
  console.log("NotesProxyFactory: " + factory.address);

  //Call the factory to create a new proxy clone
  const newResponse = await factory.createNew();
  console.log("New Response", newResponse);

  //Get the address of the clone from the event logs
  const addressOfClone = newResponse.logs[0].args['0'];
  console.log("CLONE address", addressOfClone);

  //Instantiate the clone
  const cloneAt = await NotesProxy.at(addressOfClone);
  const clone   = await NotesProxy.deployed();

  //Some logging for debugging
  console.log("Proxy Template Hello World", await proxy.hi());
  console.log("Clone Hello World", await clone.hi());

  const noteCountBefore = await clone.noteCount();
  console.log("Note count before: ", noteCountBefore.toString());

  //Test adding a note
  await clone.addNote("0x7D5A99F603F231D53A4F39D1521F98D2E8BB279CF29BEBFD0687DC98458E7F89");

  //Verify updated note count
  const noteCount = await clone.noteCount()
  console.log("Note count: ", noteCount.toString());

  //Read the note
  const note = await clone.notes(0);
  console.log("Note: ", note);

  const noteDate = note.insertionDate.toString()
  console.log("Note date: ", noteDate);
};

You can then run your migrations with:

truffle migrate --network development --reset

One could obviously write a separate node script, connect to the network using a library like ethers, and do a more thorough series of functionality tests external of the migration process. I'm just lazy..

Model

Storing data on the blockchain costs money, and building solutions takes time and effort. I would be lying if I said I hadn't given consideration to how one monetises this simple (but in my opinion useful) product idea.

Photo by Adeolu Eletu / Unsplash

The obvious answer is to simply charge a fee for storing a note. One could also charge a fee for additional service provision such as identity (KYC) verification that associates a specific user address with an actual real human.

The usage of a proxy factory and user specific contract deployment does means that each user will necessarily be interfacing with the blockchain and submitting one of more transactions. This gives an opportunity for implementing a fee without any additional friction.

Interfacing with the blockchain using common tooling like Metamask or mobile wallet providers is becoming more and more common but I think it would be fair to say that it is still not easy or common for non technical folk or people without previous exposure to blockchain to utilise products like this. That said, development of more products like this exposes people to these technologies and will help them become more common place.