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

Kodiak A-3

Security Audit

Dec 6th, 2024

Version 1.0.0

Presented by 0xMacro

Table of Contents

Introduction

This document includes the results of the security audit for Kodiak-3's smart contract code as found in the section titled ‘Source Code’. The focus of the security audit was on incremental changes in the Kodiak core codebase. Security audit performed by the Macro security team lasted 5 audit days, started on Nov 18th and finished on Nov 25th, 2024.

The purpose of this audit is to review the source code of certain Kodiak-3 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
Medium 1 - - 1
Low 1 - - 1
Code Quality 8 - - 8
Informational 3 - - 1

Kodiak-3 was quick to respond to these issues.

Specification

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

Source Code

The following source code was reviewed during the audit:

Specifically, we audited the following contract within this repository:

Source Code SHA256
src/farms/FarmFactory.sol

61574c9eb58c3c6e334e197a4f206fd6313f14f3e002f550698ee8e3a39dbef1

src/farms/KodiakFarm.sol

c243d9b351e3c38444d56c4f5862491c717a267eae977c922941ea1a446864e0

src/tokens/KDK.sol

1732022ee03585a0909c330827d688a09a541599edc7d58ad45800a6f82c046a

src/tokens/xKDK.sol

53c89693a5406600b0174e140863ebbc4737cafd2b8e5955abbdeb2272d63127

src/vaults/KodiakIsland.sol

8694e072eda7a853f4cf90577ecdaa997e9e3557d733f6b863c583a1219e6d02

src/vaults/KodiakIslandFactory.sol

e51a0c97593f7228363bb04f7d992ca3bfce945e7b579aaced8e33cc247b30dc

src/vaults/KodiakIslandWithRouter.sol

827ddb920651dc9f1a5fc37ceb2c7c2ab1aa11b38953fc51f092953eab07bd1d

src/vaults/abstract/KodiakIslandStorage.sol

1b90d6ffaf306b484e6d43b8dd9ed856be7f62444f8ffb6d34d38de734e2b030

src/vaults/abstract/OwnableUninitialized.sol

a7ad23d4857338e864a6c3cea59f001c6e4c2132eeae7f1cbd9c99e9ee597c2c

Note: This document contains an audit solely of the Solidity contracts listed above. Specifically, the audit pertains only to the contracts themselves, and does not pertain to any other programs or scripts, including deployment scripts.

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

M-1

KodiakFarm can be initialized multiple times

Topic
Spec
Status
Impact
Medium
Likelihood
Medium

In the KodiakFarm contract, the initialize() function is expected to be executed only once to perform an important initial contract setup. This setup includes setting the owner, corresponding factory and factoryOwner, and various reward configuration parameters. To prevent multiple calls, initialize checks if stakingToken is already set; if not, it sets it and allows initial configuration.

  function initialize(
      address _owner,
      address _stakingToken,
      address[] memory _rewardTokens,
      address[] memory _rewardManagers,
      uint256[] memory _rewardRates,
      bytes calldata /*_data*/
  ) external nonReentrant {
      require(address(stakingToken) == address(0), "Farm: Already initialized");
      stakingToken = IERC20(_stakingToken);
      ...
  }

However, neither the current implementation nor its caller in FarmFactory.deployFarm() checks if the provided _stakingToken is a non-zero address. Due to this, a malicious user may deploy a new KodiakFarm through the factory, which will be registered with KodiakFarm but may have a different factory value and corresponding factory owner. In addition, a malicious user may change the owner of previously created KodiakFarm instances without proper _stakingToken set.

This can be achieved by calling FarmFactory.deployFarm() with 0 address for _stakingToken, followed by the direct call to KodiakFarm.initialize() on a specific KodiakFarm instance.

Remediations to Consider

  • Add input validation to check if _stakingToken is a non-zero address.
L-1

Use TransferHelper for stakingToken transfers

Topic
Token standards
Status
Impact
Medium
Likelihood
Low

With the most recent changes in the KodiakFarm contract, the stakingToken address value is not guaranteed to be a token address under Kodiak system control. As a result, the stakingToken implementation may not comply with the ERC20 standard implementation.

For example, an operation may succeed when a token transfer fails as it returns a false value, which is not handled in the _withdrawLocked() implementation.

Remediations to Consider

  • Use TransferHelper for staking token transfers, which is similar to how reward token transfers are currently handled.
Q-1

Skip 0 reward token transfers

Topic
Unnecessary code
Status
Quality Impact
Low

In the KodiakFarm contract, the _getReward() internal function processes and transfers token rewards. Processing is performed within a loop and involves unnecessary transfer calls even when the reward amount is 0.

function _getReward(address user) internal updateRewardAndBalance(user, true) returns (uint256[] memory rewards_before) {
    require(!rewardsCollectionPaused, "Farm: Rewards emergency paused, use emergencyWithdraw if necessary");

    // Update the rewards array and distribute rewards
    rewards_before = new uint256[](rewardTokens.length);

    for (uint256 i = 0; i < rewardTokens.length; i++) {
        rewards_before[i] = rewards[user][i];
        rewards[user][i] = 0;
        TransferHelper.safeTransfer(rewardTokens[i], user, rewards_before[i]);
        emit RewardPaid(user, rewards_before[i], rewardTokens[i]);
    }

    lastRewardClaimTime[user] = block.timestamp;
}

To reduce fault probability (tokens that revert on a 0 amount transfer) and gas cost, consider skipping 0 amount token transfers with a corresponding check within the loop.

Q-2

Use OZ Ownable instead of custom OwnableUninitialized

Topic
Unnecessary code
Status
Addressed
Quality Impact
Low

The KodiakIslandStorage contract inherits from the custom OwnableUninitialized implementation. On the other hand, KodiakFarm is already relying on OZ’s Ownable, which is already a standard for simple access control implementation.

Consider replacing the custom implementation with the OZ’s Ownable in KodiakIslandStorage implementation for consistency and easier future maintenance.

Response by Kodiak-3

This is ok, the custom OwnableUnitiatilized is used to effectively rename the owner to “manager” and could be useful in future upgrades to separate owner / manager etc - will leave as is.

Q-3

Unnecessary check in setLockedStakeTimeForMinAndMaxMultiplier()

Topic
Unnecessary code
Status
Quality Impact
Low

In the KodiakFarm contract, there is now an unnecessary check in the setLockedStakeTimeForMinAndMaxMultiplier() function due to recent updates.

require(_lock_time_for_max_multiplier >= 1, "Mul max time must be >= 1");

This check is unnecessary since two subsequent checks have already enforced it.

require(_lock_time_min >= 1, "Mul min time must be >= 1");
require(_lock_time_for_max_multiplier >= _lock_time_min, "Mul max time must be >= min time");

Consider removing it.

Response by Kodiak-3

Fixed by removing: require(_lock_time_min >= 1, "Mul min time must be >= 1");

Since lock_time_min = 0 is actually a valid case.

Q-4

Hardcoded constant values across contracts

Topic
Best practices
Status
Addressed
Quality Impact
Low
  1. The xKDK:mint() literal constant value of 100e6 ether enforces the maximum total supply of the xKDK token. This constant is meant to match the value of the KDK:MAX_SUPPLY constant, which currently has the same value of 100e6 ether.

    However, since KDK.sol will be deployed in the future and the total supply may change from the one planned, consider making the limit value in xKDK:mint() admin configurable to prevent divergence.

  2. In the KodiakIsland, KodiakIslandWithStorage, and KodiakIslandWithRouter contracts, multiple functions use the literal constant value of 10000 in checks and calculations.

    Consider replacing this with BPS_PRECISION or FEE_PRECISION constant.

Response by Kodiak-3

Related to xKDK.mint() this is fine - business decision, won’t change it by an order of magnitude or more, better to have it be immutable.

Related to 10000 constant, also fine - tried it the other way and found that 10000 next to variables called xyzBPS is more readable than calling it BPS_PRECISION

Q-5

Missing function declarations in IXKDToken interface

Topic
Best practices
Status
Quality Impact
Low

The IXKdkToken interface does not declare two recently added functions accessible to the contract owner. On the other hand, updateWhitelister(), also an admin function, is present.

  • setKdkAddress()
  • mint()

Consider updating the interface with two corresponding function declarations.

Q-6

Update docs

Topic
Documentation
Status
Quality Impact
Low
  1. KodiakFarm's top-level contract natspec documenting capabilities for factoryOwner are inadequate as they do not mention that factoryOwner is practically a super admin, with a super set of privileges available to the owner and token reward managers.

  2. There is an obsolete inline comment in stakeLocked() **function referencing the old variable source_address

    // Pull the tokens from the source_address
    TransferHelper.safeTransferFrom(address(stakingToken), user, address(this), liquidity);
    
Q-7

Unnecessary imports

Topic
Unnecessary code
Status
Quality Impact
Low
  1. The FarmFactory contract imports and inherits the ReentrancyGuard contract. However, these contract features are not used. Consider removing them.
  2. The KodiakIslandStorage contract imports the test artifact forge-std/console.sol. Consider removing it from production deployments.
Q-8

Missing indexed attribute for events in KodiakFarm

Topic
Events
Status
Quality Impact
Low

In the KodiakFarm, the following events would benefit from having indexed attributes:

  • RewardPaid - token_address
  • RewardRateUpdated - token
  • RewardManagerSet - token
  • Recovered - destination_address, token
  • RewardsPeriodRenewed - token
  • GreylistSet - _address
I-1

Centralization risks

Topic
Trust model
Status
Impact
Informational

Kodiak’s FarmFactory owner has extensive privileges for each deployed KodiakFarm instance. FarmFactory owner privileges are superset of the KodiakFarm owner privileges and related reward token managers for the particular KodiakFarm instance. The only exception related to available privileges, is that only the KodiakFarm owner may invoke startFarm() function.

These privileges provide factory owner with the following capabilities:

  • Can pause withdrawals indefinitely (setWithdrawalsPaused(true))
  • Can pause rewards collection (setRewardsCollectionPaused(true))
  • Can pause staking (setStakingPaused(true))
  • Could effectively lock user funds by pausing all operations
  • Can modify reward duration (setRewardsDuration)
  • Can affect reward calculations by changing multipliers
  • Can modify lock time parameters affecting reward calculations
  • Can set reward rates to zero
  • Can change reward token managers
  • Can selectively block users via greylisting (setGreylist)
  • Can force unlock all stakes (setStakesUnlocked(true))
  • Can recover reward tokens, effectively stopping rewards

The factory owner controls the contract's operation and user assets. While some of these powers are necessary for emergencies, they represent a significant centralization risk. Users should know that their funds could be temporarily frozen or their rewards manipulated if the factory owner is compromised.

Recommended improvements include:

  1. Adding timelocks for emergency actions
  2. Implementing parameter change limits
  3. Adding fail-safe withdrawal mechanisms (releasing assets after some time automatically, even if withdrawals are still disabled)
Response by Kodiak-3

Upon review - we decided to decentralize further and remove the “factoryOwner” role entirely. Now, all controls are done by the farmOwner, which is the farm deployer - there is no control over the farms by “factoryOwner” (i.e. Kodiak team).

Also, acknowledging that the ability to permanently lock user’s funds via setting withdrawalsPaused is too big of a risk to our users, especially when there is no control over who is deploying the farm and the security characteristics of the farm Owner. Therefore, the ability to lock user withdrawals by pausing has been removed entirely.

We consider it an acceptable centralization risk for users to forfeit reward tokens as long as the stake can be withdrawn no matter what.

I-2

Unexpected slippage calculation in worstAmountOut()

Topic
Spec
Impact
Informational

In the KodiakIslandWithRouter contract, worstAmountOut() calculates min output amount for the default slippage check. The system will stop processing the transaction if the provided min amount out is less or equal to the calculated one. It will revert with an indication to the manager to provide a more appropriate minimum amount out value.

However, there is an implementation error in how worstAmountOut is calculated. In the implementation, slippageBPS (default value 5%) is applied directly to the avgSqrtPriceX96 (average square root price) instead of the price itself. As a result, the calculated worstAmountOut would be less than the expected value.

For example, with a price of 4.0 (as defined in test_WorstAmountOut_BasicZeroForOne test) and slippage of 1000 (or 10%) worstAmountOut should be 3.6 for a provided amount of 1e18. However, the calculated value by the current implementation is 3.24, which is incorrect.

Remediations to Consider

  • Update the implementation and corresponding test to apply slippageBPS to the averageSqrtPriceX96 properly. Consider applying a sqrt of slippageBPS to avgSqrtPriceX96 instead of directly applying slippageBPS.
Response by Kodiak-3

Not an issue, aware and intentional that the slippage applies to sqrtP rather than P - decided to keep it simple rather than apply too many transformations as its intended to be a backstop (and cleaner as it’s the dimension in which the univ3 logic operates).

I-3

getAvgPrice() may unexpectedly revert and affect executiveRebalanceWithRouter()

Topic
Spec
Impact
Informational

In the KodiakIslandWithRouter contract, getAvgPrice() retrieves TWAP from the Uniswap V3 pool. This function is called from executiveRebalanceWithRouter() with the compounderSlippageInterval variable, which the manager can configure. The compounderSlippageInterval defines the interval value for the TWAP.

However, when compounderSlippageInterval is greater than the oldest observation on the oracle, pool.observe() call in getAvgPrice() will fail causing executiveRebalanceWithRouter() not to be able to execute successfully.

Remediations to Consider

  • Use the try/catch mechanism to fail gracefully in situations when the oracle is not yet ready to provide TWAP (inadequate cardinality or interval out of range)
Response by Kodiak-3

No need to (spend extra gas) to have graceful fallback logic since this would just fail on simulation with the error msg, and in this case since executiveRebalanceWithRouter cannot be called with all the adequate protections that choices are to either wait to have adequate recent pricing to have a good twap, or call executiveRebalance which doesn’t have a twap based protection.

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 Kodiak-3 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.