Skip to main content
Actuator Finance - Sep 06, 2024

Actuator Finance

Smart Contract Security Assessment

September 06, 2024

Actuator

SUMMARY


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.

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

I1

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
info
I1
HTTs are always redeemable 1 for 1 for HEX - assuming all HTT holders invoke redemption no later than 14 days after redemption day

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.

I2

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
info
I2
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.

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:

Category
Description
CRITICAL
Can be profitably exploited by any knowledgeable third-party attacker to drain a portion of the system’s or users’ funds OR the contract does not function as intended and severe loss of funds may result.
HIGH
Third-party attackers or faulty functionality may block the system or cause the system or users to lose funds. Important system invariants can be violated.
MEDIUM
Examples:
  • User or system funds can be lost when third-party systems misbehave.
  • DoS, under specific conditions.
  • Part of the functionality becomes unusable due to a programming error.
LOW
Examples:
  • Breaking important system invariants but without apparent consequences.
  • Buggy functionality for trusted users where a workaround exists.
  • Security issues which may manifest when the system evolves.

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

L1

Possible unexpected behaviour of HexTimeTokenManager retireHexTimeToken

LOW
resolved
L1
Possible unexpected behaviour of HexTimeTokenManager retireHexTimeToken

Resolved

According to the team, retireHexTimeToken is behaving as intended. In the event a user wants to fully de-collateralize their stake, they have 2 options:

  • Buy the necessary amount of HTTs on the open market.
  • Create a new trivial HEX stake and mint the necessary amount of HTTs. Granted, this new small stake can't be de-collateralized apart from option 1. Nevertheless, this is known and expected.

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.)

    N1

    Unbounded ACTR Pre-Mint

    CENTRALIZATION
    resolved
    N1
    Unbounded ACTR Pre-Mint

    Resolved

    The team has hardcoded the pre-mint at commit: 4c079bb3a430c8d4208576209f0cc2f2c0f510c6. Users can verify this amount by checking the uploaded code using blockchain explorers.

    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.

    A1

    Redundant Variables in HexTimeTokenManager

    ADVISORY
    acknowledged
    A1
    Redundant Variables in HexTimeTokenManager

    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.

    A2

    Variables can be made into Constants

    ADVISORY
    acknowledged
    A2
    Variables can be made into Constants

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

    A3

    Error Messages

    ADVISORY
    acknowledged
    A3
    Error Messages

    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.

    A4

    Opaque return value in HexTimeTokenManager’s hsiDataListRange function

    ADVISORY
    acknowledged
    A4
    Opaque return value in HexTimeTokenManager’s hsiDataListRange function

    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.

    A5

    Unnecessary wrapper functions

    ADVISORY
    acknowledged
    A5
    Unnecessary wrapper functions

    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.

    A6

    Missing validations in MasterChef’s _getEmissionsInTimeframe function

    ADVISORY
    acknowledged
    A6
    Missing validations in MasterChef’s _getEmissionsInTimeframe function

    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;
    }

    A7

    Missing validation in HexTimeTokenManager’s calculateRewards function

    ADVISORY
    acknowledged
    A7
    Missing validation in HexTimeTokenManager’s calculateRewards function

    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;
    }

    A8

    Missing validations in functions over data ranges

    ADVISORY
    acknowledged
    A8
    Missing validations in functions over data ranges

    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;
    }

    A9

    Compiler bugs

    ADVISORY
    info
    A9
    Compiler bugs

    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.