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

Infinex A-14

Security Audit

December 5, 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 and conducted in two separate parts, with the ownership changes reviewed on October 31st 2024, and the rest on November 27, to 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 1 - - 1
Low 3 1 - 2
Code Quality 6 - 3 3
Informational 1 - - -

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.

Cosigning

The CoSigning key set in the Infinex Config Beacon acts as a trusted entity that generates specific user signatures used as a double verification along with a valid sudo key's signature. This entity can not interact with any user account unless a valid Sudo key signs the specific withdraw or transfer request.

Change summary

Recovery Module

Withdraw Module

Transfer Module

Infinex Protocol Config Beacon

Source Code

The following source code was reviewed during the audit:


Specifically, we audited the following contracts:

Source Code SHA256
./src/accounts/modules/AccountUtilsModule.sol

248198167e03918429fc72254fa229238903cc97d83695fb0cd07b7a8ad94514

./src/accounts/modules/AppModule.sol

9dcb821d7f898779474770af98f5a6bbd9de31f9a6610f70355e128fa38029e8

./src/accounts/modules/BaseModule.sol

1e42e73a9ffd4515bd49e97be7beaab1748045bf75ac874596fa7db411c7c9fb

./src/accounts/modules/BridgingModule.sol

334f4582588276809d3968ebd13d78b31c8f03f9b2ebfdd2720245cdcec124ad

./src/accounts/modules/RecoveryModule.sol

5f1143b88bd3b58d00767ed94bbfdd0f25a391913b63063b1e993982bd139481

./src/accounts/modules/TransferModule.sol

f73960c555e82392ea0faef01bafc56525b23e128cdc69308965ba5c39210beb

./src/accounts/modules/WithdrawModule.sol

999e0d7c2bbd3a6de6431c6bcd47158eb73eaf3d0b3b01aada10ce8f16c1e2ee

./src/accounts/storage/Account.sol

246704d480ac812c1d729dc68345dc415d80bc36e8572cbded606fe28324c21a

./src/accounts/storage/App.sol

b2373c34fd674b0f31d0e509c1b5765a6f662adafd9f0407ad0ed86d26fad8df

./src/accounts/storage/Bridge.sol

cc8f253b656a46d605b53e1e19664448d4560d412032913383641d7861abe188

./src/accounts/storage/Cosigning.sol

1308eb688d37bfee83e8a4d6e5e10f607f1eb315613dec5db18b71f2e2982946

./src/accounts/storage/EIP712.sol

4823614863254c55d2fc7e63b9887686cca6ac8fc14c7ac8a32aaca7e264d1ab

./src/accounts/storage/Recovery.sol

40dbef83e013a53d3e4948e486ccc18d03b21bb83a55737558a4692d11906176

./src/accounts/storage/SecurityKeys.sol

b4177aa5ae5d56cac6c05bbbfea50211b6acc2c95325c235cb18e90cdbfe30fb

./src/accounts/storage/Withdrawal.sol

4ebd51230bf0a903b7dfb8fedd840f32b73693c3a83ea09908ca45203a29546e

./src/accounts/utils/AccountConstants.sol

e6f4420118e4b841d54a2e31f65af8905c86f01068bba4270efdca2f867c0e98

./src/accounts/utils/RequestTypes.sol

47f7d9e0aef9cb95b439fdff5d01c232528650d5f62d29c86f4fcb67135f8c42

./src/accounts/utils/SecurityModifiers.sol

7c5a6dde338befc3983cf13fecf088e24347f05f431602ca7e3f08cf91c03305

./src/accounts/utils/SecurityUtils.sol

f5a3c07dde9a117ee23c5fc129e81f3a591ce934f29854e7c639202eae5e81f0

./src/accounts/utils/WithdrawUtil.sol

89ae343855cf4a6803c23866baf14281e033d62548f586a8e150ed84c474be84

./src/ownership/IOwnable.sol

291dc938d5ed96eaa67f6e0a837747e9a2b81d189e683fe8bc50b8b4a6ee3b83

./src/ownership/Ownable.sol

3ae255c26267ae1fd5e1eb1354c2616ace8f006e2dff72eab2165e9fa2603200

./src/ownership/OwnableStorage.sol

2041ccbd33c71a4206585640b797ae0c62a35c70e495ab62660e1d858d502de8

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

TransferRequest signature verification is not EIP712 compliant

Topic
Signatures
Status
Impact
Spec Breaking
Likelihood
N/A

In the TransferModule, when encoding the requestHash to verify and validate the user’s and cosign’s signature operations, the requiresCoSigning() modifier encodes the _request parameter directly with the typehash and message signature:

modifier requiresCoSigning(
    TransferRequest calldata _request,
    bytes calldata _sudoKeySignature,
    bytes calldata _platformKeySignature
) {
    if (!Cosigning._isValidNonce(_request.nonce)) revert InvalidNonce();

    **bytes32 requestHash = EIP712._hashTypedDataV4(keccak256(abi.encode(Cosigning._TRANSFER_REQUEST_TYPEHASH, msg.sig, _request)));**
    SecurityUtils._validateCoSignature(requestHash, _sudoKeySignature, _platformKeySignature);

    Cosigning._consumeNonce(_request.nonce);

    _;
}

Reference: TransferModule.sol#L48-61

However, the TransferRequest custom data type has two array parameters (amounts and tokenIds) and one dynamic type member (data), which should be handled separately as per EIP712 specification:

The dynamic values bytes and string are encoded as a keccak256 hash of their contents.

The array values are encoded as the keccak256 hash of the concatenated encodeData of their contents (i.e. the encoding of SomeType[5] is identical to that of a struct containing five members of type SomeType).

Remediations to Consider:

Consider implementing the encoding and hashing of tokenIds, amounts and data as per EIP712 specification:

keccak256(abi.encode(
    Cosigning._TRANSFER_REQUEST_TYPEHASH,
    msg.sig,
    _request.nonce,
    _request.token,
    _request.destination,
    _request.amount,
    _request.tokenId,
    keccak256(abi.encodePacked(_request.tokenIds)), // Array handling
    keccak256(abi.encodePacked(_request.amounts)),  // Array handling
    keccak256(request.data),                        // Dynamic bytes handling
    request.nonStandardIndex
));
L-1

No possible way to add data for ERC721 transferring

Topic
Composability
Status
Impact
Low
Likelihood
Low

The transferERC721() and transferERC721ABatch() functions in the TransferModule contract do not include a data parameter when calling the ERC721.safeTransferFrom() and ERC721ABatchTransferable.safeBatchTransferFrom() functions. This means the onERC721Received() hook will always receive empty data, potentially limiting hook functionality for contracts that rely on this parameter.

function _transferERC721(TransferRequest calldata _request) internal {
    ...

>>  IERC721(_request.token).safeTransferFrom(address(this), _request.destination, _request.tokenId);
}

function _transferERC721ABatch(TransferRequest calldata _request) internal {
    ...

>>  IERC721ABatchTransferable(_request.token).safeBatchTransferFrom(address(this), _request.destination, _request.tokenIds);
}

Reference: TransferModule.sol#L219-L238

Remediations to Consider:

Consider using the data field in the TransferRequest struct for ERC721 transfers, similar to how it's handled for ERC1155 transfers.

L-2

Ownable not compatible with ERC2771

Topic
Compatibility
Status
Acknowledged
Impact
Low
Likelihood
Low

Ownable.sol allows for ownership of contact that inherit, allowing functions that use the onlyOwner modifier to be gated to only be callable by the set owner. Ownership can allow be transferred via a nomination and acceptance process via nominateNewOwner() and acceptOwnership(), with the nominee able to renounce a nomination via renounceNomination(). However, each permission check uses msg.sender rather than _msgSender(). This means that contracts that utilize a ERC2771 trusted forwarder, which many infinex contracts do, will not be able to use this forwarder to make owner permissioned calls, nor nominate, renounce, or accept ownership.

Remediations to Consider

Use _msgSender() and inherit Openzeppelin’s Context.sol, overriding it in contracts that use a trusted forwarder.

Response by Infinex

Infinex does not forsee using a forwarder for owner functions

L-3

Cannot renounce ownership

Topic
Protocol Design
Status
Impact
Low
Likelihood
Low

Ownable.sol offers no way to renounce ownership of the contract. Currently the owner can only nominate a new owner, which cannot be the zero address, and is only set once the nominee accepts, meaning the contract will always have an owner. Renouncing ownership is sometimes beneficial to prevent permissioned functions from being callable, reducing or eliminating the trust assumptions of the contract.

Remediations to Consider

Add the ability for the owner to renounce their own ownership, leaving the contract without an owner.

Q-1

ERC721ABatchTransferred event does not log tokenIds

Topic
Events
Status
Wont Do
Quality Impact
Low

Consider including the tokenIds transferred in the event parameters, similar to the ERC1155BatchTransferred event.

Q-2

Avoid using zero as a control value

Topic
Best Practice
Status
Wont Do
Quality Impact
Low

When interacting with non-standard NFTs, the newly added functions recoverNonStandardNFT() and transferNonStandardToken() use a zero value in the _nonStandardIndex to transfer CryptoPunks. More specifically, in the WithdrawUtil contract:

function _transferNonStandardNFT(address _transferAddress, address _token, uint256 _tokenId, uint256 _nonStandardIndex) internal {
    emit TransferNonStandardNFTExecuted(_token, _transferAddress, _tokenId, _nonStandardIndex);
    **if (_nonStandardIndex == 0) {**
        ICryptoPunks(_token).transferPunk(_transferAddress, _tokenId);
    } else if (_nonStandardIndex == 1) {
        IEtherRocks(_token).giftRock(_tokenId, _transferAddress);
    } else {
        revert Error.UnsupportedNonStandardIndex();
    }
}

Reference: WithdrawUtil.sol#L151-160

Although the msg.sig is checked against the signed message, consider using non-zero values for each supported non-standard NFT.

Q-3

Standardize selector verification across request types

Topic
Code Consistency
Status
Quality Impact
Medium

There are currently two different variations of the msg.sig sanity checks to ensure the calldata payload matches the request:

  • requiresSudoKey() utilizes the _request._selector and reverts if not valid.
modifier requiresSudoKey(RequestTypes.Request calldata _request, bytes calldata _signature) {
    **if (_request._selector != msg.sig) {
        revert Error.InvalidRequest();
    }**
    bytes32 messageHash = EIP712._hashTypedDataV4(keccak256(abi.encode(SecurityKeys._SIGNATURE_REQUEST_TYPEHASH, _request)));
        ...
}

Reference: SecurityModifiers.sol#L34-49

  • requiresCoSigning() utilizes the msg.sig, encodes it in the requestHash and verifies the provided signature for both TransferRequest and RecoveryRequest.
modifier requiresCoSigning(
    TransferRequest calldata _request,
    bytes calldata _sudoKeySignature,
    bytes calldata _platformKeySignature
) {
    ...

    **bytes32 requestHash = EIP712._hashTypedDataV4(keccak256(abi.encode(Cosigning._TRANSFER_REQUEST_TYPEHASH, msg.sig, _request)));**

    ...
}

Reference: TransferModule.sol#L48-61

Consider adding the _selector member to all requests to keep the code consistent with the previous implementation. Additionally, the requestHash and signed data would be consistent with the typehash contents(_TRANSFER_REQUEST_TYPEHASH and _RECOVERY_REQUEST_TYPEHASH).

Q-4

Inconsistent sanity checks in TransferModule

Topic
Sanity Checks
Status
Wont Do
Quality Impact
Low

In the TransferModule contract, the transfer functions implement inconsistent checks on the request parameters, specifically on the destination and token address.

  • _transferERC20 lacks checks for both addresses.
  • _transferERC1155 and _transferERC1155Batch functions lack checks for address(0) for both addresses.

Consider implementing these checks to be consistent across all transfer functions.

Response by Infinex

The contracts are intended for use by the Infinex platform only, and checks are done in the platform

Q-5

Repetitive nonstandard token transferring code logic

Topic
Code Structure
Status
Quality Impact
Medium

The transfer() function in the NonStandardToken library duplicates the non-standard token transfer logic found in the _transferNonStandardNFT() function in the WithdrawUtil library. Currently, only two non-standard tokens (CryptoPunks and EtherRocks) are supported. When adding support for new non-standard tokens, both functions must be modified, which creates maintenance overhead and increases the risk of inconsistencies.

Consider consolidating the non-standard token transfer logic into a single function that both libraries can reuse.

Q-6

OwnedMockUpgreadable is referencing wrong UUPSImplementation

Topic
References
Status
Quality Impact
Low

OwnedMockUpgreadable.sol is still referencing Synthetix’s UUPSImplementation instead of the local version, see here.

Additionally, the file and contract name is spelled incorrectly. Consider renaming them to OwnedMockUpgradable.

I-1

Implementation and Owner storage slots not compatible with prior synthetix based versions

Topic
Compatability
Impact
Informational

The infinex protocol used Synthetix based proxies and ownership for deployed contract which utilized its own storage slots for implementation and owner. These new proxies will now be deployed using differing storage slots for these values, with implementation and owner. This means that deployed contracts using the prior slots will be incompatible with contracts using this new version. If attempting to upgrade from the synthetix version to the new infinex the call will currently fail during the simulation. However, upgrading should be handled with extra caution, if incomparable upgrades are possible in the future they could cause ownership to be removed unintentionally. Since there are now two incompatible storage versions it may require writing two variations of each contract for an update. It may be wise to instead create a method to migrate both the implementation and owner slots to the new version before or during an upgrade to allow for contracts to use the same codebase and remove the synthetix dependancy.

Response by Infinex

Storage slots were updated here to allow for compatablility with prior deployed versions

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.