Karak Pro League
Findings & Analysis Report

2024-07-16

Table of contents

Overview

About C4

Code4rena (C4) is an open organization consisting of security researchers, auditors, developers, and individuals with domain expertise in smart contracts.

A C4 Pro League Audit is an event where elite tier Code4rena contributors, commonly referred to as wardens, reviews, audits and analyzes smart contract logic in exchange for a bounty provided by sponsoring projects.

During the Pro League audit outlined in this document, C4 conducted an analysis of the Karak smart contract system written in Solidity. The audit took place between June 10 - July 3, 2024.

Wardens

2 Wardens contributed to Karak:

  1. xiaoming90
  2. cccz

Final report assembled by bytes032 and thebrittfactor.

Summary

The C4 Pro League analysis yielded an aggregated total of 3 HIGH severity vulnerabilities.

Additionally, C4 Pro League analysis included 10 findings with a risk rating of LOW severity or non-critical and 4 INFORMATIONAL findings.

Scope

The source code was delivered to Code4rena in a private Git repository.

Severity Criteria

C4 assesses the severity of disclosed vulnerabilities based on three primary risk categories: high, medium, and low/non-critical.

High-level considerations for vulnerabilities span the following key areas when conducting assessments:

  • Malicious Input Handling
  • Escalation of privileges
  • Arithmetic
  • Gas use

For more information regarding the severity criteria referenced throughout the submission review process, please refer to the documentation provided on the C4 website, specifically our section on Severity Categorization.


High Risk Findings (3)

[H-01] DSS can slash more assets than are allowed against a vault within a single slashing event

Lines of Code

SlasherLib.sol#L56

Description

Assume that the maxSlashingWad is set to 5%. This means that for each vault of the operator, the DSS should only be allowed to slash up to 5% in a single slashing event.

However, malicious DSS could bypass the 5% restriction by setting the slashingRequest to contain [VaultA=5%, VaultA=5%, VaultA=5%], which effectively allows the malicious DSS to slash 15% on VaultA in a single slashing event.

Recommendation

Ensure the slashingRequest.vault does not contain duplicates.

Karak

Fixed in PR 279.

Code4rena Pro League

Fixed.


[H-02] Malicious operators can bypass checks in DSS Hooks

Lines of Code

  • HookLib.sol#L44-L57
  • Operator.sol#L125-L138
  • Operator.sol#L61-L67

Description

When operators register and stake into the DSS, ignoreFailure of callHookIfInterfaceImplemented() is false, which means that the DSS can check for operators in the Hook and reject operators that do not meet the requirements.

    function registerOperatorToDSS(State storage self, IDSS dss, address operator, bytes memory registrationHookData)
        external
    {
        if (self.dssMap.length() == Constants.MAX_DSS_PER_OPERATOR) revert MaxDSSCapacityReached();
        self.dssMap.set(address(dss), 1); // Set a non zero value for dss

        HookLib.callHookIfInterfaceImplemented(
            dss,
            abi.encodeWithSelector(dss.registrationHook.selector, operator, registrationHookData),
            dss.registrationHook.selector,
            false,
            Constants.DEFAULT_HOOK_GAS
        );
    }

In callHookIfInterfaceImplemented(), a low-level call is made to check whether DSS implements the supportsInterface() interface, and then call the specific Hook.

    function callHookIfInterfaceImplemented(
        IERC165 dss,
        bytes memory data,
        bytes4 interfaceId,
        bool ignoreFailure,
        uint256 gas
    ) internal returns (bool) {
        (bool success,) = address(dss).call{gas: Math.min(Constants.SUPPORTS_INTERFACE_GAS_LIMIT, gasleft())}(
            abi.encodeWithSelector(IERC165.supportsInterface.selector, interfaceId)
        );
        if (!success) {
            emit InterfaceNotSupported();
            return false;
        }
        return callHook(address(dss), data, ignoreFailure, gas);
    }

The problem here is that even if the DSS implements the supportsInterface() interface and the corresponding Hook, a malicious operator can use a very low GAS to execute the low-level call so that the low-level call fails due to OOG(out-of-gas). Since the low-level call will only forward 63/64 of the gas, the remaining 1/64 is enough to execute the logic when success is false.

When callHookIfInterfaceImplemented() returns false, the protocol doesn’t check for it and assumes that the DSS doesn’t implement any Hooks.

    function registerOperatorToDSS(State storage self, IDSS dss, address operator, bytes memory registrationHookData)
        external
    {
        if (self.dssMap.length() == Constants.MAX_DSS_PER_OPERATOR) revert MaxDSSCapacityReached();
        self.dssMap.set(address(dss), 1); // Set a non zero value for dss

        HookLib.callHookIfInterfaceImplemented(
            dss,
            abi.encodeWithSelector(dss.registrationHook.selector, operator, registrationHookData),
            dss.registrationHook.selector,
            false,
            Constants.DEFAULT_HOOK_GAS
        );
    }

A malicious operator can exploit this issue to bypass the checks in registrationHook() and requestUpdateStakeHook().

Aince finalizeSlashing() and finalizeUpdateVaultStakeInDSS() can be called by anyone, a malicious user can exploit this issue to intentionally not execute DSS’s finishUpdateStakeHook() and finishSlashingHook().

Recommendation

It is recommended to require gasleft() to be greater than SUPPORTS_INTERFACE_GAS_LIMIT in callHookIfInterfaceImplemented().

    function callHookIfInterfaceImplemented(
        IERC165 dss,
        bytes memory data,
        bytes4 interfaceId,
        bool ignoreFailure,
        uint256 gas
    ) internal returns (bool) {
+       if (gasleft() < Constants.SUPPORTS_INTERFACE_GAS_LIMIT ) revert NotEnoughGas();
        (bool success,) = address(dss).call{gas: Math.min(Constants.SUPPORTS_INTERFACE_GAS_LIMIT, gasleft())}(
            abi.encodeWithSelector(IERC165.supportsInterface.selector, interfaceId)
        );

Karak

Fixed in PR 278.

Code4rena Pro League

The fix implements the recommendation.


[H-03] validateAndUpdateVaultStakeInDSS() forwards incorrect operator address to finishUpdateStakeHook()

Lines of Code

  • Operator.sol#L82-L88
  • IDSS.sol#L15-L16

Description

In IDSS interfaces, the parameter of finishUpdateStakeHook() is operator.

    function finishUpdateStakeHook(address operator) external;

However, in validateAndUpdateVaultStakeInDSS(), the forwarded parameter is msg.sender.

        HookLib.callHookIfInterfaceImplemented(
            dss,
            abi.encodeWithSelector(dss.finishUpdateStakeHook.selector, msg.sender),
            dss.finishUpdateStakeHook.selector,
            true,
            Constants.DEFAULT_HOOK_GAS
        );

Since anyone can call finalizeUpdateVaultStakeInDSS(), this causes the caller address to be treated as the operator in DSS’s finishUpdateStakeHook() to execute the associated logic, which could involve something like registering the address to receive rewards.

    /// @notice Allows anyone to finish the queued request for an operator to update assets delegated to a DSS
    /// @dev Only operator can finish their queued request valid only after a
    /// minimum delay of `Constants.MIN_STAKE_UPDATE_DELAY` after starting the request
    function finalizeUpdateVaultStakeInDSS(Operator.QueuedStakeUpdate memory queuedStake, address operator)

Recommendation

It is recommended to add the operator address in QueuedStakeUpdate structure and forward the operator address to finishUpdateStakeHook() in validateAndUpdateVaultStakeInDSS().

Karak

Fixed in PR 280.

Code4rena Pro League

The fix implements the recommendation.


Low Risk Findings (10)

[L-01] Hardcoded gas cost

Lines of Code

Constants.sol#L23

Description

There have been multiple instances where the gas costs for certain operations have been changed through EIPs. For instance, EIP-1884 (SLOAD 200 -> 800, BALANCE 400 -> 700) and EIP-2929 (SLOAD 800 -> 2100).

It was found that the gas cost is hardcoded. Thus, the hook might revert due to insufficient gas if the gas cost changes in the future.

File: Constants.sol
23:     uint256 public constant DEFAULT_HOOK_GAS = 500_000;
24:     uint256 public constant HOOK_GAS_BUFFER = 40_000;
25:     uint256 public constant SUPPORTS_INTERFACE_GAS_LIMIT = 20_000;

For instance, assume the DSS’s hook carries out 𝑋 number of operations and consumes up to 499,990 gas. The protocol passes 500,000 (DEFAULT_HOOK_GAS) gas to the hook. All is good, as the DSS’s hook consumes below the limit.

Assume that at some point in the future, some EIPs are implemented, resulting in the gas cost of some operations increasing. Thus, for DSS’s hook to perform same 𝑋 number of operations, the gas to be consumed becomes 500,100.

Since the protocol only passes 500,000 gas (hardcoded) to the DSS, the DSS’s hook execution will always revert due to insufficient gas.

Recommendation

Consider allowing the gas limit to be updatable by the protocol.

Karak

Fixed in PR 283.

Code4rena Pro League

Fixed.


[L-02] Low-level call against an EOA (DSS) will always succeed

Lines of Code

HookLib.sol#L51

Description

The DSS can be a smart contract or EOA.

Assume DSS is an EOA. When performing a low-level call against an EOA, the call will always succeed because EOAs do not have any code associated with them. The call will simply return without executing any contract code. In this case, the success variable will be set to true, indicating that the call itself was successful.

The program will wrongly conclude that the DSS (an EOA) supported the interface, implemented the necessary function, and proceeded to call the hook against the DSS.

File: HookLib.sol
44:     function callHookIfInterfaceImplemented(
45:         IERC165 dss,
46:         bytes memory data,
47:         bytes4 interfaceId,
48:         bool ignoreFailure,
49:         uint256 gas
50:     ) internal returns (bool) {
51:         (bool success,) = address(dss).call{gas: Math.min(Constants.SUPPORTS_INTERFACE_GAS_LIMIT, gasleft())}(
52:             abi.encodeWithSelector(IERC165.supportsInterface.selector, interfaceId)
53:         );
54:         if (!success) {
55:             emit InterfaceNotSupported();
56:             return false;
57:         }
58:         return callHook(address(dss), data, ignoreFailure, gas);
59:     }

Recommendation

Consider checking if the DSS is an EOA or smart contract. The program can exit the function immediately if it is not a smart contract.

Karak

Fixed in PR 284.

Code4rena Pro League

Fixed.


[L-03] slashablePercentageWad can exceed 100%

Lines of Code

Core.sol#L266

Description

The slashablePercentageWad should never exceed 100%. However, it was found that the DSS can set it to a percentage beyond 100% via the Core.setDssSlashablePercentageWad function.

File: Core.sol
266:     function setDssSlashablePercentageWad(uint256 slashablePercentageWad) external {
267:         CoreLib.Storage storage self = _self();
268:         uint256 currentSlashablePercentageWad = self.dssSlashablePercentageWad[IDSS(msg.sender)];
269:         if (currentSlashablePercentageWad != 0) revert SlashingPercentAlreadySet();
270:         self.dssSlashablePercentageWad[IDSS(msg.sender)] = slashablePercentageWad;
271:     }

Recommendation

To prevent any potential edge cases, ensure the DSS cannot set its slashable percentage to more than 100%.

+ require(slashablePercentageWad <= Constants.MAX_SLASHING_PERCENT_WAD)

In addition, consider disallowing DSS to set its slashable percentage to zero (if applicable).

Karak

Fixed in PR 287.

Code4rena Pro League

Fixed.


[L-04] Standard implementation can be set to reserved address (address(1))

Lines of Code

Core.sol#L171

Description

address(1) is a reserved address for default implementation within the protocol. The protocol should not allow anyone to set the standard implementation address to address(1).

File: Core.sol
171:     function changeStandardImplementation(address newVaultImpl) external onlyOwner { // @audit-ok
172:         if (newVaultImpl == address(0)) revert ZeroAddress();
173:         CoreLib.Storage storage self = _self();
174:         self.vaultImpl = newVaultImpl;
175:         emit UpgradedAllVaults(newVaultImpl);
176:     }

Recommendation

To avoid any unexpected error or mistake, consider adding the following check since address(1) is reserved for default implementation.

+ if (newVaultImpl == Constants.DEFAULT_VAULT_IMPLEMENTATION_FLAG) revert ReservedAddress();

Karak

Fixed in PR 287.

Code4rena Pro League

Fixed.


[L-05] assetSlashingHandlers can be set to address(0)

Lines of Code

CoreLib.sol#L51-L58

Description

allowlistAssets() allows setting assetSlashingHandlers[asset] to address(0) to invalidate the slashingHandler.

    function allowlistAssets(Storage storage self, address[] memory assets, address[] memory slashingHandlers)
        external
    {
        if (assets.length != slashingHandlers.length) revert LengthsDontMatch();
        for (uint256 i = 0; i < assets.length; i++) {
            self.assetSlashingHandlers[assets[i]] = slashingHandlers[i];
        }
    }

However, this invalidation may cause verifyAndFinalizeSlashing() to fail, which could cause the DOS.

        for (uint256 i = 0; i < queuedSlashing.vaults.length; i++) {
            IVault(queuedSlashing.vaults[i]).slashAssets(
                queuedSlashing.earmarkedStakes[i], self.assetSlashingHandlers[IVault(queuedSlashing.vaults[i]).asset()]
            );
        }

Recommendation

It is recommended to add the following check to prevent assetSlashingHandlers from being set to address(0).

    function allowlistAssets(Storage storage self, address[] memory assets, address[] memory slashingHandlers)
        external
    {
        if (assets.length != slashingHandlers.length) revert LengthsDontMatch();
        for (uint256 i = 0; i < assets.length; i++) {
+           if (slashingHandlers[i] == address(0)) revert ZeroAddress();
            self.assetSlashingHandlers[assets[i]] = slashingHandlers[i];
        }
    }

Karak

Fixed in PR 284.

Code4rena Pro League

The fix implements the recommendation.


[L-06] Incorrect check for low-level calls in callHookIfInterfaceImplemented()

Lines of Code

HookLib.sol#L44-L57

Description

In callHookIfInterfaceImplemented(), only the calling status is checked without checking the return value.

    function callHookIfInterfaceImplemented(
        IERC165 dss,
        bytes memory data,
        bytes4 interfaceId,
        bool ignoreFailure,
        uint256 gas
    ) internal returns (bool) {
        (bool success,) = address(dss).call{gas: Math.min(Constants.SUPPORTS_INTERFACE_GAS_LIMIT, gasleft())}(
            abi.encodeWithSelector(IERC165.supportsInterface.selector, interfaceId)
        );
        if (!success) {
            emit InterfaceNotSupported();
            return false;
        }

When the contract does not support this interface, success is generally true, but the return value is false.

    function supportsInterface(bytes4 interfaceId) public view override returns (bool) {
        return supportedInterface[interfaceId];
    }

Recommendation

It is recommended to add a condition in the following check that success is true and the return value is fasle.

        if (!success) {
            emit InterfaceNotSupported();
            return false;
        }

Karak

Fixed in PR 278.

Code4rena Pro League

The fix implements the recommendation.


[L-07] slashablePercentageWad can be 0

Lines of Code

  • SlasherLib.sol#L62-L76
  • Vault.sol#L196-L205
  • SlashingHandler.sol#L37-L38

Description

When the slashPercentagesWad provided by the DSS is 0, transferAmount will be 0 in slashAssets().

    function slashAssets(uint256 totalAssetsToSlash, address slashingHandler)
        external
        onlyCore
        returns (uint256 transferAmount)
    {
        transferAmount = Math.min(totalAssets(), totalAssetsToSlash);

        // Approve to the handler and then call the handler which will draw the funds
        SafeTransferLib.safeApproveWithRetry(asset(), slashingHandler, transferAmount);
        ISlashingHandler(slashingHandler).handleSlashing(IERC20(asset()), transferAmount);

Then in handleSlashing(), when the amount is 0, it throws the ZeroAmount() error, which will cause the whole slashing to get stuck.

    function handleSlashing(IERC20 token, uint256 amount) external {
        if (amount == 0) revert ZeroAmount();

Recommendation

It is recommended to require slashPercentagesWad to be greater than 0 in requestSlashing().

Karak

Fixed in PR 284.

Code4rena Pro League

The fix implements the recommendation.


[L-08] registerOperatorToDSS() can be called repeatedly

Lines of Code

Core.sol#L89-L101

Description

A registered operator can repeatedly call registerOperatorToDSS(), which emits repeated RegistedOperatorToDss() events and makes repeated calls to the DSS’s registrationHook(), resulting in potential side effects.

    function registerOperatorToDSS(IDSS dss, bytes memory registrationHookData)
        external
        whenFunctionNotPaused(Constants.PAUSE_CORE_REGISTER_TO_DSS)
        nonReentrant
    {
        CoreLib.Storage storage self = _self();

        address operator = msg.sender;

        self.operatorState[operator].registerOperatorToDSS(dss, operator, registrationHookData);

        emit RegistedOperatorToDss(operator, address(dss));
    }

Recommendation

It is recommended to add the following check to prevent repeated calls to registerOperatorToDSS():

    function registerOperatorToDSS(IDSS dss, bytes memory registrationHookData)
        external
        whenFunctionNotPaused(Constants.PAUSE_CORE_REGISTER_TO_DSS)
        nonReentrant
    {
+       require(!isOperatorRegisteredToDSS(msg.sender, dss));

        CoreLib.Storage storage self = _self();

        address operator = msg.sender;

        self.operatorState[operator].registerOperatorToDSS(dss, operator, registrationHookData);

Karak

Fixed in PR 284.

Code4rena Pro League

The fix implements the recommendation.


[L-09] implementation(address(0)) should not return self.vaultImpl

Lines of Code

Core.sol#L308-L316

Description

When deploying a Vault, if implementation is address(0), implementation is replaced with DEFAULT_VAULT_IMPLEMENTATION_FLAG and assigned to vaultToImplMap, which makes valid vaultToImplMap will not be address(0).

        if (implementation == address(0)) {
            // Allows us to change all the standard vaults to a new implementation
            implementation = Constants.DEFAULT_VAULT_IMPLEMENTATION_FLAG;
        }

In changeImplementationForVault(), a vaultToImplMap of address(0) is considered invalid.

        if (self.vaultToImplMap[vault] == address(0)) revert VaultNotAChildVault();

However, in implementation(), address(0) is considered valid and self.vaultImpl is returned.

This would make the implementation() of any non-Vault address be self.vaultImpl, which might have some side effects out of scope.

Recommendation

It is recommended to make implementation(address(0)) return address(0).

    function implementation(address vault) public view returns (address) {
        CoreLib.Storage storage self = _self();
        address vaultImplOverride = self.vaultToImplMap[vault];

-       if (vaultImplOverride == Constants.DEFAULT_VAULT_IMPLEMENTATION_FLAG || vaultImplOverride == address(0)) {
+       if (vaultImplOverride == Constants.DEFAULT_VAULT_IMPLEMENTATION_FLAG) {
            return self.vaultImpl;
        }
        return vaultImplOverride;
    }

Karak

Fixed in PR 287.

Code4rena Pro League

The fix implements the recommendation.


[L-10] Risks of distributing rewards directly to the vault as underlying assets

Lines of Code

Vault.sol

Description

There are multiple instances where the share price in the vault could increase, allowing users to front-run it, mint a large number of shares, and claim a majority of the upside from the price increase. As a result, the stakers will receive fewer rewards than expected. This issue occurs if the rewards are distributed directly to the vault in underlying assets.

Instance 1

If an operator or DSS distributes a large number of rewards at once to the vault in underlying assets, users can front-run the transaction and mint a large number of shares, claiming most of the rewards. As a result, the stakers will receive fewer rewards than expected.

Instance 2

The share price used to determine the number of assets the user is entitled to during a withdrawal is the minimum of startTimeSharePrice and endTimeSharePrice. If the endTimeSharePrice is larger than startTimeSharePrice at the point of time of the withdrawal’s finalization, the lower share price (startTimeSharePrice) will be used, and the “excess” assets to be re-distributed even to all remaining shares or existing users in the vault. This resulted in a sudden increase in the share price.

Similarly, users could front-run it, mint a large number of shares, and claim a majority of the upside from the price increase. As a result, the stakers will receive fewer rewards than expected.

Recommendation

It is not recommended that rewards (especially huge ones) be distributed directly into the vault as underlying assets since the vault code is not really designed for reward distribution, which often requires sophisticated logic (e.g., accruing rewards proportional to the duration of staking) for fairer reward distribution (e.g., SNX reward staking contract).

However, if one still decides to proceed to transfer rewards as underlying assets to the vault directly, the risk should be adequately mitigated with some measures to de-incentivize the attacker or raise the barrier of such an attack. These measures include, but are not limited to, the following:

  • Prevent flash-loan - Already in place.
  • Prevent the user from being able to withdraw immediately (so that the attacker will also incur the risk of downside - being slashed while in the withdrawal queue) - Already in place.
  • Charging a deposit/withdrawal fee to make it less likely to be profitable in most cases.

In addition, consider documenting the risks of directly transferring rewards as underlying assets to the vault so that the respective parties are aware of them.

Karak

Fixed in PR 299.

Code4rena Pro League

  • Instance 1: Closed. Noted from the protocol team that the DSS/Operator’s SOP will mention refraining from using the vaults for rewards distribution.
  • Instance 2: Closed. Fixed in PR 299. The root cause of this instance is that a disproportional amount of assets is transferred out of the vault when the finishRedeem is executed, as the internal formula chooses the minimum between the previous and current assets. This resulted in a sudden surge in share price that malicious users can exploit. However, the fix has updated the formula to return a proportional amount of assets to the withdrawer, thus remediating this instance.

Informational Findings (4)

[I-01] Renaming of function

Lines of Code

CoreLib.sol#L43

Description

The name of the initOrUpdate function suggests that it is used to initialize state variables and allow their updates after initialization. However, this function is only used during initialization and is not used to update state variables after initialization.

File: CoreLib.sol
43:     function initOrUpdate(Storage storage self, address _vaultImpl, address _vetoCommittee) internal { // @audit-ok

Recommendation

Consider renaming it to init instead of initOrUpdate to reflect the actual use case.

Karak

Fixed in PR 292.

Code4rena Pro League

Fixed.


[I-02] Unused functions

Lines of Code

  • Pauser.sol#L43
  • Pauser.sol#L53

Description

Instance 1: The Pauser.whenNotPaused modifier is not used anywhere in the codebase.

Instance 2: The Pauser.paused function is not being used anywhere in the codebase.

In addition, the condition here is also incorrect. During initialization, the _getPauserStorage()._paused is set to 0, meaning the system is unpaused. However, if someone calls the paused() function, it will return true, meaning the system is paused, thus contradicting the actual state of the system.

Recommendation

Consider removing the unused functions if they are not required.

Karak

Fixed in PR 292.

Code4rena Pro League

Fixed.


[I-03] Ambiguity in the DSSs slashable percentage returned from Core.getDssSlashablePercentageWad function

Lines of Code

Core.sol#L354

Description

Within the SlasherLib.validate function, if the dssSlashablePercentageWad is not initialized (equal to zero), the slashable percentage is 100%.

        uint256 maxSlashingWad = self.dssSlashablePercentageWad[dss] == 0
            ? Constants.MAX_SLASHING_PERCENT_WAD
            : self.dssSlashablePercentageWad[dss];

This might cause some confusion for users who rely on the getDssSlashablePercentageWad function to determine a DSS’s slashable percentage. When this function returns zero, it is unclear whether the DSS’s slashable percentage is 0% or 100%. Users might think that the DSS’s slashable percentage is 0%, while, in fact, it is 100%.

Recommendation

Consider having a variable that keeps track of whether the dssSlashablePercentageWad has already been initialized so it can be used within the SlasherLib.validate function to only return MAX_SLASHING_PERCENT_WAD (100%) if it is uninitialized.

Alternatively, document this behavior in the NatSpec of this function.

Karak

Fixed in PR 287.

Code4rena Pro League

Fixed.


[I-04] Lack of function to get leverage of Vault

Lines of Code

  • Operator.sol#L18-L19
  • Operator.sol#L103-L105

Description

When the operator stakes the Vault to multiple DSSs, the leverage of the Vault will increase, and DSS can reject stake of Vault with too high leverage, but currently it is difficult for DSS to get the leverage of a Vault through simple operations (it needs to read storage through extSloads()).

Recommendation

It is recommended to first implement a method to read dssMap.

+   function getDssMap(State storage self) internal view returns (address[] memory) {
+       return self.dssMap.keys();
+   }

Then for a given Vault, iterate through dssMap and call isVaultStakeToDSS() to get the amount of DSS staked by the Vault.

Karak

Fixed in PR 291.

Code4rena Pro League

The fix implements the recommendation.


Disclosures

C4 is an open organization governed by participants in the community.

C4 audits incentivize the discovery of exploits, vulnerabilities, and bugs in smart contracts. Security researchers are rewarded at an increasing rate for finding higher-risk issues. Audit submissions are judged by a knowledgeable security researcher and solidity developer and disclosed to sponsoring developers. C4 does not conduct formal verification regarding the provided code but instead provides final verification.

C4 does not provide any guarantee or warranty regarding the security of this project. All smart contract software should be used at the sole risk and responsibility of users.