Pendzl - a smart contract library

1.1 Overview:
The Pendzl Library represents the evolution of the OpenBrush v4.3.0 smart contract library, taking it to the ink 5.0.0 standard while introducing crucial features like event emission.
Our primary focus is to enhance usability and eliminate unnecessary parts of the code (ex. macros) that have historically complicated the OpenBrush library. The mentioned evolution of OpenBrush is necessary due to it becoming too complex & introducing updates that piled up and have had a detrimental effect on the development experience.
Pendzl’s core principle is simplicity. We aim to minimize the introduction of new code while maximizing utility. Our overarching vision is to establish Pendzl as an open-source project, inviting contributions from the entire ink smart contract developer community.

At C Forge, we have already initiated essential modifications to OpenBrush, stepping in when the Brushfam departed from the ecosystem. We relied on OpenBrush for the development of the Abax Lending Protocol and various other projects. We believe that Pendzl will become an invaluable resource for ink smart contract developers, aiding in their endeavors and fostering collaborative innovation within the community.

What is Pendzl?
Pendzl is a smart contract library that facilitates the reuse of commonly used smart contracts and simplifies the process of contract overriding. The primary goal of this library is to allow developers, with the use of a single macro #[pendzl::implementation(PSP22)], to derive a default implementation of a contract (in this case, PSP22). This macro can be considered similar to a #[derive()] macro for ink!. Additionally, Pendzl ensures that default implementation methods can be easily overridden.

How does Pendzl relate to Substrate?
Once completed, Pendzl will become an integral part of the ink smart contract developer experience. It will serve as a tool that significantly expedites the development process and promotes best practices in ink smart contract development, ultimately leading to substantial cost reductions in production.

Why do we want to develop Pendzl?
Our team is responsible for developing all smart contracts for the Abax Community, including projects like Abax Lending Protocol and Abax Governor. Internally, we have been using a modified version of OpenBrush, which we have named Pendzl. While OpenBrush was useful, it had become complex, and after the Brushfam stopped supporting it, we decided to fork it, making it simpler, more user-friendly, and easier to maintain. Pendzl is already in use by our team for developing complex smart contracts, and we believe that with additional work, it will benefit the entire ecosystem.

1.2 Details
Pendzl is a Rust library built on top of ink! It will provide developers with three key macros:

  1. #[pendzl::implementation]: This macro wraps the contract module and functions similarly to a #[derive] macro, allowing for the derivation of a contract’s implementation.
  2. #[pendzl::storage_item]: This macro wraps struct/enum definitions used in contract storage. It manages the ManualKey of each field, simplifying the creation of upgradable smart contracts.
  3. #[derive(StorageFieldGetter)]: This derives macro generates a StorageFieldGetter trait required for using pendzl::implementation.

As part of the first milestone, we aim to support the following smart contract implementations:

  1. PSP22, including extensions such as PSP22Mintable, PSP22Burnable, PSP22Metadata, and an analog of ERC4626.
  2. PSP34, including extensions like PSP34Mintable, PSP34Burnable, and PSP34Metadata.
  3. Ownable, an analog of ownable from the OpenZeppelin library.
  4. AccessControl, an analog of access_control from the OpenZeppelin library.
  5. Pausable, an analog of pausable from the OpenZeppelin library.
  6. GeneralVester, a contract supporting the creation of a custom vest.

An example of usage should look like this:

An example of a psp22 contract:

    // SPDX-License-Identifier: MIT
    #![cfg_attr(not(feature = "std"), no_std, no_main)]

    #[pendzl::implementation(PSP22)]
    #[ink::contract]
    pub mod my_psp22_bare_minimum {
        #[ink(storage)]
        #[derive(Storage)]
        pub struct Contract {
            #[storage_field]
            psp22: PSP22Data,
        }

        impl Contract {
            #[ink(constructor)]
            pub fn new(total_supply: Balance) -> Self {
                let mut instance = Self {
                    psp22: Default::default(),
                };

                instance
                    ._mint_to(&Self::env().caller(), &total_supply)
                    .expect("Should mint");
                instance
            }
        }
    }

A general-purpose Vester contract:

    // SPDX-License-Identifier: MIT
    #![cfg_attr(not(feature = "std"), no_std, no_main)]

    #[pendzl::implementation(Vesting)]
    #[ink::contract]
    pub mod vester_custom {
        #[ink(storage)]
        #[derive(Default, Storage)]
        pub struct Vester {
            #[storage_field]
            vesting: VestingData,
        }

        impl Vester {
            #[ink(constructor)]
            pub fn new() -> Self {
                Default::default()
            }
        }
    }

An example of an Ownable PSP22 Contract with restricted mint and burn methods:

   #[pendzl::implementation(PSP22, Ownable)]
   #[ink::contract]
   pub mod ownable {
       use pendzl::contracts::token::psp22::extensions::{
           burnable::PSP22Burnable, mintable::PSP22Mintable,
       };

       #[ink(storage)]
       #[derive(Default, Storage)]
       pub struct Contract {
           #[storage_field]
           psp22: PSP22Data,
           #[storage_field]
           ownable: OwnableData,
       }

       impl Contract {
           #[ink(constructor)]
           pub fn new() -> Self {
               let mut instance = Contract::default();
               instance._update_owner(&Some(Self::env().caller()));
               instance
           }
       }

       impl PSP22Burnable for Contract {
           #[ink(message)]
           fn burn(&mut self, account: AccountId, amount: Balance) -> Result<(), PSP22Error> {
               self._only_owner()?;
               self._update(Some(&account), None, &amount)
           }
       }

       impl PSP22Mintable for Contract {
           #[ink(message)]
           fn mint(&mut self, account: AccountId, amount: Balance) -> Result<(), PSP22Error> {
               self._only_owner()?;
               self._update(None, Some(&account), &amount)
           }
       }
   }

Pendzl is going to support 3 layers of abstraction which allow developers to modify one or more levels and still use the other.
The three levels are

  1. The ink::message layer. This is a layer on which one implements ink::trait_defiinition. For example a PSP22 trait.
  2. The contract’s internal layer. This is a layer on which internal methods of contract that provide a given functionality exist. For example, in the case of PSP22, it will be a PSP22Internal trait that contains functions like _transfer, _mint_to, and _burn_from.
  3. The deepest layer is the contract storage modification layer, on which the functionality to store necessary data is defined. In the case of PSP22, it will be a PSP22Storage trait.

When we implement PSP22, PSP22Internal, and PSP22Storage we will use the following constraints:

struct PSP22DefaultData {...}

impl <T: PSP22Internal> PSP22 for T{...}
impl PSP22Storage for PSP22DefaultData {...}
impl<T: StorageFieldGetter<PSP22Data>> PSP22Internal for T where PSP22Data: PSP22Storage {}

Pendzl will provide all the 3 above implementations and a default struct PSP22Data that implements StorageFieldGetter. In this way developer can use the default implementations in this way:

    #[pendzl::implementation(PSP22)]
    #[ink::contract]
    pub mod default_psp22 {
        #[ink(storage)]
        #[derive(StorageFieldGetter)]
        pub struct Contract {
            #[storage_field]
            psp22: PSP22Data,
        }

        impl Contract {
            #[ink(constructor)]
            pub fn new(total_supply: Balance) -> Self {...}
        }
}

Moreover, a developer can choose to override methods from PSP22, and PSP22Internal (as shown in the Examples above), and/or he can provide his struct that will be used in contract storage that implements PSP22Storage trait to change the storage of the contract but to keep the PSP22Internal and PSP22 implementations. In the simple example of a PSP22 contract a developer may decide that for the application he is building it is enough to use u32 to store user balance instead of Balance( which often is u128). In such a case it will be enough for a Developer to create his struct PSP22U32 and implement a PSP22Storage for it:

    #[pendzl::implementation(PSP22)]
    #[ink::contract]
    pub mod my_psp22_with_u32_storage_and_blocked_transfer_from {
    #[ink::storage_item]
    PSP22U32{
        balances: Mapping<AccountId,u32>,
        allowances: Mapping<AccountId, u32>,
        total_supply: u64
    }

    impl PSP22Storage for PSP22U32{....}

    #[ink(storage)]
    #[derive(StorageFieldGetter)]
    pub struct Contract {
        #[storage_field]
        psp22: PSP22U32, // instead of Default PSP22Data
    }

    #[overrider(PSP22)]
    fn transfer_from(
      from: AccountId,
      to: AccountId,
      amount: Balance,
      data: Vec<u8>,
    ) -> Returns<(), PSP22Error>{
      Return Err(PSP22Error::Custom("Our Contract doesn't support PSP22 Transfer From")
    }

    impl Contract {
        #[ink(constructor)]
        pub fn new(total_supply: Balance) -> Self {...}
    }
}

Moreover, developers can choose to implement PSP22Internal from scratch but still use the provided PSP22.

1.3 Ecosystem Fit
Where and how does your project fit into the ecosystem?
Pendzl emerged as a vital solution within the ink ecosystem following Brushfam’s departure, which left ink developers without a dedicated smart contract library. Our project is poised to bridge this critical gap.

Who is your target audience?
Our primary audience consists of ink! smart contract developers.

What need(s) does your project meet?
Pendzl serves the essential purpose of simplifying and expediting smart contract development, resulting in increased efficiency and cost-effectiveness.

How did you identify these needs?
Our insights into these needs stem from our extensive experience as ink smart contract developers spanning over two years. Furthermore, our observations within the Telegram chat group “Aleph Zero ecosystem builders” reinforced the demand for a robust smart contract library.

Are there any other projects similar to yours in the Substrate / Polkadot / Kusama ecosystem?
Yes, our project is rooted in the OpenBrush library. However, OpenBrush encountered its set of challenges and is no longer actively maintained.

1.4 Future Plans
We anticipate that once the library demonstrates its value, ongoing maintenance will be sustained through contributions from projects that benefit from it. As part of Abax, we are committed to maintaining the library. Still, we also welcome and encourage other smart contract developers to join us in the development of this open-source initiative.

1.5 Team’s experience
We take pride in the extensive experience of our team members, Konrad Wierzbik and Łukasz Łakomy, who are well-versed in the development of ink! smart contracts. Their expertise spans over two years in this domain, making them seasoned professionals in the field.

Furthermore, their recognition by the Aleph Zero Ecosystem Founding Program underscores their dedication and recognition within the ecosystem. They were among the very first to be accepted into this prestigious program, solidifying their commitment to the growth and advancement of the ink! smart contract ecosystem.

Happy to discuss here the proposal & collaborate!

1 Like