Security Audit
Feb 21, 2024
Version 1.0.2
Presented by 0xMacro
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.
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.
Our understanding of the specification was based on the following sources:
refinance), debt creation (mint and swapMint), debt redemption (burn and burnAndDistribute).The following source code was reviewed during the audit:
f357cc4bd28d44e974def3b71eb35a570aa39ecc
23f852c4bc4f84ca7dc9b3a82c347af30d29df53
f357cc4bd28d44e974def3b71eb35a570aa39ecc f1550b7af534474aa458e384e862d2a2444f3438 2b554234629dbd303fbbfe00a0aa61e3dee41e8e 6f459a8ca3e2662035606982fc197e07c24587d3 ba47b2c0f0f224c9739169b3ff695b4262d20c55 One initial and four extension commits were a part of Covenant's security review.
Specifically, we audited the following contracts for the Initial commit:
| Source Code | SHA256 |
|---|---|
| contracts/protocol/configuration/ACLManager.sol |
|
| contracts/protocol/configuration/GuildAddressesProvider.sol |
|
| contracts/protocol/configuration/GuildAddressesProviderRegistry.sol |
|
| contracts/protocol/guild/Guild.sol |
|
| contracts/protocol/guild/GuildConfigurator.sol |
|
| contracts/protocol/guild/GuildRoleManager.sol |
|
| contracts/protocol/guild/GuildStorage.sol |
|
| contracts/protocol/guild/PermissionedGuild.sol |
|
| contracts/protocol/guild/PermissionedGuildConfigurator.sol |
|
| contracts/protocol/guild/PermissionedStorage.sol |
|
| contracts/protocol/libraries/configuration/CollateralConfiguration.sol |
|
| contracts/protocol/libraries/configuration/PerpetualDebtConfiguration.sol |
|
| contracts/protocol/libraries/helpers/Errors.sol |
|
| contracts/protocol/libraries/logic/BorrowLogic.sol |
|
| contracts/protocol/libraries/logic/CollateralLogic.sol |
|
| contracts/protocol/libraries/logic/ConfiguratorLogic.sol |
|
| contracts/protocol/libraries/logic/DexOracleLogic.sol |
|
| contracts/protocol/libraries/logic/GenericLogic.sol |
|
| contracts/protocol/libraries/logic/GuildLogic.sol |
|
| contracts/protocol/libraries/logic/LiquidationLogic.sol |
|
| contracts/protocol/libraries/logic/PermissionedLogic.sol |
|
| contracts/protocol/libraries/logic/PerpetualDebtLogic.sol |
|
| contracts/protocol/libraries/logic/ValidationLogic.sol |
|
| contracts/protocol/libraries/math/DebtMath.sol |
|
| contracts/protocol/libraries/math/PercentageMath.sol |
|
| contracts/protocol/libraries/math/WadRayMath.sol |
|
| contracts/protocol/libraries/math/X96Math.sol |
|
| contracts/protocol/libraries/types/ConfiguratorInputTypes.sol |
|
| contracts/protocol/libraries/types/DataTypes.sol |
|
| contracts/protocol/libraries/upgradeability/BaseImmutableAdminUpgradeabilityProxy.sol |
|
| contracts/protocol/libraries/upgradeability/InitializableImmutableAdminUpgradeabilityProxy.sol |
|
| contracts/protocol/libraries/upgradeability/VersionedInitializable.sol |
|
| contracts/protocol/oracle/PriceOracleSentinel.sol |
|
| contracts/protocol/oracle/TazzPriceOracle.sol |
|
| contracts/protocol/oracle/proxies/AnkrEthOracleProxy.sol |
|
| contracts/protocol/oracle/proxies/BalancerStableLPOracleProxy.sol |
|
| contracts/protocol/oracle/proxies/HardcodedOracleProxy.sol |
|
| contracts/protocol/oracle/proxies/UniswapV3OracleProxy.sol |
|
| contracts/protocol/tokenization/AssetToken.sol |
|
| contracts/protocol/tokenization/LiabilityToken.sol |
|
| contracts/protocol/tokenization/base/CreditDelegation.sol |
|
| contracts/protocol/tokenization/base/ERC20Storage.sol |
|
| contracts/protocol/tokenization/base/MintableUpgradeableERC20.sol |
|
| contracts/protocol/tokenization/base/NotionalERC20.sol |
|
| contracts/protocol/tokenization/base/UpgradeableERC20.sol |
|
Specifically, we audited the following contracts for the Oracle Proxies commit:
| Source Code | SHA256 |
|---|---|
| contracts/protocol/oracle/proxies/HardcodedOracleProxy.sol |
|
| contracts/protocol/oracle/proxies/OracleProxyCommon.sol |
|
| contracts/protocol/oracle/proxies/UniswapV3OracleProxy.sol |
|
Specifically, we audited the following contracts for the Chainlink Oracle commit:
| Source Code | SHA256 |
|---|---|
| contracts/protocol/libraries/helpers/Errors.sol |
|
| contracts/protocol/oracle/proxies/ChainlinkV2V3OracleProxy.sol |
|
Specifically, we audited the following contracts for the zToken<>Money commit:
| Source Code | SHA256 |
|---|---|
| contracts/protocol/guild/Guild.sol |
|
| contracts/protocol/libraries/logic/BorrowLogic.sol |
|
| contracts/protocol/libraries/logic/PerpetualDebtLogic.sol |
|
| contracts/protocol/libraries/logic/ValidationLogic.sol |
|
| contracts/protocol/libraries/types/DataTypes.sol |
|
Specifically, we audited the following contracts for the LP Staking Rewards:
| Source Code | SHA256 |
|---|---|
| contracts/periphery/TazzFeeRouter.sol |
|
| contracts/protocol/guild/Guild.sol |
|
| contracts/protocol/libraries/helpers/Errors.sol |
|
| contracts/protocol/libraries/logic/LiquidationLogic.sol |
|
| contracts/protocol/libraries/logic/PerpetualDebtLogic.sol |
|
| contracts/protocol/oracle/TazzPriceOracle.sol |
|
| contracts/protocol/oracle/proxies/ChainlinkV2V3OracleProxy.sol |
|
Click on an issue to jump to it, or scroll down to see them all.
swapMoneyForZToken() logic to revert
TazzPriceOracle Uniswap V3 Oracles can fail to index enough observations for desired TWAP period
PermissionedGuild settings fail to prevent users from opening accounts
money token is also used as collateral
PerpetualDebt's burnAndDistribute() can brick protocol when no debt is issued
PriceContext::SPOT's endLookbackTime incorrectly
freeze/pause states causes interest to accrue slightly incorrectly
TazzPriceOracle missing validation that asset prices will resolve
PermissionedGuild roles not granular enough
getAssetPrice() will revert for PriceContext::SPOT prices with Uniswap V3 Oracles
DexOracleLogic.updateTwapPrice() called twice in same block is susceptible to price manipulation
We quantify issues in three parts:
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. |
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
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.
swapMoneyForZToken() logic to revert
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
zTokens being transferred.TazzPriceOracle Uniswap V3 Oracles can fail to index enough observations for desired TWAP period
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:
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.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.
PermissionedGuild settings fail to prevent users from opening accounts
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
PermissionedGuilds. 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
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
swapMoneyForZToken() and swapZTokenForMoney() in the PermissionedGuild with corresponding access controlled wrapper implementation functions.Team acknowledges that additional control levers might be useful when deploying a guild, but deprioritized this lever atm.
money token is also used as collateral
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
money for the swapZTokenForMoney() and swapMoneyForZToken() flows.money token as collateral.PerpetualDebt's burnAndDistribute() can brick protocol when no debt is issued
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
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
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
registerGuild() function to prevent updates for already registered Guilds.PriceContext::SPOT's endLookbackTime incorrectly
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
PriceContext::SPOT’s to a non-zero value.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
skim + dropCollateral action.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
input.asset and input.liability values from corresponding proxies.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
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
ProtocolDistributionFeeAddressChanged event and emitting this event in setProtocolDistributionFeeAddress() so off-chain monitoring and tracking tools can easily determine which configuration value was changed.freeze/pause states causes interest to accrue slightly incorrectly
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
refinance() before debt pause/freeze state changes if the refinance() code is safe to run.TazzPriceOracle missing validation that asset prices will resolve
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
PermissionedGuild roles not granular enough
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
getAssetPrice() will revert for PriceContext::SPOT prices with Uniswap V3 Oracles
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
consult() function when trying to query only the spot price of a Uniswap v4 pool.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
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.
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.
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.
The following contracts rely on SafeMath functionality which can be removed/replaced in Solidity version 0.8+, which the Tazz codebase relies on.
Consider replacing SafeMath’s add() and sub() calls with corresponding default addition and subtraction operations.
In the following cases imported contracts are not used and therefore can be removed.
using Address for address;using SafeCast for uint256;using WadRayMath for uint256;using WadRayMath for uint256;using WadRayMath for uint256;using WadRayMath for uint256;using WadRayMath for uint256;DexOracleLogic.updateTwapPrice() called twice in same block is susceptible to price manipulation
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:
sqrtPriceX96 value.DexOracleLogic:updateTwapPrice()'s 2nd call codepath, getting the manipulated price as the result.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
DexOracleData's twapPrice for that block.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
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.