Creating a Custom Module
Technical teams can easily create custom modules in Mezzanine to suit their needs (or as 3rd party software for other Mezzanine companies). Mezzanine provides a base Module contract that can be inherited for hierarchical spending and access control.
Below, we implement a custom module that exchanges a company's common shares for the company's denomination asset based on an exchange rate using Foundry. This functionality is akin to share buybacks in traditional finance. We will also be taking a 1% fee, which will go to our wallet each time a share buyback is made.
Setup
Install Foundry via the following tutorial.
Run the following command to set up a new directory with Foundry set up:
forge init custom_module_exampleGo to the directory and install the OpenZeppelin contracts:
cd custom_module_example
forge install OpenZeppelin/openzeppelin-contractsNext, install the Mezzanine contracts:
forge install mezzanine-protocol/Mezzanine-ContractsChange the foundry.toml file to look like the following:
[profile.default]
src = 'src'
out = 'out'
libs = ['lib']
remappings = ["@openzeppelin/=lib/openzeppelin-contracts/", "@mezzanine/=lib/Mezzanine-Contracts/src"]
# compiler version
solc-version = "0.8.20"
# See more config options https://github.com/foundry-rs/foundry/tree/master/config
Modules Overview
A valid module must support the IModule interface:
The initialization of a module is standardized. All bespoke initialization arguments should be abi-encoded in the params argument.
initTeam should support the Team base contract, which both departments and treasury inherit. This is unimportant for our purposes. Just remember that initTeam should be a valid Mezzanine department or treasury.
IModule inherits from IChild and ITeamControlled:
Both the team and getParent functions should return the same value for most modules. However, this logic can be overridden if desired.
We will be inheriting from Mezzanine's Module base contract, which provides an internal function to recursively spend funds from the organization's treasury given sufficient balances and approvals :
Module inherits from TeamControlled, which is a base contract that provides modifiers for hierarchical and direct access control:
Writing the Custom Module
Our module should convert its company's common shares for the company's denomination asset based on an exchange rate. This exchange rate should be set only by the module's ancestors. This module should be deployed as a proxy. We will be using EIP1167 minimal, non-upgradeable proxies.
Here is the implementation of the ShareBuybackModule:
Let's break it down.
The fee receiver is set as an immutable variable in the constructor. Since immutable variables are stored in runtime rather than storage, the fee receiver will be the same across proxy deployments.
We use ERC7201 storage slots to prevent storage collisions in case an upgradeable proxy is used for the deployment:
The initializer function decodes the initial exchange rate and the treasury's address from params. Subsequently, it sets and validates the initial exchange rate and the treasury. We make a call to the base contract Module's internal initializer function to set the team:
The exchange rate must be in the denomination asset's decimals. We adjust for decimals when calculating the amount of denomination asset owed to a seller:
Finally, let's break down selling shares for a buyback.
First, we check for potential frontrunning. Since the exchange rate can be changed, a caller might call sellShares but be frontrun by the company. The company can specifically frontrun the user such that the conversion rate is much lower than what user expected. However, we validate this by requiring the exchange rate be greater than or equal to the user's expected rate. If it is lower, the transaction will revert, protecting the user.
We next transfer the common shares from the user to the treasury. A user should approve the share buyback module to spend their shares before attempting to sell their shares:
We finally spend the denomination asset from the organization's treasury. We specifically spend the denomination asset owed to the user and the fee to send to the fee receiver:
Remember, the team that inserts this module must have approvals to spend the denomination asset from its parent, and its parent must have approvals to spend from its parent, etc. A company should use ERC20 approvals as an upper limit for the number of shares to buybacks. However, a more complex implementation may set this upper limit explicitly as a storage variable.
Deploying the Custom Module
Modules are assumed to be deployed as proxies via a factory or deployer contract. The upgradeability logic of these proxies is up to the implementer. We will deploy our custom module using non-upgradeable EIP-1167 minimal proxies via OpenZeppelin's Clones library for simplicity. The proxy must be initialized atomically upon its deployment.
Here is a simple factory contract for the module:
Security Considerations for Modules
All modules should be properly audited before being used by an organization. Teams should set their ERC20 approvals for a module to zero or remove the module from their organization if a route for exploitation is discovered. The upgradeability and security mechanisms for the module are up to the implementer.
Optimizations for the Share Buyback Module Contract
The above implementation of the Share Buyback Module is a very simple contract. Here are some potential optimizations that can be made:
The use of custom error reverts rather than strings
Buybacks of different share classes
Buybacks using different denomination assets
SLOAD optimizations
ERC165 support
Last updated
