Seneca Stablecoin
Smart Contract Audit Report
Audit Summary
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.
Recommendation: The team could modify the getAccountCollateralValue() function to only call the _getUsdValue() function whenfunction 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); }amountis 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: TherevertIfHealthFactorIsBroken(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: ThesequencerUptimeFeedandFEE_PERCENTAGEstate 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_TIMEstate 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: ThePRECISION,ADDITIONAL_FEED_PRECISION, andFEED_PRECISIONstate 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
StableCoin Contract:
- As the contracts are implemented with Solidity v0.8.0, they are safe from any possible overflows/underflows.
Oracle 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.
StableEngine 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
SenecaEngine 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.
RadiantEngine 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.
- 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 |
|
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

Function 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.