Reach out for an audit or to learn more about Macro
or Message on Telegram

Covenant A-1

Security Audit

Feb 21, 2024

Version 1.0.2

Presented by 0xMacro

Table of Contents

Introduction

This document includes the results of the security audit for Covenant Labs's smart contract code as found in the section titled ‘Source Code’. The security audit was performed by the Macro security team from Dec 18, 2023 to Feb 19, 2024.

Note: Report references Tazz contracts since at the start of the audit and before rebrand project was known as Tazz.

The purpose of this audit is to review the source code of certain Covenant Labs Solidity contracts, and provide feedback on the design, architecture, and quality of the source code with an emphasis on validating the correctness and security of the software in its entirety.

Disclaimer: While Macro’s review is comprehensive and has surfaced some changes that should be made to the source code, this audit should not solely be relied upon for security, as no single audit is guaranteed to catch all possible bugs.

Overall Assessment

The following is an aggregation of issues found by the Macro Audit team:

Severity Count Acknowledged Won't Do Addressed
Critical 1 - - 1
Medium 9 1 1 7
Low 10 1 - 9
Code Quality 5 - 1 4
Informational 2 - - 1

Covenant Labs was quick to respond to these issues.

Specification

Our understanding of the specification was based on the following sources:

Trust Model, Assumptions, and Accepted Risks (TMAAR)

Entities

System components

General Assumptions

Trust Model

Recommendations

Source Code

The following source code was reviewed during the audit:

One initial and four extension commits were a part of Covenant's security review.

Specifically, we audited the following contracts for the Initial commit:

Contract SHA256
contracts/protocol/configuration/ACLManager.sol

2cd117bc694da2324a33d5229fa813d02413d56f369e66ec10e851dfb32e3344

contracts/protocol/configuration/GuildAddressesProvider.sol

0685645f66672cf626c067d1571144d3d3163d9a75d0f1b3f98156103a0cc83a

contracts/protocol/configuration/GuildAddressesProviderRegistry.sol

f03c3b3690228199a017afb2cad7ed4721c85702509c3abd46154c2f998ca201

contracts/protocol/guild/Guild.sol

7993207f382a256efbdb20998c1e3001da35f079be6148a32bf7a1589ef40239

contracts/protocol/guild/GuildConfigurator.sol

ccedee01795e7d7f0d273ef54204a2b05cea65f71e11ce217c1c09cf954ab1ac

contracts/protocol/guild/GuildRoleManager.sol

f7ce6cce3d6b87def4c34fc1bf8ca263910c0872e757605a524a6461cb3f7a26

contracts/protocol/guild/GuildStorage.sol

0d632cabc881098d43c4906a9342b7b69b18ce9966fd8a115584f0bf2184133a

contracts/protocol/guild/PermissionedGuild.sol

938f6b462a0ef5f1974a7e864d802a05877ae2b9b741a2b7024f6ae917a6ebec

contracts/protocol/guild/PermissionedGuildConfigurator.sol

52f5018e16fa11f352869629b1f7510096ef72f2c09e9687bc9661505d506106

contracts/protocol/guild/PermissionedStorage.sol

d17c39a70ff093319eb6813ebc26481e9ed7c1bf04f2d07cf1458f88cdae3c7a

contracts/protocol/libraries/configuration/CollateralConfiguration.sol

80162e76d8a3a1188646f26a04061f4e5d071c243612aba27cbaf42df8d925aa

contracts/protocol/libraries/configuration/PerpetualDebtConfiguration.sol

c424d7bb99c1a712f07b8a2fee11932a82aac2be5cbe6834be38c490dd8ec8a0

contracts/protocol/libraries/helpers/Errors.sol

0c569fc2c8bf76d582bddf683b69118b2457507b3586d813f8dc1ad58afb0cb5

contracts/protocol/libraries/logic/BorrowLogic.sol

4968b9e0e8cde17453505bdfc4de51a38efcfb606100089199306fcdcdecac6c

contracts/protocol/libraries/logic/CollateralLogic.sol

f2931f0d152a8dbe1d907b83d5f1034536b4dbfbca74955945692c4248347bb9

contracts/protocol/libraries/logic/ConfiguratorLogic.sol

4821e5053010dce2a3871a48145553b2682193c0bb79a2fd2030d6116dae3e85

contracts/protocol/libraries/logic/DexOracleLogic.sol

101318a41009e1d88ab31fa38a42848a60600ffd79458fd9fb0091c9374eb555

contracts/protocol/libraries/logic/GenericLogic.sol

338bd1993422c56b33ddc1e34d2cfa7f9650762de0d9aa335f9a2080fddb588a

contracts/protocol/libraries/logic/GuildLogic.sol

936421d8e79c1d25233ff3da93bbc18d9d0717bb3364313460159a380a908307

contracts/protocol/libraries/logic/LiquidationLogic.sol

299b2da124895b4d6b9a37364a741d7c4ef1c37db558c769d9c2da39d57ad10f

contracts/protocol/libraries/logic/PermissionedLogic.sol

fa6a70962a525b9b010d33cb662dad9ff549fa4ed4ec9563be21c7b4e0fc0539

contracts/protocol/libraries/logic/PerpetualDebtLogic.sol

90eed81c9b0a200c8c144b14267db8ac16a4950bf535ed06fe2bc8eff847c5c0

contracts/protocol/libraries/logic/ValidationLogic.sol

8918581f29a3b68c5761f46c3aa7a5ce8f1b1e6d6640fbe66b6f77525fc22d60

contracts/protocol/libraries/math/DebtMath.sol

0ca4e9fc6f0b244444b49664063fdca6336342f526d13c3cd1fb9e2d504a4f0d

contracts/protocol/libraries/math/PercentageMath.sol

f9d138bedf7112b401f5c6040bbbba498c481666780c4c283f443072b7f6d876

contracts/protocol/libraries/math/WadRayMath.sol

f13c71103ecf08c888f3f61646ec77f400b1ee18f3a1acb5739425084a1b18c8

contracts/protocol/libraries/math/X96Math.sol

9d086a81f904d280e43b3ad8a912038a99faf1a7308f0cf4616db97d44e1b74c

contracts/protocol/libraries/types/ConfiguratorInputTypes.sol

c6128eb80a7e8e1146d8d4a2a3fa2b5a3f52b24f2047a157448a5143673c4511

contracts/protocol/libraries/types/DataTypes.sol

f1495947d5150648d978337db0d0ebbfebe5acde96a42c04b616c210e9489cb6

contracts/protocol/libraries/upgradeability/BaseImmutableAdminUpgradeabilityProxy.sol

f94b59b4d94e470b40b070caa60739cd9a92e863bbc658c46ac451639f3045f5

contracts/protocol/libraries/upgradeability/InitializableImmutableAdminUpgradeabilityProxy.sol

89b61beba84a1071ca511a5d21ad3d6ea8fe2c8163ff750e41b7ae5fa96dd306

contracts/protocol/libraries/upgradeability/VersionedInitializable.sol

b1cdf61fd1d195527a1f4adac0d24a325a7da73cb1baaa4596e428e88030bcc0

contracts/protocol/oracle/PriceOracleSentinel.sol

2e8295b5bc16ea846ffefbb33642f1675b63b89d2a595d1b3a8164f061953f19

contracts/protocol/oracle/TazzPriceOracle.sol

a4c5c5921a059d34205588cf42effe3ceca10d0f8cb5c036d2edb71e7cfdcef7

contracts/protocol/oracle/proxies/AnkrEthOracleProxy.sol

3aa489c3ed99d95d31892e6e17f95f0187c7b511b1a52954b6847f9c7e6fdf76

contracts/protocol/oracle/proxies/BalancerStableLPOracleProxy.sol

2256fb75eefbbc384b16f55ff8c2a7bb86deaa982b359a4bf71af5a573362cd4

contracts/protocol/oracle/proxies/HardcodedOracleProxy.sol

b06eff456c163c4f41c3080618952948868d73edf9d1b62eba5acbf99c73bb77

contracts/protocol/oracle/proxies/UniswapV3OracleProxy.sol

5c69ec902f88caa99eb4f9453e878dcb7384cf5ba2034025b40decf933baa6ac

contracts/protocol/tokenization/AssetToken.sol

257a11b6ef4fe8819be172f46faedc9ca11d7a166656b45bb1c6610012177301

contracts/protocol/tokenization/LiabilityToken.sol

219f12b2c8b90daff5630f7906067755e572976ff7846f669249ca0d96b45662

contracts/protocol/tokenization/base/CreditDelegation.sol

3986e2d5dde3d885e083add9e9bab49439b3f5fd890980e788ac1e5e23767f00

contracts/protocol/tokenization/base/ERC20Storage.sol

2562482f2320a41755fad414158f7b8e02626e4a6b3f75da28b213d378c536af

contracts/protocol/tokenization/base/MintableUpgradeableERC20.sol

29cf13d7b39c87dddbc9dc204bf356f90e41cabf92a6557332c31966a69f2109

contracts/protocol/tokenization/base/NotionalERC20.sol

756439950327700f3f3c6f1300aeeb7b8f181e4cfea1c10a3092e433b8168594

contracts/protocol/tokenization/base/UpgradeableERC20.sol

37a0604e6f2d022a92a074b79402d3e42846bedde315d25d38058a19d2be0ff2

Specifically, we audited the following contracts for the Oracle Proxies commit:

Contract SHA256
contracts/protocol/oracle/proxies/HardcodedOracleProxy.sol

fb2e436e46f872a2797e2e94f2dfe5056f7faf74431571dab93927dc74059e13

contracts/protocol/oracle/proxies/OracleProxyCommon.sol

7e3bece1a98c748ce797bc4994866cb8f88adfb4ff5bae1f649f8c783725e1ed

contracts/protocol/oracle/proxies/UniswapV3OracleProxy.sol

14827be708df9faab9fb91f301b92465ce5105a515252ff27e32ad4810601487

Specifically, we audited the following contracts for the Chainlink Oracle commit:

Contract SHA256
contracts/protocol/libraries/helpers/Errors.sol

7a538d3f981f7df20b4c9611f926bf38448322e3cd4e6883388b8ad2b2c55196

contracts/protocol/oracle/proxies/ChainlinkV2V3OracleProxy.sol

48c2d500a36e5a74809ad89c7017d7115b762e3eba3b9ea74fea399b5c49ee74

Specifically, we audited the following contracts for the zToken<>Money commit:

Contract SHA256
contracts/protocol/guild/Guild.sol

460c82b3a40e50b1885e52ec864affb2dccf3b065a90306076fe7fee7be85c74

contracts/protocol/libraries/logic/BorrowLogic.sol

4581ad0587e1e00060bc96da076cdf0c6bc808df776f3c2e625d0953d6e870eb

contracts/protocol/libraries/logic/PerpetualDebtLogic.sol

81052e5cbf01f39c4c3ab8e83933b8496ecf09702ee1c0d9d63b096c98e80f85

contracts/protocol/libraries/logic/ValidationLogic.sol

0dcd0dc874d8cd1c6efd67e22b48098f4a3dd7c1dc54939d88e86f5b39011a28

contracts/protocol/libraries/types/DataTypes.sol

5c1975843e427c85e9ed0e592c11e9fcedd8a5686bef0228104aed15ec96e9c6

Specifically, we audited the following contracts for the LP Staking Rewards:

Contract SHA256
contracts/periphery/TazzFeeRouter.sol

a35d3b1b71d7a763b2c5d4feb55e18e33483925ff596b55d97c7d755c793f91b

contracts/protocol/guild/Guild.sol

b8a357579af68567ca47ffa5174e0f692bda22fa8a522978851f1825fd0ad3a5

contracts/protocol/libraries/helpers/Errors.sol

1282edb6fab2d2aa25539af789dae84a392458b7da9d9602346045ab3b3e2eb0

contracts/protocol/libraries/logic/LiquidationLogic.sol

35af188d34900c1182c244e788cdbb5ae6a0a7ddd245c499accf3522161f5610

contracts/protocol/libraries/logic/PerpetualDebtLogic.sol

b15ff872f5b0dade22f34303e8aff4988d83123e79cce811e9fb34506d00dc8e

contracts/protocol/oracle/TazzPriceOracle.sol

e11395b2e2023808660e30eb451a06de7615049536c4d1c62721b882bd6a149b

contracts/protocol/oracle/proxies/ChainlinkV2V3OracleProxy.sol

c64e0c9848d0e052850e60c7451ab1c2c892a58618f650abb521907bf59d5206

Issue Descriptions and Recommendations

Click on an issue to jump to it, or scroll down to see them all.

Security Level Reference

We quantify issues in three parts:

  1. The high/medium/low/spec-breaking impact of the issue:
    • How bad things can get (for a vulnerability)
    • The significance of an improvement (for a code quality issue)
    • The amount of gas saved (for a gas optimization)
  2. The high/medium/low likelihood of the issue:
    • How likely is the issue to occur (for a vulnerability)
  3. The overall critical/high/medium/low severity of the issue.

This third part – the severity level – is a summary of how much consideration the client should give to fixing the issue. We assign severity according to the table of guidelines below:

Severity Description
(C-x)
Critical

We recommend the client must fix the issue, no matter what, because not fixing would mean significant funds/assets WILL be lost.

(H-x)
High

We recommend the client must address the issue, no matter what, because not fixing would be very bad, or some funds/assets will be lost, or the code’s behavior is against the provided spec.

(M-x)
Medium

We recommend the client to seriously consider fixing the issue, as the implications of not fixing the issue are severe enough to impact the project significantly, albiet not in an existential manner.

(L-x)
Low

The risk is small, unlikely, or may not relevant to the project in a meaningful way.

Whether or not the project wants to develop a fix is up to the goals and needs of the project.

(Q-x)
Code Quality

The issue identified does not pose any obvious risk, but fixing could improve overall code quality, on-chain composability, developer ergonomics, or even certain aspects of protocol design.

(I-x)
Informational

Warnings and things to keep in mind when operating the protocol. No immediate action required.

(G-x)
Gas Optimizations

The presented optimization suggestion would save an amount of gas significant enough, in our opinion, to be worth the development cost of implementing it.

Issue Details

C-1

Multi-collateral under-collateralized liquidations will lead to protocol loss of value

Topic
Protocol Design
Status
Impact
High
Likelihood
High

Tazz’s liquidation logic in LiquidationLogic.sol is written to only seize one collateral type at a time. The logic also allows for a liquidator to completely close out a borrower’s debt in a single call when the borrower is under-collateralized. This becomes problematic when a borrower is collateralized with multiple types of collateral and is under-collateralized, as they can have only one type of collateral seized while having all of their debt burned.

Using this logic pathway, under-collateralized borrowers can undesirably self-liquidate a single type of collateral, burn all of their debt, and then withdraw their other types of collateral from the system. This will allow borrowers to keep their borrowed funds and reclaim collateral while creating a negative distribution for the rest of the involved parties in the Guild.

Remediations to consider

  • For under-collateralized multi-collateral liquidations that are only requesting to receive a single collateral type, determine how much debt belongs to that collateral type and only allow for burning of a proportional amount of debt.
  • Creating a ‘full liquidation’ logic pathway that allows a liquidator to completely close out a borrower’s account and seize all collateral.
Response by Covenant Labs

Team implemented a fix to correctly account for undercollateralized liquidation of multicollateral vaults. Over/underCollateralization during liquidations now utilizes the value across all collaterals, and for under-collateralized multi-collateral liquidations that are only requesting to receive a single collateral type, we determine how much debt belongs to that collateral type and only allow for burning that amount of debt.

Guild only allows for 1 collateral to be liquidated at a time, and a “FullLiquidation” function, as suggested in the Audit, will be evaluated for a later deployment.

M-1

Use of protocol mint fee can cause a Guild’s swapMoneyForZToken() logic to revert

Topic
Use Cases
Status
Impact
Medium
Likelihood
Medium

The current codebase will prevent users from being able to swap money for zTokens when the protocol mint fee is turned on with a receiving address different from the Guild.

In BorrowLogic:executeSwapMoneyForZTokens(), the code assumes that the total amount of zTokens minted are transferred to the Guild’s address and are available for transfer to the requesting user:

function executeSwapMoneyForZTokens(
        DataTypes.PerpetualDebtData storage perpData,
        DataTypes.MoneyForZTokenParams memory params
    ) external returns (uint256) {
        ...
        // mint zTokens & dTokens (guild)
        perpData.mint(params.onBehalfOf, params.onBehalfOf, zTokenMintAmountBase); // emits a Mint event

        // transfer zTokens to user
        perpData.getAsset().transfer(params.user, zTokenMintAmountBase);

       ...
    }

This code fails to take into account the possible usage of a protocol mint fee with a fee address different from the Guild’s address. In this scenario, via the logic in PerpetualDebtLogic:mint(), not enough zTokens would be minted to the Guild causing the transfer of zTokens to revert.

Remediations to consider

  • Taking into account the protocol mint fee into the amount of zTokens being transferred.
M-2

TazzPriceOracle Uniswap V3 Oracles can fail to index enough observations for desired TWAP period

Topic
3rd Party Behavior
Status
Acknowledged
Impact
High
Likelihood
Low

TazzPriceOracle is able to use Uniswap V3 oracles under the hood. Currently there are no checks to make sure that the used Uniswap Pools have enough room in their Observation array to hold enough data for the desired TWAP range. This can cause specific oracles to be more susceptible to price attacks and endanger the protocol’s funds if not protected against.

Remediations to consider:

  • Have TazzPriceOracle perform checks to grow the Observation sizes as needed. Note: there will be a lag between a Oracle’s Observation growth and recorded times, so manually performing this growth before an oracle’s addition or increased TWAP time would be optimal.
Response by Covenant Labs

Acknowledge - We leave it up to the deployer to calculate off-chain the # of observations needed to ensure the TWAP length requested will be maintained. At a maximum, the number of observations is dependent on the average block-time for the network on which the guild is being deployed. In practice, many pools have less than 1 transaction per block the # of observations needed will depend on the expected transaction volume in the uniswap pool.

M-3

PermissionedGuild settings fail to prevent users from opening accounts

Topic
Spec
Status
Impact
Low
Likelihood
Medium

PermissionedGuild uses roles with the intention to only enable allowed users to move collateral and debt through the Guild. The current code breaks this design by allowing users who are not granted the DEPOSIT role to open accounts with collateral through the liquidation process. This is problematic because it breaks spec and liquidators who do this will have their collateral trapped unless they are granted the DEPOSIT role which would allow them to withdraw the collateral.

Liquidators are able to open account by performing successful liquidations via Guild:liquidationCall() with the parameter receiveCollateral set to true. This flag will send the liquidator’s reward collateral into the Guild’s user collateral accounting system, thus opening an account.

Remediations to consider

  • Not allowing liquidators to select for receiving collateral when they are not permissioned properly in PermissionedGuilds.
M-4

User supply cap not enforced in the liquidation flow

Topic
Spec
Status
Impact
Low
Likelihood
Medium

Guild has user supply cap configuration setting for each collateral asset. This config setting, when used, prevents specific accounts from becoming too large to be efficiently processed in future liquidations. This setting is enforced when user performs deposit, and before account balance is updated.

require(
    collateral.balances[onBehalfOf] + amount) <= (userSupplyCap * collateralUnits) / 100,
    Errors.SUPPLY_CAP_EXCEEDED
);

However, liquidators are able to update their account balance by performing successful liquidations via Guild:liquidationCall() with the parameter receiveCollateral set to true. This flag will cause liquidator’s collateral balance to be updated with the liquidator’s reward collateral. However, since corresponding user supply cap check is not present in this flow, liquidator’s collateral balance may become greater than the configured user supply cap for a Guild. This breaks specification.

Remediations to consider

  • Performing necessary user supply cap check on liquidation when liquidator selects to receive collateral, or
  • Not allowing liquidators to select for receiving collateral and instead send all collateral reward to the liquidator.
M-5

Missing access control for swapMoneyForZToken() and swapZTokenForMoney()

Topic
Spec
Status
Wont Do
Impact
Medium
Likelihood
High

The PermissionedGuild contract contains access-controlled wrappers for Guild functionality. Each of the most important functions is access-protected with role-based access control. These include: deposit(), withdraw(), borrow(), and repay().

However, PermissionedGuild does not override and wrap swapMoneyForZToken() and swapZTokenForMoney() from the parent Guild contract in the same way. Therefore, these important system functions are externally accessible to anyone.

Remediations to consider

  • Override swapMoneyForZToken() and swapZTokenForMoney() in the PermissionedGuild with corresponding access controlled wrapper implementation functions.
Response by Covenant Labs

Team acknowledges that additional control levers might be useful when deploying a guild, but deprioritized this lever atm.

M-6

User collateral can be misused when Guild’s money token is also used as collateral

Topic
Spec
Status
Impact
High
Likelihood
High

In the scenario where a Guild’s money token is also registered as a collateral, users’s collateral can be misused as funds for the Guild’s Guild:swapZTokenForMoney() flow.

When a user deposits collateral to the Guild the collateral is transferred to the Guild’s address. It is the intention that the collateral remains there until either the user is liquidated or performs a withdraw.

The logic for swapZTokenForMoney() problematically treats the Guild’s balance of the money token as the signal for available funds for swapping zTokens for money. Meaning, when a user deposits money as collateral, those funds can be swapped out of the protocol via swapZTokenForMoney(), leaving the protocol short funded when a user is withdrawing or being liquidated.

Remediations to consider

  • Using separate accounting for the inflow and outflow of money for the swapZTokenForMoney() and swapMoneyForZToken() flows.
  • Do not allow Guilds to use the money token as collateral.
M-7

PerpetualDebt's burnAndDistribute() can brick protocol when no debt is issued

Topic
Use Cases
Status
Impact
High
Likelihood
Low

When a Guild attempts to rebalance internal accounting to the zN * zT == dN * dT invariant, it can accidentally set the zTokenNotionalFactor to zero and brick the protocol’s operations. This will happen if a distribution is made when the total supply of the debt notional is zero.

In PerpetualDebt:burnAndDistribute(), the protocol will attempt update the zTokenNotionalFactor to match the desired invariant with the below logic:

if (zTokenTotalNotional > 0) {
  uint256 distributeFactor = perpDebt.dToken.totalNotionalSupply().wadToRay().wadDiv(zTokenTotalNotional);
  perpDebt.zToken.updateNotionalFactor(distributeFactor);
}

Note that no check is made that the debt’s notional supply is larger than zero. If the debt’s notional supply is zero, the zTokenNotionalFactor is then updated to be zero itself.

This will cause the protocol to become bricked because in PerpetualDebt:refinance() the logic assumes that the zTokenNotionalFactor is not zero and will attempt to use the value as a denominator:

uint256 notionalPrice = zPrice.rayDiv(perpDebt.zToken.getNotionalFactor());

This will revert and cause protocol operations to halt as refinance() is called before most operations.

Remediations to consider

  • Only make distribution adjustments when the Guild has debt issued.
M-8

By default Guild contract accrues fees that cannot be withdrawn

Topic
Use Cases
Status
Impact
High
Likelihood
Low

When Guild and consequently perpetual debt is configured in PerpetualDebtLogic.init() function fee address for various operations is by default set to the Guild address itself.

function init(DataTypes.PerpetualDebtData storage perpDebt, DataTypes.ExecuteInitPerpetualDebtParams memory params) internal {
  ...  
  perpDebt.protocolServiceFeeAddress = address(this); //default is for guild to accrue protocol fees
  perpDebt.protocolMintFeeAddress = address(this); //default is for guild to accrue protocol fees
  perpDebt.protocolDistributionFeeAddress = address(this); //default is for guild to accrue protocol fees
  perpDebt.protocolSwapFeeAddress = address(this); //default is for guild to accrue protocol fees
  ...
}

However, since the Guild contract does not contain corresponding functionality for withdrawing fees that are accrued in the asset token (zToken), these assets will be stuck, and not accessible.

Remediations to consider

  • Initialize fee addresses to non-Guild contract address, or
  • Add functionality for accounting and withdrawing accrued fees.
M-9

Accrued fees may get locked in TazzFeeRouter

Topic
Griefing
Status
Impact
Medium
Likelihood
Medium

By design, all the fees charged for various Guild operations are accrued in the TazzFeeRouter contract. These fees accrue for a specific period before they can be transferred to the specific UniswapV3Staker instance for reward distribution. The reward distribution is triggered in _checkAndCreateIncentive() based on the initial guild registration time and previously mentioned reward accrual period.

function _checkAndCreateIncentive(IGuild guild) internal {
    GuildInfo memory guildRegister = register[guild];

    // if guild not registered, try to register
    // otherwise , checks if an incentive can be launched
    if (address(guildRegister.dexPool) == address(0)) {
        registerGuild(guild);
    } **else if (block.timestamp > guildRegister.timeStart + incentiveTimeRewardAccrual) {**
        //update new epoch timestamp
        register[guild].timeStart = block.timestamp;

        // @dev - all assets associated with guild deposited into FeeRouter sent to new incentive
        // @dev - this also includes potential assets recovered from previously terminated incentives
        uint256 reward = guildRegister.asset.balanceOf(address(this));

        //kick off incentive, sending the asset associated with the guild as reward
        if (reward > 0) {
           // incentive creation for UniswapV3Staker
        }
    }
}

However, registerGuild() function is public and can be called by anyone. This function can be called multiple times for a single Guild. As a result, stored Guild entry can be updated with different timeStart which is based on the block.timestamp when registerGuild() was invoked. Malicious caller may invoke this function repeatedly before specific accrual period passes to extend it and consequently lock assets within TazzFeeRouter while preventing reward being distributed.

Remediations to consider

  • Add a check to registerGuild() function to prevent updates for already registered Guilds.
L-1

TazzPriceOracle.sol initializes the PriceContext::SPOT's endLookbackTime incorrectly

Topic
Spec
Status
Impact
Low
Likelihood
Medium

In TazzPriceOracle.sol, the endLookbackTime represents the oldest time that should be included in an asset price’s average. PriceContext::SPOT prices should have this value set to zero.

TazzPriceOracle.sol’s constructor currently initializes this price to something non-zero:

constructor(
        address addressesProvider,
        address baseCurrency,
        uint32 lookbackPeriod
    ) {
        require(lookbackPeriod > 0, Errors.ORACLE_LOOKBACKPERIOD_IS_ZERO);
        ADDRESSES_PROVIDER = IGuildAddressesProvider(addressesProvider);
        BASE_CURRENCY = baseCurrency;
        for (uint8 i; i <= uint8(type(DataTypes.PriceContext).max); i++) {
            _lookbackTime[DataTypes.PriceContext(i)].endLookbackTime = lookbackPeriod;
        }
    }

If this variable is not set correctly it will cause PriceContext::SPOT prices not to return the intended price.

Remediations to consider

  • Do not set the PriceContext::SPOT’s to a non-zero value.
L-2

External actor may hinder dropCollateral() processing

Topic
Griefing
Status
Impact
Low
Likelihood
Low

In the Guild contract, dropCollateral() enables removing unused collateral and freeing the slot for new collateral in the list of limited size of potential collateral assets.

The dropCollateral() relies upon GuildLogic.executeDropCollateral() for its operation.

function executeDropCollateral(
    mapping(address => DataTypes.CollateralData) storage collateralData,
    mapping(uint256 => address) storage collateralList,
    address asset
) internal {
    DataTypes.CollateralData storage collateral = collateralData[asset];
    ValidationLogic.validateDropCollateral(collateral, collateralList, asset);
    collateralList[collateralData[asset].id] = address(0);
    delete collateralData[asset];
}

In turn, GuildLogic.executeDropCollateral() relies on ValidationLogic.validateDropCollateral() to check if necessary preconditions for collateral removal are fulfilled.

function validateDropCollateral(
    DataTypes.CollateralData storage collateralData,
    mapping(uint256 => address) storage collateralList,
    address asset
) internal view {
    require(collateralList[collateralData.id] != address(0), Errors.COLLATERAL_NOT_LISTED);
    require(IERC20Metadata(asset).balanceOf(address(this)) == 0, Errors.POSITIVE_COLLATERAL_BALANCE);
}

One of these conditions includes checking that the contract balance of the collateral asset is 0. If the balance is not 0, the collateral asset would not be dropped.

A malicious external actor may intentionally keep the contract balance non-0 by transferring minuscule asset amounts directly to the contract.

Remediations to consider

  • Consider relaxing the check so that collateral can be dropped even if the balance is not 0 but is less than some relatively small threshold, or
  • When performing a dropCollateral operation, consider performing skim + dropCollateral action.
L-3

Externally provided event argument not validated

Topic
Events
Status
Impact
Low
Likelihood
Low

In the ConfiguratorLogic, executeUpdateAssetToken() and executeUpdateLiabilityToken() functions do not validate input.asset and input.liability at all. Therefore, any values would be accepted and correspondingly emitted in AssetTokenUpgraded and LiabilityTokenUpgraded events for these parameters.

Currently, only the guild admin can trigger these functions. Therefore, only if the admin provides an incorrect value for these inputs would the events emitted not represent a correct on-chain update.

Remediations to consider

  • Consider retrieving input.asset and input.liability values from corresponding proxies.
L-4

Mismatch between event definition and event emission

Topic
Events
Status
Impact
Low
Likelihood
Low

The PerpetualDebtInitialized event is defined in two places, IGuildConfigurator and ConfiguratorLogic contract, where it is also emitted.

/**
    * @dev Emitted when a collateral is initialized. <=== *
    * @param assetToken The address of the associated assetToken contract
    * @param liabilityToken The address of the associated liability token
    * @param monetToken The address of the associated money token
    * @param duration The perpetual debt duration
    * @param dex The dex address (asset/money)
    **/
event PerpetualDebtInitialized(
    address indexed assetToken,
    address liabilityToken,
    address monetToken, <=== *
    uint256 duration,
    address dex
);
// See `IGuildConfigurator` for descriptions
event PerpetualDebtInitialized(
    address indexed assetToken,
    address liabilityToken,
    address moneyToken,
    uint256 duration,
    uint24 dexFee //<=== this is different
);

However, these two definitions differ in type and meaning of the last parameter address dex vs uint24 dexFee. As a result, off-chain tools and processing can be affected by expecting one type of parameter but receiving something else.

Remediations to consider

  • Sync event definitions and ensure there is no ambiguity that what is emitted matches the corresponding declaration.
L-5

Same event emitted for different state updates

Topic
Events
Status
Impact
Low
Likelihood
Low

In the GuildConfigurator contract, setProtocolDistributionFeeAddress() and setProtocolServiceFeeAddress() functions emit the same event ProtocolServiceFeeAddressChanged even though they update different config variables.

Also, the ProtocolDistributionFeeAddressChanged event is not even defined in IGuildConfigurator.

Remediations to consider

  • Consider defining the ProtocolDistributionFeeAddressChanged event and emitting this event in setProtocolDistributionFeeAddress() so off-chain monitoring and tracking tools can easily determine which configuration value was changed.
L-6

Toggling Guild’s Perpetual Debt freeze/pause states causes interest to accrue slightly incorrectly

Topic
Spec
Status
Impact
Low
Likelihood
Low

Guilds aim to accrue interest when their perpetual debt is not frozen or paused. Currently, the protocol will fail to accrue interest properly, when switching between freeze and pause states. This is caused by failing to account for accrued interest between the last PerpetualDebtLogic::refinance() call and the debt’s state change.

Right now, if refinance() has not been called in a while and the debt is frozen/paused, then the interest set to accrue between the last refinance() call and the debt freeze is lost. Conversely, if the debt is unfrozen/unpaused with the refinance() call being a while back, the next refinance() call will accrue interest from the frozen time period.

This behavior can be seen as desirable if the reason for the freeze/pause was due to issues with the refinance() logic.

Remediations to consider

  • Call refinance() before debt pause/freeze state changes if the refinance() code is safe to run.
L-7

TazzPriceOracle missing validation that asset prices will resolve

Topic
Spec
Status
Impact
Medium
Likelihood
Low

TazzPriceOracle allows for the use of oracles which are not of the token pair asset<>baseCurrency. For assets with this oracle type, TazzPriceOracle will attempt to combine different oracles until the asset is priced in the base currency in _getAvgTick().

Currently, the code does not ensure that the needed assets can be resolved. This could cause issues if a needed asset fails to resolve due to a different asset being removed.

Remediations to consider

  • Programmatically checking that desired assets are able to be resolved when adding/removing different underlying oracles.
L-8

PermissionedGuild roles not granular enough

Topic
Use Cases
Status
Impact
Low
Likelihood
Low

PermissionGuild uses singular roles to guard both the inflow and outflow of assets. The DEPOSITOR role allows users to add and withdraw collateral and the BORROWER role allows users to take-out and repay debt. This setup lacks the ability to handle some potentially desired secnarios. For example, if the protocol operators would like to stop a user from taking out any more debt removing the BORROWER role would also prevent that user from being able to repay their outstanding debt. This lack of expressivity could fail to enable the protocol operators to operate their protocol as they would wish.

Remediations to consider

  • Splitting the roles into a role per function. This would enable finer grained control on what users are allowed to do.
L-9

TazzPriceOracle.sol’s getAssetPrice() will revert for PriceContext::SPOT prices with Uniswap V3 Oracles

Topic
Use Cases
Status
Impact
Medium
Likelihood
Low

In TazzPriceOracle.sol, querying for a PriceContext::SPOT’s price with getAssetPrice() will revert when the used underlying oracle is of type UniswapV3OracleProxy. This is due to the oracle’s use of the Uniswap provided OracleLibrary:consult() function. The PriceContext::SPOT price has its endLookbackTime/secondsAgo parameter resolve to zero which will cause the consult() function to revert:

function consult(address pool, uint32 secondsAgo)
        internal
        view
        returns (int24 arithmeticMeanTick, uint128 harmonicMeanLiquidity)
    {
        require(secondsAgo != 0, "BP");
        ...
    }

This issue is currently masked in testing due to issue [L-1], which initialized the PriceContext::SPOT's endLookbackTime to be something besides zero.

Remediations to consider

  • Don’t use the consult() function when trying to query only the spot price of a Uniswap v4 pool.
L-10

Small debt positions not profitable to be liquidated

Topic
Use case
Status
Acknowledged
Impact
Low
Likelihood
Medium

The implemented system features several cap mechanisms, including a total collateral supply cap, user supply cap, and total debt cap. Each of these parameters serves to tune the system for an appropriate risk profile.

However, currently, the system does not feature minimum debt restriction per position. As a result, small debt positions, even if liquidatable, may end up not being profitable to liquidate. This could lead to the accrual of bad debt.

This issue may have a larger impact on chains and networks where gas is more expensive, and the profitability threshold for liquidations is relatively higher.

Remediations to consider

  • Introduce a restriction on minimum borrow amount, and enforce minimum debt position on partial repays.
Q-1

Incorrect inline documentation for important structs in DataTypes

Topic
Documentation
Status
Quality Impact
Low

In the DataTypes contract, CollateralConfigurationMap and PerpDebtConfigurationMap are defined. Inline documentation indicates what particular beats within packed uint256 data variable represent.

However, these inline comments are incorrect and are not aligned with the functionality defined in CollateralConfiguration and PerpetualDebtConfiguration.

For example, for CollateralConfigurationMap, bits 80-115 represent the supply cap, bits 116-151 represent the user supply cap, and bits 152-255 are unused. The corresponding inline comment on the contrary says the following:

//bit 61-115: unused
//bit 116-151: supply cap in whole tokens, supplyCap == 0 => no cap
//bit 152-167: liquidation protocol fee
//bit 168-255: unused

For PerpDebtConfigurationMap bits 80-95 are used for protocol distribution fee (bps). However, inline comment indicates they are not used.

//bit 80-255: unused

Consider updating inline comments for previously mentioned structs in DataTypes to match related functionality in CollateralConfiguration and PerpetualDebtConfiguration contracts.

Q-2

AssetToken.mint() does not emit Mint event

Topic
Events
Status
Wont Do
Quality Impact
Low

In the LiabilityToken contract, the mint() function implementation emits a Mint event in addition to the underlying Transfer event.

emit Mint(user, onBehalfOf, amount);

However, the corresponding mint() function in the AssetToken contract does not emit a similar event.

Consider emitting a Mint event in the AssetToken.mint() for consistency.

Response by Covenant Labs

MintableUpgradeableERC20.sol is already emitting a transfer event during minting and burning. LiabilityToken.sol has a mint event in addition, to indicate the additional info of user + onBehalfOf that the transfer event is not recording.

Q-3

Usage of SafeMath in Solidity 0.8+

Topic
Unnecessary code
Status
Quality Impact
Low

The following contracts rely on SafeMath functionality which can be removed/replaced in Solidity version 0.8+, which the Tazz codebase relies on.

  • MintableUpgradeableERC20.sol
  • UpgradeableERC20.sol
  • CollateralLogic.sol
  • PerpetualDebtLogic.sol
  • LiquidationLogic.sol

Consider replacing SafeMath’s add() and sub() calls with corresponding default addition and subtraction operations.

Q-4

Unused imports

Topic
Unnecessary code
Status
Quality Impact
Low

In the following cases imported contracts are not used and therefore can be removed.

  • NotionalERC20.sol → SafeMath import
  • UpgradeableERC20.sol → WadRayMath import
  • CollateralLogic.sol → WadRayMath import
  • ValidationLogic.sol → Address import and using Address for address;
  • ValidationLogic.sol → SafeCast import and using SafeCast for uint256;
  • BalancerStableLPOracleProxy → WadRayMath import and using WadRayMath for uint256;
  • TazzPriceOracle → WadRayMath import and using WadRayMath for uint256;
  • UniswapV3OracleProxy → WadRayMath import and using WadRayMath for uint256;
  • HardcodedOracleProxy → WadRayMath import and using WadRayMath for uint256;
  • AnkrEthOracleProxy → WadRayMath import and using WadRayMath for uint256;
Q-5

Commented-out code in GuildConfigurator.sol

Topic
Extra Code
Status
Quality Impact
Low

On lines 184 and 200 of GuildConfigurator.sol, calls to a non-existent function _checkNoSupplers(asset) are commented out with a TODO. Consider removing the commend out code since, as per discussion with the team, the code does not need to perform the suggested logic.

I-1

DexOracleLogic.updateTwapPrice() called twice in same block is susceptible to price manipulation

Topic
Protocol Design
Status
Impact
Informational

In DexOracleLogic:updateTwapPrice(), the returned asset price can potentially be manipulated by a third party if the codebase used the function differently. This is an informational because the current codebase does not contain any codepaths which will enable this to be used in an exploit.

When the function does not need to update the TWAP price (as it is written to only update once per timestamp), it defaults to returning the Uniswap V3 pool’s actual spot price.

This spot price is manipulatable by a third party who can trigger the codepath externally. Consider the actions of a malicious third party in a single transaction:

  • Flash loans needed assets from another pool.
  • Swaps large amount of token in the Dex’s pool, manipulating the sqrtPriceX96 value.
  • Triggers the DexOracleLogic:updateTwapPrice()'s 2nd call codepath, getting the manipulated price as the result.
  • Does other actions with this price for profit.
  • Swaps back and returns initial flash loans.

The updateTwapPrice() function is currently only able to be called multiple times per timestamp on Arbitrum due to the chain being able to have the same block.timestamp for multiple blocks. The codebase only invokes this function once per block.number during refinance(). This doesn’t lead to an exploit because the returned price is only used for updating interest rates, but no interest rate change will occur since the elapsed time for the manipulated spot price would be zero.

Remediations to consider

  • Return the already computed and stored DexOracleData's twapPrice for that block.
I-2

Centralisation risks

Topic
Trust model
Impact
Informational

Tazz system design in current phase relies on multiple multisig accounts acting in different privileged roles: GuildAdmin, EmergencyAdmin, RiskAdmin. Each of these roles has associated privileges which enable execution of various sensitive operations that modify core system configuration parameters, such as setting various fees, pausing or freezing collateral and debt assets.

Currently system does not have any delay in application of these important configuration changes. As a result, in case of undesired changes users of the systems will not be able to react. Due to all this, current system implementation requires users to trust protocol admins to perform privileged actions properly which is not ideal.

Remediations to consider

  • Use Timelock mechanism for performing important configuration updates, and/or
  • Consider introducing governance mechanism for decentralized system management

Disclaimer

Macro makes no warranties, either express, implied, statutory, or otherwise, with respect to the services or deliverables provided in this report, and Macro specifically disclaims all implied warranties of merchantability, fitness for a particular purpose, noninfringement and those arising from a course of dealing, usage or trade with respect thereto, and all such warranties are hereby excluded to the fullest extent permitted by law.

Macro will not be liable for any lost profits, business, contracts, revenue, goodwill, production, anticipated savings, loss of data, or costs of procurement of substitute goods or services or for any claim or demand by any other party. In no event will Macro be liable for consequential, incidental, special, indirect, or exemplary damages arising out of this agreement or any work statement, however caused and (to the fullest extent permitted by law) under any theory of liability (including negligence), even if Macro has been advised of the possibility of such damages.

The scope of this report and review is limited to a review of only the code presented by the Covenant Labs team and only the source code Macro notes as being within the scope of Macro’s review within this report. This report does not include an audit of the deployment scripts used to deploy the Solidity contracts in the repository corresponding to this audit. Specifically, for the avoidance of doubt, this report does not constitute investment advice, is not intended to be relied upon as investment advice, is not an endorsement of this project or team, and it is not a guarantee as to the absolute security of the project. In this report you may through hypertext or other computer links, gain access to websites operated by persons other than Macro. Such hyperlinks are provided for your reference and convenience only, and are the exclusive responsibility of such websites’ owners. You agree that Macro is not responsible for the content or operation of such websites, and that Macro shall have no liability to your or any other person or entity for the use of third party websites. Macro assumes no responsibility for the use of third party software and shall have no liability whatsoever to any person or entity for the accuracy or completeness of any outcome generated by such software.