Seneca Stablecoin

Smart Contract Audit Report

Audit Summary

Seneca Stablecoin Audit Report Seneca is building a new lending platform allowing users to borrow SenUSD after supplying collateral.

For this audit, we reviewed the project team's StableEngine, SenecaEngine, RadiantEngine, StableCoin, and Oracle contracts at 23c7d082b9e7e8f354ea26b0592d35f02cf2f7b5 on the team's private GitHub repository.

Audit Findings

Low findings were identified and the team should consider resolving these issues.
Date: July 21st, 2023.
Updated: August 14th, 2023 to include the SenecaEngine and RadiantEngine contracts and to reflect changes to the Stable Engine contract from commit d42b80f8e30e3b70f23a37f7cc3e71105bdb00df to commit 23c7d082b9e7e8f354ea26b0592d35f02cf2f7b5.

Finding #1 - StableEngine - Low

Description: Within the mintSenUSD() function, the vaultMintedAmount variable is incorrectly increased twice; once after increasing the user's s_SenUSDMinted value, and another time at the end of the function.
Risk/Impact: The value of vaultMintedAmount variable will be at least double the amount that it should be and the vault mint cap will be reached earlier than intended.
Recommendation: The team should only increase the vaultMintedAmount after increasing the user's s_SenUSDMinted value.
Resolution: The team has not yet addressed this issue.

Finding #2 - StableEngine - Low

Description: Upon burning SenUSD, the user's s_SenUSDMintedLastTimestamp is reset even though only interest has been calculated for the amount of SenUSD the user is burning.
Risk/Impact: Any interest accrued on the other portion of the SenUSD the user has minted will be eliminated since the timestamp has been reset.
Recommendation: The team should modify the mintSenUSD() function to add any accrued interest that will not be paid out yet to the user's s_SenUSDMinted balance.
Resolution: The team has not yet addressed this issue.

Finding #3 - StableEngine - Low (Acknowledged)

Description: Users are allowed to borrow as long as their resulting health factor would be above the liquidation threshold.
Risk/Impact: An inexperienced user could borrow at very marginally above the liquidation threshold. A subsequent price drop in their collateral would result in liquidation.
Recommendation: The team should consider adding a buffer to the health factor users are allowed to borrow at to prevent immediate liquidation.
Resolution: The team has elected to not resolve this issue as they acknowledge this will only affect inexperienced users.

Finding #4 - StableEngine - Informational (Resolved)

Description: The getAccountCollateralValue() function loops through each supported collateral token and always calls the _getUsdValue() function even if the user has not deposited any amount of the respective token.
function getAccountCollateralValue(address user) public view returns (uint256 totalCollateralValueInUsd) {
  for (uint256 index = 0; index < s_collateralTokens.length; index++) {
      address token = s_collateralTokens[index];
      uint256 amount = s_collateralDeposited[user][token];
      totalCollateralValueInUsd += _getUsdValue(token, amount);
  }
Recommendation: The team could modify the getAccountCollateralValue() function to only call the _getUsdValue() function when amount is not equal to zero for additional gas savings on each call.
Resolution: The team has implemented the above recommendation.

Finding #5 - StableEngine - Informational

Description: The liquidate() function verifies the caller's health factor exceeds the minimum threshold at the end of the function, even though the function's logic doesn't alter the caller's health factor.
Recommendation: The revertIfHealthFactorIsBroken(msg.sender); logic could be repositioned to the beginning of the function for additional gas savings on each call.

Finding #6 - Oracle & StableEngine - Informational (Resolved)

Description: The sequencerUptimeFeed and FEE_PERCENTAGE state variables can only be set one time in the constructor but are not declared immutable.
Recommendation: The above state variables could be declared immutable for additional gas savings on each reference.
Resolution: The team has implemented the above recommendation.

Finding #7 - Oracle - Informational (Resolved)

Description: The _PERIOD_TIME state variable can never be modified but is not declared constant.
Recommendation: The above state variable could be declared constant for additional gas savings on each reference.
Resolution: The team has implemented the above recommendation.

Finding #8 - StableEngine - Informational (Resolved)

Description: The PRECISION, ADDITIONAL_FEED_PRECISION, and FEED_PRECISION state variables are not used in the contract.
Recommendation: The team should either remove the above state variables to reduce contract size and deployment costs or utilize them in a way that fits their intended functionality.
Resolution: The team has implemented the above recommendation.

Contracts Overview

  • As the contracts are implemented with Solidity v0.8.0, they are safe from any possible overflows/underflows.
StableCoin Contract:
  • Any address that has been added as an "Engine contract" can mint any number of tokens to any address at any time.
  • Any address that has been added as an Engine contract can burn any of their tokens to reduce the total supply at any time.
  • The contract's Admin can add/remove any address from the Engine contract list at any time.
  • The contract's Admin can transfer their role to another address at any time.
  • The contract contains logic that enables users to transfer tokens to another address on a specified chain.
  • An OFT fee is charged on cross-chain transfers and is sent to the Fee owner address set by the team.
  • During transfers, the sender can specify a minimum amount of tokens that must be sent to the recipient in order for the transfer to successfully occur.
  • The owner can set the default OFT fee to any percentage at any time.
  • The owner can set the OFT fee percentage for a specified destination chain to any percentage at any time.
  • The owner can update the Fee owner address to any address at any time.
  • The owner can update the path used for cross-chain communication at any time.
  • The owner can update the remote address used for a specific chain ID at any time.
  • The owner can set the Pre-crime address to any address at any time.
  • The owner can set a minimum gas limit for a specific destination chain ID and packet type to any value greater than zero at any time.
  • The owner can set a payload size limit for a specific destination chain to any value at any time.
  • The owner can enable/disable the use of the contract's custom adapter parameters during the sending process at any time.
  • The owner can update the configuration, send version, receive version, and execute a force-resume receive transaction in the lzEndpoint contract at any time. The lzEndpoint contract was out of scope for this audit so our team is unable to provide an assessment with regard to its security.
Oracle Contract:
  • This contract is used to fetch the latest prices of assets added by the team.
  • The team will set the "sequencer uptime feed" contract address upon deployment.
  • The owner can add a new asset to the platform by specifying the asset's address, the price feed contract that will be associated with the asset, and the decimals values of both the asset and the oracle.
  • Any user can fetch the price of a specified asset using its assigned price feed contract. The price feed contracts are out of scope for this audit so our team cannot provide an assessment with regard to their security and functionality.
  • The transaction will not successfully occur if the sequencer is currently offline or if the 3-hour grace period since the start time has not yet passed. The sequencerUptimeFeed contract is out of scope for this audit so our team cannot provide an assessment with regard to its security and functionality
StableEngine Contract:
  • Any user can specify a token address and an amount of tokens to deposit into the contract as collateral.
  • The specified token address must have been added to the approved list of assets in the Oracle contract.
  • Any user can specify a number of SenUSD tokens to mint at any time.
  • The total number of minted SenUSD tokens by all users must not exceed the mint cap (set by the owner) after the transaction occurs.
  • A portion of the SenUSD is taken as a fee and minted to the Treasury address. The SenUSD taken as a fee is added to the total SenUSD amount the user has borrowed.
  • The remaining SenUSD is minted to the user.
  • The user is charged interest on the amount of SenUSD minted (including the SenUSD minted to the Treasury) over time based on the team's compound interest forumla.
  • On each mint, the compound interest is calculated and added to the SenUSD amount the user has borrowed.
  • Due to the interest charged on minted SenUSD tokens, there will not be enough collateral-backed SenUSD created in order to fully pay back the SenUSD borrowed across all users.
  • The user's deposited collateral assets to borrowed SenUSD collateral rate must not fall below the contract's minimum collateral rate after all mints, burns, redemptions, and liquidations.
  • Users may elect to deposit collateral tokens and mint SenUSD tokens in a single transaction.
  • Any user can specify a number of their SenUSD tokens to burn at any time. The tokens are initially transferred to the contract and then subsequently burned via the StableCoin contract.
  • On each burn, the amount of interest owed on the amount of SenUSD tokens that are being burned is calculated and transferred from the user to the Treasury address.
  • Any user that has initiated a deposit may specify a collateral address and a number of tokens to redeem from the contract at any time.
  • Users may elect to initiate a redemption and burn a specified number of SenUSD tokens in a single transaction.
  • Any user can initiate a liquidation process against a user whose collateral to debt ratio falls below the contract's minimum threshold at any time.
  • The caller will specify the amount of SenUSD tokens to burn to cover part of the liquidated user's debt.
  • Upon initiation of liquidation, the contract calculates the equivalent amount of collateral tokens that would cover the debt specified, and an additional bonus as an incentive for the liquidator.
  • A percentage of the liquidation bonus is deducted as a fee and transferred to the Treasury address.
  • The team must properly set the liquidation bonus percentage and the contract's fee percentage to ensure that the fee does not offset the liquidation bonus.
  • The collateral is redeemed on the user's behalf and is transferred to the liquidator. The specified SenUSD tokens are burned on behalf of the liquidator.
  • The liquidator's collateral to debt ratio must be above the minimum threshold value in order to successfully initiate a liquidation process.
  • Users may elect to flash liquidate which combines the process of depositing collateral, minting SenUSD tokens, and liquidating a user in a single transaction.
  • The team should exercise caution when adding collateral tokens as to not use a fee-on-transfer token as fee-on-transfer tokens are not supported in the platform.
  • The owner can set the Treasury address to any address at any time.
  • The owner can set the Vault Mint Cap to any value at any time.
  • The owner can set the Interest Rate to any value at any time.
SenecaEngine Contract:
  • This contract exends the StableEngine functionality.
  • The owner can use this contract to intialize the SenecaEngine.
  • The collateral token addresses, oracle address, and SenUSD token address are set on intialization.
  • The liquidation threshold, liquidation bonus, vault mint cap, fee percentage, and interest rate values are set on initialization.
  • The contract cannot be initialized twice.
RadiantEngine Contract:
  • This contract exends the StableEngine functionality.
  • The oracle address and SenUSD token address are set on intialization.
  • The liquidation threshold, liquidation bonus, vault mint cap, fee percentage, and interest rate values are set on initialization.
  • The contract cannot be initialized twice.
  • Upon depositing collateral, the user is minted shares representing their deposit amount relative to the total amount of assets deposited in the vault.
  • Upon redeeming collateral, the user's shares are burnt and an equivalent portion of assets are returned to the user.

Audit Results

Vulnerability Category Notes Result
Arbitrary Jump/Storage Write N/A PASS
Centralization of Control
  • Any address that has been added as an Engine contract can mint any number of tokens to any address at any time in the StableCoin contract.
  • The owner can set the Interest Rate to any value at any time.
  • WARNING
    Compiler Issues N/A PASS
    Delegate Call to Untrusted Contract N/A PASS
    Dependence on Predictable Variables N/A PASS
    Ether/Token Theft N/A PASS
    Flash Loans N/A PASS
    Front Running N/A PASS
    Improper Events N/A PASS
    Improper Authorization Scheme N/A PASS
    Integer Over/Underflow N/A PASS
    Logical Issues N/A PASS
    Oracle Issues N/A PASS
    Outdated Compiler Version N/A PASS
    Race Conditions N/A PASS
    Reentrancy N/A PASS
    Signature Issues N/A PASS
    Sybil Attack N/A PASS
    Unbounded Loops N/A PASS
    Unused Code N/A PASS
    Overall Contract Safety   WARNING

    Inheritance Chart

    Smart Contract Audit - Inheritance

    Function Graph

    Smart Contract Audit - Graph

    Functions Overview

    
     ($) = payable function
     # = non-constant function
     
     Int = Internal
     Ext = External
     Pub = Public
    
     +  ReentrancyGuard 
        - [Pub]  #
    
     + [Int] IERC20 
        - [Ext] totalSupply
        - [Ext] balanceOf
        - [Ext] transfer #
        - [Ext] allowance
        - [Ext] approve #
        - [Ext] transferFrom #
    
     + [Int] IERC20Metadata (IERC20)
        - [Ext] name
        - [Ext] symbol
        - [Ext] decimals
    
     +  Context 
        - [Int] _msgSender
        - [Int] _msgData
    
     +  Ownable (Context)
        - [Pub]  #
        - [Pub] owner
        - [Int] _checkOwner
        - [Pub] renounceOwnership #
           - modifiers: onlyOwner
        - [Pub] transferOwnership #
           - modifiers: onlyOwner
        - [Int] _transferOwnership #
    
     +  ERC20 (Context, IERC20, IERC20Metadata)
        - [Pub]  #
        - [Pub] name
        - [Pub] symbol
        - [Pub] decimals
        - [Pub] totalSupply
        - [Pub] balanceOf
        - [Pub] transfer #
        - [Pub] allowance
        - [Pub] approve #
        - [Pub] transferFrom #
        - [Pub] increaseAllowance #
        - [Pub] decreaseAllowance #
        - [Int] _transfer #
        - [Int] _mint #
        - [Int] _burn #
        - [Int] _approve #
        - [Int] _spendAllowance #
        - [Int] _beforeTokenTransfer #
        - [Int] _afterTokenTransfer #
    
     +  ERC20Burnable (Context, ERC20)
        - [Pub] burn #
        - [Pub] burnFrom #
    
     + [Int] ILayerZeroReceiver 
        - [Ext] lzReceive #
    
     + [Int] ILayerZeroUserApplicationConfig 
        - [Ext] setConfig #
        - [Ext] setSendVersion #
        - [Ext] setReceiveVersion #
        - [Ext] forceResumeReceive #
    
     + [Int] ILayerZeroEndpoint (ILayerZeroUserApplicationConfig)
        - [Ext] send ($)
        - [Ext] receivePayload #
        - [Ext] getInboundNonce
        - [Ext] getOutboundNonce
        - [Ext] estimateFees
        - [Ext] getChainId
        - [Ext] retryPayload #
        - [Ext] hasStoredPayload
        - [Ext] getSendLibraryAddress
        - [Ext] getReceiveLibraryAddress
        - [Ext] isSendingPayload
        - [Ext] isReceivingPayload
        - [Ext] getConfig
        - [Ext] getSendVersion
        - [Ext] getReceiveVersion
    
     + [Lib] BytesLib 
        - [Int] concat
        - [Int] concatStorage #
        - [Int] slice
        - [Int] toAddress
        - [Int] toUint8
        - [Int] toUint16
        - [Int] toUint32
        - [Int] toUint64
        - [Int] toUint96
        - [Int] toUint128
        - [Int] toUint256
        - [Int] toBytes32
        - [Int] equal
        - [Int] equalStorage
    
     +  LzApp (Ownable, ILayerZeroReceiver, ILayerZeroUserApplicationConfig)
        - [Pub]  #
        - [Pub] lzReceive #
        - [Int] _blockingLzReceive #
        - [Int] _lzSend #
        - [Int] _checkGasLimit
        - [Int] _getGasLimit
        - [Int] _checkPayloadSize
        - [Ext] getConfig
        - [Ext] setConfig #
           - modifiers: onlyOwner
        - [Ext] setSendVersion #
           - modifiers: onlyOwner
        - [Ext] setReceiveVersion #
           - modifiers: onlyOwner
        - [Ext] forceResumeReceive #
           - modifiers: onlyOwner
        - [Ext] setTrustedRemote #
           - modifiers: onlyOwner
        - [Ext] setTrustedRemoteAddress #
           - modifiers: onlyOwner
        - [Ext] getTrustedRemoteAddress
        - [Ext] setPrecrime #
           - modifiers: onlyOwner
        - [Ext] setMinDstGas #
           - modifiers: onlyOwner
        - [Ext] setPayloadSizeLimit #
           - modifiers: onlyOwner
        - [Ext] isTrustedRemote
    
     + [Lib] ExcessivelySafeCall 
        - [Int] excessivelySafeCall #
        - [Int] excessivelySafeStaticCall
        - [Int] swapSelector
    
     +  NonblockingLzApp (LzApp)
        - [Pub]  #
           - modifiers: LzApp
        - [Int] _blockingLzReceive #
        - [Int] _storeFailedMessage #
        - [Pub] nonblockingLzReceive #
        - [Int] _nonblockingLzReceive #
        - [Pub] retryMessage ($)
    
     + [Int] IERC165 
        - [Ext] supportsInterface
    
     + [Int] ICommonOFT (IERC165)
        - [Ext] estimateSendFee
        - [Ext] estimateSendAndCallFee
        - [Ext] circulatingSupply
        - [Ext] token
    
     + [Int] IOFTReceiverV2 
        - [Ext] onOFTReceived #
    
     +  OFTCoreV2 (NonblockingLzApp)
        - [Pub]  #
           - modifiers: NonblockingLzApp
        - [Pub] callOnOFTReceived #
        - [Pub] setUseCustomAdapterParams #
           - modifiers: onlyOwner
        - [Int] _estimateSendFee
        - [Int] _estimateSendAndCallFee
        - [Int] _nonblockingLzReceive #
        - [Int] _send #
        - [Int] _sendAck #
        - [Int] _sendAndCall #
        - [Int] _sendAndCallAck #
        - [Int] _isContract
        - [Int] _checkAdapterParams #
        - [Int] _ld2sd
        - [Int] _sd2ld
        - [Int] _removeDust
        - [Int] _encodeSendPayload
        - [Int] _decodeSendPayload
        - [Int] _encodeSendAndCallPayload
        - [Int] _decodeSendAndCallPayload
        - [Int] _addressToBytes32
        - [Int] _debitFrom #
        - [Int] _creditTo #
        - [Int] _transferFrom #
        - [Int] _ld2sdRate
    
     + [Int] IOFTWithFee (ICommonOFT)
        - [Ext] sendFrom ($)
        - [Ext] sendAndCall ($)
    
     +  Fee (Ownable)
        - [Pub]  #
        - [Pub] setDefaultFeeBp #
           - modifiers: onlyOwner
        - [Pub] setFeeBp #
           - modifiers: onlyOwner
        - [Pub] setFeeOwner #
           - modifiers: onlyOwner
        - [Pub] quoteOFTFee
        - [Int] _payOFTFee #
        - [Int] _transferFrom #
    
     +  ERC165 (IERC165)
        - [Pub] supportsInterface
    
     +  BaseOFTWithFee (OFTCoreV2, Fee, ERC165, IOFTWithFee)
        - [Pub]  #
           - modifiers: OFTCoreV2
        - [Pub] sendFrom ($)
        - [Pub] sendAndCall ($)
        - [Pub] supportsInterface
        - [Pub] estimateSendFee
        - [Pub] estimateSendAndCallFee
        - [Pub] circulatingSupply
        - [Pub] token
        - [Int] _transferFrom #
    
     +  OFTWithFee (BaseOFTWithFee, ERC20)
        - [Pub]  #
           - modifiers: ERC20,BaseOFTWithFee
        - [Pub] circulatingSupply
        - [Pub] token
        - [Int] _debitFrom #
        - [Int] _creditTo #
        - [Int] _transferFrom #
        - [Int] _ld2sdRate
    
     +  StableCoin (OFTWithFee)
        - [Pub]  #
           - modifiers: OFTWithFee
        - [Ext] transferAdmin #
           - modifiers: onlyAdmin
        - [Ext] allowEngineContract #
           - modifiers: onlyAdmin
        - [Ext] disallowEngineContract #
           - modifiers: onlyAdmin
        - [Pub] burn #
           - modifiers: onlyEngineContracts
        - [Ext] mint #
           - modifiers: onlyEngineContracts
    
     + [Int] AggregatorV3Interface 
        - [Ext] decimals
        - [Ext] description
        - [Ext] version
        - [Ext] getRoundData
        - [Ext] latestRoundData
    
     + [Int] AggregatorInterface 
        - [Ext] latestAnswer
        - [Ext] latestTimestamp
        - [Ext] latestRound
        - [Ext] getAnswer
        - [Ext] getTimestamp
    
     + [Int] AggregatorV2V3Interface (AggregatorInterface, AggregatorV3Interface)
    
     + [Lib] FixedPointMathLib 
        - [Int] mulWadDown
        - [Int] mulWadUp
        - [Int] divWadDown
        - [Int] divWadUp
        - [Int] mulDivDown
        - [Int] mulDivUp
        - [Int] rpow
        - [Int] sqrt
        - [Int] unsafeMod
        - [Int] unsafeDiv
        - [Int] unsafeDivUp
    
     +  Oracle (Ownable)
        - [Pub]  #
        - [Ext] addAsset #
           - modifiers: onlyOwner
        - [Pub] getOraclePrice
        - [Pub] getSpotPrice
        - [Pub] getSpotPriceInAssetDecimals
        - [Ext] getAmountPriced
        - [Ext] getAmountInAsset
    
     +  StableEngine (ReentrancyGuard, Ownable)
        - [Pub]  #
        - [Ext] setTreasury #
           - modifiers: onlyOwner
        - [Ext] changeVaultMintCap #
           - modifiers: onlyOwner
        - [Ext] flashLiquidate #
        - [Ext] depositCollateralAndMintDsc #
        - [Ext] redeemCollateralForDsc #
           - modifiers: moreThanZero
        - [Ext] redeemCollateral #
           - modifiers: moreThanZero,nonReentrant
        - [Ext] burnDsc #
           - modifiers: moreThanZero
        - [Pub] liquidate #
           - modifiers: moreThanZero,nonReentrant
        - [Pub] mintDsc #
           - modifiers: moreThanZero,nonReentrant
        - [Pub] depositCollateral #
           - modifiers: moreThanZero,nonReentrant,isAllowedToken
        - [Prv] _redeemCollateral #
        - [Prv] _burnDsc #
        - [Prv] _getAccountInformation
        - [Prv] _healthFactor
        - [Prv] _getUsdValue
        - [Int] _calculateHealthFactor
        - [Int] revertIfHealthFactorIsBroken
        - [Int] _takeCollateralFee #
        - [Ext] calculateHealthFactor
        - [Ext] getAccountInformation
        - [Ext] getUsdValue
        - [Ext] getCollateralBalanceOfUser
        - [Pub] getAccountCollateralValue
        - [Pub] getTokenAmountFromUsd
        - [Ext] getLiquidationThreshold
        - [Ext] getLiquidationBonus
        - [Ext] getCollateralTokens
        - [Ext] getDsc
        - [Ext] getHealthFactor
        - [Ext] getMinHealthFactor

    About SourceHat

    SourceHat has quickly grown to have one of the most experienced and well-equipped smart contract auditing teams in the industry. Our team has conducted 1800+ solidity smart contract audits covering all major project types and protocols, securing a total of over $50 billion U.S. dollars in on-chain value!
    Our firm is well-reputed in the community and is trusted as a top smart contract auditing company for the review of solidity code, no matter how complex. Our team of experienced solidity smart contract auditors performs audits for tokens, NFTs, crowdsales, marketplaces, gambling games, financial protocols, and more!

    Contact us today to get a free quote for a smart contract audit of your project!

    What is a SourceHat Audit?

    Typically, a smart contract audit is a comprehensive review process designed to discover logical errors, security vulnerabilities, and optimization opportunities within code. A SourceHat Audit takes this a step further by verifying economic logic to ensure the stability of smart contracts and highlighting privileged functionality to create a report that is easy to understand for developers and community members alike.

    How Do I Interpret the Findings?

    Each of our Findings will be labeled with a Severity level. We always recommend the team resolve High, Medium, and Low severity findings prior to deploying the code to the mainnet. Here is a breakdown on what each Severity level means for the project:

    • High severity indicates that the issue puts a large number of users' funds at risk and has a high probability of exploitation, or the smart contract contains serious logical issues which can prevent the code from operating as intended.
    • Medium severity issues are those which place at least some users' funds at risk and has a medium to high probability of exploitation.
    • Low severity issues have a relatively minor risk association; these issues have a low probability of occurring or may have a minimal impact.
    • Informational issues pose no immediate risk, but inform the project team of opportunities for gas optimizations and following smart contract security best practices.