Actuator

Actuator Finance audit report

Actuator

SUMMARY

Critical 0
High 0
Medium 0
Low 1
Advisory 9
Total: 10

ABSTRACT

Dedaub was commissioned to perform a security audit of the Actuator Finance protocol. A single low severity issue was found. A number of advisory items are also being recommended.

BACKGROUND

The Actuator Protocol aims to provide users a liquid asset backed by underlying stakes in the HEX token. It aims to achieve this through the use of HexTimeTokens (HTT) which it mints via the HexTimeTokenManager. Each instance of a HEX Time Token contract has an associated maturity, which is a given number of days after the first hex day.

Users can stake their hex through the HexTimeTokenManager (which utilizes Hedron’s HexStakedInstanceManager to create HexStakedInstance’s (HSI) which represent the users staked position) and then mint HTTs against their staked positions. The amount of HTTs that can be minted are calculated based on the worst case value of the position on redemption day (the day of the maturity). This can be broken down into:

  • Redemption Day being equal to the End Stake Day
  • Redemption Day being after the End Stake Day
  • Which would subject the position to late penalties
  • Redemption Day being before the End Stake Day
  • Which would subject the position to early penalties

HTTs can be created for any maturity day, which could result in fractured stakes with very little liquidity for any given HTT. In an aim to counteract this the protocol introduces the Actuator Token (ACTR), which is distributed to users who provide liquidity to certain liquidity pools.

This behavior is implemented inside MasterChef (forked from the original SushiSwap MasterChef), which hardcodes 6 LP Pools distributed 1000 days apart from 3000 days to 8000 days. By depositing the LPTokens into MasterChef users are paid out a number of ACTR tokens per-day, which they can then deposit into a given HTT to collect a portion of the tax which is collected when HTTs are minted. This therefore ties the value of the ACTR token to the value of HEX.

SETTING & CAVEATS

This audit report covers the contracts of the Actuator-Protocol repository at commit [a168cd94cf0de33536d64b0a9396196653f2586c](https://github.com/actuator-finance/Actuator-Protocol/commit/a168cd94cf0de33536d64b0a9396196653f2586c).

2 auditors worked on the codebase for 5 days (each) on the following contracts:

contracts//
├── Actuator.sol
├── HEXTimeToken.sol
├── HEXTimeTokenManager.sol
└── MasterChef.sol

In addition, the Actuator team asked Dedaub to verify the following two financial invariants:

  • HTTs are always redeemable 1 for 1 for HEX - assuming HTT holders invoke redemption no later than 14 days after redemption day
  • The quantity of HEX that HEX stakers forgo at end stake equals the quantity of outstanding HTTs against the stake - assuming the stake is ended before the ’end stake subsidy’ kicks in on the fourth day after end stake.

These two invariants were verified manually by the audit team, see items I1 and I2 below.

The audit’s main target is security threats, i.e., what the community understanding would likely call “hacking”, rather than the regular use of the protocol. Functional correctness (i.e. issues in “regular use”) is a secondary consideration. Typically it can only be covered if we are provided with unambiguous (i.e. full-detail) specifications of what is the expected, correct behavior. In terms of functional correctness, we often trusted the code’s calculations and interactions, in the absence of any other specification. Functional correctness relative to low-level calculations (including units, scaling and quantities returned from external protocols) is generally most effectively done through thorough testing rather than human auditing.

This audit does not act as an endorsement of the underlying assets, namely HEX.

FINANCIAL INVARIANT VERIFICATION

HTTs are always redeemable 1 for 1 for HEX - assuming all HTT holders invoke redemption no later than 14 days after redemption day

Financial Invariant Verification | Status: INFO

Suppose that there are y HTT tokens with maturity M in circulation. Each such HTT token must have been obtained through some user calling the mintHexTimeTokens function of the HexTimeTokenManager contract on some HSI (HexStakedInstance). Now each time mintHexTimeTokens is called, the function calls the private function getExtractableAmount to calculate the maximum number of HTT tokens which can be extracted from a given HSI, as a function of the maturity of the HTT tokens and other parameters.

The audit team has compared the getExtractableAmount function with the staking algorithm used by HEX. It was found that this function is conservative, in the sense that it uses a worst case computation for HEX rewards and penalties (both early and late redemption). Hence it is the case that the mintHexTimeTokens function will always constrain the minting of HTT from an HSI to a quantity which is less than or equal to the amount of HEX in the HSI at maturity.

Now, when an amount of tokens is minted by mintHexTimeTokens, this results in a corresponding increase of the collateral.amount field of the Collateral struct associated to the HSI. It follows that the set of HSIs on which HTT tokens with maturity M have been minted have collateral.amount fields which total to y.

Now suppose that on a day after maturity, but before 14 days have elapsed, any end staker can call the endCollateralizedHexStake function of the HexTimeTokenManager contract on any HSI which collateralizes HTT tokens with maturity M.

For every such HSI, we have that HTT tokens were minted from the HSI. Therefore it must be the case that getExtractableAmount returned a non-zero value y' at mint time for the HSI. In addition, since getExtractableAmount is conservative, it must be the case that at mint time, there was some amount y' >= y of HEX tokens in the HSI to collateralize the HTT tokens. Finally, since endCollateralizedHexStake was called prior to 14 days after maturity, there will still be y' tokens in the HSI, and thus, the hsiBalance variable is y' (non-zero) as well.

Hence, endCollateralizedHexStake will first call the private function calcEndStakeSubsidy to reward the end staker. If the value is non-zero, an amount of HEX tokens will be transferred to the end staker. However, this end stake subsidy is taken from funds in escrow, and not from the HEX which is collateralising the HTT tokens.

It follows that the value of the hsiBalance variable is still non-zero, and therefore the function will deduct the collateral.amount from the hsiBalance and add the corresponding amount to the hexBalance of the pool of HEX tokens associated to HTTs of the maturity under consideration.

Now, any user holding HTT tokens can act as an end staker for any HSI collateralising HTT tokens with maturity M, because the maturity date has elapsed. Using this fact, the user can redeem his HTT tokens on a 1:1 basis as follows. First, the user determines how many HEX tokens are available in the pool as a result of previous calls to endCollateralizedHexStake which were not matched by a call to redeemHexTimeTokens. Then the user calls endCollateralizedHexStake on a subset of outstanding HSIs which are sufficient to cover the remainder of his HTT tokens. Finally the user calls redeemHexTimeTokens to burn his HTT tokens and retrieve the corresponding amount of HEX tokens from the pool.

It is essential that the calls to endCollateralizedHexStake and redeemHexTimeTokens are carried out transactionally to avoid failures preferably by calling endHexStakesAndRedeem.

The quantity of HEX that HEX stakers forgo at end stake equals the quantity of outstanding HTTs against the stake - assuming the stake is ended before the ’end stake subsidy’ kicks in on the fourth day after end stake.

Financial Invariant Verification | Status: INFO

Suppose that a user controls a particular HSI which contains x HEX tokens during the end stake phase. Prior to the end stake phase, the user has minted a total of y HTT tokens using the HSI as collateral, by calling the function mintHexTimeTokens function of the HexTimeTokenManager contract.

Each call to mintHexTimeTokens increases the collateral.amount field of the Collateral struct associated with the HSI. In particular the value of collateral.amount is increased by the amount of HTT tokens which the user wants to mint. In this manner, by the end stake phase, the collateral.amount field of this struct is equal to y.

Now suppose that the current day is greater than or equal to the maturity, but prior to the day that the end stake subsidy is in effect. Assume that an end staker calls the endCollateralizedHexStake function of the HexTimeTokenManager contract on this day. This end staker could be the original user, who wishes to redeem his HEX, or it could be a different user.

Now, the HSI contained x HEX tokens at mint time. In addition to this, the endCollateralizedHexStake is called prior to the fourth day after maturity. This is also prior to fourteen days after maturity. Hence by the proof of Invariant 1, we have that the value of the HSI is preserved and that the value of the hsiBalance variable is x and non-zero.

Therefore the function will first try to calculate the end stake subsidy by calling the private function calcEndStakeSubsidy. But because the function was called before the end stake subsidy comes into effect, the value returned by the function calcEndStakeSubsidy is 0. Therefore the end staker, which can be the original user or a different user, receives 0 in HEX.

Next, the value of the collateral.amount (which is equal to y) is subtracted from the value of the hsiBalance variable, which now has a value of x-y. By the proof of Invariant 1 we know that y <= x, because the number of HTT tokens which can be minted based on an HSI cannot exceed the number of HEX tokens in the HSI.

Therefore it follows that hsiBalance is non-zero, and that the owner of the HSI, which is the original user, receives x-y HEX tokens. Hence the user has forgone y tokens from the x tokens which were inside his HSI, which is exactly the number of HTT tokens which he has in his possession.

VULNERABILITIES & FUNCTIONAL ISSUES

This section details issues affecting the functionality of the contract. Dedaub generally categorizes issues according to the following severities, but may also take other considerations into account such as impact or difficulty in exploitation:

Issue resolution includes “dismissed” or “acknowledged” but no action taken, by the client, or “resolved”, per the auditors.

CRITICAL SEVERITY

[No critical severity issues]

HIGH SEVERITY

[No high severity issues]

MEDIUM SEVERITY

[No medium severity issues]

LOW SEVERITY

Possible unexpected behaviour of HexTimeTokenManager retireHexTimeToken

Low | Status: RESOLVED

The retireHexTimeToken function of the HexTimeTokenManager contract allows a user to burn HTT tokens in order to de-collateralize his staked HEX position. However it should be noted that this function cannot be used to de-collateralize the entire staked position, as a number of HTT tokens are taken as a tax by HexTimeToken::mint (this tax comes into effect for HTTs which have ACTR deposited into them). This is probably an undesirable behaviour as retireHexTimeToken even has cleanup code which runs when the collateral drops to zero.

CENTRALIZATION ISSUES

It is often desirable for DeFi protocols to assume no trust in a central authority, including the protocol’s owner. Even if the owner is reputable, users are more likely to engage with a protocol that guarantees no catastrophic failure even in the case the owner gets hacked/compromised. We list issues of this kind below. (These issues should be considered in the context of usage/deployment, as they are not uncommon. Several high-profile, high-value protocols have significant centralization threats.)

Unbounded ACTR Pre-Mint

Centralization | Status: RESOLVED

At the moment the owner of the MasterChef contract can mint an arbitrary amount of ACTR in the constructor. This amount is not hard-coded and neither is the recipient address.

OTHER / ADVISORY ISSUES

This section details issues that are not thought to directly affect the functionality of the project, but we recommend considering them.

Redundant Variables in HexTimeTokenManager

Advisory | Status: ACKNOWLEDGED

The contract stores HEX_ADDRESS, HSIM_ADDRESS and HEDRON_ADDRESS as constants, but then casts them to interfaces and stores them into the _hx, _hsim and _hedron contract variables respectively. The original constants are never used in the code. Storing the values directly would save on deployment cost.

Variables can be made into Constants

Advisory | Status: ACKNOWLEDGED

The variables actuatorAddress and masterChefAddress inside HexTimeTokenManager can be immutable since they are only set inside the constructor.

Error Messages

Advisory | Status: ACKNOWLEDGED

In the version of the protocol we audited all the error messages were replaced with error codes which could be looked up in scripts/constants.ts. We suggest placing the error messages inside the contract prior to deployment to aid debugging transactions and provide a better user experience.

Opaque return value in HexTimeTokenManager’s hsiDataListRange function

Advisory | Status: ACKNOWLEDGED

The HexTimeTokenManager’s hsiDataListRange function returns an array of manually encoded values. Consider returning a struct with the collateral.amount, stakeShares, lockedDay and stakedDays values instead to improve readability and decrease the chance of future encoding and decoding mistakes.

Unnecessary wrapper functions

Advisory | Status: ACKNOWLEDGED

The function calls resulting from the getFarmEmissions function of the MasterChef contract can be simplified. Right now getFarmEmissions calls _getFarmEmissions which calls _getEmissions, which finally calls _getEmissionsInTimeframe which does the actual computation. The other wrappers can be done away with.

Missing validations in MasterChef’s _getEmissionsInTimeframe function

Advisory | Status: ACKNOWLEDGED

The function __getEmissionsInTimeframe function of the MasterChef contract does not check that start <= end. This can cause the computation elapsed = effectiveEnd - effectiveStart to revert due to an underflow when start > end. On the other hand, reverting early would save gas.

MasterChef::__getEmissionsInTimeframe:172-188

function __getEmissionsInTimeframe(uint256 start, uint256 end, uint256[3] memory emissionSchedule) private pure returns (uint256) {
        uint256 mintAmount = 0;
        for (uint256 year = 0; year < emissionSchedule.length; year++) {
            uint256 yearStart = year * YEAR;

            uint256 yearEnd = (year + 1) * YEAR;

            // Check for timeframe overlap
            if (end > yearStart && start < yearEnd) {
                uint256 effectiveStart = start > yearStart ? start : yearStart;
                uint256 effectiveEnd = end < yearEnd ? end : yearEnd;
                uint256 elapsed = effectiveEnd - effectiveStart;

                mintAmount += (emissionSchedule[year] * elapsed) / YEAR;

            }
        }

        return mintAmount;
    }

Missing validation in HexTimeTokenManager’s calculateRewards function

Advisory | Status: ACKNOWLEDGED

The calculateRewards function of the HexTimeTokenManager contract should check that beginDay > PAYOUT_START_DAY, because otherwise the function will revert ungracefully when it calculates the start value due to indexing the payouts array at a negative index.

HexTimeTokenManager::calculateRewards:757-777

function calculateRewards(uint256 beginDay, uint256 endDay, uint256 stakeShares)
        public
        view
        returns (uint256)
    {
        if (beginDay >= endDay) return 0;

        uint256 start = payouts[beginDay - 1 - PAYOUT_START_DAY];

        uint256 end = payouts[endDay - 1 - PAYOUT_START_DAY];

        uint256 rewards = (end - start) * stakeShares / PAYOUT_RESOLUTION;

        // hex contract has less precision for rewards and results up to 1 heart
           of precision per day loss when calculating rewards
        // thus we assume worst case scenario and subtract the maximal possible
           precision loss from the rewards (i.e. 1 heart per day)
        uint256 precisionLoss = endDay - beginDay;

        return precisionLoss < rewards? rewards - precisionLoss: 0;

    }

Missing validations in functions over data ranges

Advisory | Status: ACKNOWLEDGED

The function hsiListRange of the HEXTimeTokenManager contract does not check whether start <= end. This causes it to revert ungracefully when it computes end - start whenever start > end, whereas reverting early would save gas.

hsiListRange::HEXTimeTokenManager:608-639

function hsiListRange(uint256 maturity, uint256 start, uint256 end)
        external
        view
        returns (address[] memory list)
    {
        address[] memory hsiList = maturityToCollateralizedHsiList[uint16(maturity)];
        end = end > hsiList.length? hsiList.length: end;
        if (end - start == 0) return list;

        list = new address[](end - start);

        uint256 dst;
        uint256 i = start;
        do {
            list[dst++] = hsiList[i];
        } while (++i < end);

        return list;
    }

The function hsiDataListRange of the HEXTimeTokenManager contract has the same issue.

hsiDataListRange::HEXTimeTokenManager:757-777

function hsiDataListRange(uint256 maturity,uint256 start,uint256 end)
        external
        view
        returns (uint256[] memory list)
    {
        address[] memory hsiList = maturityToCollateralizedHsiList[uint16(maturity)];
        end = end > hsiList.length? hsiList.length: end;
        if (end - start == 0) return list;

        list = new uint256[](end - start);

        uint256 i = start;
        uint256 dst;
        uint256 v;
        do {
            address hsiAddress = hsiList[i];
            (,, uint72 stakeShares, uint16 lockedDay, uint16 stakedDays,,) = _hx.stakeLists(hsiAddress, 0);
            Collateral memory collateral = hsiToCollateral[hsiAddress];
            v = uint256(collateral.amount) << (72 * 2);

            v |= uint256(stakeShares) << 72;
            v |= uint256(lockedDay) << 16;
            v |= uint256(stakedDays);

            list[dst++] = v;
        } while (++i < end);

        return list;
    }

The function dailyDataRange of the HEXTimeTokenManager contract does not check whether beginDay < endDay. Thus it will revert when beginDay >= endDay. For if beginDay > endDay the array allocation will fail, and if beginDay == endDay the attempt to store the first element of the array will fail.

dailyDataRange::HexTimeTokenManager:647-657

function dailyDataRange(uint256 beginDay, uint256 endDay) external view returns (uint256[] memory list) {
        list = new uint256[](endDay - beginDay);

        uint256 src = beginDay;
        uint256 dst;
        do {
            list[dst++] = payouts[src - PAYOUT_START_DAY];

        } while (++src < endDay);

        return list;
    }

Compiler bugs

Advisory | Status: INFO

The code is compiled with Solidity 0.8.24, which at the time of writing has no known issues.

DISCLAIMER

The audited contracts have been analyzed using automated techniques and extensive human inspection in accordance with state-of-the-art practices as of the date of this report. The audit makes no statements or warranties on the security of the code. On its own, it cannot be considered a sufficient assessment of the correctness of the contract. While we have conducted an analysis to the best of our ability, it is our recommendation for high-value contracts to commission several independent audits, a public bug bounty program, as well as continuous security auditing and monitoring through Dedaub Security Suite.

ABOUT DEDAUB

Dedaub offers significant security expertise combined with cutting-edge program analysis technology to secure some of the most prominent protocols in DeFi. The founders, as well as many of Dedaub’s auditors, have a strong academic research background together with a real-world hacker mentality to secure code. Protocol blockchain developers hire us for our foundational analysis tools and deep expertise in program analysis, reverse engineering, DeFi exploits, cryptography and financial mathematics.