Liquid Staking

Stakelink
DeFiHardhatOracle
50,000 USDC
View results
Submission Details
Severity: medium
Invalid

test/core/lpl-migration.test.ts

Let's do a detailed security analysis of your code, highlighting potential vulnerabilities, edge cases, improvements, and architectural flaws. The goal is to ensure the system is robust against attacks and follows best practices for smart contract design.


1. Code Overview

Your test suite revolves around:

  1. Deployment and Setup

    • Deploys three contracts: ERC677 token, StakingAllowance, and LPLMigration.

    • Sets up token balances and transfers tokens to the migration contract.

  2. Token Migration Mechanism

    • Uses ERC677.transferAndCall() for token migration.

    • Calls LPLMigration.onTokenTransfer() to perform logic during token transfer.

  3. Assertions and Edge Case Testing

    • Tests for proper balances after swaps.

    • Verifies constraints, like who can call onTokenTransfer.

    • Ensures users cannot swap more tokens than they own.


2. Security Vulnerabilities & Weaknesses

2.1 Reentrancy Attack Vector

A reentrancy attack can occur if the LPLMigration contract sends tokens or Ether before updating its internal state. Since you're using the ERC677.transferAndCall function, it’s crucial to prevent reentrancy because malicious contracts can call back into the migration contract before the state changes are committed.

Risk:

  • If onTokenTransfer triggers logic in another contract, reentrancy can occur, draining funds or corrupting the state.

Mitigation:

  • Use the nonReentrant modifier to prevent multiple entry points into sensitive functions.

  • Ensure internal state changes occur before external calls. Example in Solidity:

    function onTokenTransfer(address from, uint256 amount, bytes calldata data)
    external nonReentrant returns (bool)
    {
    require(msg.sender == lplTokenAddress, "Sender must be LPL token");
    // Perform state changes before any external interaction
    // ...
    return true;
    }

2.2 Lack of Allowance Handling for transferAndCall

Your code assumes transferAndCall() will work, but this might not always be the case. Depending on the token’s implementation, allowances may still need to be set by users, or transfers could fail silently due to missing approvals.

Risk:

  • Users might call transferAndCall without sufficient allowance, resulting in failed transactions.

  • Depending on token implementation, silent failures or unexpected reverts could occur.

Mitigation:

  • Confirm that the token supports transferAndCall correctly, or wrap the token logic to ensure sufficient allowance is in place.


2.3 Token Compatibility and Decimal Precision Issues

Different ERC20 tokens may have varying decimal precision (e.g., 6, 18). Your use of toEther() and fromEther() assumes 18 decimals, but if one of the tokens uses a different decimal configuration, balance calculations will be incorrect. This can break the logic during swaps or lead to dust tokens remaining in the migration contract.

Risk:

  • Miscalculations could result in incorrect swaps or locked funds.

  • Users may receive fewer tokens than expected if decimals aren't aligned.

Mitigation:

  • Add logic to normalize token decimals for both LPL and SDL tokens:

    const normalize = (amount, decimals) => BigInt(amount) * 10n ** BigInt(18 - decimals);
  • Ensure your tests account for different decimal places and rounding errors.


2.4 Access Control Risks

The migration contract relies on the token address verification (msg.sender == lplTokenAddress) in onTokenTransfer. However, if a malicious actor can replace or spoof the token address, it could call the function fraudulently.

Risk:

  • If the address of the token is mutable or accidentally changed, unauthorized calls could occur.

  • External dependencies (like token contracts) increase the risk of unexpected behavior.

Mitigation:

  • Use immutable variables for token addresses:

    address public immutable lplTokenAddress;
    address public immutable sdlTokenAddress;
  • Ensure the token address is final after deployment to avoid tampering.


2.5 Missing Emergency Controls / Pausing Functionality

Migration contracts often handle large amounts of tokens. Without emergency controls or a pause mechanism, the system is vulnerable to misbehavior if an exploit is discovered or if the migration needs to be halted for maintenance.

Risk:

  • If an exploit is detected, there’s no way to pause operations.

  • Funds could be drained by users before an admin can act.

Mitigation:

  • Implement a pause mechanism controlled by an admin:

    bool public paused = false;
    function setPaused(bool _paused) external onlyOwner {
    paused = _paused;
    }
    modifier whenNotPaused() {
    require(!paused, "Migration is paused");
    _;
    }
  • Apply the whenNotPaused modifier to sensitive functions like onTokenTransfer.


2.6 Handling of Edge Cases in Token Transfers

Some ERC20 tokens behave differently under various conditions, such as when the account has insufficient gas or when the transfer fails internally. Silent failures can occur if you don't handle the return value of transferAndCall.

Risk:

  • Silent transfer failures might leave users in unexpected states.

  • Uncaught errors could cause token imbalances or locked funds in the migration contract.

Mitigation:

  • Always check the return value of transfers:

    require(lplToken.transferAndCall(adrs.lplMigration, amount, '0x'), "Transfer failed");

2.7 Dust Tokens Remaining in Contract

If users migrate tokens but the amounts involve fractions (e.g., dust tokens) due to decimal differences or rounding errors, small amounts could be left locked in the migration contract, making it harder to reconcile the balances.

Risk:

  • Users might receive slightly fewer tokens due to rounding errors.

  • Dust tokens could accumulate in the migration contract, resulting in lost value.

Mitigation:

  • Implement a sweep function that allows the owner to reclaim dust tokens after migration:

    function sweep(address token) external onlyOwner {
    uint256 balance = IERC20(token).balanceOf(address(this));
    IERC20(token).transfer(owner(), balance);
    }

2.8 Insufficient Testing for Complex Cases

Your tests cover basic migration scenarios, but they may not account for:

  • Extreme scenarios like large migrations that exceed gas limits.

  • Failures during halfway migrations (e.g., if only some tokens are swapped and others fail).

  • Multiple users attempting to migrate simultaneously (concurrent transactions).

Mitigation:

  • Add tests for concurrent migrations and edge cases like large inputs, dust tokens, and partial swaps.

  • Simulate gas-heavy scenarios to ensure the contract functions correctly under stress.


3. Suggested Improvements

  1. Modularize the Fixture Setup:
    Break deployFixture into smaller pieces for easier reuse and to reduce the chance of silent setup errors.

  2. Use Named Constants:
    Replace magic numbers (e.g., 50,000,000 tokens) with named constants for better clarity and maintainability.

  3. Consider Using Hardhat’s Assertions Utilities:
    Hardhat provides assertion utilities beyond chai. This can improve error handling and readability.


4. Final Thoughts

The migration system in your code is generally well-structured but requires security improvements to protect against reentrancy, token handling issues, and access control risks. Adding emergency controls and improving token precision handling will also make the contract more resilient.

Key Takeaways:

  • Add reentrancy guards where necessary.

  • Use immutable addresses for token contracts to prevent tampering.

  • Implement pause functionality for emergencies.

  • Improve edge case handling with more comprehensive tests.

Updates

Lead Judging Commences

inallhonesty Lead Judge about 1 year ago
Submission Judgement Published
Invalidated
Reason: Lack of quality

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.