Security Audit
March 11th, 2026
Version 1.0.0
Presented by 0xMacro
This document includes the results of the security audit for KatanaPerps's smart contract code as found in the section titled ‘Source Code’. The security audit was performed in two parts by the Macro security team. Part 1 was conducted from January 19th to January 26th, 2026. The second part was conducted from February 11th to February 16th, 2026.
The purpose of this audit is to review the source code of certain KatanaPerps 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 |
|---|---|---|---|---|
| Low | 8 | - | 3 | 5 |
| Informational | 1 | - | - | - |
KatanaPerps was quick to respond to these issues.
Our understanding of the specification was based on the following sources:
The following source code was reviewed during the audit:
2ac8f8ed5806a23a2486ac64166836b9c5601623
5995ab6b61af12f7f60cfb38c6fb0463be3a8b3a
For part 1, we audited the diff between commits 4b15268 and 2ac8f8e for the following contracts, as well as a full review of the index price adapters.
| Source Code | SHA256 |
|---|---|
| contracts/Custodian.sol |
|
| contracts/EarningsEscrow.sol |
|
| contracts/Exchange_v1.sol |
|
| contracts/Governance.sol |
|
| contracts/Owned.sol |
|
| contracts/index-price-adapters/ChainlinkDataStreamsIndexPriceAdapter.sol |
|
| contracts/index-price-adapters/KatanaPerpsIndexAndOraclePriceAdapter.sol |
|
| contracts/index-price-adapters/RedStoneIndexPriceAdapter.sol |
|
| contracts/libraries/Address.sol |
|
| contracts/libraries/AssetUnitConversions.sol |
|
| contracts/libraries/BalanceLoading.sol |
|
| contracts/libraries/BalanceTracking.sol |
|
| contracts/libraries/ClosureDeleveraging.sol |
|
| contracts/libraries/Constants.sol |
|
| contracts/libraries/Depositing.sol |
|
| contracts/libraries/Enums.sol |
|
| contracts/libraries/ExchangeErrors.sol |
|
| contracts/libraries/ExchangeEvents.sol |
|
| contracts/libraries/ExitFund.sol |
|
| contracts/libraries/Funding.sol |
|
| contracts/libraries/FundingMultiplierQuartetHelper.sol |
|
| contracts/libraries/Hashing.sol |
|
| contracts/libraries/IndexPriceMargin.sol |
|
| contracts/libraries/Interfaces.sol |
|
| contracts/libraries/LiquidationValidations.sol |
|
| contracts/libraries/ManagedAccounts.sol |
|
| contracts/libraries/MarketAdmin.sol |
|
| contracts/libraries/MarketHelper.sol |
|
| contracts/libraries/Math.sol |
|
| contracts/libraries/NonceInvalidations.sol |
|
| contracts/libraries/OraclePriceMargin.sol |
|
| contracts/libraries/PositionBelowMinimumLiquidation.sol |
|
| contracts/libraries/PositionInDeactivatedMarketLiquidation.sol |
|
| contracts/libraries/SortedStringSet.sol |
|
| contracts/libraries/String.sol |
|
| contracts/libraries/Structs.sol |
|
| contracts/libraries/Time.sol |
|
| contracts/libraries/TradeValidations.sol |
|
| contracts/libraries/Trading.sol |
|
| contracts/libraries/Transferring.sol |
|
| contracts/libraries/UUID.sol |
|
| contracts/libraries/Validations.sol |
|
| contracts/libraries/WalletExitAcquisitionDeleveraging.sol |
|
| contracts/libraries/WalletExitLiquidation.sol |
|
| contracts/libraries/WalletExits.sol |
|
| contracts/libraries/WalletInMaintenanceAcquisitionDeleveraging.sol |
|
| contracts/libraries/WalletInMaintenanceLiquidation.sol |
|
| contracts/libraries/Withdrawing.sol |
|
| contracts/oracle-price-adapters/ChainlinkOraclePriceAdapter.sol |
|
For part 2, we audited the following contracts:
| Source Code | SHA256 |
|---|---|
| contracts/bridge-adapters/ExchangeLayerZeroAdapter_v1.sol |
|
| contracts/bridge-adapters/KatanaPerpsStargateForwarder_v1.sol |
|
| contracts/bridge-adapters/LayerZeroFeeEstimation.sol |
|
| contracts/bridge-adapters/libraries/BridgeAdapterEvents.sol |
|
| contracts/bridge-adapters/libraries/ExchangeAdapterComposing_v1.sol |
|
| contracts/bridge-adapters/libraries/KatanaPerpsStargateForwarderComposing_v1.sol |
|
Click on an issue to jump to it, or scroll down to see them all.
lzCompose catch block sends funds to owner() without attempting wallet identification
setComposeParameters bypasses bounds validation for minimumWithdrawQuantityMultiplier
previewRedeem/redeem return value discrepancy
withdrawNativeAsset uses .transfer() with 2300 gas stipend
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. |
In ChainlinkDataStreamsIndexPriceAdapter, the validFromTimestamp from Chainlink's ReportV3 is in seconds, but it is assigned directly to timestampInMs without conversion:
return
IndexPrice({
baseAssetSymbol: market.baseAssetSymbol,
timestampInMs: SafeCast.toUint64(report.validFromTimestamp),
price: priceInPips
});
Reference: ChainlinkDataStreamsIndexPriceAdapter.sol#L195-L200
As a result, timestampInMs will contain an incorrect value, leading to unexpected behavior in any logic that relies on it.
Remediation to Consider
Multiply by 1000: timestampInMs = validFromTimestamp * 1000
The ChainlinkDataStreamsIndexPriceAdapter’s validateIndexPricePayload doesn’t validate key fields from the ReportV3 struct, such as:
report.expiresAt > block.timestampreport.price > 0Similar validation checks are missing in the RedStoneIndexPriceAdapter.
As a result, expired or invalid price reports could be accepted, leading to incorrect index prices being used in the protocol
Remediation to Consider
Add meaningful validation checks for both adapters.
We ended up with just the validations wrapped into MarketAdmin’s publishIndexPrices_delegatecall. There are a few operational limits on this front. There can be delays in dispatching transactions from the off-chain components to the contracts, in which case validating older price updates is actually a requirement. Pricing > 0 could work, but any such price update would run afoul of the upstream off-chain logic, and practically we would delist markets well before encountering anything close to 0.
The ChainlinkDataStreamsIndexPriceAdapter does not handle verification fees, as noted in the comments that Katana does not currently have a feeManager set:
// Katana does not have a FeeManager and no funding is needed to verify
// reports. Validate no FeeManager is set as a sanity check
require(verifier.s_feeManager() == address(0), "FeeManager not supported");
Reference: ChainlinkDataStreamsIndexPriceAdapter.sol#L183-L185
If Chainlink enables fees in the future, the adapter will fail to verify reports.
Remediation to Consider
Two options:
The Chainlink team has assured us that they will not be deploying a feeManager to Katana.
The RedStoneIndexPriceAdapter assumes all RedStone price feeds use 8 decimals, but this is not guaranteed for all feeds. As a result, if a feed with non-standard decimals is added, the price will be incorrectly scaled, leading to wrong index prices.
Remediation to Consider
Either:
decimals configurable per market in the RedStoneMarket struct (as done in ChainlinkDataStreamsIndexPriceAdapter), orGiven the predominance of 8-decimal feeds, we opted to make that decimal count an operational requirement for adding new market pricing.
lzCompose catch block sends funds to owner() without attempting wallet identification
When compose() reverts entirely, the user's tokens go to owner(). There are scenarios where the destinationWallet could be successfully decoded. Consider the case where vbUSDC.deposit inside _forwardDeposit reverts due to a paused vault. In this case all user funds from bridging attempts would go to the owner, causing temporary loss of custody and increasing operational burden since affected users need to be identified and refunded.
Remediation to Consider
Trying to decode destinationWallet to reduce operational burden and avoid temporary loss of custody.
After discussion and consideration, we landed on a slightly adjacent change. The forwarder did not explicitly validate against a 0x0 destinationWallet. Our rationale is:
- A 0x0 destinationWallet is definitely an error condition, and the Forwarder’s catch block handles this correctly. This is “expected” in that it is user input prone to errors.
- None of the other granular failures in _forwardDeposit are expected conditions. Rather than reason about which tokens to return or whether the destination wallet is valid, we believe it’s cleaner and safer to allow the catch to handle it universally.
setComposeParameters bypasses bounds validation for minimumWithdrawQuantityMultiplier
The ExchangeLayerZeroAdapter_v1.setMinimumWithdrawQuantityMultiplier enforces bounds of [90%, 100%]. But setComposeParameters sets the same variable without any bounds check. This also applies during deployment, since the constructor routes through setComposeParameters. An accidental value of 0 disables slippage protection entirely; a value above 100% makes all withdrawals fail.
Remediation to Consider
Add the boundary checks <= MAX_MINIMUM_WITHDRAW_QUANTITY_MULTIPLIER and >= MIN_MINIMUM_WITHDRAW_QUANTITY_MULTIPLIER to the setComposeParameters function.
previewRedeem/redeem return value discrepancy
In _forwardWithdrawal, previewRedeem is called to compute usdcAmount, which is then used for the Stargate send and the Ethereum direct transfer. However, the actual redeem call may return a slightly different (higher) amount. Per EIP-4626, previewRedeem "MUST return as close to and no more than the exact amount of assets that would be withdrawn in a redeem call in the same transaction." This means redeem can
return ≥ previewRedeem. Aa a result, any dust difference between redeem's actual return and previewRedeem's estimate accumulates in the forwarder contract.
Remediation to Consider
Use the amount returned by the actual redeem call instead of previewRedeem for subsequent transfers.
withdrawNativeAsset uses .transfer() with 2300 gas stipend
The ExchangeLayerZeroAdapter_v1.withdrawNativeAsset correctly uses .call{value: quantity}(""), but the StargateForwarder uses .transfer() which forwards only 2300 gas. This will fail if the destination is a contract with a non-trivial receive() (e.g., a multisig). Since this is admin-only, impact is limited to operational inconvenience.
Remediation to Consider
Use .call instead of transfer.
In BalanceTracking, the balance migration logic in loadBalanceStructAndMigrateIfNeeded copies balance data from a source Exchange but omits the managedAccountProvider field. This is intentional and works for migration from an old exchange (without managedAccountProvider support) to a new exchange. However, if a future Exchange upgrade migrates from a version that already supports managedAccountProvider, all managed account associations would be lost.
Document this limitation for future upgrade planning. When migrating from an Exchange that supports managed accounts, the migration logic must be updated accordingly.
This is an intentional design decision for the current upgrade path. We don’t yet know what target structure we will need to support in any future upgrade. As a result, we plan to update the balance migration logic as part of any upgrade.
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 KatanaPerps 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.