robertu
robertu

fork the world a better place.

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:

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%!

Gas Price = 30 Gwei, 1 MATIC ≈ 1.5 USD, 1 ETH ≈ 3000 USD

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.

CC BY-NC-ND 2.0 版权声明

喜欢我的文章吗?
别忘了给点支持与赞赏,让我知道创作的路上有你陪伴。

加载中…
加载中…

发布评论