Socket logo

Socket Supermodular Contracts Security Review Report

March 2024

Overview

This audit covered Socket’s new Supermodular smart contracts which makes use of the Socket plugs to facilitate bridging of tokens between blockchains. This report of this audit only covers the findings for components of the Supermodular smart contracts that are not part of YieldHook. The findings for the YieldHook component are covered in a separate report. Our security assessment was a full review of the new smart contracts, spanning a total of 2 weeks. During our audit we have identified several major severity vulnerabilities, minor severity vulnerabilities and code optimisations. Finally, all of our reported issues were fixed or acknowledged by the development team and consequently validated by us. We can confidently say that the overall security and code quality have increased after completion of our audit.

Scope

The analyzed resources are located on:

https://github.com/SocketDotTech/socket-plugs/tree/3c289f3e2278f1f9439652bb4cac69c17f909a1f

The issues described in this report were fixed in the following commit:

https://github.com/SocketDotTech/socket-plugs/tree/60ba635c7e4999f7884cc3d21e465a94d5005ad7

Summary

Total number of findings
10

Weaknesses

This section contains the list of discovered weaknesses.

SCKMA-1. INCORRECT PERMIT LOGIC IN YIELDTOKENBASE

Severity:

High

Status:

Fixed

Path:

YieldTokenBase.sol:permit:L161-207

Description:

The contract YieldToken has a permit function, allowing token approvals to be made via signatures. The signature incorporates a token amount equivalent to convertToShares(value), meaning that value is supposed to be in underlying assets, yet it effectively provides approval for the specified value, and allowance is expressed in shares.

This means that the effective approval value will always be higher than the user meant and signed for. It may lead to user fund loss, when the user plans to approve X amount, but will approve more.

Remediation:

Consider changing this line of code in the permit() function:

allowance[recoveredAddress][spender] = convertToShares(value);

SCKMA-2. INCONSISTENT ASSET CONVERSION LOGIC IN YIELDTOKEN WITHDRAWAL AND REDEMPTION FUNCTIONS

Severity:

Medium

Status:

Fixed

Path:

YieldToken.sol:maxWithdraw, maxRedeem:L68-70, L72-74

Description:

In the maxWithdraw function within the YieldToken contract, where convertToAssets is called twice on _balanceOf[owner], leading to an incorrect calculation of the maximum amount of underlying assets that can be withdrawn. Similarly, the maxRedeem function currently does not align with typical expectations for such a function in an investment or yield token contract, as it also incorrectly calculates the redeemable amount due to improper use of convertToAssets.

    function maxWithdraw(address owner) public view virtual returns (uint256) {
        return convertToAssets(convertToAssets(_balanceOf[owner]));
    }
 
    function maxRedeem(address owner) public view virtual returns (uint256) {
        return convertToAssets(_balanceOf[owner]);
    }

If the described logic is correct change the maxWithdraw and maxRedeem functions as follows:

    function maxWithdraw(address owner) public view virtual returns (uint256) { // @audit-issue
        return convertToAssets(_balanceOf[owner]);
    }
 
    function maxRedeem(address owner) public view virtual returns (uint256) {
        return _balanceOf[owner];
    }

SCKMA-15. INCORRECT USE OF ROUNDING-UP WHEN CALCULATING THE SHARESTOMINT

Severity:

Medium

Status:

Fixed

Path:

contracts/token/yield-token/YieldToken.sol:L24-L35

Description:

In the function YieldToken::calculateMintAmount(), the minted shares are determined using a round-up calculation. However, this rounding method introduces a vulnerability, allowing an attacker to mint shares with a minimal amount of tokens and subsequently steal tokens from other users.

For instance, consider the following scenario:

  • totalUnderlyingAssets = 100
  • supply = 10 If an attacker uses 1 token, they will obtain a number of shares calculated as follows:
  • calculateMintAmount() = ceil(1 * 10 / 100) = 1 Upon burning 1 share of tokens, the attacker can acquire:
  • convertToAssets() = floor(1 * (100 + 1) / (10 + 1)) = 9 By employing this strategy, the attacker gains a profit of 9 - 1 = 8 tokens.

Remediation:

Consider using the round-down calculation to calculate the minted shares.

SCKMA-18. TOKENS CAN BECOME FROZEN IN CASE OF PERSISTENCE EXECUTION FAILURE

Severity:

Medium

Status:

Acknowledged

Path:

contracts/hub/Vault.sol:L34-L59

Description:

The Vault::bridge() function facilitates the transfer of tokens from the current chain to another chain. Initially, users must transfer their tokens to the contract. Subsequently, the connector.outbound() function is triggered to relay the message to the destination chain. However, successful delivery of the message from the socket to the connector on the destination chain is not guaranteed due to factors such as gasLimit or fees: Message Failure & Retry | Socket Data Layer .

If the message delivery fails on the destination chain, there may be a need to resend the message from the source chain. The issue arises when the message cannot be delivered on the destination chain, resulting in users' tokens transferred to the vault on the source chain not being refunded. Consequently, users' funds become locked within the contract.

Remediation:

We would recommend to consider implement a rescue or cancellation mechanism for stuck transfers.

SCKMA-19 — LIMITHOOK RESTRICTIONS CAN BE BYPASSED

Severity:

Medium

Status:

Fixed

Path:

contracts/hooks/LimitHook.sol

Description:

The limit imposed by LimitHook could be circumvented. Imagine a scenario where a Controller or Vault has connectors for various chains (e.g., Optimism and Arbitrum) and utilizes the LimitHook contract to enforce daily limits.

The issue arises from the fact that identifierCache does not take into account the connector value, only incorporating the receiver and pending amount:

identifierCache = abi.encode(updatedReceiver, pendingAmount)

When a user receives tokens from Optimism and reaches the daily limit, an entry is made in the identifierCache mapping for the message id with the value abi.encode(params_.transferInfo.receiver, pendingAmount). Consequently, the user can later retry the message id once the daily limit resets for Optimism.

Instead, the user could invoke Controller.retry() with a connector for a different chain (Arbitrum) where the limit has not been reached yet, specifying the message id received from Optimism.

This action will trigger _limitDstHook() for a different connector (Arbitrum) but with the pendingMint received from Optimism:

(uint256 consumedAmount, uint256 pendingAmount) = _limitDstHook(
    params_.connector,
    pendingMint
);

Remediation:

Incorporate the connector value into identifierCache and verify within preRetryHook() whether params_.connector matches the connector decoded from the identifierCache value.

SCKMA-4. UNUSED TIMESTAMP VARIABLE IN YIELDTOKEN

Severity:

Low

Status:

Fixed

Path:

YieldTokenBase.sol:lastSyncTime:L47

Description:

The lastSyncTimestamp variable in the YieldToken contract is only set but never used for any aspect of the code.

As of right now, the variable serves no purpose and should therefore not be stored on the blockchain. If the timestamp is required for front-end purposes, the timestamp could be stored off-chain or emitted in an event and tracked off-chain.

Removing the variable would reduce gas costs by one SSTORE (5.000 gas) for each call to updateTotalUnderlyingAssets. At an ETH price of $4000 and gas price of 80 Gwei that equals and extra ~$1.60 per call.

Remediation:

Unused variables should be removed, this will reduce gas costs for storage and computation.

SCKMA-3. USE CUSTOM ERRORS

Severity:

Informational

Status:

Fixed

Path:

YieldTokenBase.sol:permit:L161-207

Description:

YieldTokenBase.permit() uses require statements instead of custom errors.

Remediation:

Replace the statements with custom errors to reduce the deployment gas and make the error-handling technique more consistent.

SCKMA-10. BAD UX NATIVE VALUE CHECK DURING VAULT BRIDGE

Severity:

Informational

Status:

Fixed

Path:

Base.sol:_afterBridge:L117-162

Description:

The function Base._afterBridge() uses an implicit approach to check whether the ETH amount received is bigger than the amount bridged: msg.value - transferInfo.amount during the fee calculation will revert with an underflow error otherwise. It is recommended that an explicit check with a custom error be added to improve the UX.

SCKMA-21 — THE ALLOWANCE FOR THE OLD HOOK SHOULD BE REVOKED WHEN THE NEW HOOK IS SET

Severity:

Informational

Status:

Fixed

Path:

contracts/hub/Base.sol:L55-L63

Description:

The function Base::updateHook() is utilized to update the new hook address for the hub. This function grants the new hook_ address an unlimited allowance of tokens. However, it does not revoke the allowance of the contract with the old hook yet. This failure to revoke can lead to a scenario where the old hook, if controlled by malicious actors or hacked, can still steal assets from hub contracts through the allowance, even if the new hook is set for the hub.

Remediation:

Consider revoking the allowance associated with the old hook in the updateHook function.

SCKMA-22. REDUNDANT USE OF MODIFIER NOTSHUTDOWN IN POST-HOOK CALL

Severity:

Informational

Status:

Fixed

Description:

In each pre-hook call function, the notShutDown modifier is used to check the shutdown status of the hub. However, the post-hook call function also contains the same modifier, making it redundant for these functions, assuming that the flow will always be that these are called in pairs from the Vault/Controller.

The only exception is srcPreHookCall in Controller_YieldLimitExecHook because it doesn't have the notShutdown modifier (srcPostHookCall does).

Remediation:

Remove the notShutDown modifier in the post-hook call function.

Table of contents