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

Infinex A-5

Security Audit

August 14, 2024

Version 1.0.0

Presented by 0xMacro

Table of Contents

Introduction

This document includes the results of the security audit for Infinex's smart contract code as found in the section titled ‘Source Code’. The security audit was performed by the Macro security team from July 12 to July 15, and July 26 to July 29 2024.

The purpose of this audit is to review the source code of certain Infinex 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 3 2 - 1
Low 4 - 1 3
Code Quality 3 - 1 2

Infinex was quick to respond to these issues.

Specification

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

Trust Model, Assumptions, and Accepted Risks (TMAAR)

The changes audited rely on the following assumptions specifically.

CCQ Sync recovery address:

Account changes

Source Code

The following source code was reviewed during the audit:


Specifically, we audited the following contracts for
CCQ Sync Recovery Address within commit bf5b1fbade08e501d6604b94249318bf815e71d9:

Contract SHA256
./accounts/modules/AccountUtilsModule.sol

02641ece428a28654021472a9ee8a4575a37643d7d31cd0fe6cf09af83dbcf33

./accounts/modules/RecoveryModule.sol

9333a6bd66e8500eec684faab5c92093429c21742103cd3428fb11b974957418

./accounts/utils/SecurityModifiers.sol

7c5a6dde338befc3983cf13fecf088e24347f05f431602ca7e3f08cf91c03305

./storage/Recovery.sol

07afd734305b84623e91f09376f558127c87437fb01128e908d45f9ff2fdb3c6

./beacons/InfinexProtocolConfigBeacon.sol

aba4a55086017628b140167cb28f00b2c5b04c0ae6c0fce10b291b8f59b8125c

./libraries/Error.sol

cab4d36d6c04c97bbdb0b543c0d6ef8adc6d5184f0981adfc8b68579946e0711

./libraries/WormholeQueryResponse.sol

95a5de5cc2d7e792fef5b996ada4e2a4e9887f2f3c2405e938e9dd5bfd79cc28

Additional Account Changes within commit 950f3568622ba185aa01ae1a65f75804071a2b47:

Contract SHA256
./accounts/modules/AppModule.sol

5ebd6e96612f378ec78736c20a3d5f8b0d8da2a95165545525c42facb1fb933e

./apps/AppRegistry.sol

2f167b3c13ed7850e916a1d7659988ec2b2b64a616bcd8d78b3a4ef846c01b85

./apps/base/AppAccountBase.sol

f6955189a8d4ca58947d03d9f2fd1fbbfc977cf2f852725dcbbf62fc4ecd3e86

./apps/base/AppBeaconBase.sol

e0fb8426d02bbca775e77ec05b29d5ed891d8307ed53465a4799782df6f1bfd8

./accounts/modules/BridgingModule.sol

f59d32943deedd34c62db6106602d0a880dd65df1db58f54050ee7d621b54c99

./accounts/modules/RecoveryModule.sol

c568de5259b28b310d1073209bfac6ebfbada7078700c9ced7b4984448529959

./beacons/InfinexProtocolConfigBeacon.sol

ebe425bbe76c00762f961e0de0fa5bdf78ebb5c2b1f4604827fbcff30c1213f1

./integrations/BridgeIntegrations.sol

610764409311457584ceb24a9615098215eaa0dec6096d83791b585b4a9acdf3

./libraries/Error.sol

cab4d36d6c04c97bbdb0b543c0d6ef8adc6d5184f0981adfc8b68579946e0711

Note: Currently the referenced repository is private, but there are plans to make it public in the future.

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

App accounts can be deployed with an old beacon

Topic
Sanity Checks
Status
Acknowledged
Impact
Medium
Likelihood
N/A

In AppModule, deployAppAccounts is called to deploy a new app account. Inside the function, isValidAppBeacon is called to check if the provided appBeacon address is registered and valid. However, the function isValidAppBeacon was recently changed and doesn’t check against the latestAppBeacon anymore.

function isValidAppBeacon(address _appBeacon) external view returns (bool) {
-    return appBeacons[_appBeacon] && IAppBeaconBase(_appBeacon).getLatestAppBeacon() == _appBeacon;
+    return appBeacons[_appBeacon]
}

Reference: AppRegistry.sol#L48-L50

This allows for deploying app accounts with an outdated appBeacon as long as the appBeacon is still registered in the AppRegistry.

Remediation to Consider

Preventing app accounts deployment with an outdated appBeacon by undoing the above change.

Response by Infinex

We have moved to a posture where an appBeacon is valid only if it is in the AppRegistry. This makes it easy to reason about whether an AppBeacon is valid or not. In addition, this makes it so that we can execute all the deployment steps for adding a new AppBeacon version without breaking any of the platform functionality.

M-2

Protocol Beacon breaking changes can prevent modules from working correctly

Topic
Upgrade Process
Status
Acknowledged
Impact
Medium
Likelihood
N/A

With the latest changes on Infinex contracts, PR #169 introduces some changes in the InfinexProtocolConfigBeacon including breaking changes such as renaming getters and adding configuration variables and constants for bridging directly using CCTP and syncing the funds recovery address cross-chain, as mentioned in the PR’s description:

It has some breaking changes on the InfinexBeacon but that's fine because it'll only be used by new accounts (since we are only upgrading the beacon when we upgrade the account version), and I've confirmed with the platform that they aren't reading any of the functions that are getting changed.

It is not an issue if the new beacon is only used by new accounts. However, this is not enforced and the following misconfigurations can occur that will break functionality:

  1. An account owner (sudoKey) can upgrade to a new beacon without upgrading to a new account implementation, causing the following functions to revert:
    • BridgingModule:bridgeUSDCWithWormholeEVM() as the isSupportedWormholeChainId() was renamed to isSupportedEVMWormholeChainId().
    • BridgingModule:bridgeUSDCWithCCTPSolana() as the getSolanaCCTPDestinationDomain() is no longer in the contract.
    • RecoveryModule:recoverUSDCToEVMChain() will revert as the getMinimumUSDCBridgeAmount() is no longer in the contract, as the new account implementation uses solanaCCTPDestinationDomain public variable getter.
  2. An account owner (sudoKey) can upgrade to a new implementation without upgrading to a new beacon causing the following functions to revert in addition to the previously mentioned functions:
    • BridgingModule:bridgeUSDCWithCCTPEVM() will revert as CCTP bridging was added in the latest beacon version.
    • RecoveryModule.syncFundsRecoveryAddressWithCCQ() will revert as the public constant WORMHOLE_CORE was added in the latest beacon version.

Essentially rendering some module’s functions non-usable and forcing the account to be upgraded to a compatible version.

Remediations to Consider:

The following remediations can be made to prevent the above scenarios correspondingly:

  1. Automatically upgrade to a new beacon when upgrading the account’s implementation and,
  2. Allow upgrading to a new beacon only if the beacon is compatible with the current implementation.
Response by Infinex

Yes that is what we're currently doing. Our upgrade process involves doing these steps atomically: Upgrade to new beacon, upgrade account version, reinitialize legacy account if necessary.

M-3

App beacon lastImplementation is not immutable

Topic
Specification
Status
Impact
Medium
Likelihood
Low

With the latest App beacon release, specifically In PR165, the app address implementation of each beacon was intended to be set as an immutable parameter. This was done by removing the address setter setLatestAppImplementation().

-    /**
-    * @notice Sets the latest app implementation address.
-    * @param _latestAppImplementation The address of the latest implementation for the app.
-    */
-   function setLatestAppImplementation(address _latestAppImplementation) external onlyOwner {
-       if (_latestAppImplementation == address(0)) revert ZeroAddress();
-       emit LatestAppImplementationSet(_latestAppImplementation);
-       appBeaconConfig.latestAppImplementation = _latestAppImplementation;
-   }

This would guarantee the App implementation address’s compatibility with a given App Beacon.

However, since the appBeaconConfig is a storage variable. App Beacon contracts inherent in the AppBeaconBase contract could implement a setter and change the implementation address.

abstract contract AppBeaconBase is IAppBeaconBase, ERC165, Ownable2Step {
    AppBeaconConfig public appBeaconConfig;

Reference: AppBeaconBase.sol#L28-29

struct AppBeaconConfig {
    string appName;
    address latestAppImplementation;
    address latestAppBeacon;
}

Reference: IAppBeaconBase.sol#L19-23

Since the App deployment and predict address results rely only on the _appBeacon address input but utilize the latestAppImplementation, not making it explicitly immutable would restrain the user’s control over the deployed apps.

Remediations to Consider:

Consider declaring the latestAppImplementation as an immutable address variable.

L-1

No result length check for the ethQueryResponse result

Topic
Sanity Validation
Status
Wont Do
Impact
Medium
Likelihood
Low

The syncFundsRecoveryAddressWithCCQ() function decodes the ethQueryResponse result as an address after verifying only one result exists and ensuring the expected signature selector and contract address match the request's data, along with some other validations.

if (ethQueryResponse.result.length != 1) {
    revert Error.UnexpectedResultLength();
}
WormholeQueryResponse.validateBlockNum(ethQueryResponse.blockNum, Recovery._getLastRecoveryAddressSetBlock());
WormholeQueryResponse.validateBlockTime(ethQueryResponse.blockTime, block.timestamp - 12 hours);
if (ethQueryResponse.requestFinality.length != 9
  || keccak256(ethQueryResponse.requestFinality) != keccak256("finalized")) {
    revert Error.InvalidBlockFinality();
}
address[] memory expectedContractAddresses = new address[](1);
expectedContractAddresses[0] = address(this);
bytes4[] memory expectedFunctionSignatures = new bytes4[](1);
expectedFunctionSignatures[0] = IRecoveryModule.getFundsRecoveryAddress.selector;
WormholeQueryResponse.validateEthCallData(ethQueryResponse.result[0], expectedContractAddresses, expectedFunctionSignatures);

(address fundsRecoveryAddress) = abi.decode(ethQueryResponse.result[0].result, (address));

Reference: RecoveryModule.sol#L101-115

However, the result[0] length is not being validated. Although the current getFundsRecoveryAddress() implementation returns a unique address. If Base’s account changes the function structure and returns multiple values, the abi.decode() method will decode and parse the first returned address, potentially allowing the function returned values to be changed without reverting.

Remediations to Consider:

Consider ensuring the result[0].length == 32.

Response by Infinex

This has been done intentionally to preserve the functionality of syncing the funds recovery address even if multiple exist. Since we might enable setting multiple funds recovery addresses in the future.

L-2

Lack of calldata length validation

Topic
Sanity Validation
Status
Impact
Medium
Likelihood
Low

To validate that the proper function call data was used in the cross-chain query, the syncFundsRecoveryAddressWithCCQ() uses the WormholeQueryResponse.validateEthCallData() library method (from Wormhole SDK QueryResponse.sol contract) to check that:

  1. The expected address is the same as the Account in the destination chain since all accounts are deployed to the same address.
  2. The call data function selector matches the getFundsRecoveryAddress.selector.
address[] memory expectedContractAddresses = new address[](1);
expectedContractAddresses[0] = address(this);
bytes4[] memory expectedFunctionSignatures = new bytes4[](1);
expectedFunctionSignatures[0] = IRecoveryModule.getFundsRecoveryAddress.selector;
WormholeQueryResponse.validateEthCallData(ethQueryResponse.result[0], expectedContractAddresses, expectedFunctionSignatures);

Reference: RecoveryModule.sol#L109-113

However, the EthCallData result’s calldata length is not verified to match the payload for the getFundsRecoveryAddress call. Although unlikely, a function signature clash could cause a different function to render the same bytes4 signature.

Remediations to Consider:

Consider asserting the calldata in the EthCallData.result is only 4 bytes.

Note: This will not completely mitigate function signature collision, as the hashed function name alone can generate a signature clash. However, it will add an additional security layer to ensure transparency with the current Account code version and specification.

L-3

Setting Base block number in non-Base accounts

Topic
Cross-chain
Status
Impact
Low
Likelihood
Medium

The Recovery module data layout now includes a new storage parameter, which saves the block number on which the fundsRecoveryAddress was set.

struct Data {
    address fundsRecoveryAddress;
    uint256 lastRecoveryAddressSetBlock;
}

Reference: Recovery.sol#L8-11

This value is later used in the RecoveryModule to verify the ethQueryResponse.blockNum is higher than the set number.

WormholeQueryResponse.validateBlockNum(ethQueryResponse.blockNum, Recovery._getLastRecoveryAddressSetBlock());

Reference: RecoveryModule.sol#L104

However, this value can be set using two different entry points: one using block.number and the other using the responses ethQueryResponse.blockNum. Since the block.number parameter varies from chain to chain, and specifically, the Base’s block number is numerically lower than most of the other supported chains. Comparing this value as a minimum bound will not give any assurance on the contract’s storage state.

Remediations to Consider:

  • Using the block.number in syncFundsRecoveryAddressWithCCQ() setter function too.

    function syncFundsRecoveryAddressWithCCQ(bytes memory _response, IWormhole.Signature[] memory _signatures)
        external
        requiresTrustedRecoveryKeeper
        nonReentrant
    {	
        ...
    
        WormholeQueryResponse.validateBlockNum(block.number, Recovery._getLastRecoveryAddressSetBlock());
    
        ...
        _setFundsRecoveryAddress(fundsRecoveryAddress, block.number);
    }
    
  • Implementing checks to verify whether this value was set using the ethQueryResponse.blockNum or block.number.

L-4

predictAppAccountAddress() can return available for deprecated accounts with valid beacon

Topic
Data Consistency
Status
Impact
Low
Likelihood
N/A

App account deployment now includes an arbitrary input salt parameter which can also be used to predict a deterministic address where that app account will be deployed with create2.

function predictAppAccountAddress(address _appBeacon, bytes32 _salt) external view returns (address newAppAccount, bool isAvailable) {
    IInfinexProtocolConfigBeacon protocolConfigBeacon = Account._infinexProtocolConfig();
    IAppRegistry appRegistry = IAppRegistry(protocolConfigBeacon.appRegistry());
    if (_appBeacon == address(0)) revert Error.NullAddress();
    if (!appRegistry.isValidAppBeacon(_appBeacon)) revert Error.InvalidAppBeacon();

    address latestAppImplementation = IAppBeaconBase(_appBeacon).getLatestAppImplementation();
    newAppAccount = Create2.computeAddress(_salt, keccak256(_getProxyBytecode(latestAppImplementation)));
    isAvailable = App._getAppBeacon(newAppAccount) == address(0);
}

Reference: AppModule.sol#L63-72

However, if an account with a valid beacon was deployed and later deprecated, the predictAppAccountAddress() will incorrectly return the predicted app’s address as available if the beacon is still valid. This could mistakenly be used to call the deployAppAccount(), expecting it to deploy an account while reverting in the create2 call as this address will contain code.

Remediations to Consider:

Consider checking if the predicted address has code and returning false inisAvailable.

Q-1

Using constants instead of magic numbers

Topic
Best practice
Status
Quality Impact
Low

Consider using a constant or variable for the timestamp validation block time in syncFundsRecoveryAddressWithCCQ().

WormholeQueryResponse.validateBlockTime(ethQueryResponse.blockTime, block.timestamp - 12 hours);

Reference: RecoveryModule.sol#L105

Q-2

Unnecessary length check in requestFinality

Topic
Redundant Code
Status
Wont Do
Quality Impact
Low

In the function syncFundsRecoveryAddressWithCCQ(), we have this check to make sure ethQueryResponce.requestFinality is equal to finalized :

if (ethQueryResponse.requestFinality.length != 9
 || keccak256(ethQueryResponse.requestFinality) != keccak256("finalized")) {
    revert Error.InvalidBlockFinality();
}

Reference: RecoveryModule.sol#L106

The ethQueryResponse.requestFinality.length != 9 check is unnecessary here. Even if the caller (TrustedRecoveryKeeper) is manipulated and brute-forcing successfully with a requestFinality that its hash is still equal to keccak256("finalized"), the requestFinality is still included previously in the digest hash to verify the wormhole’s signature in WormholeQueryReponse.verifyQueryResponseSignatures().

Remediations to Consider

Consider removing this check.

-    if (ethQueryResponse.requestFinality.length != 9 || keccak256(ethQueryResponse.requestFinality) != keccak256("finalized")) {
+    if (keccak256(ethQueryResponse.requestFinality) != keccak256("finalized")) {
Response by Infinex

We want to keep the standard string comparison check even if the constraints of this function mean they're not needed.

Q-3

Inconsistent Wormhole default destination ID retrieval

Topic
Protocol Design
Status
Quality Impact
Medium

In RecoveryModule, functions syncFundsRecoveryAddressWithCCQ() and bridgeUSDCWithWormholeForRecovery() use the default destination Wormhole chain ID to verify funds recovery address sync and bridge USDC to the main account correspondingly. However, bridgeUSDCWithWormholeForRecovery() function fetches this value through the Infinex Protocol Beacon. This differs from the syncFundsRecoveryAddressWithCCQ() function, which uses the stored defaultDestinationWormholeChainId in the Bridge storage layout.

Consider using a consistent chain ID retrieval for both functions.

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