Security Audit
December 5, 2024
Version 1.0.0
Presented by 0xMacro
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.
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.
Our understanding of the specification was based on the following sources:
The changes audited rely on the following assumptions specifically.
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.
Recovery Module
modifyFundsRecoveryAddress()
- allows the recovery address to be updated.Withdraw Module
recoverERC721ABatch()
- recovers multiple ERC721As to the recovery address.recoverNonStandardNFT()
- recovers non-standard NFTs to the recovery address.Transfer Module
transferEther()
- transfers Ether to a destination address with platform co-signing.transferERC20()
- transfers ERC20 tokens to a destination address with platform co-signing.transferERC721()
- transfers ERC721 tokens to a destination address with platform co-signing.transferERC721ABatch()
- transfers multiple ERC721A tokens to a destination address with platform co-signing.transferERC1155()
- transfers ERC1155 tokens to a destination address with platform co-signing.transferERC1155Batch()
- transfers multiple ERC1155 tokens to a destination address with platform co-signing.transferNonStandardToken()
- transfers non-standard tokens (e.g. Cryptopunks, Etherrocks) to a destination address with platform co-signing.Infinex Protocol Config Beacon
setPlatformKey()
- sets the platform key used for co-signing transfers.The following source code was reviewed during the audit:
9ad2835d989960b09d07fc4cb3aab874e7d65253
67829a7e4d34b877c3f2473c65489bb69f974fb9
4f3f116579b91a9b25e0304fa0cff4f46c786357
0e142681656af4854ca5eeef5421da5612230b92
Specifically, we audited the following contracts:
Source Code | SHA256 |
---|---|
./src/accounts/modules/AccountUtilsModule.sol |
|
./src/accounts/modules/AppModule.sol |
|
./src/accounts/modules/BaseModule.sol |
|
./src/accounts/modules/BridgingModule.sol |
|
./src/accounts/modules/RecoveryModule.sol |
|
./src/accounts/modules/TransferModule.sol |
|
./src/accounts/modules/WithdrawModule.sol |
|
./src/accounts/storage/Account.sol |
|
./src/accounts/storage/App.sol |
|
./src/accounts/storage/Bridge.sol |
|
./src/accounts/storage/Cosigning.sol |
|
./src/accounts/storage/EIP712.sol |
|
./src/accounts/storage/Recovery.sol |
|
./src/accounts/storage/SecurityKeys.sol |
|
./src/accounts/storage/Withdrawal.sol |
|
./src/accounts/utils/AccountConstants.sol |
|
./src/accounts/utils/RequestTypes.sol |
|
./src/accounts/utils/SecurityModifiers.sol |
|
./src/accounts/utils/SecurityUtils.sol |
|
./src/accounts/utils/WithdrawUtil.sol |
|
./src/ownership/IOwnable.sol |
|
./src/ownership/Ownable.sol |
|
./src/ownership/OwnableStorage.sol |
|
Note: Currently the referenced repository is private, but there are plans to make it public in the future.
Click on an issue to jump to it, or scroll down to see them all.
TransferRequest
signature verification is not EIP712 compliant
ERC721ABatchTransferred
event does not log tokenIds
TransferModule
OwnedMockUpgreadable
is referencing wrong UUPSImplementation
We quantify issues in three parts:
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. |
TransferRequest
signature verification is not EIP712 compliant
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
andstring
are encoded as akeccak256
hash of their contents.The array values are encoded as the
keccak256
hash of the concatenatedencodeData
of their contents (i.e. the encoding ofSomeType[5]
is identical to that of a struct containing five members of typeSomeType
).
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
));
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.
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.
Infinex does not forsee using a forwarder for owner functions
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.
ERC721ABatchTransferred
event does not log tokenIds
Consider including the tokenIds
transferred in the event parameters, similar to the ERC1155BatchTransferred
event.
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.
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
).
TransferModule
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.
The contracts are intended for use by the Infinex platform only, and checks are done in the platform
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.
OwnedMockUpgreadable
is referencing wrong UUPSImplementation
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.
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.
Storage slots were updated here to allow for compatablility with prior deployed versions
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.