A Deep Dive Into Logbook Smart Contract
On experimenting with digital ownership, Matters Lab is blurring the line between private and public ownership with three Web3 projects: Traveloggers, a profile picture (pfp) NFT on private ownership; The Space, a draw-to-earn pixel canvas with Universal Basic Income (UBI) and Harberger Tax on common ownership; And, Logbook, a co-creation writing dApp on collective ownership.
Logbook originated as a feature of Traveloggers, allowing NFT owners to write down thoughts on Ethereum blockchain. As the pain of gas fees and new ideas on collaborative content arise, we redesigned the Logbook smart contract with many new features: lower gas fee, fork and donation, royalty-splitting, decentralized frontend, and on-chain NFT.
You don't completely own your NFTs. Each logbook is an NFT, you have rights to transfer and benefit from your writing. But it's open to fork, like open-source software, once public, never stop. Anyone can use or fork your code, live and rebirth in other projects, your code commits remain. Logbook is the same, your content is still yours, but it can be in any logbook.
Overview
We have three GitHub repositories about Logbook:
- thematters/logbook: Logbook web app, hosting on logbook.matters.news;
- thematters/subgraph: The Graph subgraph;
- thematters/contracts: Logbook smart contract, deployed on PolygonScan;
Writing a secure, gas-efficient, and easy-to-read smart contract is hard, especially in this fast-growing and highly uncertain industry. We would love to share more details on how we design and implement the Logbook smart contract.
Hardhat, the most popular Solidity development environment, provides a smooth experience for JavaScript developers and excellent extensibility through plugins, but context switching hurts productivity. We write contracts in Solidity but tests in JavaScript. Even worse, we have to deal with the large package dependencies. Thanks to Foundry, we can build faster, test better, all in Solidity.
Diving into the src/Logbook/Logbook.sol
, we can see the dependencies of the Logbook contract:
Logbook ↖ ERC721 ↖ Ownable ↖ Royalty
ERC721
is the implementation of ERC-721 standard from OpenZeppelin, Ownable
allows an address to call claim logbook function and change public sale status (Traveloggers can claim logbook for free, after that, they can be minted from public sale).
At the core, Logbook
inherits from Royalty
, contains business logic on how a logbook can be minted, how the owner can interact with the logbook, and how to split royalty from fork and donation.
For blockchain platform, we chose Polygon, mainly because of the low gas fee and the mature ecosystem. As developers, we can use services like Alchemy and The Graph. As creators, we can buy/sell Logbook NFT on OpenSea, swap the incomes to stablecoins on Uniswap, etc.
Last but not least, to query contract data more accessible, we use The Graph, a decentralized indexing service that provides GraphQL API for clients.
Royalty-Splitting and Decentralized Frontend
Let’s take a closer look at external functions:
// src/Logbook/ILogbook.sol ILogbook - setTitle - setDescription - publish - getLogbook - getLogs - setForkPrice - fork - forkWithCommission - donate - donateWithCommission - ... // src/Logbook/IRoyalty.sol IRoyalty - withdraw - getBalance
setTitle
, setDescription
and publish
are for owners to update the logbook. getLogbook
and getLogs
just simple getters to get details of a logbook. With these interfaces, we can already make a nice dApp that creators can publish content on-chain. But there is more than that. Creators can make money from their work.
Anyone can donate $MATIC to a logbook, or the owner can set a fork price (setForkPrice
) allowing others to fork the logbook and continue to write. 80% of these incomes goes to the owner, the rest, up to 20%, are equally divided to contents' authors. If some contents are inherited from the fork, the original authors get this share.
Why up to 20%? It depends on how users call the functions.
Most of the time, users view content or interact with contracts via a website. Although Matters Lab designed and built logbook.matters.news, it's centralized, our taste, our limitation. By leveraging these contract interfaces, developers can make a customized frontend by themselves, for others, and take a commission from forkWithCommission
and donateWithCommission
. Assume a developer created a Logbook web app with great UI/UX, readers and creators love to use it. Since maintaining and hosting it costs time and money, the developer decides to take a 5% cut from every fork and donation. Win-Win!
Function parameters commission_
(the address to receive the commission) and commissionBPS_
(the percentage of the commission in basis points) can be passed into forkWithCommission
and donateWithCommission
. The commissionBPS_
is capped at 2,000 (20%), which means the developer can decide how much an address takes after the logbook owner takes 80% of the fees.
Join to hack the Logbook!
Gas Optimization
Blockchain, as its name, is a chain consisting of data blocks. A block contains a limited number of transactions. Transaction fee goes to miners of the blockchain network who confirm the service requests, by providing computation and storage resources. Although Moore's Law makes these resources abundant, decentralized service is scarce. We are paying for the promise of decentralization, bidding with a gas price.
So what efforts have we made to fight this scarcity of abundance?
Space.
Different EVM opcode executed in a transaction costs different units of gas. One of the most expensive opcodes is SSTORE
, to put data into the contract storage, maximum ~20,000 gas per storage slot (32-byte). We should store minimal data on storage.
There are several gas-efficient ways to store data: stateless contract, off-chain storage, and emitting data as an event. For stateless contracts, data is passed to functions that do nothing. For off-chain storage, data is stored on storage services like IPFS or Arweave, then submit the identifier (URL, CID, etc.) on-chain. Both solutions aren't easy to access the data by the client. We took the last one, data are emitted as events using LOG*
opcodes, saving ~90% gas compares to SSTORE
, besides, clients can retrieve on-chain data with topic filters directly or using The Graph API.
Another optimization is on the data structure. Logbook supports collaborative content creation, a logbook can be forked by anyone without the owner's permission, with inherited contents.
Instead of copying data from the parent, we can just link to it with a minimal set of metadata.
// https://github.com/thematters/contracts/blob/81246e4/src/Logbook/ILogbook.sol#L26-L41 struct Book { // end position of a range of logs uint32 endAt; // parent book uint256 parent; // all logs hashes in the book bytes32[] contentHashes; ... }
Finally, the gas of content publishing is dropped up to 90%. With Polygon, the cost in USD dropped up to 99.99%!
Time.
Block has a gas limit, of 30M on Polygon, so a transaction may run out of gas. Royalty-splitting iterates contents and updates the author's balance. SSTORE
costs ~20,000 gas if the slot is a zero value, but only ~5,000 if it's non-zero, a vast difference!
// https://github.com/thematters/contracts/blob/81246e4/src/Logbook/Logbook.sol#L387-L397 for (uint32 i = 0; i < logCount; i++) { Log memory log = logs[contentHashes[i]]; _balances[log.author] += fees.perLogAuthor; ... }
We did a little trick to maximize the number of iterations that can be run. The balance of an address will always be a non-zero value once it owns a token. In the long run, the bottleneck on gas limit is mitigated by distributing to specific transactions in different blocks.
// https://github.com/thematters/contracts/blob/81246e4/src/Logbook/Logbook.sol#L401-L414 function _afterTokenTransfer( address from_, address to_, uint256 tokenId_ ) internal virtual override { ... if (_balances[to_] == 0) { _balances[to_] = 1 wei; } } // https://github.com/thematters/contracts/blob/81246e4/src/Logbook/Royalty.sol#L12-L24 function withdraw() public { ... _balances[msg.sender] = 1 wei; ... }
On-chain NFT
Logbook is not only an on-chain writing dApp but also a generative NFT collection. Every publishing or transfer will change the colors of the image. Under the 24KB contract code size limit, SVG is light and flexible, an ideal format to generate images on-chain.
Another limit is when concatenating a large string. It's pretty easy to see the "Stack Too Deep" error. To solve it, we can pack the parameters with struct
and split generateSVG
into four functions:
// https://github.com/thematters/contracts/blob/81246e487008740f3515b7a6c91c7c43b58262dd/src/Logbook/NFTSVG.sol struct SVGParams { uint32 logCount; uint32 transferCount; uint160 createdAt; uint256 tokenId; } function generateSVG(SVGParams memory params) internal pure returns (string memory svg) { svg = string( abi.encodePacked( '<svg width="800" height="800" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd">', generateSVGBackground(params), generateSVGPathsA(params), generateSVGPathsB(params), generateSVGTexts(params), "</g></svg>" ) ); } function generateSVGBackground() {} function generateSVGPathsA() {} function generateSVGPathsB() {} function generateSVGTexts() {}
Art is never finished, only abandoned.
Logbook, though its contract code is static on the blockchain, when we write, transfer, fork, donate…every interaction makes it comes to life. We can co-create our story, in this untold digital space.