banner
zach

zach

github
twitter
medium

Smart Contract Architecture - The Default Framework

When reading the OlympusV3 contract, the design of the architecture impressed me. The architecture is clear and implements the design concept of high cohesion and low coupling in the smart contract field, allowing for focused and independent module reading, reducing cognitive burden.

The architecture diagram of OlympusV3 is shown below:
image

Later, I learned that this framework is not unique to Olympus. In the article introducing the contract architecture of Olympus, it mentioned the Default framework.

Complexity of Smart Contract Architecture#

Before introducing the Default framework, the author first compared the architecture diagrams of traditional protocols such as AAVE and MakerDAO.
MakerDAO Architecture Diagram
AAVE Architecture Diagram

Just by looking at the architecture diagrams, it is difficult for developers to understand the calling chain. The author explains this design as a process-centric approach, where each smart contract is developed to manage a process along a chain of logic that is triggered when external interactions occur.

As the business upgrades and changes, the complexity of the logical chain will exponentially increase. This is because the entire protocol presents a mesh architecture, and perhaps there is a logical chain between every two points. Therefore, the complexity brought by adding a point is difficult to estimate and cover.

Just like the evolution process of design patterns in the field of software engineering, the engineering architecture of smart contracts is also constantly improving, and the Default Framework is designed to solve this problem.

What is The Default Framework#

Just by looking at the name, you can't tell the design concept, unlike the Diamond framework that can evoke associations. The Default Framework does not make any special innovations, but instead draws on the design concepts of traditional development in other domains and divides the entire contract into layers.

  • Modules: Facing internally, modules can be understood as microservices, each managing a part of the data, with no dependencies between them, highly independent, and access to modules is constrained by permission management.
  • Policy: Facing external calls, responsible for organizing different modules to implement business logic.

The core management logic is consolidated in the Kernal.sol file, which is responsible for controlling the global policy and modules.

Modules#

Each module includes a unique KEYCODE and VERSION. When defining a module, this method needs to be implemented, and the Kernal will maintain the mapping relationship between all keycodes and modules.

The Kernal includes the following methods:

  • _installModule: Install a module
  • _upgradeModule: Upgrade a module

Example:

    /// @inheritdoc Module
    function KEYCODE() public pure override returns (Keycode) {
        return toKeycode("MINTR");
    }

    /// @inheritdoc Module
    function VERSION() external pure override returns (uint8 major, uint8 minor) {
        major = 1;
        minor = 0;
    }

Policy#

Policy can be understood as the service layer in traditional software development, used to organize a series of modules. It includes the following methods:

  • configureDependencies: Returns which modules are supported
  • requestPermissions: Returns the specific function selectors supported by the modules

The Kernal includes the following methods:

  • _activatePolicy
  • _deactivatePolicy

Example:

    /// @inheritdoc Policy
    function configureDependencies() external override returns (Keycode[] memory dependencies) {
        dependencies = new Keycode[](3);
        dependencies[0] = toKeycode("MINTR");
        dependencies[1] = toKeycode("TRSRY");
        dependencies[2] = toKeycode("ROLES");

        MINTR = MINTRv1(getModuleAddress(dependencies[0]));
        TRSRY = TRSRYv1(getModuleAddress(dependencies[1]));
        ROLES = ROLESv1(getModuleAddress(dependencies[2]));

        (uint8 TRSRY_MAJOR, ) = TRSRY.VERSION();
        (uint8 MINTR_MAJOR, ) = MINTR.VERSION();
        (uint8 ROLES_MAJOR, ) = ROLES.VERSION();

        // Ensure Modules are using the expected major version.
        // Modules should be sorted in alphabetical order.
        bytes memory expected = abi.encode([1, 1, 1]);
        if (MINTR_MAJOR != 1 || ROLES_MAJOR != 1 || TRSRY_MAJOR != 1)
            revert Policy_WrongModuleVersion(expected);
    }

    /// @inheritdoc Policy
    function requestPermissions() external view override returns (Permissions[] memory requests) {
        Keycode MINTR_KEYCODE = MINTR.KEYCODE();

        requests = new Permissions[](4);
        requests[0] = Permissions(MINTR_KEYCODE, MINTR.mintOhm.selector);
        requests[1] = Permissions(MINTR_KEYCODE, MINTR.burnOhm.selector);
        requests[2] = Permissions(MINTR_KEYCODE, MINTR.increaseMintApproval.selector);
        requests[3] = Permissions(MINTR_KEYCODE, MINTR.decreaseMintApproval.selector);
    }
Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.