Skip to content

Instantly share code, notes, and snippets.

@ChaseTheLight01
Created April 18, 2025 16:10
Show Gist options
  • Select an option

  • Save ChaseTheLight01/21c1d7e191b13063f6e534f659077c52 to your computer and use it in GitHub Desktop.

Select an option

Save ChaseTheLight01/21c1d7e191b13063f6e534f659077c52 to your computer and use it in GitHub Desktop.
LightChaserV3_Cantina_Mighty

LightChaser-V3

Generated for: Cantina : Mighty

Generated on: 2025-04-18

Total findings: 195

Total HIGH findings: 5

Total Medium findings: 9

Total Low findings: 64

Total Gas findings: 56

Total NonCritical findings: 61

Summary for High findings

Number Details Instances
[High-1] No refunds for swaps 1
[High-2] Scaling calculation do not divide/multiply by 1e<decimals()> when they should [PROTOTYPE] 1
[High-3] Uniswap Slot0 value used in accounting arithmetic instead of TWAP value 1
[High-4] Anyone can become owner/admin by calling initializer function which lacks proper access control 1
[High-5] Arbitrary staking/deposit on a arbitrary token with an arbitrary amount has no checks to ensure token amount isn't type(uint256).max thus allowing wrong stake amount for certain tokens with custom logic such as cUSDCv3 2

Summary for Medium findings

Number Details Instances
[Medium-1] Permanent DoS due to non-shrinking array usage in an unbounded loop 8
[Medium-2] Privileged functions can create points of failure 32
[Medium-3] Public or external initialize functions should be protected with the initializer modifier 1
[Medium-4] Contract can accept funds with no way of extracting them 2
[Medium-5] Using block.timestamp as the deadline/expiry invites MEV 4
[Medium-6] Withdraw/Redeem functions can fail due to blocked USDT/USDC accounts 2
[Medium-7] Function uses gasLeft() or gas() as a parameter of an external call without first checking if gasLeft() is sufficient when considering th 63/64 rule (EIP-150) 1
[Medium-8] Deposit mechanism can be DOS'd with dust deposits due to lack of minAmount checks against deposit amount 1
[Medium-9] Transfer flow from msg.sender to protocol favours msg sender by its amount value rounding down when it should round up to prevent exploitation 1

Summary for Low findings

Number Details Instances
[Low-1] Potential division by zero should have zero checks in place 2
[Low-2] Division operations should always be performed after multiplication operations 4
[Low-3] Storage Write Removal Bug On Conditional Early Termination 1
[Low-4] Missing checks for address(0x0) when updating address state variables 7
[Low-5] Code does not follow the best practice of check-effects-interaction 16
[Low-6] Solidity version 0.8.23 won't work on all chains due to MCOPY 2
[Low-7] Function call without checking or using it's return values 5
[Low-8] Some tokens may revert when zero value transfers are made 9
[Low-9] deposit/redeem functions found which implement accounting arithmetic vulnerable to the donation attack 3
[Low-10] Require should be used instead of assert as assert in some solidity versions does not refund remaining gas 1
[Low-11] The call abi.encodeWithSignature is not safe from typographical errors 1
[Low-12] Upgradable contracts should have a __gap variable 5
[Low-13] The nonReentrant modifier should be first in a function declaration 13
[Low-14] No limits when setting min/max amounts 3
[Low-15] Initializer function can be front run 3
[Low-16] Use _disableInitializers() to ensure initialization occurs once 2
[Low-17] The function decimals() is not part of the ERC20 standard 3
[Low-18] Minting to the zero address should be avoided 1
[Low-19] No limits when setting fees 3
[Low-20] Loss of precision 2
[Low-21] Missing zero address check in constructor 4
[Low-22] Use of onlyOwner functions can be lost 5
[Low-23] Off-by-one timestamp error 1
[Low-24] approve()/safeApprove() may revert if the current approval is not zero 17
[Low-25] Constant decimal values 3
[Low-26] Arrays can grow in size without a way to shrink them 1
[Low-27] Sending tokens in a for loop 4
[Low-28] Large approvals such as type(uint256).max may not work with some tokens 6
[Low-29] Revert on Transfer to the Zero Address 4
[Low-30] Return values not checked for approve() 18
[Low-31] The function symbol() is not part of the ERC20 standard 1
[Low-32] Events may be emitted out of order due to code not follow the best practice of check-effects-interaction 7
[Low-33] Missing zero address check in initializer 2
[Low-34] Critical functions should have a timelock 18
[Low-35] Unbounded loop may run out of gas 7
[Low-36] Mapping arrays can grow in size without a way to shrink them 1
[Low-37] Consider implementing two-step procedure for updating protocol addresses 6
[Low-38] The function name() is not part of the ERC20 standard 1
[Low-39] Avoid mutating function parameters 4
[Low-40] approve will always revert as the IERC20 interface mismatch 11
[Low-41] transfer will always revert as the IERC20 interface mismatch 1
[Low-42] No license selected for project 1
[Low-43] Constructors missing validation 3
[Low-44] Contract contains payable functions but no withdraw/sweep function 3
[Low-45] State variables not capped at reasonable values 8
[Low-46] The WETH address should not be assignable post deployment 1
[Low-47] Use forceApprove in place of approve 17
[Low-48] Lack of support for rebasing tokens 3
[Low-49] Constructor doesn't set initial owner 3
[Low-50] Functions calling contracts/addresses with transfer hooks are missing reentrancy guards 2
[Low-51] Solidity version 0.8.20 won't work on all chains due to PUSH0 2
[Low-52] Missing events in functions that are either setters, privileged or voting related 32
[Low-53] Unsafe use of transfer()/transferFrom() with IERC20 4
[Low-54] Avoid floating pragma in non interface/library files 15
[Low-55] Upgradeable contract uses SafeERC20 rather than SafeERC20Upgradeable thus isn't upgrade safe as SafeERC20 utilizes Address.sol which uses delegatecall 1
[Low-56] Read only reentrancy risk detected 15
[Low-57] Common tokens such as WETH9 work differently on chains such a Blast which isn't taken into account during transfer calls. 2
[Low-58] Pyth Oracle getPriceUnsafe used instead of getPrice or getPriceNoOlderThan. Thus introducing stale price risks 1
[Low-59] Function transfer's amount value differs from amount approved by user in same flow 3
[Low-60] deposit/redeem functions found which implement accounting arithmetic vulnerable to the donation attack 5
[Low-61] return used in yul assembly block which results in post code from not being executed as this halts the performance of logic even outside of the yul block 1
[Low-62] Contract's initialization function can fail due to out of GAS error on certain chains due to iteration which can prevent accurate intended deployment 1
[Low-63] Transfers which take place within iteration do not compare the amounts[i] value against zero which can result in the transfer chain reverting 2
[Low-64] Consider a uptime feed on L2 deployments to prevent issues caused by downtime 15

Summary for NonCritical findings

Number Details Instances
[NonCritical-1] Subtraction may underflow if multiplication is too large 1
[NonCritical-2] Local variable shadowing 10
[NonCritical-3] Greater than comparisons made on state uints that can be set to max 6
[NonCritical-4] Floating pragma should be avoided 2
[NonCritical-5] Interfaces should be declared in a separate file 2
[NonCritical-6] Require statements should have error string 4
[NonCritical-7] In functions which accept an address as a parameter, there should be a zero address check to prevent bugs 39
[NonCritical-8] Default bool values are manually set 1
[NonCritical-9] Default int values are manually set 6
[NonCritical-10] Ownable2Step should be used in place of Ownable 4
[NonCritical-11] Revert statements within external and public functions can be used to perform DOS attacks 2
[NonCritical-12] Reverts should use custom errors instead of strings 2
[NonCritical-13] Functions which are either private or internal should have a preceding _ in their name 11
[NonCritical-14] Private and internal state variables should have a preceding _ in their name unless they are constants 1
[NonCritical-15] Contract lines should not be longer than 120 characters for readability 2
[NonCritical-16] Avoid updating storage when the value hasn't changed 17
[NonCritical-17] Specific imports should be used where possible so only used code is imported 32
[NonCritical-18] Old Solidity version 2
[NonCritical-19] Not all event definitions are utilizing indexed variables. 1
[NonCritical-20] Explicitly define visibility of state variables to prevent misconceptions on what can access the variable 1
[NonCritical-21] Contracts should have all public/external functions exposed by interfaces 111
[NonCritical-22] Functions within contracts are not ordered according to the solidity style guide 7
[NonCritical-23] Double type casts create complexity within the code 1
[NonCritical-24] Emits without msg.sender parameter 4
[NonCritical-25] A function which defines named returns in it's declaration doesn't need to use return 9
[NonCritical-26] Keccak hashes which never change can be made into a constant state variable 1
[NonCritical-27] Constants should be on the left side of the comparison 35
[NonCritical-28] Defined named returns not used within function 9
[NonCritical-29] Initialize functions do not emit an event 4
[NonCritical-30] Both immutable and constant state variables should be CONSTANT_CASE 4
[NonCritical-31] Overly complicated arithmetic 5
[NonCritical-32] Using zero as a parameter 3
[NonCritical-33] Use of non-named numeric constants 9
[NonCritical-34] Long powers of ten should use scientific notation 1eX 8
[NonCritical-35] Event emit should emit a parameter 2
[NonCritical-36] Two or more functions contain the exact same code 2
[NonCritical-37] Empty bytes check is missing 1
[NonCritical-38] Use OpenZeppelin's ReentrancyGuard/ReentrancyGuardUpgradeable rather than writing your own 1
[NonCritical-39] Cyclomatic complexity in functions 8
[NonCritical-40] Incorrect withdraw declaration 1
[NonCritical-41] Contract does not follow the Solidity style guide's suggested layout ordering 3
[NonCritical-42] Don't only depend on Solidity's arithmetic ordering. 5
[NonCritical-43] Contracts with only unimplemented functions can be labeled as abstract 6
[NonCritical-44] A event should be emitted if a non immutable state variable is set in a constructor 4
[NonCritical-45] No limit when setting fees in initializer 1
[NonCritical-46] Non constant/immutable state variables are missing a setter post deployment 1
[NonCritical-47] Contracts use both += 1 and ++ (-- and -= 1) 1
[NonCritical-48] Long numbers should include underscores to improve readability and prevent typos 10
[NonCritical-49] Inconsistent checks of address params against address(0) 2
[NonCritical-50] Avoid declaring variables with the names of defined functions within the project 1
[NonCritical-51] Constructors should emit an event 7
[NonCritical-52] Contract and Abstract files should have a fixed compiler version 17
[NonCritical-53] Function call in event emit 1
[NonCritical-54] For loop iterates on arrays not indexed 4
[NonCritical-55] Constructor with array/string/bytes parameters has no empty array checks 2
[NonCritical-56] Avoid external calls in modifiers 2
[NonCritical-57] Avoid using 'owner' or '_owner' as a parameter name 4
[NonCritical-58] Unnecessary struct attribute prefix 2
[NonCritical-59] Redundant function returns constant 2
[NonCritical-60] ERC777 tokens can introduce reentrancy risks 1
[NonCritical-61] Avoid assembly in view or pure functions 2

Summary for Gas findings

Number Details Instances Gas
[Gas-1] The usage of SafeMath is useless in solidity versions 0.8.0 and above hence wasting gas 2 0.0
[Gas-2] Mappings are cheaper than arrays for state variable iteration 4 16000
[Gas-3] State variables used within a function more than once should be cached to save gas 2 400
[Gas-4] Avoid updating storage when the value hasn't changed 2 3200
[Gas-5] Using constants directly, rather than caching the value, saves gas 1 0.0
[Gas-6] Shortcircuit rules can be be used to optimize some gas usage 1 2100
[Gas-7] x + y is more efficient than using += for state variables (likewise for -=) 4 80
[Gas-8] There is a 32 byte length threshold for error strings, strings longer than this consume more gas 3 126
[Gas-9] Public functions not used internally can be marked as external to save gas 19 0.0
[Gas-10] Calldata should be used in place of memory function parameters when not mutated 1 13
[Gas-11] Usage of smaller uint/int types causes overhead 38 79420
[Gas-12] Use != 0 instead of > 0 33 3267
[Gas-13] Default bool values are manually reset 2 0.0
[Gas-14] Default int values are manually reset 1 0.0
[Gas-15] Function calls within for loops 8 0.0
[Gas-16] For loops in public or external functions should be avoided due to high gas costs and possible DOS 2 0.0
[Gas-17] Use assembly to check for the zero address 13 0.0
[Gas-18] Some error strings are not descriptive 16 0.0
[Gas-19] Can transfer 0 1 0.0
[Gas-20] State variables which are not modified within functions should be set as constants or immutable for values set at deployment 1 0.0
[Gas-21] Structs can be packed into fewer storage slots 2 10000
[Gas-22] Don't use _msgSender() if not supporting EIP-2771 17 4624
[Gas-23] Consider using OZ EnumerateSet in place of nested mappings 3 9000
[Gas-24] Use selfBalance() in place of address(this).balance 3 7200
[Gas-25] Use assembly to emit events 35 46550
[Gas-26] Use solady library where possible to save gas 8 64000
[Gas-27] Identical Deployments Should be Replaced with Clone 1 0.0
[Gas-28] Mark Functions That Revert For Normal Users As payable 32 25600
[Gas-29] Lack of unchecked in loops 16 17280
[Gas-30] Assembly let var only used on once 1 0.0
[Gas-31] Use assembly to validate msg.sender 9 0.0
[Gas-32] Simple checks for zero uint can be done using assembly to save gas 3 54
[Gas-33] Using nested if to save gas 17 1734
[Gas-34] Using delete instead of setting mapping to 0 saves gas 1 5
[Gas-35] Stack variable cost less than state variables while used in emiting event 9 729
[Gas-36] Stack variable cost less than mappings while used in emiting event 1 9
[Gas-37] Using constants directly, rather than caching the value, saves gas 1 0.0
[Gas-38] Inline modifiers used only once 1 0.0
[Gas-39] Solidity versions 0.8.19 and above are more gas efficient 1 1000
[Gas-40] Calling .length in a for loop wastes gas 18 31428
[Gas-41] Internal functions only used once can be inlined to save gas 4 480
[Gas-42] Constructors can be marked as payable to save deployment gas 8 0.0
[Gas-43] Assigning to structs can be more efficient 5 3250
[Gas-44] Internal functions never used once can be removed 2 0.0
[Gas-45] Empty functions should be removed to save gas 1 0.0
[Gas-46] Only emit event in setter function if the state variable was changed 5 0.0
[Gas-47] Use OZ Array.unsafeAccess() to avoid repeated array length checks 41 3530100
[Gas-48] State variable written in a loop 2 23176
[Gas-49] State variable read in a loop 4 139056
[Gas-50] Use uint256(1)/uint256(2) instead of true/false to save gas for changes 7 833000
[Gas-51] Avoid emitting events in loops 4 6000
[Gas-52] Consider pre-calculating the address of address(this) to save gas 50 0.0
[Gas-53] Use 'storage' instead of 'memory' for struct/array state variables 2 8400
[Gas-54] Empty blocks should be removed or emit something 1 0.0
[Gas-55] Use constants instead of type(uint).max 8 0.0
[Gas-56] Using named returns for pure and view functions is cheaper than using regular returns 42 45864

[High-1] No refunds for swaps

Resolution

When using Uniswaps exactOutput method, there must be a check if there was any remaining tokens after the swap followed by a refund if so. Not doing so can lead users to lose their funds. This is detailed in official uniswap documentation: https://docs.uniswap.org/contracts/v3/guides/swaps/single-swaps

Num of instances: 1

Findings

Click to show findings

['454']

454:     function _swapTokenExactOutput(address tokenIn, address tokenOut, uint256 amountOut, uint256 amountInMaximum)
455:         internal
456:         returns (uint256 amountIn)
457:     {
458:         address router =
459:             IAddressRegistry(IVault(vault).addressProvider()).getAddress(AddressId.ADDRESS_ID_SHADOW_ROUTER);
460:         IERC20(tokenIn).approve(router, amountInMaximum);
461: 
462:         amountIn = IShadowSwapRouter(router).exactOutputSingle(
463:             IShadowSwapRouter.ExactOutputSingleParams({
464:                 tokenIn: tokenIn,
465:                 tokenOut: tokenOut,
466:                 tickSpacing: tickSpacing,
467:                 recipient: address(this),
468:                 deadline: block.timestamp,
469:                 amountOut: amountOut,
470:                 amountInMaximum: amountInMaximum,
471:                 sqrtPriceLimitX96: 0
472:             })
473:         );
474:     }

[High-2] Scaling calculation do not divide/multiply by 1e<decimals()> when they should [PROTOTYPE]

Resolution

Scaling calculations that fail to divide/multiply by 1e<decimals()> introduce significant inaccuracies, particularly when handling token values in pricing, rewards, or conversions. Many ERC-20 tokens use varying decimal formats (e.g., 6 for USDC, 18 for ETH), and omitting proper scaling can result in inflated or deflated values. For example, when calculating price, failing to divide by 1e<decimals()> means raw token values are treated as if they have uniform decimals, leading to incorrect outputs.

This issue can cause overestimations or underestimations, misaligning expected and actual results, and potentially introducing financial losses or inconsistencies across the protocol. It becomes particularly problematic in multi-token environments or cross-chain operations where tokens with differing decimal standards interact.

To address this, always normalize values by dividing by 1e<decimals()> or multiplying by 10^<requiredDecimals> to ensure accurate scaling. Additionally, dynamically fetch decimals() during runtime for tokens to prevent hardcoding and maintain compatibility with various standards. This ensures precise calculations and protects the protocol's integrity.

Num of instances: 1

Findings

Click to show findings

['13']

13: contract StakingRewards is Ownable, IStakingRewards { // <= FOUND
14:     using SafeMath for uint256;
15:     using SafeERC20 for IERC20;
16: 
17:     IERC20 public immutable stakedToken;
18:     address public immutable lendingPool;
19: 
20:     address[] public rewardTokens;
21: 
22:     mapping(address => bool) public inRewardsTokenList;
23: 
24:     uint256 public totalStaked;
25: 
26:     mapping(address => Reward) public rewardData;
27:     mapping(address => uint256) public balanceOf;
28: 
29:     mapping(address => mapping(address => uint256)) public userRewardPerTokenPaid;
30:     mapping(address => mapping(address => uint256)) public userRewardsClaimable;
31: 
32:     uint256 internal _unlocked = 1;
33: 
34:     modifier nonReentrant() {
35:         require(_unlocked == 1, "reentrant call");
36:         _unlocked = 2;
37:         _;
38:         _unlocked = 1;
39:     }
40: 
41:     modifier onlyLendingPool() {
42:         require(lendingPool == msg.sender);
43:         _;
44:     }
45: 
46:     modifier updateReward(address user) {
47:         for (uint256 i; i < rewardTokens.length; i++) {
48:             address rewardToken = rewardTokens[i];
49:             rewardData[rewardToken].rewardPerTokenStored = rewardPerToken(rewardToken);
50:             rewardData[rewardToken].lastUpdateTime = Math.min(rewardData[rewardToken].endTime, block.timestamp);
51: 
52:             if (user != address(0)) {
53:                 userRewardsClaimable[user][rewardToken] =
54:                     earned(user, rewardToken, rewardData[rewardToken].rewardPerTokenStored);
55:                 userRewardPerTokenPaid[user][rewardToken] = rewardData[rewardToken].rewardPerTokenStored;
56:             }
57:         }
58:         _;
59:     }
60: 
62:     constructor(address _stakingToken) {
63:         stakedToken = IERC20(_stakingToken);
64:         lendingPool = msg.sender;
65:     }
66: 
67:     function rewardPerToken(address rewardToken) public view returns (uint256) {
68:         if (block.timestamp <= rewardData[rewardToken].startTime) {
69:             
70:             return rewardData[rewardToken].rewardPerTokenStored;
71:         }
72: 
73:         uint256 dt =
74:             Math.min(rewardData[rewardToken].endTime, block.timestamp) - (rewardData[rewardToken].lastUpdateTime);
75: 
76:         if (dt == 0 || totalStaked == 0) {
77:             return rewardData[rewardToken].rewardPerTokenStored;
78:         }
79: 
80:         return rewardData[rewardToken].rewardPerTokenStored
81:             + (rewardData[rewardToken].rewardRate * dt * 1e18) / totalStaked;
82:     }
83: 
84:     function earned(address user, address rewardToken) public view returns (uint256) {
85:         uint256 curRewardPerToken = rewardPerToken(rewardToken);
86: 
87:         return earned(user, rewardToken, curRewardPerToken);
88:     }
89: 
90:     function earned(address user, address rewardToken, uint256 curRewardPerToken) internal view returns (uint256) {
91:         uint256 d = curRewardPerToken - userRewardPerTokenPaid[user][rewardToken];
92: 
93:         return (balanceOf[user] * d) / 1e18 + userRewardsClaimable[user][rewardToken];
94:     }
95: 
96:     function setReward(address rewardToken, uint256 startTime, uint256 endTime, uint256 totalRewards)
97:         public
98:         onlyOwner
99:         nonReentrant
100:         updateReward(address(0))
101:     {
102:         require(startTime < endTime, "start must lt end");
103:         require(rewardData[rewardToken].endTime < block.timestamp, "not end");
104: 
105:         if (!inRewardsTokenList[rewardToken]) {
106:             rewardTokens.push(rewardToken);
107:             inRewardsTokenList[rewardToken] = true;
108:         }
109: 
110:         rewardData[rewardToken].startTime = startTime;
111:         rewardData[rewardToken].endTime = endTime;
112:         rewardData[rewardToken].lastUpdateTime = block.timestamp;
113:         rewardData[rewardToken].rewardRate = totalRewards / (endTime - startTime);
114: 
115:         if (block.timestamp > startTime && totalStaked > 0) {
116:             uint256 dt = block.timestamp - startTime;
117: 
118:             rewardData[rewardToken].rewardPerTokenStored +=
119:                 (rewardData[rewardToken].rewardRate * dt * 1e18) / totalStaked;
120:         }
121: 
122:         if (block.timestamp > startTime && totalStaked == 0) {
123:             
124:             
125:             uint256 dt = block.timestamp - startTime;
126:             totalRewards -= rewardData[rewardToken].rewardRate * dt; // <= FOUND
127:         }
128: 
129:         IERC20(rewardToken).safeTransferFrom(msg.sender, address(this), totalRewards);
130: 
131:         emit RewardsSet(rewardToken, startTime, endTime, totalRewards);
132:     }
133: 
141:     function stake(uint256 amount, address onBehalfOf) external nonReentrant updateReward(onBehalfOf) {
142:         require(amount > 0, "amount = 0");
143: 
144:         stakedToken.safeTransferFrom(msg.sender, address(this), amount);
145: 
146:         balanceOf[onBehalfOf] += amount;
147:         totalStaked += amount;
148: 
149:         emit Staked(msg.sender, onBehalfOf, amount);
150:     }
151: 
159:     function withdraw(uint256 amount, address to) external nonReentrant updateReward(msg.sender) {
160:         require(amount > 0, "amount = 0");
161: 
162:         balanceOf[msg.sender] -= amount;
163:         totalStaked -= amount;
164: 
165:         require(stakedToken.transfer(to, amount), "transfer failed");
166: 
167:         emit Withdraw(msg.sender, to, amount);
168:     }
169: 
179:     function withdrawByLendingPool(uint256 amount, address user, address to)
180:         external
181:         onlyLendingPool
182:         nonReentrant
183:         updateReward(user)
184:     {
185:         require(amount > 0, "amount = 0");
186: 
187:         balanceOf[user] -= amount;
188:         totalStaked -= amount;
189: 
190:         require(stakedToken.transfer(to, amount), "transfer falied");
191: 
192:         emit Withdraw(user, to, amount);
193:     }
194: 
195:     function claim() external nonReentrant updateReward(msg.sender) {
196:         for (uint256 i = 0; i < rewardTokens.length; i++) {
197:             address rewardToken = rewardTokens[i];
198:             uint256 claimable = userRewardsClaimable[msg.sender][rewardToken];
199:             if (claimable > 0) {
200:                 userRewardsClaimable[msg.sender][rewardToken] = 0;
201:                 require(IERC20(rewardToken).transfer(msg.sender, claimable), "transfer failed");
202:                 emit RewardPaid(msg.sender, rewardToken, claimable);
203:             }
204:         }
205:     }
206: 
207:     function update() external updateReward(address(0)) onlyOwner {}
208: 
209:     function rewardsTokenListLength() external view returns (uint256) {
210:         return rewardTokens.length;
211:     }
212: }

[High-3] Uniswap Slot0 value used in accounting arithmetic instead of TWAP value

Resolution

Relying on Uniswap’s slot0 value for accounting arithmetic instead of using the Time-Weighted Average Price (TWAP) can introduce significant risks and inaccuracies. The slot0 value represents the current price of the token pair in the pool, but it is highly volatile and can be easily manipulated, especially in short timeframes or during low liquidity periods. This volatility makes slot0 unsuitable for critical accounting operations, such as determining fair value, calculating collateralization, or settling trades, where stability and accuracy are paramount.

In contrast, TWAP provides a more reliable and robust pricing mechanism by averaging prices over a specified time window. This averaging process smooths out short-term fluctuations and reduces the impact of market manipulation. TWAP is particularly valuable in DeFi protocols where price accuracy and resistance to manipulation are crucial for maintaining security and fairness.

Using slot0 instead of TWAP in accounting can lead to skewed financial results, opening the door to potential exploits, such as flash loan attacks or other forms of market manipulation. To mitigate these risks and ensure the integrity of financial calculations, it's essential to use TWAP values, which provide a more stable and trustworthy price reference for sensitive operations. This approach safeguards the protocol against volatility-induced errors and malicious activities.

Num of instances: 1

Findings

Click to show findings

['18']

18:     function principal(IShadowNonfungiblePositionManager positionManager, uint256 tokenId, address pool) // <= FOUND
19:         public
20:         view
21:         returns (uint256 amount0, uint256 amount1)
22:     {
23:         IShadowV3Pool v3Pool = IShadowV3Pool(pool);
24:         (uint160 sqrtPriceX96,,,,,,) = v3Pool.slot0(); // <= FOUND
25: 
26:         (,,, int24 tickLower, int24 tickUpper, uint128 liquidity,,,,) = positionManager.positions(tokenId);
27: 
28:         return LiquidityAmounts.getAmountsForLiquidity(
29:             sqrtPriceX96, TickMath.getSqrtRatioAtTick(tickLower), TickMath.getSqrtRatioAtTick(tickUpper), liquidity // <= FOUND
30:         );
31:     }

[High-4] Anyone can become owner/admin by calling initializer function which lacks proper access control

Resolution

An initialization function that sets the owner variable without access control allows any user to call it first and claim ownership, leading to a complete contract takeover. This is a critical vulnerability, as the attacker could modify critical settings, drain funds, or lock out legitimate users.

To prevent this, ensure the function is callable only once using a modifier like initializer (for upgradeable contracts) or restrict access to a predefined deployer. Alternatively, immediately assign the owner during deployment to eliminate reliance on an open initialization function, securing contract integrity and preventing unauthorized ownership.

Num of instances: 1

Findings

Click to show findings

['41']

41:     function initialize(uint256 _positionId) external {
42:         require(vault == address(0), "Already initialized");
43:         xShadow = 0x5050bc082FF4A74Fb6B0B04385dEfdDB114b2424;
44:         x33 = 0x3333111A391cC08fa51353E9195526A70b333333;
45:         vault = msg.sender; // <= FOUND
46:         positionId = _positionId;
47: 
48:         token0 = IVault(msg.sender).token0();
49:         token1 = IVault(msg.sender).token1();
50: 
51:         IERC20(token0).approve(vault, type(uint256).max);
52:         IERC20(token1).approve(vault, type(uint256).max);
53: 
54:         shadowV3Pool = IShadowRangeVault(vault).shadowV3Pool();
55:         tickSpacing = IShadowV3Pool(shadowV3Pool).tickSpacing();
56: 
57:         _initializePayments(IPayments(msg.sender).WETH9());
58: 
59:         addressProvider = IVault(msg.sender).addressProvider();
60:     }

[High-5] Arbitrary staking/deposit on a arbitrary token with an arbitrary amount has no checks to ensure token amount isn't type(uint256).max thus allowing wrong stake amount for certain tokens with custom logic such as cUSDCv3

Resolution

Certain tokens such as cUSDCv3 have transfer logic where when a transfer takes place their transfer functionality checks if the 'amount' to be transferred is type(uint256).max, if this is the case the balance of the sender is transferred. So if a user has a dust amount cUSDCv3 attempts to stake/deposit amount 'type(uint256).max' in this protocol, the actual transfer amount will be procesed as the user's total balance of that token, not type(uint256).max. Thus the staking function will register/queue the stake as type(uint256).max but in actuality only cUSDCv3.balanceOf(msg.sender) will have been transferred. This can cause serious discrepancies with the protocols intended logic. To remedy this, there can be a check to prevent users from passing type(uint256).max as the stake/deposit amount.

Num of instances: 2

Findings

Click to show findings

['141']

141:     function stake(uint256 amount, address onBehalfOf) external nonReentrant updateReward(onBehalfOf) { // <= FOUND
142:         require(amount > 0, "amount = 0");
143: 
144:         stakedToken.safeTransferFrom(msg.sender, address(this), amount);
145: 
146:         balanceOf[onBehalfOf] += amount;
147:         totalStaked += amount;
148: 
149:         emit Staked(msg.sender, onBehalfOf, amount);
150:     }

['96']

96:     function setReward(address rewardToken, uint256 startTime, uint256 endTime, uint256 totalRewards) // <= FOUND
97:         public
98:         onlyOwner
99:         nonReentrant
100:         updateReward(address(0))
101:     {
102:         require(startTime < endTime, "start must lt end");
103:         require(rewardData[rewardToken].endTime < block.timestamp, "not end");
104: 
105:         if (!inRewardsTokenList[rewardToken]) {
106:             rewardTokens.push(rewardToken);
107:             inRewardsTokenList[rewardToken] = true;
108:         }
109: 
110:         rewardData[rewardToken].startTime = startTime;
111:         rewardData[rewardToken].endTime = endTime;
112:         rewardData[rewardToken].lastUpdateTime = block.timestamp;
113:         rewardData[rewardToken].rewardRate = totalRewards / (endTime - startTime);
114: 
115:         if (block.timestamp > startTime && totalStaked > 0) {
116:             uint256 dt = block.timestamp - startTime;
117: 
118:             rewardData[rewardToken].rewardPerTokenStored +=
119:                 (rewardData[rewardToken].rewardRate * dt * 1e18) / totalStaked;
120:         }
121: 
122:         if (block.timestamp > startTime && totalStaked == 0) {
123:             
124:             
125:             uint256 dt = block.timestamp - startTime;
126:             totalRewards -= rewardData[rewardToken].rewardRate * dt;
127:         }
128: 
129:         IERC20(rewardToken).safeTransferFrom(msg.sender, address(this), totalRewards);
130: 
131:         emit RewardsSet(rewardToken, startTime, endTime, totalRewards);
132:     }

[Medium-1] Permanent DoS due to non-shrinking array usage in an unbounded loop

Resolution

Using non-shrinking state variable arrays in unbounded loops within smart contracts can lead to a permanent Denial of Service (DoS) vulnerability. When a loop iterates over a continually growing state variable array without any bound, the gas cost for each iteration increases. Eventually, the loop may require more gas than the block gas limit allows, making the function and possibly the entire contract unusable. To prevent this, it's essential to design contracts with mechanisms that either limit the size of these arrays or process data in smaller, manageable chunks. This approach ensures that the contract functions remain gas-efficient and operable, safeguarding against unmanageable growth in storage and execution costs.

Num of instances: 8

Findings

Click to show findings

['47']

47:        for (uint256 i; i < rewardTokens.length; i++) { // <= FOUND
48:             address rewardToken = rewardTokens[i];
49:             rewardData[rewardToken].rewardPerTokenStored = rewardPerToken(rewardToken);
50:             rewardData[rewardToken].lastUpdateTime = Math.min(rewardData[rewardToken].endTime, block.timestamp);
51: 
52:             if (user != address(0)) {
53:                 userRewardsClaimable[user][rewardToken] =
54:                     earned(user, rewardToken, rewardData[rewardToken].rewardPerTokenStored);
55:                 userRewardPerTokenPaid[user][rewardToken] = rewardData[rewardToken].rewardPerTokenStored;
56:             }
57:         }

['196']

196:        for (uint256 i = 0; i < rewardTokens.length; i++) { // <= FOUND
197:             address rewardToken = rewardTokens[i];
198:             uint256 claimable = userRewardsClaimable[msg.sender][rewardToken];
199:             if (claimable > 0) {
200:                 userRewardsClaimable[msg.sender][rewardToken] = 0;
201:                 require(IERC20(rewardToken).transfer(msg.sender, claimable), "transfer failed");
202:                 emit RewardPaid(msg.sender, rewardToken, claimable);
203:             }
204:         }

['47']

47:         for (uint256 i; i < rewardTokens.length; i++) { // <= FOUND
48:             address rewardToken = rewardTokens[i];
49:             rewardData[rewardToken].rewardPerTokenStored = rewardPerToken(rewardToken);
50:             rewardData[rewardToken].lastUpdateTime = Math.min(rewardData[rewardToken].endTime, block.timestamp);
51: 
52:             if (user != address(0)) {
53:                 userRewardsClaimable[user][rewardToken] =
54:                     earned(user, rewardToken, rewardData[rewardToken].rewardPerTokenStored);
55:                 userRewardPerTokenPaid[user][rewardToken] = rewardData[rewardToken].rewardPerTokenStored;
56:             }
57:         }

['196']

196:         for (uint256 i = 0; i < rewardTokens.length; i++) { // <= FOUND
197:             address rewardToken = rewardTokens[i];
198:             uint256 claimable = userRewardsClaimable[msg.sender][rewardToken];
199:             if (claimable > 0) {
200:                 userRewardsClaimable[msg.sender][rewardToken] = 0;
201:                 require(IERC20(rewardToken).transfer(msg.sender, claimable), "transfer failed");
202:                 emit RewardPaid(msg.sender, rewardToken, claimable);
203:             }
204:         }

['47']

47:        for (uint256 i; i < rewardTokens.length; i++) {
48:             address rewardToken = rewardTokens[i]; // <= FOUND
49:             rewardData[rewardToken].rewardPerTokenStored = rewardPerToken(rewardToken);
50:             rewardData[rewardToken].lastUpdateTime = Math.min(rewardData[rewardToken].endTime, block.timestamp);
51: 
52:             if (user != address(0)) {
53:                 userRewardsClaimable[user][rewardToken] =
54:                     earned(user, rewardToken, rewardData[rewardToken].rewardPerTokenStored);
55:                 userRewardPerTokenPaid[user][rewardToken] = rewardData[rewardToken].rewardPerTokenStored;
56:             }
57:         }

['196']

196:        for (uint256 i = 0; i < rewardTokens.length; i++) {
197:             address rewardToken = rewardTokens[i]; // <= FOUND
198:             uint256 claimable = userRewardsClaimable[msg.sender][rewardToken];
199:             if (claimable > 0) {
200:                 userRewardsClaimable[msg.sender][rewardToken] = 0;
201:                 require(IERC20(rewardToken).transfer(msg.sender, claimable), "transfer failed");
202:                 emit RewardPaid(msg.sender, rewardToken, claimable);
203:             }
204:         }

['47']

47:         for (uint256 i; i < rewardTokens.length; i++) {
48:             address rewardToken = rewardTokens[i]; // <= FOUND
49:             rewardData[rewardToken].rewardPerTokenStored = rewardPerToken(rewardToken);
50:             rewardData[rewardToken].lastUpdateTime = Math.min(rewardData[rewardToken].endTime, block.timestamp);
51: 
52:             if (user != address(0)) {
53:                 userRewardsClaimable[user][rewardToken] =
54:                     earned(user, rewardToken, rewardData[rewardToken].rewardPerTokenStored);
55:                 userRewardPerTokenPaid[user][rewardToken] = rewardData[rewardToken].rewardPerTokenStored;
56:             }
57:         }

['196']

196:         for (uint256 i = 0; i < rewardTokens.length; i++) {
197:             address rewardToken = rewardTokens[i]; // <= FOUND
198:             uint256 claimable = userRewardsClaimable[msg.sender][rewardToken];
199:             if (claimable > 0) {
200:                 userRewardsClaimable[msg.sender][rewardToken] = 0;
201:                 require(IERC20(rewardToken).transfer(msg.sender, claimable), "transfer failed");
202:                 emit RewardPaid(msg.sender, rewardToken, claimable);
203:             }
204:         }

[Medium-2] Privileged functions can create points of failure

Resolution

Ensure such accounts are protected and consider implementing multi sig to prevent a single point of failure

Num of instances: 32

Findings

Click to show findings

['14']

14:     function setAddress(uint256 id, address _addr) public onlyOwner  // <= FOUND

['49']

49:     function changeOwner(address newOwner) external onlyOwner  // <= FOUND

['69']

69:     function initReserve(address asset) external onlyOwner notPaused  // <= FOUND

['535']

535:     function emergencyPauseAll() external onlyOwner  // <= FOUND

['540']

540:     function unPauseAll() external onlyOwner  // <= FOUND

['545']

545:     function enableVaultToBorrow(uint256 vaultId) external onlyOwner notPaused  // <= FOUND

['552']

552:     function disableVaultToBorrow(uint256 vaultId) external onlyOwner notPaused  // <= FOUND

['559']

559:     function setCreditsOfVault(uint256 vaultId, uint256 reserveId, uint256 credit) external onlyOwner notPaused  // <= FOUND

['565']

565:     function activateReserve(uint256 reserveId) public onlyOwner notPaused  // <= FOUND

['572']

572:     function deActivateReserve(uint256 reserveId) public onlyOwner notPaused  // <= FOUND

['578']

578:     function freezeReserve(uint256 reserveId) public onlyOwner notPaused  // <= FOUND

['584']

584:     function unFreezeReserve(uint256 reserveId) public onlyOwner notPaused  // <= FOUND

['590']

590:     function enableBorrowing(uint256 reserveId) public onlyOwner notPaused  // <= FOUND

['596']

596:     function disableBorrowing(uint256 reserveId) public onlyOwner notPaused  // <= FOUND

['602']

602:     function setReserveFeeRate(uint256 reserveId, uint16 _rate) public onlyOwner notPaused  // <= FOUND

['610']

610:     function setBorrowingRateConfig(
611:         uint256 reserveId,
612:         uint16 utilizationA,
613:         uint16 borrowingRateA,
614:         uint16 utilizationB,
615:         uint16 borrowingRateB,
616:         uint16 maxBorrowingRate
617:     ) public onlyOwner notPaused  // <= FOUND

['626']

626:     function setReserveCapacity(uint256 reserveId, uint256 cap) public onlyOwner notPaused  // <= FOUND

['96']

96:     function setReward(address rewardToken, uint256 startTime, uint256 endTime, uint256 totalRewards)
97:         public
98:         onlyOwner // <= FOUND
99:         nonReentrant
100:         updateReward(address(0))
101:     

['207']

207:     function update() external updateReward(address(0)) onlyOwner  // <= FOUND

['110']

110:     function setLiquidator(address _liquidator, bool _isLiquidator) external onlyOwner  // <= FOUND

['114']

114:     function setLiquidationFeeParams(uint256 _liquidationFee, uint256 _liquidationCallerFeeRatio) external onlyOwner  // <= FOUND

['119']

119:     function setOpenLiquidationEnabled(bool _openLiquidationEnabled) external onlyOwner  // <= FOUND

['123']

123:     function setPositionImplementation(address _positionImplementation) external onlyOwner  // <= FOUND

['127']

127:     function setLiquidationDebtRatio(uint256 _liquidationDebtRatio) external onlyOwner  // <= FOUND

['131']

131:     function setReserveIds(uint256 _token0ReserveId, uint256 _token1ReserveId) external onlyOwner  // <= FOUND

['140']

140:     function setMinPositionSize(uint256 _minPositionSize) external onlyOwner  // <= FOUND

['144']

144:     function setPerformanceFee(uint256 _performanceFee) external onlyOwner  // <= FOUND

['148']

148:     function setShadowGauge(address _shadowGauge) external onlyOwner  // <= FOUND

['29']

29:     function newVault(address _newVault) external onlyOwner returns (uint256 vaultId)  // <= FOUND

['39']

39:     function setOracleManager(address _oracleManager) external onlyOracleManager  // <= FOUND

['44']

44:     function setPythPriceId(address token, bytes32 priceId) external onlyOracleManager  // <= FOUND

['48']

48:     function setMaxPriceAge(uint256 _maxPriceAge) external onlyOracleManager  // <= FOUND

[Medium-3] Public or external initialize functions should be protected with the initializer modifier

Resolution

The initializer modifiers ensures two key aspects. A) Only an authorised account can initialize B) initialization can only be done once. Consider using such a modifier and in instances where initialization can be done more than once, the function name should be changed to reflect that.

Num of instances: 1

Findings

Click to show findings

['41']

41:     function initialize(uint256 _positionId) external { // <= FOUND
42:         require(vault == address(0), "Already initialized");
43:         xShadow = 0x5050bc082FF4A74Fb6B0B04385dEfdDB114b2424;
44:         x33 = 0x3333111A391cC08fa51353E9195526A70b333333;
45:         vault = msg.sender;
46:         positionId = _positionId;
47: 
48:         token0 = IVault(msg.sender).token0();
49:         token1 = IVault(msg.sender).token1();
50: 
51:         IERC20(token0).approve(vault, type(uint256).max);
52:         IERC20(token1).approve(vault, type(uint256).max);
53: 
54:         shadowV3Pool = IShadowRangeVault(vault).shadowV3Pool();
55:         tickSpacing = IShadowV3Pool(shadowV3Pool).tickSpacing();
56: 
57:         _initializePayments(IPayments(msg.sender).WETH9());
58: 
59:         addressProvider = IVault(msg.sender).addressProvider();
60:     }

[Medium-4] Contract can accept funds with no way of extracting them

Resolution

Contracts with payable functions can accumulate Ether over time. If there's no method to withdraw these funds, they can remain trapped in the contract indefinitely, resulting in lost resources and a potential financial loss. To resolve this, a secure withdraw function should be implemented. It's recommended to limit access to this function, typically to the contract's owner or a specific set of trusted addresses. Also, to prevent re-entrancy attacks, the Checks-Effects-Interactions (CEI) pattern should be followed where state changes (effect) occur before external calls (interactions).

Num of instances: 2

Findings

Click to show findings

['9']

9: contract IndividualPosition 

['30']

30: contract ShadowRangeVault is
31:     Initializable,
32:     ReentrancyGuardUpgradeable,
33:     Ownable2StepUpgradeable,
34:     PaymentsUpgradeable,
35:     IVault
36: 

[Medium-5] Using block.timestamp as the deadline/expiry invites MEV

Resolution

Using block.timestamp for operation deadlines can be exploited by malicious entities in the Miner Extractable Value (MEV) landscape. Instead of ensuring immediacy, this approach lets miners choose when to execute the transaction within the block, potentially manipulating outcomes for their gain. For instance, they might delay the transaction until market conditions shift, triggering maximum allowable slippage or even causing other significant events like liquidations. To mitigate this risk, deadlines should be determined off-chain and explicitly set by the transaction initiator, ensuring a more predictable and secure execution environment and reducing potential MEV opportunities.

Num of instances: 4

Findings

Click to show findings

['83']

83:     function openPosition(OpenPositionParams memory params)
84:         external
85:         onlyVault
86:         returns (uint256 tokenId, uint128 liquidity, uint256 amount0, uint256 amount1)
87:     {
88:         uint256 token0Balance = IERC20(token0).balanceOf(address(this));
89:         if (params.amount0Desired > token0Balance) {
90:             _swapTokenExactInput(
91:                 token1,
92:                 token0,
93:                 IERC20(token1).balanceOf(address(this)) - params.amount1Desired,
94:                 params.amount0Desired - token0Balance
95:             );
96:         }
97: 
98:         uint256 token1Balance = IERC20(token1).balanceOf(address(this));
99:         if (params.amount1Desired > token1Balance) {
100:             _swapTokenExactInput(
101:                 token0,
102:                 token1,
103:                 IERC20(token0).balanceOf(address(this)) - params.amount0Desired,
104:                 params.amount1Desired - token1Balance
105:             );
106:         }
107:         address shadowNonfungiblePositionManager = getShadowNonfungiblePositionManager();
108: 
109:         IERC20(token0).approve(shadowNonfungiblePositionManager, params.amount0Desired);
110:         IERC20(token1).approve(shadowNonfungiblePositionManager, params.amount1Desired);
111: 
112:         (tokenId, liquidity, amount0, amount1) = IShadowNonfungiblePositionManager(shadowNonfungiblePositionManager)
113:             .mint(
114:             IShadowNonfungiblePositionManager.MintParams({
115:                 token0: token0,
116:                 token1: token1,
117:                 tickSpacing: tickSpacing,
118:                 tickLower: params.tickLower,
119:                 tickUpper: params.tickUpper,
120:                 amount0Desired: params.amount0Desired,
121:                 amount1Desired: params.amount1Desired,
122:                 amount0Min: 0,
123:                 amount1Min: 0,
124:                 recipient: address(this),
125:                 deadline: block.timestamp // <= FOUND
126:             })
127:         );
128: 
129:         shadowPositionId = tokenId;
130:         positionOwner = params.positionOwner;
131: 
132:         unwrapWETH9(0, positionOwner);
133: 
134:         token0Balance = IERC20(token0).balanceOf(address(this));
135:         token1Balance = IERC20(token1).balanceOf(address(this));
136: 
137:         if (token0Balance > 0) {
138:             pay(token0, address(this), positionOwner, token0Balance);
139:         }
140:         if (token1Balance > 0) {
141:             pay(token1, address(this), positionOwner, token1Balance);
142:         }
143:     }

['408']

408:     function _decreasePosition(uint128 liquidity) internal returns (uint256 amount0, uint256 amount1) {
409:         IShadowNonfungiblePositionManager(getShadowNonfungiblePositionManager()).decreaseLiquidity(
410:             IShadowNonfungiblePositionManager.DecreaseLiquidityParams({
411:                 tokenId: shadowPositionId,
412:                 liquidity: liquidity,
413:                 amount0Min: 0,
414:                 amount1Min: 0,
415:                 deadline: block.timestamp // <= FOUND
416:             })
417:         );
418: 
419:         IShadowNonfungiblePositionManager(getShadowNonfungiblePositionManager()).collect(
420:             IShadowNonfungiblePositionManager.CollectParams({
421:                 tokenId: shadowPositionId,
422:                 recipient: address(this),
423:                 amount0Max: type(uint128).max,
424:                 amount1Max: type(uint128).max
425:             })
426:         );
427: 
428:         amount0 = IERC20(token0).balanceOf(address(this));
429:         amount1 = IERC20(token1).balanceOf(address(this));
430:     }

['432']

432:     function _swapTokenExactInput(address tokenIn, address tokenOut, uint256 amountIn, uint256 amountOutMinimum)
433:         internal
434:         returns (uint256 amountOut)
435:     {
436:         address router =
437:             IAddressRegistry(IVault(vault).addressProvider()).getAddress(AddressId.ADDRESS_ID_SHADOW_ROUTER);
438:         IERC20(tokenIn).approve(router, amountIn);
439: 
440:         amountOut = IShadowSwapRouter(router).exactInputSingle(
441:             IShadowSwapRouter.ExactInputSingleParams({
442:                 tokenIn: tokenIn,
443:                 tokenOut: tokenOut,
444:                 tickSpacing: tickSpacing,
445:                 recipient: address(this),
446:                 deadline: block.timestamp, // <= FOUND
447:                 amountIn: amountIn,
448:                 amountOutMinimum: amountOutMinimum,
449:                 sqrtPriceLimitX96: 0
450:             })
451:         );
452:     }

['454']

454:     function _swapTokenExactOutput(address tokenIn, address tokenOut, uint256 amountOut, uint256 amountInMaximum)
455:         internal
456:         returns (uint256 amountIn)
457:     {
458:         address router =
459:             IAddressRegistry(IVault(vault).addressProvider()).getAddress(AddressId.ADDRESS_ID_SHADOW_ROUTER);
460:         IERC20(tokenIn).approve(router, amountInMaximum);
461: 
462:         amountIn = IShadowSwapRouter(router).exactOutputSingle(
463:             IShadowSwapRouter.ExactOutputSingleParams({
464:                 tokenIn: tokenIn,
465:                 tokenOut: tokenOut,
466:                 tickSpacing: tickSpacing,
467:                 recipient: address(this),
468:                 deadline: block.timestamp, // <= FOUND
469:                 amountOut: amountOut,
470:                 amountInMaximum: amountInMaximum,
471:                 sqrtPriceLimitX96: 0
472:             })
473:         );
474:     }

[Medium-6] Withdraw/Redeem functions can fail due to blocked USDT/USDC accounts

Resolution

Withdraw and redeem functions in smart contracts can fail if they involve transferring USDT, USDC, or other centralized stablecoins from blocked or blacklisted accounts. Both Tether (USDT) and USD Coin (USDC) are issued by centralized entities with the authority to freeze or blacklist addresses. When an account is blacklisted, any attempts to transfer these tokens from the blacklisted address will be blocked, causing the transaction to fail.

This issue can significantly impact decentralized finance (DeFi) protocols, which rely on seamless token transfers for operations like withdrawals and redemptions. If a user's address or the contract itself gets blacklisted, users may be unable to withdraw their funds, leading to a loss of trust and potentially severe financial consequences.

A potential way to fix this issue is to allow users to specify an alternative address for the transfer to go to. However, the contract must still validate that the withdrawal amount is correctly debited from the msg.sender's balance to prevent any unauthorized transfers. This ensures that users cannot steal other users' assets by specifying different addresses.

Num of instances: 2

Findings

Click to show findings

['195']

195:     function claim() external nonReentrant updateReward(msg.sender) {
196:         for (uint256 i = 0; i < rewardTokens.length; i++) {
197:             address rewardToken = rewardTokens[i];
198:             uint256 claimable = userRewardsClaimable[msg.sender][rewardToken];
199:             if (claimable > 0) {
200:                 userRewardsClaimable[msg.sender][rewardToken] = 0;
201:                 require(IERC20(rewardToken).transfer(msg.sender, claimable), "transfer failed"); // <= FOUND
202:                 emit RewardPaid(msg.sender, rewardToken, claimable);
203:             }
204:         }
205:     }

['381']

381:     function claimCallback(address reward, uint256 amount) external {
382:         uint256 positionId = positionAddressToId[msg.sender];
383:         require(positionId != 0, "Not a position");
384:         uint256 feeAmount = amount * performanceFee / 10000;
385:         pay(reward, msg.sender, positionInfos[positionId].owner, amount - feeAmount);
386:         pay(reward, msg.sender, performanceFeeRecipient(), feeAmount);
387: 
388:         emit RewardClaimed(positionInfos[positionId].owner, vaultId, positionId, reward, amount - feeAmount, feeAmount);
389:     }

[Medium-7] Function uses gasLeft() or gas() as a parameter of an external call without first checking if gasLeft() is sufficient when considering th 63/64 rule (EIP-150)

Resolution

When a function relies on the gasLeft parameter without first verifying that it accounts for the 63/64 rule introduced in EIP-150, it risks running out of gas unexpectedly. The 63/64 rule limits the amount of gas forwarded in a call to 63/64 of the caller's remaining gas, leaving the caller with a small gas buffer to handle operations after the call. If the gasLeft parameter is used without considering this reduction, it may lead to calls failing due to insufficient gas, even when the initial calculation suggests adequacy.

This issue is particularly problematic in contracts that rely on dynamic gas usage, external calls, or nested operations. Malicious actors can exploit this oversight to cause transaction failures or disrupt contract functionality. To mitigate this, always validate gasLeft to ensure it exceeds the minimum threshold required for execution, factoring in the 63/64 forwarding rule. This ensures reliable and predictable function behavior.

Num of instances: 1

Findings

Click to show findings

['55']

55:     function _delegate() internal { // <= FOUND
56:         address impl = IPositionImplGetter(_owner()).positionImplementation();
57:         require(impl != address(0));
58:         assembly {
59:             
60:             
61:             
62:             calldatacopy(0, 0, calldatasize()) // <= FOUND
63: 
64:             
65:             
66:             let result := delegatecall(gas(), impl, 0, calldatasize(), 0, 0)
67: 
68:             
69:             returndatacopy(0, 0, returndatasize())
70: 
71:             switch result
72:             
73:             case 0 { revert(0, returndatasize()) }
74:             default { return(0, returndatasize()) }
75:         }
76:     }

[Medium-8] Deposit mechanism can be DOS'd with dust deposits due to lack of minAmount checks against deposit amount

Resolution

A contract's deposit mechanism can be vulnerable to a Denial-of-Service (DoS) attack if malicious users spam it with "dust deposits" (very small amounts of tokens or ETH). This attack exploits the fact that deposit mechanisms often update internal accounting, emit events, or execute additional logic for every deposit. By making numerous small deposits, a malicious actor can congest the contract, cause storage bloat, or inflate gas costs, making it prohibitively expensive or inefficient for legitimate users to interact with the contract. For example, if a contract tracks user balances in a mapping, each tiny deposit might unnecessarily create or update entries. This attack could flood on-chain logs with events, increasing monitoring difficulties for off-chain systems. It could also fragment balances, leading to operational inefficiencies and rounding issues during future withdrawals. To prevent this, introducing a minimum deposit threshold is common, such as: require(amount >= MIN_DEPOSIT, "Deposit amount too small"); However, this safeguard can unintentionally prevent legitimate deposits if a user’s intended deposit is below the threshold. For instance, smaller investors or users testing the contract with small amounts may find themselves excluded. To address this, implement reasonable thresholds while considering user accessibility. Additionally, periodic aggregation or batching mechanisms can mitigate spam without deterring legitimate participation, balancing security with inclusivity.

Num of instances: 1

Findings

Click to show findings

['104']

104:     function deposit(uint256 reserveId, uint256 amount, address onBehalfOf, uint16 referralCode) // <= FOUND
105:         public
106:         payable
107:         notPaused
108:         nonReentrant
109:         returns (uint256 eTokenAmount)
110:     {
111:         eTokenAmount = _deposit(reserveId, amount, onBehalfOf);
112: 
113:         
114:         if (msg.value > 0) {
115:             refundETH();
116:         }
117: 
118:         
119:         emit Deposited(reserveId, _msgSender(), onBehalfOf, amount, eTokenAmount, referralCode);
120:     }

[Medium-9] Transfer flow from msg.sender to protocol favours msg sender by its amount value rounding down when it should round up to prevent exploitation

Resolution

Transfer logic that calculates the amount deducted from msg.sender using rounding down (e.g. via integer division) can introduce unintended advantages for the sender. This is particularly problematic in fee or reward calculations where precision is critical. Rounding down favors the sender by potentially underpaying or avoiding marginal amounts, which can accumulate over many transactions and be exploited. Instead, the calculation should round up to ensure the full value owed is captured. Using safe mathematical utilities like mulDivUp or explicitly checking for remainders before rounding can help mitigate this. Failure to address this can lead to economic imbalances or protocol loss over time.

Num of instances: 1

Findings

Click to show findings

['381']

381:     function claimCallback(address reward, uint256 amount) external {
382:         uint256 positionId = positionAddressToId[msg.sender];
383:         require(positionId != 0, "Not a position");
384:         uint256 feeAmount = amount * performanceFee / 10000;
385:         pay(reward, msg.sender, positionInfos[positionId].owner, amount - feeAmount);
386:         pay(reward, msg.sender, performanceFeeRecipient(), feeAmount);
387: 
388:         emit RewardClaimed(positionInfos[positionId].owner, vaultId, positionId, reward, amount - feeAmount, feeAmount);
389:     }

[Low-1] Potential division by zero should have zero checks in place

Resolution

Implement a zero address check for found instances

Num of instances: 2

Findings

Click to show findings

['447']

447:     function getDebtRatioFromAmounts( // <= FOUND
448:         uint256 amount0Principal,
449:         uint256 amount1Principal,
450:         uint256 amount0Borrow,
451:         uint256 amount1Borrow
452:     ) public view returns (uint256 debtRatio) {
453:         uint256 principalValue = amount0Principal * getTokenPrice(token0) / 10 ** token0Decimals
454:             + amount1Principal * getTokenPrice(token1) / 10 ** token1Decimals;
455:         uint256 borrowValue = amount0Borrow * getTokenPrice(token0) / 10 ** token0Decimals
456:             + amount1Borrow * getTokenPrice(token1) / 10 ** token1Decimals;
457:         return borrowValue * 10000 / (principalValue + borrowValue); // <= FOUND
458:     }

['460']

460:     function getDebtRatio(uint256 positionId) public view returns (uint256 debtRatio) { // <= FOUND
461:         return getDebtValue(positionId) * 10000 / getPositionValue(positionId); // <= FOUND
462:     }

[Low-2] Division operations should always be performed after multiplication operations

Resolution

Perform multiplication operations first

Num of instances: 4

Findings

Click to show findings

['419']

419:     function getPositionValue(uint256 positionId) public view returns (uint256 value) { // <= FOUND
420:         (uint256 amount0, uint256 amount1) = getPositionAmounts(positionId);
421:         return amount0 * getTokenPrice(token0) / 10 ** token0Decimals // <= FOUND
422:             + amount1 * getTokenPrice(token1) / 10 ** token1Decimals;
423:     }

['425']

425:     function getPositionValueByNftId(uint256 nftId) public view returns (uint256 value) { // <= FOUND
426:         (uint256 amount0, uint256 amount1) = getPositionAmountsByNftId(nftId);
427:         return amount0 * getTokenPrice(token0) / 10 ** token0Decimals // <= FOUND
428:             + amount1 * getTokenPrice(token1) / 10 ** token1Decimals;
429:     }

['431']

431:     function getDebtValue(uint256 positionId) public view returns (uint256 value) { // <= FOUND
432:         (uint256 amount0Debt, uint256 amount1Debt) = getPositionDebt(positionId);
433:         return amount0Debt * getTokenPrice(token0) / 10 ** token0Decimals // <= FOUND
434:             + amount1Debt * getTokenPrice(token1) / 10 ** token1Decimals;
435:     }

['447']

447:     function getDebtRatioFromAmounts( // <= FOUND
448:         uint256 amount0Principal,
449:         uint256 amount1Principal,
450:         uint256 amount0Borrow,
451:         uint256 amount1Borrow
452:     ) public view returns (uint256 debtRatio) {
453:         uint256 principalValue = amount0Principal * getTokenPrice(token0) / 10 ** token0Decimals // <= FOUND
454:             + amount1Principal * getTokenPrice(token1) / 10 ** token1Decimals;
455:         uint256 borrowValue = amount0Borrow * getTokenPrice(token0) / 10 ** token0Decimals
456:             + amount1Borrow * getTokenPrice(token1) / 10 ** token1Decimals;
457:         return borrowValue * 10000 / (principalValue + borrowValue);
458:     }

[Low-3] Storage Write Removal Bug On Conditional Early Termination

Resolution

In September 2022, a bug in Solidity’s Yul optimizer was identified through differential fuzzing. This bug, introduced in version 0.8.13 and fixed by version 0.8.17, can be activated easier with optimized via-IR code generation but can also potentially occur in optimized legacy code generation. This bug is of medium/high severity, which requires contracts that use large inline assembly blocks containing user-defined assembly functions involving return(...) or stop() instructions to be reviewed.

The bug is associated with the Unused Store Eliminator in Yul optimizer, which removes redundant storage writes, and is triggered when function calls are performed. If the function call conditionally continues after the call to a function and terminates using return(...) or stop(), the optimizer may incorrectly remove storage writes before calls to the function.

To evaluate if a contract is affected, you need to check contracts that include inline assembly block with return(...) or stop() statements. If early termination happens conditionally within a function, the optimizer might remove storage writes before function calls.

To avoid this, contracts should not allow for conditional early termination within a function. It is also recommended to use a version of Solidity where this bug has been fixed (version 0.8.17 or later).

Num of instances: 1

Findings

Click to show findings

['58']

58:         assembly {
59:             
60:             
61:             
62:             calldatacopy(0, 0, calldatasize())
63: 
64:             
65:             
66:             let result := delegatecall(gas(), impl, 0, calldatasize(), 0, 0)
67: 
68:             
69:             returndatacopy(0, 0, returndatasize())
70: 
71:             switch result
72:             
73:             case 0 { revert(0, returndatasize()) }
74:             default { return(0, returndatasize()) } // <= FOUND
75:         }

[Low-4] Missing checks for address(0x0) when updating address state variables

Num of instances: 7

Findings

Click to show findings

['14']

14:     function setAddress(uint256 id, address _addr) public onlyOwner {
15:         libraryAndContractAddresses[id] = _addr;
16:         emit SetAddress(_msgSender(), id, _addr);
17:     }

['96']

96:     function setReward(address rewardToken, uint256 startTime, uint256 endTime, uint256 totalRewards)
97:         public
98:         onlyOwner
99:         nonReentrant
100:         updateReward(address(0))
101:     {
102:         require(startTime < endTime, "start must lt end");
103:         require(rewardData[rewardToken].endTime < block.timestamp, "not end");
104: 
105:         if (!inRewardsTokenList[rewardToken]) {
106:             rewardTokens.push(rewardToken);
107:             inRewardsTokenList[rewardToken] = true;
108:         }
109: 
110:         rewardData[rewardToken].startTime = startTime;
111:         rewardData[rewardToken].endTime = endTime;
112:         rewardData[rewardToken].lastUpdateTime = block.timestamp;
113:         rewardData[rewardToken].rewardRate = totalRewards / (endTime - startTime);
114: 
115:         if (block.timestamp > startTime && totalStaked > 0) {
116:             uint256 dt = block.timestamp - startTime;
117: 
118:             rewardData[rewardToken].rewardPerTokenStored +=
119:                 (rewardData[rewardToken].rewardRate * dt * 1e18) / totalStaked;
120:         }
121: 
122:         if (block.timestamp > startTime && totalStaked == 0) {
123:             
124:             
125:             uint256 dt = block.timestamp - startTime;
126:             totalRewards -= rewardData[rewardToken].rewardRate * dt;
127:         }
128: 
129:         IERC20(rewardToken).safeTransferFrom(msg.sender, address(this), totalRewards);
130: 
131:         emit RewardsSet(rewardToken, startTime, endTime, totalRewards);
132:     }

['110']

110:     function setLiquidator(address _liquidator, bool _isLiquidator) external onlyOwner {
111:         liquidators[_liquidator] = _isLiquidator;
112:     }

['123']

123:     function setPositionImplementation(address _positionImplementation) external onlyOwner {
124:         positionImplementation = _positionImplementation;
125:     }

['148']

148:     function setShadowGauge(address _shadowGauge) external onlyOwner {
149:         shadowGauge = _shadowGauge;
150:     }

['44']

44:     function setPythPriceId(address token, bytes32 priceId) external onlyOracleManager {
45:         pythPriceIds[token] = priceId;
46:     }

['207']

207:     function update() external updateReward(address(0)) onlyOwner {}

[Low-5] Code does not follow the best practice of check-effects-interaction

Resolution

The "check-effects-interaction" pattern is a best practice in smart contract development, emphasizing the order of operations in functions to prevent reentrancy attacks. Violations arise when a function interacts with external contracts before settling internal state changes or checks. This misordering can expose the contract to potential threats. To adhere to this pattern, first ensure all conditions or checks are satisfied, then update any internal states, and only after these steps, interact with external contracts or addresses. Rearranging operations to this recommended sequence bolsters contract security and aligns with established best practices in the Ethereum community.

Num of instances: 16

Findings

Click to show findings

['69']

69:     function initReserve(address asset) external onlyOwner notPaused { // <= FOUND
70:         uint256 id = nextReserveId;
71:         nextReserveId += 1;
72: 
73:         
74:         string memory name = string(abi.encodePacked(ERC20(asset).name(), "(MightyFi Interest Bearing Token)")); // <= FOUND
75:         string memory symbol = string(abi.encodePacked("m", ERC20(asset).symbol()));
76:         uint8 decimals = ERC20(asset).decimals();
77: 
78:         address eTokenAddress = ETokenDeployer.deploy(name, symbol, decimals, asset, id);
79: 
80:         DataTypes.ReserveData storage reserveData = reserves[id];
81:         reserveData.setActive(true);
82:         reserveData.setBorrowingEnabled(true);
83: 
84:         initReserve(reserveData, asset, eTokenAddress, type(uint256).max, id);
85: 
86:         createStakingPoolForReserve(id);
87: 
88:         emit InitReserve(asset, eTokenAddress, reserveData.stakingAddress, id);
89:     }

['433']

433:     function getVault(uint256 vaultId) internal view returns (address vaultAddress) { // <= FOUND
434:         address vaultFactory = AddressRegistry(addressRegistry).getAddress(AddressId.ADDRESS_ID_VAULT_FACTORY);
435: 
436:         vaultAddress = IVaultFactory(vaultFactory).vaults(vaultId); // <= FOUND
437:         require(vaultAddress != address(0), "Invalid VaultId");
438:     }

['96']

96:     function setReward(address rewardToken, uint256 startTime, uint256 endTime, uint256 totalRewards) // <= FOUND
97:         public
98:         onlyOwner
99:         nonReentrant
100:         updateReward(address(0))
101:     {
102:         require(startTime < endTime, "start must lt end");
103:         require(rewardData[rewardToken].endTime < block.timestamp, "not end");
104: 
105:         if (!inRewardsTokenList[rewardToken]) {
106:             rewardTokens.push(rewardToken);
107:             inRewardsTokenList[rewardToken] = true;
108:         }
109: 
110:         rewardData[rewardToken].startTime = startTime;
111:         rewardData[rewardToken].endTime = endTime;
112:         rewardData[rewardToken].lastUpdateTime = block.timestamp;
113:         rewardData[rewardToken].rewardRate = totalRewards / (endTime - startTime);
114: 
115:         if (block.timestamp > startTime && totalStaked > 0) {
116:             uint256 dt = block.timestamp - startTime;
117: 
118:             rewardData[rewardToken].rewardPerTokenStored +=
119:                 (rewardData[rewardToken].rewardRate * dt * 1e18) / totalStaked;
120:         }
121: 
122:         if (block.timestamp > startTime && totalStaked == 0) {
123:             
124:             
125:             uint256 dt = block.timestamp - startTime;
126:             totalRewards -= rewardData[rewardToken].rewardRate * dt;
127:         }
128: 
129:         IERC20(rewardToken).safeTransferFrom(msg.sender, address(this), totalRewards); // <= FOUND
130: 
131:         emit RewardsSet(rewardToken, startTime, endTime, totalRewards);
132:     }

['195']

195:     function claim() external nonReentrant updateReward(msg.sender) { // <= FOUND
196:         for (uint256 i = 0; i < rewardTokens.length; i++) {
197:             address rewardToken = rewardTokens[i];
198:             uint256 claimable = userRewardsClaimable[msg.sender][rewardToken];
199:             if (claimable > 0) {
200:                 userRewardsClaimable[msg.sender][rewardToken] = 0;
201:                 require(IERC20(rewardToken).transfer(msg.sender, claimable), "transfer failed"); // <= FOUND
202:                 emit RewardPaid(msg.sender, rewardToken, claimable);
203:             }
204:         }
205:     }

['33']

33:     function getCurrentTick(address v3Pool) public view returns (int24) { // <= FOUND
34:         (uint160 sqrtPriceX96,,,,,,) = IShadowV3Pool(v3Pool).slot0(); // <= FOUND
35:         return TickMath.getTickAtSqrtRatio(sqrtPriceX96);
36:     }

['336']

336:     function claimableRewards() external view returns (address[] memory, uint256[] memory) { // <= FOUND
337:         address shadowGauge = IShadowRangeVault(vault).shadowGauge();
338:         if (shadowGauge != address(0)) {
339:             address[] memory tokens = IShadowGaugeV3(shadowGauge).getRewardTokens(); // <= FOUND
340:             uint256[] memory amounts = new uint256[](tokens.length);
341:             for (uint256 i = 0; i < tokens.length; i++) {
342:                 amounts[i] = IShadowGaugeV3(shadowGauge).earned(tokens[i], shadowPositionId);
343:             }
344:             return (tokens, amounts);
345:         }
346:         return (new address[](0), new uint256[](0));
347:     }

['376']

376:     function _claimRewards() internal { // <= FOUND
377:         address shadowGauge = IShadowRangeVault(vault).shadowGauge();
378:         if (shadowGauge != address(0)) {
379:             address[] memory tokens = IShadowGaugeV3(shadowGauge).getRewardTokens(); // <= FOUND
380:             IShadowGaugeV3(shadowGauge).getReward(shadowPositionId, tokens);
381: 
382:             for (uint256 i = 0; i < tokens.length; i++) {
383:                 uint256 balance = IERC20(tokens[i]).balanceOf(address(this));
384:                 if (balance > 0) {
385:                     if (tokens[i] == xShadow) {
386:                         
387:                         
388:                         
389:                         
390:                         
391:                         if (IShadowX33(x33).isUnlocked()) {
392:                             address x33Adapter = getX33Adapter();
393:                             IERC20(tokens[i]).approve(x33Adapter, balance);
394:                             IERC4626(x33Adapter).deposit(balance, address(this));
395:                             balance = IERC20(x33).balanceOf(address(this));
396:                             IERC20(x33).approve(vault, balance);
397:                             IVault(vault).claimCallback(x33, balance);
398:                         }
399:                     } else {
400:                         IERC20(tokens[i]).approve(vault, balance);
401:                         IVault(vault).claimCallback(tokens[i], balance);
402:                     }
403:                 }
404:             }
405:         }
406:     }

['432']

432:     function _swapTokenExactInput(address tokenIn, address tokenOut, uint256 amountIn, uint256 amountOutMinimum) // <= FOUND
433:         internal
434:         returns (uint256 amountOut)
435:     {
436:         address router =
437:             IAddressRegistry(IVault(vault).addressProvider()).getAddress(AddressId.ADDRESS_ID_SHADOW_ROUTER);
438:         IERC20(tokenIn).approve(router, amountIn); // <= FOUND
439: 
440:         amountOut = IShadowSwapRouter(router).exactInputSingle(
441:             IShadowSwapRouter.ExactInputSingleParams({
442:                 tokenIn: tokenIn,
443:                 tokenOut: tokenOut,
444:                 tickSpacing: tickSpacing,
445:                 recipient: address(this),
446:                 deadline: block.timestamp,
447:                 amountIn: amountIn,
448:                 amountOutMinimum: amountOutMinimum,
449:                 sqrtPriceLimitX96: 0
450:             })
451:         );
452:     }

['454']

454:     function _swapTokenExactOutput(address tokenIn, address tokenOut, uint256 amountOut, uint256 amountInMaximum) // <= FOUND
455:         internal
456:         returns (uint256 amountIn)
457:     {
458:         address router =
459:             IAddressRegistry(IVault(vault).addressProvider()).getAddress(AddressId.ADDRESS_ID_SHADOW_ROUTER);
460:         IERC20(tokenIn).approve(router, amountInMaximum); // <= FOUND
461: 
462:         amountIn = IShadowSwapRouter(router).exactOutputSingle(
463:             IShadowSwapRouter.ExactOutputSingleParams({
464:                 tokenIn: tokenIn,
465:                 tokenOut: tokenOut,
466:                 tickSpacing: tickSpacing,
467:                 recipient: address(this),
468:                 deadline: block.timestamp,
469:                 amountOut: amountOut,
470:                 amountInMaximum: amountInMaximum,
471:                 sqrtPriceLimitX96: 0
472:             })
473:         );
474:     }

['83']

83:     function initialize( // <= FOUND
84:         address _addressProvider,
85:         address _vaultRegistry,
86:         address _shadowV3PoolAddress,
87:         address _initialPositionImplementation
88:     ) public initializer {
89:         _initializePayments(IAddressRegistry(_addressProvider).getAddress(AddressId.ADDRESS_ID_WETH9)); // <= FOUND
90:         __ReentrancyGuard_init();
91:         __Ownable2Step_init();
92:         token0 = IShadowV3Pool(_shadowV3PoolAddress).token0();
93:         token1 = IShadowV3Pool(_shadowV3PoolAddress).token1();
94:         token0Decimals = IERC20Metadata(token0).decimals();
95:         token1Decimals = IERC20Metadata(token1).decimals();
96: 
97:         addressProvider = _addressProvider;
98:         vaultRegistry = _vaultRegistry;
99:         nextPositionID = 1;
100:         shadowV3Pool = _shadowV3PoolAddress;
101: 
102:         liquidationDebtRatio = 8600;
103:         liquidationFee = 500;
104:         performanceFee = 3000;
105:         minPositionSize = 100e8;
106: 
107:         positionImplementation = _initialPositionImplementation;
108:     }

['152']

152:     function openPosition(OpenPositionParams calldata params) // <= FOUND
153:         external
154:         payable
155:         nonReentrant
156:         checkDeadline(params.deadline)
157:     {
158:         (uint256 _positionId, address _positionAddress) = _createNewPosition();
159: 
160:         PositionInfo storage positionInfo = positionInfos[_positionId];
161:         positionInfo.owner = msg.sender;
162:         positionInfo.vaultId = vaultId;
163:         positionInfo.positionAddress = _positionAddress;
164:         positionInfo.positionId = _positionId;
165:         positionIds[msg.sender].push(_positionId);
166:         positionAddressToId[_positionAddress] = _positionId;
167: 
168:         
169:         if (params.amount0Principal > 0) {
170:             pay(token0, msg.sender, address(this), params.amount0Principal);
171:         }
172:         if (params.amount1Principal > 0) {
173:             pay(token1, msg.sender, address(this), params.amount1Principal);
174:         }
175: 
176:         
177:         address lendingPool = getLendingPool();
178:         if (params.amount0Borrow > 0) {
179:             positionInfo.token0DebtId = ILendingPool(lendingPool).newDebtPosition(token0ReserveId); // <= FOUND
180: 
181:             ILendingPool(lendingPool).borrow(address(this), positionInfo.token0DebtId, params.amount0Borrow);
182:         }
183:         if (params.amount1Borrow > 0) {
184:             positionInfo.token1DebtId = ILendingPool(lendingPool).newDebtPosition(token1ReserveId);
185: 
186:             ILendingPool(lendingPool).borrow(address(this), positionInfo.token1DebtId, params.amount1Borrow);
187:         }
188: 
189:         require(
190:             getDebtRatioFromAmounts(
191:                 params.amount0Principal, params.amount1Principal, params.amount0Borrow, params.amount1Borrow
192:             ) < liquidationDebtRatio,
193:             "Borrow value is too high"
194:         );
195: 
196:         if (params.amount0Principal > 0 || params.amount0Borrow > 0) {
197:             pay(token0, address(this), positionInfo.positionAddress, params.amount0Principal + params.amount0Borrow);
198:         }
199:         if (params.amount1Principal > 0 || params.amount1Borrow > 0) {
200:             pay(token1, address(this), positionInfo.positionAddress, params.amount1Principal + params.amount1Borrow);
201:         }
202:         
203:         (uint256 tokenId, uint128 liquidity, uint256 amount0, uint256 amount1) = IShadowRangePositionImpl(
204:             positionInfo.positionAddress
205:         ).openPosition(
206:             IShadowRangePositionImpl.OpenPositionParams({
207:                 amount0Desired: params.amount0Desired,
208:                 amount1Desired: params.amount1Desired,
209:                 tickLower: params.tickLower,
210:                 tickUpper: params.tickUpper,
211:                 positionOwner: msg.sender
212:             })
213:         );
214: 
215:         require(getPositionValueByNftId(tokenId) > minPositionSize, "PVL");
216: 
217:         positionInfo.shadowPositionId = tokenId;
218:         positionInfo.tickLower = params.tickLower;
219:         positionInfo.tickUpper = params.tickUpper;
220:         positionInfo.ul = params.ul;
221:         positionInfo.ll = params.ll;
222: 
223:         emit PositionOpened(
224:             msg.sender,
225:             vaultId,
226:             _positionId,
227:             _positionAddress,
228:             params.amount0Principal,
229:             params.amount1Principal,
230:             amount0,
231:             amount1,
232:             liquidity,
233:             block.timestamp
234:         );
235: 
236:         if (msg.value > 0) {
237:             refundETH();
238:         }
239:         require(getDebtRatio(_positionId) < liquidationDebtRatio, "DRH");
240:     }

['309']

309:     function repayExact(uint256 positionId, uint256 amount0, uint256 amount1) external payable { // <= FOUND
310:         PositionInfo storage positionInfo = positionInfos[positionId];
311: 
312:         
313:         address lendingPool = getLendingPool();
314:         if (amount0 > 0 && positionInfo.token0DebtId != 0) {
315:             (uint256 currentDebt0,) = ILendingPool(lendingPool).getCurrentDebt(positionInfo.token0DebtId); // <= FOUND
316:             if (amount0 > currentDebt0) {
317:                 amount0 = currentDebt0;
318:             }
319: 
320:             pay(token0, msg.sender, address(this), amount0);
321: 
322:             ILendingPool(lendingPool).repay(address(this), positionInfo.token0DebtId, amount0);
323:         }
324:         if (amount1 > 0 && positionInfo.token1DebtId != 0) {
325:             (uint256 currentDebt1,) = ILendingPool(lendingPool).getCurrentDebt(positionInfo.token1DebtId);
326:             if (amount1 > currentDebt1) {
327:                 amount1 = currentDebt1;
328:             }
329: 
330:             pay(token1, msg.sender, address(this), amount1);
331: 
332:             ILendingPool(lendingPool).repay(address(this), positionInfo.token1DebtId, amount1);
333:         }
334: 
335:         if (msg.value > 0) {
336:             refundETH();
337:         }
338:     }

['437']

437:     function getPositionDebt(uint256 positionId) public view returns (uint256 amount0Debt, uint256 amount1Debt) { // <= FOUND
438:         address lendingPool = getLendingPool();
439:         if (positionInfos[positionId].token0DebtId != 0) {
440:             (amount0Debt,) = ILendingPool(lendingPool).getCurrentDebt(positionInfos[positionId].token0DebtId); // <= FOUND
441:         }
442:         if (positionInfos[positionId].token1DebtId != 0) {
443:             (amount1Debt,) = ILendingPool(lendingPool).getCurrentDebt(positionInfos[positionId].token1DebtId);
444:         }
445:     }

['18']

18:     function principal(ISwapXNonfungiblePositionManager positionManager, uint256 tokenId, address pool) // <= FOUND
19:         public
20:         view
21:         returns (uint256 amount0, uint256 amount1)
22:     {
23:         (uint160 sqrtPriceX96,,,,,) = ISwapXV3Pool(pool).globalState(); // <= FOUND
24: 
25:         (,,,, int24 tickLower, int24 tickUpper, uint128 liquidity,,,,) = positionManager.positions(tokenId);
26: 
27:         return LiquidityAmounts.getAmountsForLiquidity(
28:             sqrtPriceX96, TickMath.getSqrtRatioAtTick(tickLower), TickMath.getSqrtRatioAtTick(tickUpper), liquidity
29:         );
30:     }

['29']

29:     function newVault(address _newVault) external onlyOwner returns (uint256 vaultId) { // <= FOUND
30:         require(!isRegistered[_newVault], "Vault already registered");
31:         vaultId = nextVaultID;
32:         nextVaultID = nextVaultID + 1;
33: 
34:         IVault(_newVault).onReceiveRegisterCallback(vaultId); // <= FOUND
35: 
36:         vaults[vaultId] = _newVault;
37:         isRegistered[_newVault] = true;
38:         emit NewVault(_newVault, vaultId);
39:     }

['68']

68:     function getUserPosition(address vault, address owner) public view returns (PositionInfo[] memory) { // <= FOUND
69:         uint256[] memory ids = getPositionIds(vault, owner);
70:         PositionInfo[] memory positions = new PositionInfo[](ids.length);
71:         for (uint256 i = 0; i < ids.length; i++) {
72:             positions[i].vault = vault;
73:             positions[i].id = ids[i];
74:             positions[i].token0 = IVault(vault).token0(); // <= FOUND
75:             positions[i].token1 = IVault(vault).token1();
76:             (positions[i].amount0, positions[i].amount1) = IVault(vault).getPositionAmounts(ids[i]);
77:             if (positions[i].amount0 > 0 || positions[i].amount1 > 0) {
78:                 (positions[i].amount0Debt, positions[i].amount1Debt) = IVault(vault).getPositionDebt(ids[i]);
79:                 positions[i].debtRatio = IVault(vault).getDebtRatio(ids[i]);
80:             }
81:             positions[i].price0 = IVault(vault).getTokenPrice(positions[i].token0);
82:             positions[i].price1 = IVault(vault).getTokenPrice(positions[i].token1);
83: 
84:             IVault.PositionInfo memory positionInfo = IVault(vault).getPositionInfos(ids[i]);
85: 
86:             positions[i].positionAddress = positionInfo.positionAddress;
87:             positions[i].positionNftId = positionInfo.shadowPositionId;
88:             positions[i].tickUpper = positionInfo.tickUpper;
89:             positions[i].tickLower = positionInfo.tickLower;
90:             positions[i].ul = positionInfo.ul;
91:             positions[i].ll = positionInfo.ll;
92: 
93:             positions[i].inRange = positions[i].amount0 > 0 && positions[i].amount1 > 0;
94: 
95:             (positions[i].rewards, positions[i].rewardsAmounts) =
96:                 IPositionImpl(positionInfo.positionAddress).claimableRewards();
97: 
98:             positions[i].currentTick = IVault(vault).getCurrentTick();
99: 
100:             (,,,,, positions[i].liquidity,,,,) = INFTPositionManager(
101:                 IAddressRegistry(addressProvider).getAddress(AddressId.ADDRESS_ID_SHADOW_NONFUNGIBLE_POSITION_MANAGER)
102:             ).positions(positionInfo.shadowPositionId);
103:         }
104:         return positions;
105:     }

[Low-6] Solidity version 0.8.23 won't work on all chains due to MCOPY

Resolution

Solidity version 0.8.23 introduces the MCOPY opcode, this may not be implemented on all chains and L2 thus reducing the portability and compatibility of the code. Consider using a earlier solidity version.

Num of instances: 2

Findings

Click to show findings

['3']

3: pragma solidity ^0.8.0; // <= FOUND

['3']

3: pragma solidity ^0.8.18; // <= FOUND

[Low-7] Function call without checking or using it's return values

Resolution

Calling a function which has returns without capturing said returns can indicate poor validation as it is standard to check function returns to ensure proper operation of the function call, especially when a bool return is involved as this is a clear indication that a silent revert may have been missed i.e the function returns false rather than reverting

Num of instances: 5

Findings

Click to show findings

['83']

83:     function openPosition(OpenPositionParams memory params) // <= FOUND
84:         external
85:         onlyVault
86:         returns (uint256 tokenId, uint128 liquidity, uint256 amount0, uint256 amount1)
87:     {
88:         uint256 token0Balance = IERC20(token0).balanceOf(address(this));
89:         if (params.amount0Desired > token0Balance) {
90:             _swapTokenExactInput( // <= FOUND
91:                 token1,
92:                 token0,
93:                 IERC20(token1).balanceOf(address(this)) - params.amount1Desired,
94:                 params.amount0Desired - token0Balance
95:             );
96:         }
97: 
98:         uint256 token1Balance = IERC20(token1).balanceOf(address(this));
99:         if (params.amount1Desired > token1Balance) {
100:             _swapTokenExactInput( // <= FOUND
101:                 token0,
102:                 token1,
103:                 IERC20(token0).balanceOf(address(this)) - params.amount0Desired,
104:                 params.amount1Desired - token1Balance
105:             );
106:         }
107:         address shadowNonfungiblePositionManager = getShadowNonfungiblePositionManager();
108: 
109:         IERC20(token0).approve(shadowNonfungiblePositionManager, params.amount0Desired);
110:         IERC20(token1).approve(shadowNonfungiblePositionManager, params.amount1Desired);
111: 
112:         (tokenId, liquidity, amount0, amount1) = IShadowNonfungiblePositionManager(shadowNonfungiblePositionManager)
113:             .mint(
114:             IShadowNonfungiblePositionManager.MintParams({
115:                 token0: token0,
116:                 token1: token1,
117:                 tickSpacing: tickSpacing,
118:                 tickLower: params.tickLower,
119:                 tickUpper: params.tickUpper,
120:                 amount0Desired: params.amount0Desired,
121:                 amount1Desired: params.amount1Desired,
122:                 amount0Min: 0,
123:                 amount1Min: 0,
124:                 recipient: address(this),
125:                 deadline: block.timestamp
126:             })
127:         );
128: 
129:         shadowPositionId = tokenId;
130:         positionOwner = params.positionOwner;
131: 
132:         unwrapWETH9(0, positionOwner);
133: 
134:         token0Balance = IERC20(token0).balanceOf(address(this));
135:         token1Balance = IERC20(token1).balanceOf(address(this));
136: 
137:         if (token0Balance > 0) {
138:             pay(token0, address(this), positionOwner, token0Balance);
139:         }
140:         if (token1Balance > 0) {
141:             pay(token1, address(this), positionOwner, token1Balance);
142:         }
143:     }

['145']

145:     function closePosition() // <= FOUND
146:         external
147:         onlyVault
148:         returns (
149:             uint128 liquidity,
150:             uint256 token0Reduced,
151:             uint256 token1Reduced,
152:             uint256 token0Balance,
153:             uint256 token1Balance
154:         )
155:     {
156:         _claimFees();
157:         _claimRewards();
158: 
159:         (,,,,, liquidity,,,,) =
160:             IShadowNonfungiblePositionManager(getShadowNonfungiblePositionManager()).positions(shadowPositionId);
161: 
162:         (token0Reduced, token1Reduced) = _decreasePosition(liquidity);
163: 
164:         (uint256 currentDebt0, uint256 currentDebt1) = IVault(vault).getPositionDebt(positionId);
165: 
166:         if (currentDebt0 > 0) {
167:             
168:             if (currentDebt0 > token0Reduced) {
169:                 
170:                 
171:                 
172:                 uint256 amount1Excess = token1Reduced - currentDebt1;
173:                 _swapTokenExactInput(token1, token0, amount1Excess, currentDebt0 - token0Reduced); // <= FOUND
174:             }
175:             IERC20(token0).approve(vault, currentDebt0);
176:         }
177: 
178:         if (currentDebt1 > 0) {
179:             
180:             if (currentDebt1 > token1Reduced) {
181:                 
182:                 
183:                 
184:                 uint256 amount0Excess = token0Reduced - currentDebt0;
185:                 _swapTokenExactInput(token0, token1, amount0Excess, currentDebt1 - token1Reduced);
186:             }
187:             IERC20(token1).approve(vault, currentDebt1);
188:         }
189: 
190:         IVault(vault).repayExact(positionId, currentDebt0, currentDebt1);
191: 
192:         token0Balance = IERC20(token0).balanceOf(address(this));
193:         token1Balance = IERC20(token1).balanceOf(address(this));
194:         if (token0Balance > 0) {
195:             pay(token0, address(this), positionOwner, token0Balance);
196:         }
197:         if (token1Balance > 0) {
198:             pay(token1, address(this), positionOwner, token1Balance);
199:         }
200:     }

['202']

202:     function reducePosition(uint128 reducePercentage, uint256 amount0ToSwap, uint256 amount1ToSwap) // <= FOUND
203:         external
204:         onlyVault
205:         returns (uint128 reduceLiquidity, uint256 token0Reduced, uint256 token1Reduced)
206:     {
207:         _claimFees();
208: 
209:         (,,,,, uint128 liquidity,,,,) =
210:             IShadowNonfungiblePositionManager(getShadowNonfungiblePositionManager()).positions(shadowPositionId);
211: 
212:         reduceLiquidity = liquidity * reducePercentage / 10000;
213: 
214:         (token0Reduced, token1Reduced) = _decreasePosition(reduceLiquidity);
215: 
216:         if (amount0ToSwap > 0) {
217:             _swapTokenExactInput(token0, token1, amount0ToSwap, 0); // <= FOUND
218:         }
219: 
220:         if (amount1ToSwap > 0) {
221:             _swapTokenExactInput(token1, token0, amount1ToSwap, 0);
222:         }
223: 
224:         uint256 token0Left = IERC20(token0).balanceOf(address(this));
225:         uint256 token1Left = IERC20(token1).balanceOf(address(this));
226: 
227:         if (token0Left > 0) {
228:             IERC20(token0).approve(vault, token0Left);
229:         }
230: 
231:         if (token1Left > 0) {
232:             IERC20(token1).approve(vault, token1Left);
233:         }
234: 
235:         IVault(vault).repayExact(positionId, token0Left, token1Left);
236: 
237:         token0Left = IERC20(token0).balanceOf(address(this));
238:         token1Left = IERC20(token1).balanceOf(address(this));
239:         if (token0Left > 0) {
240:             pay(token0, address(this), positionOwner, token0Left);
241:         }
242:         if (token1Left > 0) {
243:             pay(token1, address(this), positionOwner, token1Left);
244:         }
245:     }

['254']

254:     function liquidatePosition(address caller) // <= FOUND
255:         external
256:         onlyVault
257:         returns (
258:             uint128 liquidity,
259:             uint256 token0Reduced,
260:             uint256 token1Reduced,
261:             uint256 token0Fees,
262:             uint256 token1Fees,
263:             uint256 token0Left,
264:             uint256 token1Left
265:         )
266:     {
267:         _claimFees();
268:         _claimRewards();
269:         (,,,,, liquidity,,,,) =
270:             IShadowNonfungiblePositionManager(getShadowNonfungiblePositionManager()).positions(shadowPositionId);
271: 
272:         (token0Reduced, token1Reduced) = _decreasePosition(liquidity);
273:         (uint256 currentDebt0, uint256 currentDebt1) = IVault(vault).getPositionDebt(positionId);
274: 
275:         LiquidationFeeVars memory vars = LiquidationFeeVars({
276:             liquidationFee: IVault(vault).liquidationFee(),
277:             liquidationCallerFee: IVault(vault).liquidationCallerFee(),
278:             liquidationFeeRecipient: IVault(vault).liquidationFeeRecipient()
279:         });
280:         
281:         if (token0Reduced > 0) {
282:             token0Fees = token0Reduced * vars.liquidationFee / 10000;
283:             pay(token0, address(this), vars.liquidationFeeRecipient, token0Fees);
284: 
285:             uint256 token0CallerFees = token0Fees * vars.liquidationCallerFee / 10000;
286:             if (token0CallerFees > 0) {
287:                 pay(token0, address(this), caller, token0CallerFees);
288:             }
289: 
290:             token0Reduced = token0Reduced - token0Fees - token0CallerFees;
291:         }
292: 
293:         if (token1Reduced > 0) {
294:             token1Fees = token1Reduced * vars.liquidationFee / 10000;
295: 
296:             pay(token1, address(this), vars.liquidationFeeRecipient, token1Fees);
297: 
298:             uint256 token1CallerFees = token1Fees * vars.liquidationCallerFee / 10000;
299:             if (token1CallerFees > 0) {
300:                 pay(token1, address(this), caller, token1CallerFees);
301:             }
302: 
303:             token1Reduced = token1Reduced - token1Fees - token1CallerFees;
304:         }
305: 
306:         if (currentDebt0 > 0) {
307:             
308:             if (currentDebt0 > token0Reduced) {
309:                 uint256 amount1Excess = token1Reduced - currentDebt1;
310:                 _swapTokenExactInput(token1, token0, amount1Excess, currentDebt0 - token0Reduced); // <= FOUND
311:             }
312:             IERC20(token0).approve(vault, currentDebt0);
313:         }
314: 
315:         if (currentDebt1 > 0) {
316:             
317:             if (currentDebt1 > token1Reduced) {
318:                 uint256 amount0Excess = token0Reduced - currentDebt0;
319:                 _swapTokenExactInput(token0, token1, amount0Excess, currentDebt1 - token1Reduced);
320:             }
321:             IERC20(token1).approve(vault, currentDebt1);
322:         }
323: 
324:         IVault(vault).repayExact(positionId, currentDebt0, currentDebt1);
325: 
326:         token0Left = IERC20(token0).balanceOf(address(this));
327:         token1Left = IERC20(token1).balanceOf(address(this));
328:         if (token0Left > 0) {
329:             pay(token0, address(this), positionOwner, token0Left);
330:         }
331:         if (token1Left > 0) {
332:             pay(token1, address(this), positionOwner, token1Left);
333:         }
334:     }

['253']

253:     function fullfillLimitOrder(uint256 positionId) external nonReentrant { // <= FOUND
254:         PositionInfo memory positionInfo = positionInfos[positionId];
255:         int24 currentTick = IPositionValueCalculator(
256:             IAddressRegistry(addressProvider).getAddress(AddressId.ADDRESS_ID_SHADOW_POSITION_VALUE_CALCULATOR)
257:         ).getCurrentTick(shadowV3Pool);
258: 
259:         if (
260:             (positionInfo.ll != 0 && currentTick < positionInfo.ll)
261:                 || (positionInfo.ul != 0 && currentTick > positionInfo.ul)
262:         ) {
263:             _closePosition(positionId); // <= FOUND
264:         } else {
265:             revert("Current tick is not in the position range");
266:         }
267: 
268:         require(getDebtRatio(positionId) < liquidationDebtRatio, "DRH");
269:     }

[Low-8] Some tokens may revert when zero value transfers are made

Resolution

Reason: In Solidity, ERC20 token transfers of value 0 can sometimes lead to unexpected issues. This is particularly relevant when dealing with fractional token amounts that round to 0 when less than 1 of the smallest unit is transferred, leading to an effective transfer of nothing while still consuming gas. Furthermore, some ERC20 token implementations may revert on attempts to transfer a value of 0. However, note that this issue doesn't generally apply to wrapper native tokens like WETH.

Resolution: It's advisable to include a condition before any transfer operation to bypass the transaction if the transfer amount is 0. This saves unnecessary gas expenditure and prevents potential function reverts. For handling fractions, ensure token decimals are appropriately assigned and contemplate setting a minimum transfer threshold to avoid rounding down to 0. When dealing with wrapped tokens like WETH, special consideration should be given to their unique characteristics.

Num of instances: 9

Findings

Click to show findings

['61']

61:     function burn(address receiverOfUnderlying, uint256 eTokenAmount, uint256 underlyingTokenAmount) // <= FOUND
62:         external
63:         onlyLendingPool
64:         nonReentrant
65:     {
66:         _burn(msg.sender, eTokenAmount);
67: 
68:         IERC20(underlyingAsset).safeTransfer(receiverOfUnderlying, underlyingTokenAmount); // <= FOUND
69: 
70:         emit Burn(msg.sender, receiverOfUnderlying, eTokenAmount, underlyingTokenAmount);
71:     }

['92']

92:     function transferUnderlyingTo(address target, uint256 amount) // <= FOUND
93:         external
94:         onlyLendingPool
95:         nonReentrant
96:         returns (uint256)
97:     {
98:         IERC20(underlyingAsset).safeTransfer(target, amount); // <= FOUND
99:         return amount;
100:     }

['160']

160:     function redeem(uint256 reserveId, uint256 eTokenAmount, address to, bool receiveNativeETH) // <= FOUND
161:         public
162:         payable
163:         notPaused
164:         nonReentrant
165:         avoidUsingNativeEther
166:         returns (uint256)
167:     {
168:         DataTypes.ReserveData storage reserve = getReserve(reserveId);
169: 
170:         if (eTokenAmount == type(uint256).max) {
171:             eTokenAmount = IExtraInterestBearingToken(reserve.eTokenAddress).balanceOf(_msgSender());
172:         }
173:         
174:         IERC20(reserve.eTokenAddress).safeTransferFrom(_msgSender(), address(this), eTokenAmount); // <= FOUND
175: 
176:         
177:         uint256 underlyingTokenAmount = _redeem(reserveId, eTokenAmount, to, receiveNativeETH);
178: 
179:         emit Redeemed(reserveId, _msgSender(), to, eTokenAmount, underlyingTokenAmount);
180: 
181:         return (underlyingTokenAmount);
182:     }

['324']

324:     function repay(address onBehalfOf, uint256 debtId, uint256 amount) // <= FOUND
325:         external
326:         notPaused
327:         nonReentrant
328:         returns (uint256)
329:     {
330:         require(borrowingWhiteList[_msgSender()], Errors.VL_BORROWING_CALLER_NOT_IN_WHITELIST);
331: 
332:         DataTypes.DebtPositionData storage debtPosition = debtPositions[debtId];
333:         require(_msgSender() == debtPosition.owner, Errors.VL_INVALID_DEBT_OWNER);
334: 
335:         DataTypes.ReserveData storage reserve = getReserve(debtPosition.reserveId);
336: 
337:         
338:         reserve.updateState(getTreasury());
339:         updateDebtPosition(debtPosition, reserve.borrowingIndex);
340: 
341:         
342:         
343:         
344:         uint256 credit = credits[debtPosition.reserveId][_msgSender()];
345:         credits[debtPosition.reserveId][_msgSender()] = credit.add(amount);
346: 
347:         if (amount > debtPosition.borrowed) {
348:             amount = debtPosition.borrowed;
349:         }
350:         reserve.totalBorrows = reserve.totalBorrows.sub(amount);
351:         debtPosition.borrowed = debtPosition.borrowed.sub(amount);
352: 
353:         
354:         IERC20(reserve.underlyingTokenAddress).safeTransferFrom(_msgSender(), reserve.eTokenAddress, amount); // <= FOUND
355: 
356:         reserve.updateInterestRates();
357: 
358:         emit Repay(debtPosition.reserveId, onBehalfOf, _msgSender(), amount);
359:         return amount;
360:     }

['46']

46:     function pay(address token, address payer, address recipient, uint256 value) internal { // <= FOUND
47:         if (token == WETH9 && address(this).balance >= value) {
48:             
49:             IWETH9(WETH9).deposit{value: value}(); 
50:             require(IWETH9(WETH9).transfer(recipient, value), "transfer failed");
51:         } else if (payer == address(this)) {
52:             
53:             TransferHelper.safeTransfer(token, recipient, value); // <= FOUND
54:         } else {
55:             
56:             TransferHelper.safeTransferFrom(token, payer, recipient, value);
57:         }
58:     }

['96']

96:     function setReward(address rewardToken, uint256 startTime, uint256 endTime, uint256 totalRewards) // <= FOUND
97:         public
98:         onlyOwner
99:         nonReentrant
100:         updateReward(address(0))
101:     {
102:         require(startTime < endTime, "start must lt end");
103:         require(rewardData[rewardToken].endTime < block.timestamp, "not end");
104: 
105:         if (!inRewardsTokenList[rewardToken]) {
106:             rewardTokens.push(rewardToken);
107:             inRewardsTokenList[rewardToken] = true;
108:         }
109: 
110:         rewardData[rewardToken].startTime = startTime;
111:         rewardData[rewardToken].endTime = endTime;
112:         rewardData[rewardToken].lastUpdateTime = block.timestamp;
113:         rewardData[rewardToken].rewardRate = totalRewards / (endTime - startTime);
114: 
115:         if (block.timestamp > startTime && totalStaked > 0) {
116:             uint256 dt = block.timestamp - startTime;
117: 
118:             rewardData[rewardToken].rewardPerTokenStored +=
119:                 (rewardData[rewardToken].rewardRate * dt * 1e18) / totalStaked;
120:         }
121: 
122:         if (block.timestamp > startTime && totalStaked == 0) {
123:             
124:             
125:             uint256 dt = block.timestamp - startTime;
126:             totalRewards -= rewardData[rewardToken].rewardRate * dt;
127:         }
128: 
129:         IERC20(rewardToken).safeTransferFrom(msg.sender, address(this), totalRewards); // <= FOUND
130: 
131:         emit RewardsSet(rewardToken, startTime, endTime, totalRewards);
132:     }

['159']

159:     function withdraw(uint256 amount, address to) external nonReentrant updateReward(msg.sender) { // <= FOUND
160:         require(amount > 0, "amount = 0");
161: 
162:         balanceOf[msg.sender] -= amount;
163:         totalStaked -= amount;
164: 
165:         require(stakedToken.transfer(to, amount), "transfer failed"); // <= FOUND
166: 
167:         emit Withdraw(msg.sender, to, amount);
168:     }

['179']

179:     function withdrawByLendingPool(uint256 amount, address user, address to) // <= FOUND
180:         external
181:         onlyLendingPool
182:         nonReentrant
183:         updateReward(user)
184:     {
185:         require(amount > 0, "amount = 0");
186: 
187:         balanceOf[user] -= amount;
188:         totalStaked -= amount;
189: 
190:         require(stakedToken.transfer(to, amount), "transfer falied"); // <= FOUND
191: 
192:         emit Withdraw(user, to, amount);
193:     }

['195']

195:     function claim() external nonReentrant updateReward(msg.sender) { // <= FOUND
196:         for (uint256 i = 0; i < rewardTokens.length; i++) {
197:             address rewardToken = rewardTokens[i];
198:             uint256 claimable = userRewardsClaimable[msg.sender][rewardToken];
199:             if (claimable > 0) {
200:                 userRewardsClaimable[msg.sender][rewardToken] = 0;
201:                 require(IERC20(rewardToken).transfer(msg.sender, claimable), "transfer failed"); // <= FOUND
202:                 emit RewardPaid(msg.sender, rewardToken, claimable);
203:             }
204:         }
205:     }

[Low-9] deposit/redeem functions found which implement accounting arithmetic vulnerable to the donation attack

Resolution

Calculations using non-internal accounting, such as balanceOf or totalSupply, can introduce potential donation attack vectors. For instance, consider a scenario where a reward calculation uses totalSupply during a deposit. An attacker could exploit this by donating a large amount of tokens to the vault before the user tries to redeem their withdrawal. This manipulation causes the totalSupply to change significantly, altering the reward calculations and potentially leading to unexpected or unfair outcomes for users.

Let's say a smart contract calculates user rewards based on their share of the total token supply:

uint256 reward = (userDeposit * totalRewards) / totalSupply;

If an attacker donates a large number of tokens to the vault before users withdraw, totalSupply increases, thereby diluting each user's share of the rewards. This discrepancy between expected and actual rewards can undermine user trust and contract integrity.

The risks associated with this attack include reward manipulation, where attackers can alter totalSupply to reduce the share of honest users, and economic attacks, where significant token donations destabilize the contract's economic assumptions, leading to unfair advantages or financial losses. Additionally, users may find their rewards significantly lower than expected, causing confusion and dissatisfaction.

To prevent such vulnerabilities, it's essential to use internal variables to track deposits, withdrawals, and rewards instead of relying on balanceOf or totalSupply. This approach isolates contract logic from external token movements. Implementing a snapshot mechanism to capture totalSupply at specific points in time ensures consistent reward calculations. Introducing limits on the number of tokens that can be deposited or donated in a single transaction can also prevent significant manipulation.

Num of instances: 3

Findings

Click to show findings

['145']

145:     function closePosition()
146:         external
147:         onlyVault
148:         returns (
149:             uint128 liquidity,
150:             uint256 token0Reduced,
151:             uint256 token1Reduced,
152:             uint256 token0Balance,
153:             uint256 token1Balance
154:         )
155:     {
156:         _claimFees();
157:         _claimRewards(); // <= FOUND
158: 
159:         (,,,,, liquidity,,,,) =
160:             IShadowNonfungiblePositionManager(getShadowNonfungiblePositionManager()).positions(shadowPositionId);
161: 
162:         (token0Reduced, token1Reduced) = _decreasePosition(liquidity); // <= FOUND
163: 
164:         (uint256 currentDebt0, uint256 currentDebt1) = IVault(vault).getPositionDebt(positionId);
165: 
166:         if (currentDebt0 > 0) {
167:             
168:             if (currentDebt0 > token0Reduced) {
169:                 
170:                 
171:                 
172:                 uint256 amount1Excess = token1Reduced - currentDebt1;
173:                 _swapTokenExactInput(token1, token0, amount1Excess, currentDebt0 - token0Reduced);
174:             }
175:             IERC20(token0).approve(vault, currentDebt0);
176:         }
177: 
178:         if (currentDebt1 > 0) {
179:             
180:             if (currentDebt1 > token1Reduced) {
181:                 
182:                 
183:                 
184:                 uint256 amount0Excess = token0Reduced - currentDebt0;
185:                 _swapTokenExactInput(token0, token1, amount0Excess, currentDebt1 - token1Reduced);
186:             }
187:             IERC20(token1).approve(vault, currentDebt1);
188:         }
189: 
190:         IVault(vault).repayExact(positionId, currentDebt0, currentDebt1);
191: 
192:         token0Balance = IERC20(token0).balanceOf(address(this));
193:         token1Balance = IERC20(token1).balanceOf(address(this));
194:         if (token0Balance > 0) {
195:             pay(token0, address(this), positionOwner, token0Balance);
196:         }
197:         if (token1Balance > 0) {
198:             pay(token1, address(this), positionOwner, token1Balance);
199:         }
200:     }

['254']

254:     function liquidatePosition(address caller) // <= FOUND
255:         external
256:         onlyVault
257:         returns (
258:             uint128 liquidity,
259:             uint256 token0Reduced,
260:             uint256 token1Reduced,
261:             uint256 token0Fees,
262:             uint256 token1Fees,
263:             uint256 token0Left,
264:             uint256 token1Left
265:         )
266:     {
267:         _claimFees();
268:         _claimRewards(); // <= FOUND
269:         (,,,,, liquidity,,,,) =
270:             IShadowNonfungiblePositionManager(getShadowNonfungiblePositionManager()).positions(shadowPositionId);
271: 
272:         (token0Reduced, token1Reduced) = _decreasePosition(liquidity); // <= FOUND
273:         (uint256 currentDebt0, uint256 currentDebt1) = IVault(vault).getPositionDebt(positionId);
274: 
275:         LiquidationFeeVars memory vars = LiquidationFeeVars({
276:             liquidationFee: IVault(vault).liquidationFee(),
277:             liquidationCallerFee: IVault(vault).liquidationCallerFee(),
278:             liquidationFeeRecipient: IVault(vault).liquidationFeeRecipient()
279:         });
280:         
281:         if (token0Reduced > 0) {
282:             token0Fees = token0Reduced * vars.liquidationFee / 10000;
283:             pay(token0, address(this), vars.liquidationFeeRecipient, token0Fees);
284: 
285:             uint256 token0CallerFees = token0Fees * vars.liquidationCallerFee / 10000;
286:             if (token0CallerFees > 0) {
287:                 pay(token0, address(this), caller, token0CallerFees);
288:             }
289: 
290:             token0Reduced = token0Reduced - token0Fees - token0CallerFees;
291:         }
292: 
293:         if (token1Reduced > 0) {
294:             token1Fees = token1Reduced * vars.liquidationFee / 10000;
295: 
296:             pay(token1, address(this), vars.liquidationFeeRecipient, token1Fees);
297: 
298:             uint256 token1CallerFees = token1Fees * vars.liquidationCallerFee / 10000;
299:             if (token1CallerFees > 0) {
300:                 pay(token1, address(this), caller, token1CallerFees);
301:             }
302: 
303:             token1Reduced = token1Reduced - token1Fees - token1CallerFees;
304:         }
305: 
306:         if (currentDebt0 > 0) {
307:             
308:             if (currentDebt0 > token0Reduced) {
309:                 uint256 amount1Excess = token1Reduced - currentDebt1;
310:                 _swapTokenExactInput(token1, token0, amount1Excess, currentDebt0 - token0Reduced);
311:             }
312:             IERC20(token0).approve(vault, currentDebt0);
313:         }
314: 
315:         if (currentDebt1 > 0) {
316:             
317:             if (currentDebt1 > token1Reduced) {
318:                 uint256 amount0Excess = token0Reduced - currentDebt0;
319:                 _swapTokenExactInput(token0, token1, amount0Excess, currentDebt1 - token1Reduced);
320:             }
321:             IERC20(token1).approve(vault, currentDebt1);
322:         }
323: 
324:         IVault(vault).repayExact(positionId, currentDebt0, currentDebt1);
325: 
326:         token0Left = IERC20(token0).balanceOf(address(this));
327:         token1Left = IERC20(token1).balanceOf(address(this));
328:         if (token0Left > 0) {
329:             pay(token0, address(this), positionOwner, token0Left);
330:         }
331:         if (token1Left > 0) {
332:             pay(token1, address(this), positionOwner, token1Left);
333:         }
334:     }

['202']

202:     function reducePosition(uint128 reducePercentage, uint256 amount0ToSwap, uint256 amount1ToSwap)
203:         external
204:         onlyVault
205:         returns (uint128 reduceLiquidity, uint256 token0Reduced, uint256 token1Reduced)
206:     {
207:         _claimFees();
208: 
209:         (,,,,, uint128 liquidity,,,,) =
210:             IShadowNonfungiblePositionManager(getShadowNonfungiblePositionManager()).positions(shadowPositionId);
211: 
212:         reduceLiquidity = liquidity * reducePercentage / 10000;
213: 
214:         (token0Reduced, token1Reduced) = _decreasePosition(reduceLiquidity); // <= FOUND
215: 
216:         if (amount0ToSwap > 0) {
217:             _swapTokenExactInput(token0, token1, amount0ToSwap, 0);
218:         }
219: 
220:         if (amount1ToSwap > 0) {
221:             _swapTokenExactInput(token1, token0, amount1ToSwap, 0);
222:         }
223: 
224:         uint256 token0Left = IERC20(token0).balanceOf(address(this));
225:         uint256 token1Left = IERC20(token1).balanceOf(address(this));
226: 
227:         if (token0Left > 0) {
228:             IERC20(token0).approve(vault, token0Left);
229:         }
230: 
231:         if (token1Left > 0) {
232:             IERC20(token1).approve(vault, token1Left);
233:         }
234: 
235:         IVault(vault).repayExact(positionId, token0Left, token1Left);
236: 
237:         token0Left = IERC20(token0).balanceOf(address(this));
238:         token1Left = IERC20(token1).balanceOf(address(this));
239:         if (token0Left > 0) {
240:             pay(token0, address(this), positionOwner, token0Left);
241:         }
242:         if (token1Left > 0) {
243:             pay(token1, address(this), positionOwner, token1Left);
244:         }
245:     }

[Low-10] Require should be used instead of assert as assert in some solidity versions does not refund remaining gas

Resolution

In Solidity, require should be used instead of assert for input validation and error handling due to its more efficient and flexible nature. While both functions check for a specified condition and revert the transaction if it evaluates to false, assert consumes all remaining gas upon failure, which can lead to higher costs for users. On the other hand, require refunds unused gas, making it a more gas-efficient option. Additionally, require allows for custom error messages, which improves debugging and error reporting.

Num of instances: 1

Findings

Click to show findings

['13']

13:         assert(_ADMIN_SLOT == bytes32(uint256(keccak256("eip1967.proxy.admin")) - 1)); // <= FOUND

[Low-11] The call abi.encodeWithSignature is not safe from typographical errors

Resolution

When you use abi.encodeWithSignature in Solidity, you're creating an ABI-encoded string representing a function call. This method relies on you, the programmer, correctly typing the function signature (i.e., the function name and types of arguments) as a string.

For instance, if you want to call a function foo(uint256, address), you'd write abi.encodeWithSignature("foo(uint256,address)", arg1, arg2). If there's a typographical error like abi.encodeWithSignature("foo(uinnt256,address)", arg1, arg2), the Solidity compiler will not raise an error because the string is syntactically correct. However, this will fail at runtime because there's no function with that incorrect signature.

This makes abi.encodeWithSignature less safe than directly calling the function or using interfaces to call functions on other contracts. An effective resolution would be using a Solidity interface. An interface provides compile-time type checking, ensuring that you're calling existing functions with the correct parameters.

Num of instances: 1

Findings

Click to show findings

['17']

17:         (bool success,) = impl.delegatecall(abi.encodeWithSignature("initialize(uint256)", _positionId)); // <= FOUND

[Low-12] Upgradable contracts should have a __gap variable

Resolution

This is to ensure the team can add new state variables without compromising compatability.

Num of instances: 5

Findings

Click to show findings

['10']

10: contract PrimaryPriceOracle is IPriceOracle, Initializable 

['30']

30: contract ShadowRangeVault is
31:     Initializable,
32:     ReentrancyGuardUpgradeable,
33:     Ownable2StepUpgradeable,
34:     PaymentsUpgradeable,
35:     IVault
36: 

['24']

24: contract LendingPool is ILendingPool, OwnableUpgradeable, PaymentsUpgradeable, ReentrancyGuardUpgradeable 

['26']

26: contract ShadowRangePositionImpl is IShadowRangePositionImpl, PaymentsUpgradeable 

['10']

10: abstract contract PaymentsUpgradeable 

[Low-13] The nonReentrant modifier should be first in a function declaration

Resolution

In Solidity, the nonReentrant modifier is essential for securing smart contracts against reentrancy attacks. It should be positioned first in a function declaration. This placement is critical because it ensures that the modifier's protection is applied before any other code or modifiers in the function. If placed later, other modifiers could potentially be executed in a reentrant manner before nonReentrant has a chance to lock the function, leaving the contract vulnerable. Thus, prioritizing nonReentrant as the first modifier is a best practice for robust smart contract security, effectively guarding against unintended reentries and related exploits.

Num of instances: 13

Findings

Click to show findings

['96']

96:     function setReward(address rewardToken, uint256 startTime, uint256 endTime, uint256 totalRewards)
97:         public
98:         onlyOwner
99:         nonReentrant // <= FOUND
100:         updateReward(address(0))
101:     {
102:         require(startTime < endTime, "start must lt end");
103:         require(rewardData[rewardToken].endTime < block.timestamp, "not end");
104: 
105:         if (!inRewardsTokenList[rewardToken]) {
106:             rewardTokens.push(rewardToken);
107:             inRewardsTokenList[rewardToken] = true;
108:         }
109: 
110:         rewardData[rewardToken].startTime = startTime;
111:         rewardData[rewardToken].endTime = endTime;
112:         rewardData[rewardToken].lastUpdateTime = block.timestamp;
113:         rewardData[rewardToken].rewardRate = totalRewards / (endTime - startTime);
114: 
115:         if (block.timestamp > startTime && totalStaked > 0) {
116:             uint256 dt = block.timestamp - startTime;
117: 
118:             rewardData[rewardToken].rewardPerTokenStored +=
119:                 (rewardData[rewardToken].rewardRate * dt * 1e18) / totalStaked;
120:         }
121: 
122:         if (block.timestamp > startTime && totalStaked == 0) {
123:             
124:             
125:             uint256 dt = block.timestamp - startTime;
126:             totalRewards -= rewardData[rewardToken].rewardRate * dt;
127:         }
128: 
129:         IERC20(rewardToken).safeTransferFrom(msg.sender, address(this), totalRewards);
130: 
131:         emit RewardsSet(rewardToken, startTime, endTime, totalRewards);
132:     }

['47']

47:     function mint(address user, uint256 amount) external onlyLendingPool nonReentrant { // <= FOUND
48:         _mint(user, amount);
49:         emit Mint(user, amount);
50:     }

['61']

61:     function burn(address receiverOfUnderlying, uint256 eTokenAmount, uint256 underlyingTokenAmount)
62:         external
63:         onlyLendingPool
64:         nonReentrant // <= FOUND
65:     {
66:         _burn(msg.sender, eTokenAmount);
67: 
68:         IERC20(underlyingAsset).safeTransfer(receiverOfUnderlying, underlyingTokenAmount);
69: 
70:         emit Burn(msg.sender, receiverOfUnderlying, eTokenAmount, underlyingTokenAmount);
71:     }

['78']

78:     function mintToTreasury(address treasury, uint256 amount) external onlyLendingPool nonReentrant { // <= FOUND
79:         require(treasury != address(0), "zero address");
80:         _mint(treasury, amount);
81:         emit MintToTreasury(treasury, amount);
82:     }

['92']

92:     function transferUnderlyingTo(address target, uint256 amount)
93:         external
94:         onlyLendingPool
95:         nonReentrant // <= FOUND
96:         returns (uint256)
97:     {
98:         IERC20(underlyingAsset).safeTransfer(target, amount);
99:         return amount;
100:     }

['179']

179:     function withdrawByLendingPool(uint256 amount, address user, address to)
180:         external
181:         onlyLendingPool
182:         nonReentrant // <= FOUND
183:         updateReward(user)
184:     {
185:         require(amount > 0, "amount = 0");
186: 
187:         balanceOf[user] -= amount;
188:         totalStaked -= amount;
189: 
190:         require(stakedToken.transfer(to, amount), "transfer falied");
191: 
192:         emit Withdraw(user, to, amount);
193:     }

['104']

104:     function deposit(uint256 reserveId, uint256 amount, address onBehalfOf, uint16 referralCode)
105:         public
106:         payable
107:         notPaused
108:         nonReentrant // <= FOUND
109:         returns (uint256 eTokenAmount)
110:     {
111:         eTokenAmount = _deposit(reserveId, amount, onBehalfOf);
112: 
113:         
114:         if (msg.value > 0) {
115:             refundETH();
116:         }
117: 
118:         
119:         emit Deposited(reserveId, _msgSender(), onBehalfOf, amount, eTokenAmount, referralCode);
120:     }

['123']

123:     function depositAndStake(uint256 reserveId, uint256 amount, address onBehalfOf, uint16 referralCode)
124:         external
125:         payable
126:         notPaused
127:         nonReentrant // <= FOUND
128:         returns (uint256 eTokenAmount)
129:     {
130:         eTokenAmount = _deposit(reserveId, amount, address(this));
131: 
132:         address stakingPool = reserves[reserveId].stakingAddress;
133:         require(stakingPool != address(0), "Address=0");
134:         IERC20(getETokenAddress(reserveId)).approve(stakingPool, eTokenAmount);
135:         IStakingRewards(stakingPool).stake(eTokenAmount, onBehalfOf);
136: 
137:         
138:         if (msg.value > 0) {
139:             refundETH();
140:         }
141: 
142:         
143:         emit Deposited(reserveId, _msgSender(), onBehalfOf, amount, eTokenAmount, referralCode);
144:     }

['160']

160:     function redeem(uint256 reserveId, uint256 eTokenAmount, address to, bool receiveNativeETH)
161:         public
162:         payable
163:         notPaused
164:         nonReentrant // <= FOUND
165:         avoidUsingNativeEther
166:         returns (uint256)
167:     {
168:         DataTypes.ReserveData storage reserve = getReserve(reserveId);
169: 
170:         if (eTokenAmount == type(uint256).max) {
171:             eTokenAmount = IExtraInterestBearingToken(reserve.eTokenAddress).balanceOf(_msgSender());
172:         }
173:         
174:         IERC20(reserve.eTokenAddress).safeTransferFrom(_msgSender(), address(this), eTokenAmount);
175: 
176:         
177:         uint256 underlyingTokenAmount = _redeem(reserveId, eTokenAmount, to, receiveNativeETH);
178: 
179:         emit Redeemed(reserveId, _msgSender(), to, eTokenAmount, underlyingTokenAmount);
180: 
181:         return (underlyingTokenAmount);
182:     }

['185']

185:     function unStakeAndWithdraw(uint256 reserveId, uint256 eTokenAmount, address to, bool receiveNativeETH)
186:         external
187:         payable
188:         notPaused
189:         nonReentrant // <= FOUND
190:         avoidUsingNativeEther
191:         returns (uint256)
192:     {
193:         address stakingPool = reserves[reserveId].stakingAddress;
194:         require(stakingPool != address(0), "Address=0");
195: 
196:         IStakingRewards(stakingPool).withdrawByLendingPool(eTokenAmount, _msgSender(), address(this));
197: 
198:         uint256 underlyingTokenAmount = _redeem(reserveId, eTokenAmount, to, receiveNativeETH);
199: 
200:         emit Redeemed(reserveId, _msgSender(), to, eTokenAmount, underlyingTokenAmount);
201: 
202:         return (underlyingTokenAmount);
203:     }

['256']

256:     function newDebtPosition(uint256 reserveId) external notPaused nonReentrant returns (uint256 debtId) { // <= FOUND
257:         DataTypes.ReserveData storage reserve = getReserve(reserveId);
258:         require(!reserve.getFrozen(), Errors.VL_RESERVE_FROZEN);
259:         require(reserve.getBorrowingEnabled(), Errors.VL_BORROWING_NOT_ENABLED);
260: 
261:         debtId = nextDebtPositionId;
262:         nextDebtPositionId = nextDebtPositionId + 1;
263:         DataTypes.DebtPositionData storage newPosition = debtPositions[debtId];
264:         newPosition.owner = _msgSender();
265: 
266:         reserve.updateState(getTreasury());
267:         reserve.updateInterestRates();
268: 
269:         newPosition.reserveId = reserveId;
270:         newPosition.borrowedIndex = reserve.borrowingIndex;
271:     }

['282']

282:     function borrow(address onBehalfOf, uint256 debtId, uint256 amount) external notPaused nonReentrant { // <= FOUND
283:         require(borrowingWhiteList[_msgSender()], Errors.VL_BORROWING_CALLER_NOT_IN_WHITELIST);
284: 
285:         DataTypes.DebtPositionData storage debtPosition = debtPositions[debtId];
286:         require(_msgSender() == debtPosition.owner, Errors.VL_INVALID_DEBT_OWNER);
287: 
288:         DataTypes.ReserveData storage reserve = getReserve(debtPosition.reserveId);
289:         require(!reserve.getFrozen(), Errors.VL_RESERVE_FROZEN);
290:         require(reserve.getBorrowingEnabled(), Errors.VL_BORROWING_NOT_ENABLED);
291: 
292:         
293:         reserve.updateState(getTreasury());
294:         updateDebtPosition(debtPosition, reserve.borrowingIndex);
295: 
296:         
297:         
298:         
299:         uint256 credit = credits[debtPosition.reserveId][_msgSender()];
300:         require(amount <= credit, Errors.VL_OUT_OF_CREDITS);
301:         credits[debtPosition.reserveId][_msgSender()] = credit.sub(amount);
302: 
303:         require(amount <= reserve.availableLiquidity(), Errors.LP_NOT_ENOUGH_LIQUIDITY_TO_BORROW);
304:         reserve.totalBorrows += amount;
305:         debtPosition.borrowed += amount;
306:         
307:         IExtraInterestBearingToken(reserve.eTokenAddress).transferUnderlyingTo(_msgSender(), amount);
308: 
309:         reserve.updateInterestRates();
310: 
311:         emit Borrow(debtPosition.reserveId, _msgSender(), onBehalfOf, amount);
312:     }

['324']

324:     function repay(address onBehalfOf, uint256 debtId, uint256 amount)
325:         external
326:         notPaused
327:         nonReentrant // <= FOUND
328:         returns (uint256)
329:     {
330:         require(borrowingWhiteList[_msgSender()], Errors.VL_BORROWING_CALLER_NOT_IN_WHITELIST);
331: 
332:         DataTypes.DebtPositionData storage debtPosition = debtPositions[debtId];
333:         require(_msgSender() == debtPosition.owner, Errors.VL_INVALID_DEBT_OWNER);
334: 
335:         DataTypes.ReserveData storage reserve = getReserve(debtPosition.reserveId);
336: 
337:         
338:         reserve.updateState(getTreasury());
339:         updateDebtPosition(debtPosition, reserve.borrowingIndex);
340: 
341:         
342:         
343:         
344:         uint256 credit = credits[debtPosition.reserveId][_msgSender()];
345:         credits[debtPosition.reserveId][_msgSender()] = credit.add(amount);
346: 
347:         if (amount > debtPosition.borrowed) {
348:             amount = debtPosition.borrowed;
349:         }
350:         reserve.totalBorrows = reserve.totalBorrows.sub(amount);
351:         debtPosition.borrowed = debtPosition.borrowed.sub(amount);
352: 
353:         
354:         IERC20(reserve.underlyingTokenAddress).safeTransferFrom(_msgSender(), reserve.eTokenAddress, amount);
355: 
356:         reserve.updateInterestRates();
357: 
358:         emit Repay(debtPosition.reserveId, onBehalfOf, _msgSender(), amount);
359:         return amount;
360:     }

[Low-14] No limits when setting min/max amounts

Resolution

When settings min/max state variables, ensure there a require checks in place to prevent incorrect values from being set

Num of instances: 3

Findings

Click to show findings

['610']

610:     function setBorrowingRateConfig( // <= FOUND
611:         uint256 reserveId,
612:         uint16 utilizationA,
613:         uint16 borrowingRateA,
614:         uint16 utilizationB,
615:         uint16 borrowingRateB,
616:         uint16 maxBorrowingRate // <= FOUND
617:     ) public onlyOwner notPaused {
618:         DataTypes.ReserveData storage reserve = reserves[reserveId];
619:         setBorrowingRateConfig(reserve, utilizationA, borrowingRateA, utilizationB, borrowingRateB, maxBorrowingRate); // <= FOUND
620: 
621:         emit SetInterestRateConfig(
622:             reserveId, utilizationA, borrowingRateA, utilizationB, borrowingRateB, maxBorrowingRate // <= FOUND
623:         );
624:     }

['48']

48:     function setMaxPriceAge(uint256 _maxPriceAge) external onlyOracleManager { // <= FOUND
49:         maxPriceAge = _maxPriceAge; // <= FOUND
50:     }

['140']

140:     function setMinPositionSize(uint256 _minPositionSize) external onlyOwner { // <= FOUND
141:         minPositionSize = _minPositionSize; // <= FOUND
142:     }

[Low-15] Initializer function can be front run

Resolution

In Solidity contract deployment, not making the initialize() function call atomic with the contract creation can leave a vulnerability window. A malicious actor could exploit this time gap and call initialize() before the intended initialization. This action could disrupt the contract's setup, potentially necessitating a full contract re-deployment to ensure proper initialization. To mitigate such risks, it's advised to use a factory contract. This factory contract can be programmed to deploy and initialize a new contract in a single atomic transaction, closing the window of vulnerability and ensuring correct and secure contract initialization.

Num of instances: 3

Findings

Click to show findings

['57']

57:     function initialize(address _addressRegistry, address _WETH9) public initializer  // <= FOUND

['18']

18:     function initialize(
19:         address _pyth,
20:         address _oracleManager,
21:         address[] memory initialTokens,
22:         bytes32[] memory priceIds
23:     ) external initializer  // <= FOUND

['83']

83:     function initialize(
84:         address _addressProvider,
85:         address _vaultRegistry,
86:         address _shadowV3PoolAddress,
87:         address _initialPositionImplementation
88:     ) public initializer  // <= FOUND

[Low-16] Use _disableInitializers() to ensure initialization occurs once

Resolution

disableInitializers() should be used in upgradeable contracts to ensure the initializer functions can't be called more than once. In upgradeable contracts, initializer functions set initial state and values, but if they can be invoked multiple times, it could lead to unexpected behavior or vulnerabilities. By calling disableInitializers() after the initial setup, you essentially lock the initializer functions, ensuring they can only be called once during the contract's lifecycle. This prevents repeated initializations, helping to maintain the integrity and security of the contract, and providing a safeguard against potential manipulation or misuse of the initialization functions.

Num of instances: 2

Findings

Click to show findings

['10']

10: contract PrimaryPriceOracle is IPriceOracle, Initializable 

['30']

30: contract ShadowRangeVault is
31:     Initializable,
32:     ReentrancyGuardUpgradeable,
33:     Ownable2StepUpgradeable,
34:     PaymentsUpgradeable,
35:     IVault
36: 

[Low-17] The function decimals() is not part of the ERC20 standard

Resolution

The decimals() function in an ERC20 token contract is used to specify how many decimal places the token can be divided into. However, it should be used with caution because not all ERC20 token contracts implement decimals(), and the function isn't required by the ERC20 standard. Calling decimals() on a contract that doesn't implement it will result in a runtime error. Moreover, even when implemented, the returned value can be manipulated or artificially set, which may cause unexpected behavior. Therefore, always verify the decimal count independently if precision is crucial for your contract logic. When interacting with other ERC20 tokens, consider implementing safeguards or checks to handle potential errors from calling decimals().

Num of instances: 3

Findings

Click to show findings

['76']

76:         uint8 decimals = ERC20(asset).decimals(); // <= FOUND

['94']

94:         token0Decimals = IERC20Metadata(token0).decimals(); // <= FOUND

['95']

95:         token1Decimals = IERC20Metadata(token1).decimals(); // <= FOUND

[Low-18] Minting to the zero address should be avoided

Resolution

Minting tokens to the zero address in Solidity is a potential pitfall that should be carefully guarded against. The zero address (0x0) is generally used as a default value and represents an uninitialized or null address. Minting tokens to this address effectively burns them, making them inaccessible and permanently removing them from the total supply. This could lead to unintended token loss if performed accidentally. To prevent this, it's important to include a check in the minting function to ensure that the target address is not the zero address. Using a library like OpenZeppelin's Address can provide utility functions like requireNonZero, which simplifies this check and enhances the security of the minting function.

Num of instances: 1

Findings

Click to show findings

['47']

47:     function mint(address user, uint256 amount) external onlyLendingPool nonReentrant {
48:         _mint(user, amount); // <= FOUND
49:         emit Mint(user, amount);
50:     }

[Low-19] No limits when setting fees

Resolution

When settings fees state variables, ensure there a require checks in place to prevent incorrect values from being set. This is particularly important when dealing with fee values as without checks fees can be set to 100%

Num of instances: 3

Findings

Click to show findings

['602']

602:     function setReserveFeeRate(uint256 reserveId, uint16 _rate) public onlyOwner notPaused {
603:         require(_rate <= Constants.PERCENT_100, "invalid percent");
604:         DataTypes.ReserveData storage reserve = reserves[reserveId];
605:         reserve.reserveFeeRate = _rate;
606: 
607:         emit SetReserveFeeRate(reserveId, _rate);
608:     }

['114']

114:     function setLiquidationFeeParams(uint256 _liquidationFee, uint256 _liquidationCallerFeeRatio) external onlyOwner {
115:         liquidationFee = _liquidationFee;
116:         liquidationCallerFee = _liquidationCallerFeeRatio;
117:     }

['144']

144:     function setPerformanceFee(uint256 _performanceFee) external onlyOwner {
145:         performanceFee = _performanceFee;
146:     }

[Low-20] Loss of precision

Resolution

Dividing by large numbers in Solidity can cause a loss of precision due to the language's inherent integer division behavior. Solidity does not support floating-point arithmetic, and as a result, division between integers yields an integer result, truncating any fractional part. When dividing by a large number, the resulting value may become significantly smaller, leading to a loss of precision, as the fractional part is discarded.

Num of instances: 2

Findings

Click to show findings

['447']

447:     function getDebtRatioFromAmounts( // <= FOUND
448:         uint256 amount0Principal,
449:         uint256 amount1Principal,
450:         uint256 amount0Borrow,
451:         uint256 amount1Borrow
452:     ) public view returns (uint256 debtRatio) {
453:         uint256 principalValue = amount0Principal * getTokenPrice(token0) / 10 ** token0Decimals
454:             + amount1Principal * getTokenPrice(token1) / 10 ** token1Decimals;
455:         uint256 borrowValue = amount0Borrow * getTokenPrice(token0) / 10 ** token0Decimals
456:             + amount1Borrow * getTokenPrice(token1) / 10 ** token1Decimals;
457:         return borrowValue * 10000 / (principalValue + borrowValue); // <= FOUND
458:     }

['460']

460:     function getDebtRatio(uint256 positionId) public view returns (uint256 debtRatio) { // <= FOUND
461:         return getDebtValue(positionId) * 10000 / getPositionValue(positionId); // <= FOUND
462:     }

[Low-21] Missing zero address check in constructor

Resolution

In Solidity, constructors often take address parameters to initialize important components of a contract, such as owner or linked contracts. However, without a check, there's a risk that an address parameter could be mistakenly set to the zero address (0x0). This could occur due to a mistake or oversight during contract deployment. A zero address in a crucial role can cause serious issues, as it cannot perform actions like a normal address, and any funds sent to it are irretrievable. Therefore, it's crucial to include a zero address check in constructors to prevent such potential problems. If a zero address is detected, the constructor should revert the transaction.

Num of instances: 4

Findings

Click to show findings

['10']

10:     constructor(address _weth9) { // <= FOUND
11:         setAddress(AddressId.ADDRESS_ID_WETH9, _weth9);
12:     }

['18']

18:     constructor(address _WETH9) { // <= FOUND
19:         WETH9 = _WETH9;
20:     }

['62']

62:     constructor(address _stakingToken) { // <= FOUND
63:         stakedToken = IERC20(_stakingToken);
64:         lendingPool = msg.sender;
65:     }

['34']

34:     constructor(address _addressProvider) { // <= FOUND
35:         addressProvider = _addressProvider;
36:     }

[Low-22] Use of onlyOwner functions can be lost

Resolution

In Solidity, renouncing ownership of a contract essentially transfers ownership to the zero address. This is an irreversible operation and has considerable security implications. If the renounceOwnership function is used, the contract will lose the ability to perform any operations that are limited to the owner. This can be problematic if there are any bugs, flaws, or unexpected events that require owner intervention to resolve. Therefore, in some instances, it is better to disable or omit the renounceOwnership function, and instead implement a secure transferOwnership function. This way, if necessary, ownership can be transferred to a new, trusted party without losing the potential for administrative intervention.

Num of instances: 5

Findings

Click to show findings

['7']

7: contract AddressRegistry is IAddressRegistry, Ownable  // <= FOUND

['13']

13: contract StakingRewards is Ownable, IStakingRewards  // <= FOUND

['11']

11: contract VaultRegistry is Ownable, IVaultFactory  // <= FOUND

['24']

24: contract LendingPool is ILendingPool, OwnableUpgradeable, PaymentsUpgradeable, ReentrancyGuardUpgradeable  // <= FOUND

['30']

30: contract ShadowRangeVault is
31:     Initializable,
32:     ReentrancyGuardUpgradeable,
33:     Ownable2StepUpgradeable, // <= FOUND
34:     PaymentsUpgradeable,
35:     IVault
36: 

[Low-23] Off-by-one timestamp error

Resolution

In Solidity, using >= or <= to compare against block.timestamp (alias now) may introduce off-by-one errors due to the fact that block.timestamp is only updated once per block and its value remains constant throughout the block's execution. If an operation happens at the exact second when block.timestamp changes, it could result in unexpected behavior. To avoid this, it's safer to use strict inequality operators (> or <). For instance, if a condition should only be met after a certain time, use block.timestamp > time rather than block.timestamp >= time. This way, potential off-by-one errors due to the exact timing of block mining are mitigated, leading to safer, more predictable contract behavior.

Num of instances: 1

Findings

Click to show findings

['67']

67:     function rewardPerToken(address rewardToken) public view returns (uint256) { // <= FOUND
68:         if (block.timestamp <= rewardData[rewardToken].startTime) {
69:             
70:             return rewardData[rewardToken].rewardPerTokenStored;
71:         }
72: 
73:         uint256 dt =
74:             Math.min(rewardData[rewardToken].endTime, block.timestamp) - (rewardData[rewardToken].lastUpdateTime);
75: 
76:         if (dt == 0 || totalStaked == 0) {
77:             return rewardData[rewardToken].rewardPerTokenStored;
78:         }
79: 
80:         return rewardData[rewardToken].rewardPerTokenStored
81:             + (rewardData[rewardToken].rewardRate * dt * 1e18) / totalStaked;
82:     }

[Low-24] approve()/safeApprove() may revert if the current approval is not zero

Resolution

Reason: ERC20 token standard has an inherent race condition problem in its approve function. This issue particularly arises when you try to change an existing non-zero allowance. For tokens like USDT, which enforce anti-race condition checks, trying to change a non-zero allowance directly might fail.

Resolution: Always set the allowance to zero before setting a new value. By approving the spender to 0 first, it mitigates the risk of the race condition. This ensures you have a consistent allowance setup and prevent any potential transaction failure or mishap due to tokens enforcing anti-race condition checks.

Num of instances: 17

Findings

Click to show findings

['123']

123:     function depositAndStake(uint256 reserveId, uint256 amount, address onBehalfOf, uint16 referralCode)
124:         external
125:         payable
126:         notPaused
127:         nonReentrant
128:         returns (uint256 eTokenAmount)
129:     {
130:         eTokenAmount = _deposit(reserveId, amount, address(this));
131: 
132:         address stakingPool = reserves[reserveId].stakingAddress;
133:         require(stakingPool != address(0), "Address=0");
134:         IERC20(getETokenAddress(reserveId)).approve(stakingPool, eTokenAmount); // <= FOUND
135:         IStakingRewards(stakingPool).stake(eTokenAmount, onBehalfOf);
136: 
137:         
138:         if (msg.value > 0) {
139:             refundETH();
140:         }
141: 
142:         
143:         emit Deposited(reserveId, _msgSender(), onBehalfOf, amount, eTokenAmount, referralCode);
144:     }

['145']

145:     function closePosition()
146:         external
147:         onlyVault
148:         returns (
149:             uint128 liquidity,
150:             uint256 token0Reduced,
151:             uint256 token1Reduced,
152:             uint256 token0Balance,
153:             uint256 token1Balance
154:         )
155:     {
156:         _claimFees();
157:         _claimRewards();
158: 
159:         (,,,,, liquidity,,,,) =
160:             IShadowNonfungiblePositionManager(getShadowNonfungiblePositionManager()).positions(shadowPositionId);
161: 
162:         (token0Reduced, token1Reduced) = _decreasePosition(liquidity);
163: 
164:         (uint256 currentDebt0, uint256 currentDebt1) = IVault(vault).getPositionDebt(positionId);
165: 
166:         if (currentDebt0 > 0) {
167:             
168:             if (currentDebt0 > token0Reduced) {
169:                 
170:                 
171:                 
172:                 uint256 amount1Excess = token1Reduced - currentDebt1;
173:                 _swapTokenExactInput(token1, token0, amount1Excess, currentDebt0 - token0Reduced);
174:             }
175:             IERC20(token0).approve(vault, currentDebt0); // <= FOUND
176:         }
177: 
178:         if (currentDebt1 > 0) {
179:             
180:             if (currentDebt1 > token1Reduced) {
181:                 
182:                 
183:                 
184:                 uint256 amount0Excess = token0Reduced - currentDebt0;
185:                 _swapTokenExactInput(token0, token1, amount0Excess, currentDebt1 - token1Reduced);
186:             }
187:             IERC20(token1).approve(vault, currentDebt1); // <= FOUND
188:         }
189: 
190:         IVault(vault).repayExact(positionId, currentDebt0, currentDebt1);
191: 
192:         token0Balance = IERC20(token0).balanceOf(address(this));
193:         token1Balance = IERC20(token1).balanceOf(address(this));
194:         if (token0Balance > 0) {
195:             pay(token0, address(this), positionOwner, token0Balance);
196:         }
197:         if (token1Balance > 0) {
198:             pay(token1, address(this), positionOwner, token1Balance);
199:         }
200:     }

['254']

254:     function liquidatePosition(address caller)
255:         external
256:         onlyVault
257:         returns (
258:             uint128 liquidity,
259:             uint256 token0Reduced,
260:             uint256 token1Reduced,
261:             uint256 token0Fees,
262:             uint256 token1Fees,
263:             uint256 token0Left,
264:             uint256 token1Left
265:         )
266:     {
267:         _claimFees();
268:         _claimRewards();
269:         (,,,,, liquidity,,,,) =
270:             IShadowNonfungiblePositionManager(getShadowNonfungiblePositionManager()).positions(shadowPositionId);
271: 
272:         (token0Reduced, token1Reduced) = _decreasePosition(liquidity);
273:         (uint256 currentDebt0, uint256 currentDebt1) = IVault(vault).getPositionDebt(positionId);
274: 
275:         LiquidationFeeVars memory vars = LiquidationFeeVars({
276:             liquidationFee: IVault(vault).liquidationFee(),
277:             liquidationCallerFee: IVault(vault).liquidationCallerFee(),
278:             liquidationFeeRecipient: IVault(vault).liquidationFeeRecipient()
279:         });
280:         
281:         if (token0Reduced > 0) {
282:             token0Fees = token0Reduced * vars.liquidationFee / 10000;
283:             pay(token0, address(this), vars.liquidationFeeRecipient, token0Fees);
284: 
285:             uint256 token0CallerFees = token0Fees * vars.liquidationCallerFee / 10000;
286:             if (token0CallerFees > 0) {
287:                 pay(token0, address(this), caller, token0CallerFees);
288:             }
289: 
290:             token0Reduced = token0Reduced - token0Fees - token0CallerFees;
291:         }
292: 
293:         if (token1Reduced > 0) {
294:             token1Fees = token1Reduced * vars.liquidationFee / 10000;
295: 
296:             pay(token1, address(this), vars.liquidationFeeRecipient, token1Fees);
297: 
298:             uint256 token1CallerFees = token1Fees * vars.liquidationCallerFee / 10000;
299:             if (token1CallerFees > 0) {
300:                 pay(token1, address(this), caller, token1CallerFees);
301:             }
302: 
303:             token1Reduced = token1Reduced - token1Fees - token1CallerFees;
304:         }
305: 
306:         if (currentDebt0 > 0) {
307:             
308:             if (currentDebt0 > token0Reduced) {
309:                 uint256 amount1Excess = token1Reduced - currentDebt1;
310:                 _swapTokenExactInput(token1, token0, amount1Excess, currentDebt0 - token0Reduced);
311:             }
312:             IERC20(token0).approve(vault, currentDebt0); // <= FOUND
313:         }
314: 
315:         if (currentDebt1 > 0) {
316:             
317:             if (currentDebt1 > token1Reduced) {
318:                 uint256 amount0Excess = token0Reduced - currentDebt0;
319:                 _swapTokenExactInput(token0, token1, amount0Excess, currentDebt1 - token1Reduced);
320:             }
321:             IERC20(token1).approve(vault, currentDebt1); // <= FOUND
322:         }
323: 
324:         IVault(vault).repayExact(positionId, currentDebt0, currentDebt1);
325: 
326:         token0Left = IERC20(token0).balanceOf(address(this));
327:         token1Left = IERC20(token1).balanceOf(address(this));
328:         if (token0Left > 0) {
329:             pay(token0, address(this), positionOwner, token0Left);
330:         }
331:         if (token1Left > 0) {
332:             pay(token1, address(this), positionOwner, token1Left);
333:         }
334:     }

['432']

432:     function _swapTokenExactInput(address tokenIn, address tokenOut, uint256 amountIn, uint256 amountOutMinimum)
433:         internal
434:         returns (uint256 amountOut)
435:     {
436:         address router =
437:             IAddressRegistry(IVault(vault).addressProvider()).getAddress(AddressId.ADDRESS_ID_SHADOW_ROUTER);
438:         IERC20(tokenIn).approve(router, amountIn); // <= FOUND
439: 
440:         amountOut = IShadowSwapRouter(router).exactInputSingle(
441:             IShadowSwapRouter.ExactInputSingleParams({
442:                 tokenIn: tokenIn,
443:                 tokenOut: tokenOut,
444:                 tickSpacing: tickSpacing,
445:                 recipient: address(this),
446:                 deadline: block.timestamp,
447:                 amountIn: amountIn,
448:                 amountOutMinimum: amountOutMinimum,
449:                 sqrtPriceLimitX96: 0
450:             })
451:         );
452:     }

['454']

454:     function _swapTokenExactOutput(address tokenIn, address tokenOut, uint256 amountOut, uint256 amountInMaximum)
455:         internal
456:         returns (uint256 amountIn)
457:     {
458:         address router =
459:             IAddressRegistry(IVault(vault).addressProvider()).getAddress(AddressId.ADDRESS_ID_SHADOW_ROUTER);
460:         IERC20(tokenIn).approve(router, amountInMaximum); // <= FOUND
461: 
462:         amountIn = IShadowSwapRouter(router).exactOutputSingle(
463:             IShadowSwapRouter.ExactOutputSingleParams({
464:                 tokenIn: tokenIn,
465:                 tokenOut: tokenOut,
466:                 tickSpacing: tickSpacing,
467:                 recipient: address(this),
468:                 deadline: block.timestamp,
469:                 amountOut: amountOut,
470:                 amountInMaximum: amountInMaximum,
471:                 sqrtPriceLimitX96: 0
472:             })
473:         );
474:     }

['131']

131:     function setReserveIds(uint256 _token0ReserveId, uint256 _token1ReserveId) external onlyOwner {
132:         address lendingPool = getLendingPool();
133: 
134:         IERC20(token0).approve(lendingPool, type(uint256).max); // <= FOUND
135:         IERC20(token1).approve(lendingPool, type(uint256).max); // <= FOUND
136:         token0ReserveId = _token0ReserveId;
137:         token1ReserveId = _token1ReserveId;
138:     }

['123']

123:     function depositAndStake(uint256 reserveId, uint256 amount, address onBehalfOf, uint16 referralCode)
124:         external
125:         payable
126:         notPaused
127:         nonReentrant
128:         returns (uint256 eTokenAmount)
129:     {
130:         eTokenAmount = _deposit(reserveId, amount, address(this));
131: 
132:         address stakingPool = reserves[reserveId].stakingAddress;
133:         require(stakingPool != address(0), "Address=0");
134:         IERC20(getETokenAddress(reserveId)).approve(stakingPool, eTokenAmount); // <= FOUND
135:         IStakingRewards(stakingPool).stake(eTokenAmount, onBehalfOf);
136: 
137:         
138:         if (msg.value > 0) {
139:             refundETH();
140:         }
141: 
142:         
143:         emit Deposited(reserveId, _msgSender(), onBehalfOf, amount, eTokenAmount, referralCode);
144:     }

['41']

41:     function initialize(uint256 _positionId) external {
42:         require(vault == address(0), "Already initialized");
43:         xShadow = 0x5050bc082FF4A74Fb6B0B04385dEfdDB114b2424;
44:         x33 = 0x3333111A391cC08fa51353E9195526A70b333333;
45:         vault = msg.sender;
46:         positionId = _positionId;
47: 
48:         token0 = IVault(msg.sender).token0();
49:         token1 = IVault(msg.sender).token1();
50: 
51:         IERC20(token0).approve(vault, type(uint256).max); // <= FOUND
52:         IERC20(token1).approve(vault, type(uint256).max); // <= FOUND
53: 
54:         shadowV3Pool = IShadowRangeVault(vault).shadowV3Pool();
55:         tickSpacing = IShadowV3Pool(shadowV3Pool).tickSpacing();
56: 
57:         _initializePayments(IPayments(msg.sender).WETH9());
58: 
59:         addressProvider = IVault(msg.sender).addressProvider();
60:     }

['83']

83:     function openPosition(OpenPositionParams memory params)
84:         external
85:         onlyVault
86:         returns (uint256 tokenId, uint128 liquidity, uint256 amount0, uint256 amount1)
87:     {
88:         uint256 token0Balance = IERC20(token0).balanceOf(address(this));
89:         if (params.amount0Desired > token0Balance) {
90:             _swapTokenExactInput(
91:                 token1,
92:                 token0,
93:                 IERC20(token1).balanceOf(address(this)) - params.amount1Desired,
94:                 params.amount0Desired - token0Balance
95:             );
96:         }
97: 
98:         uint256 token1Balance = IERC20(token1).balanceOf(address(this));
99:         if (params.amount1Desired > token1Balance) {
100:             _swapTokenExactInput(
101:                 token0,
102:                 token1,
103:                 IERC20(token0).balanceOf(address(this)) - params.amount0Desired,
104:                 params.amount1Desired - token1Balance
105:             );
106:         }
107:         address shadowNonfungiblePositionManager = getShadowNonfungiblePositionManager();
108: 
109:         IERC20(token0).approve(shadowNonfungiblePositionManager, params.amount0Desired); // <= FOUND
110:         IERC20(token1).approve(shadowNonfungiblePositionManager, params.amount1Desired); // <= FOUND
111: 
112:         (tokenId, liquidity, amount0, amount1) = IShadowNonfungiblePositionManager(shadowNonfungiblePositionManager)
113:             .mint(
114:             IShadowNonfungiblePositionManager.MintParams({
115:                 token0: token0,
116:                 token1: token1,
117:                 tickSpacing: tickSpacing,
118:                 tickLower: params.tickLower,
119:                 tickUpper: params.tickUpper,
120:                 amount0Desired: params.amount0Desired,
121:                 amount1Desired: params.amount1Desired,
122:                 amount0Min: 0,
123:                 amount1Min: 0,
124:                 recipient: address(this),
125:                 deadline: block.timestamp
126:             })
127:         );
128: 
129:         shadowPositionId = tokenId;
130:         positionOwner = params.positionOwner;
131: 
132:         unwrapWETH9(0, positionOwner);
133: 
134:         token0Balance = IERC20(token0).balanceOf(address(this));
135:         token1Balance = IERC20(token1).balanceOf(address(this));
136: 
137:         if (token0Balance > 0) {
138:             pay(token0, address(this), positionOwner, token0Balance);
139:         }
140:         if (token1Balance > 0) {
141:             pay(token1, address(this), positionOwner, token1Balance);
142:         }
143:     }

['145']

145:     function closePosition()
146:         external
147:         onlyVault
148:         returns (
149:             uint128 liquidity,
150:             uint256 token0Reduced,
151:             uint256 token1Reduced,
152:             uint256 token0Balance,
153:             uint256 token1Balance
154:         )
155:     {
156:         _claimFees();
157:         _claimRewards();
158: 
159:         (,,,,, liquidity,,,,) =
160:             IShadowNonfungiblePositionManager(getShadowNonfungiblePositionManager()).positions(shadowPositionId);
161: 
162:         (token0Reduced, token1Reduced) = _decreasePosition(liquidity);
163: 
164:         (uint256 currentDebt0, uint256 currentDebt1) = IVault(vault).getPositionDebt(positionId);
165: 
166:         if (currentDebt0 > 0) {
167:             
168:             if (currentDebt0 > token0Reduced) {
169:                 
170:                 
171:                 
172:                 uint256 amount1Excess = token1Reduced - currentDebt1;
173:                 _swapTokenExactInput(token1, token0, amount1Excess, currentDebt0 - token0Reduced);
174:             }
175:             IERC20(token0).approve(vault, currentDebt0); // <= FOUND
176:         }
177: 
178:         if (currentDebt1 > 0) {
179:             
180:             if (currentDebt1 > token1Reduced) {
181:                 
182:                 
183:                 
184:                 uint256 amount0Excess = token0Reduced - currentDebt0;
185:                 _swapTokenExactInput(token0, token1, amount0Excess, currentDebt1 - token1Reduced);
186:             }
187:             IERC20(token1).approve(vault, currentDebt1); // <= FOUND
188:         }
189: 
190:         IVault(vault).repayExact(positionId, currentDebt0, currentDebt1);
191: 
192:         token0Balance = IERC20(token0).balanceOf(address(this));
193:         token1Balance = IERC20(token1).balanceOf(address(this));
194:         if (token0Balance > 0) {
195:             pay(token0, address(this), positionOwner, token0Balance);
196:         }
197:         if (token1Balance > 0) {
198:             pay(token1, address(this), positionOwner, token1Balance);
199:         }
200:     }

['254']

254:     function liquidatePosition(address caller)
255:         external
256:         onlyVault
257:         returns (
258:             uint128 liquidity,
259:             uint256 token0Reduced,
260:             uint256 token1Reduced,
261:             uint256 token0Fees,
262:             uint256 token1Fees,
263:             uint256 token0Left,
264:             uint256 token1Left
265:         )
266:     {
267:         _claimFees();
268:         _claimRewards();
269:         (,,,,, liquidity,,,,) =
270:             IShadowNonfungiblePositionManager(getShadowNonfungiblePositionManager()).positions(shadowPositionId);
271: 
272:         (token0Reduced, token1Reduced) = _decreasePosition(liquidity);
273:         (uint256 currentDebt0, uint256 currentDebt1) = IVault(vault).getPositionDebt(positionId);
274: 
275:         LiquidationFeeVars memory vars = LiquidationFeeVars({
276:             liquidationFee: IVault(vault).liquidationFee(),
277:             liquidationCallerFee: IVault(vault).liquidationCallerFee(),
278:             liquidationFeeRecipient: IVault(vault).liquidationFeeRecipient()
279:         });
280:         
281:         if (token0Reduced > 0) {
282:             token0Fees = token0Reduced * vars.liquidationFee / 10000;
283:             pay(token0, address(this), vars.liquidationFeeRecipient, token0Fees);
284: 
285:             uint256 token0CallerFees = token0Fees * vars.liquidationCallerFee / 10000;
286:             if (token0CallerFees > 0) {
287:                 pay(token0, address(this), caller, token0CallerFees);
288:             }
289: 
290:             token0Reduced = token0Reduced - token0Fees - token0CallerFees;
291:         }
292: 
293:         if (token1Reduced > 0) {
294:             token1Fees = token1Reduced * vars.liquidationFee / 10000;
295: 
296:             pay(token1, address(this), vars.liquidationFeeRecipient, token1Fees);
297: 
298:             uint256 token1CallerFees = token1Fees * vars.liquidationCallerFee / 10000;
299:             if (token1CallerFees > 0) {
300:                 pay(token1, address(this), caller, token1CallerFees);
301:             }
302: 
303:             token1Reduced = token1Reduced - token1Fees - token1CallerFees;
304:         }
305: 
306:         if (currentDebt0 > 0) {
307:             
308:             if (currentDebt0 > token0Reduced) {
309:                 uint256 amount1Excess = token1Reduced - currentDebt1;
310:                 _swapTokenExactInput(token1, token0, amount1Excess, currentDebt0 - token0Reduced);
311:             }
312:             IERC20(token0).approve(vault, currentDebt0); // <= FOUND
313:         }
314: 
315:         if (currentDebt1 > 0) {
316:             
317:             if (currentDebt1 > token1Reduced) {
318:                 uint256 amount0Excess = token0Reduced - currentDebt0;
319:                 _swapTokenExactInput(token0, token1, amount0Excess, currentDebt1 - token1Reduced);
320:             }
321:             IERC20(token1).approve(vault, currentDebt1); // <= FOUND
322:         }
323: 
324:         IVault(vault).repayExact(positionId, currentDebt0, currentDebt1);
325: 
326:         token0Left = IERC20(token0).balanceOf(address(this));
327:         token1Left = IERC20(token1).balanceOf(address(this));
328:         if (token0Left > 0) {
329:             pay(token0, address(this), positionOwner, token0Left);
330:         }
331:         if (token1Left > 0) {
332:             pay(token1, address(this), positionOwner, token1Left);
333:         }
334:     }

['349']

349:     function claimRewards() external onlyVault {
350:         _claimFees();
351:         _claimRewards();
352:     }

['354']

354:     function _claimFees() internal {
355:         IShadowNonfungiblePositionManager(getShadowNonfungiblePositionManager()).collect(
356:             IShadowNonfungiblePositionManager.CollectParams({
357:                 tokenId: shadowPositionId,
358:                 recipient: address(this),
359:                 amount0Max: type(uint128).max,
360:                 amount1Max: type(uint128).max
361:             })
362:         );
363: 
364:         uint256 token0Fees = IERC20(token0).balanceOf(address(this));
365:         uint256 token1Fees = IERC20(token1).balanceOf(address(this));
366:         if (token0Fees > 0) {
367:             IERC20(token0).approve(vault, token0Fees); // <= FOUND
368:             IVault(vault).claimCallback(token0, token0Fees);
369:         }
370:         if (token1Fees > 0) {
371:             IERC20(token1).approve(vault, token1Fees); // <= FOUND
372:             IVault(vault).claimCallback(token1, token1Fees);
373:         }
374:     }

['376']

376:     function _claimRewards() internal {
377:         address shadowGauge = IShadowRangeVault(vault).shadowGauge();
378:         if (shadowGauge != address(0)) {
379:             address[] memory tokens = IShadowGaugeV3(shadowGauge).getRewardTokens();
380:             IShadowGaugeV3(shadowGauge).getReward(shadowPositionId, tokens);
381: 
382:             for (uint256 i = 0; i < tokens.length; i++) {
383:                 uint256 balance = IERC20(tokens[i]).balanceOf(address(this));
384:                 if (balance > 0) {
385:                     if (tokens[i] == xShadow) {
386:                         
387:                         
388:                         
389:                         
390:                         
391:                         if (IShadowX33(x33).isUnlocked()) {
392:                             address x33Adapter = getX33Adapter();
393:                             IERC20(tokens[i]).approve(x33Adapter, balance); // <= FOUND
394:                             IERC4626(x33Adapter).deposit(balance, address(this));
395:                             balance = IERC20(x33).balanceOf(address(this));
396:                             IERC20(x33).approve(vault, balance); // <= FOUND
397:                             IVault(vault).claimCallback(x33, balance);
398:                         }
399:                     } else {
400:                         IERC20(tokens[i]).approve(vault, balance); // <= FOUND
401:                         IVault(vault).claimCallback(tokens[i], balance);
402:                     }
403:                 }
404:             }
405:         }
406:     }

['432']

432:     function _swapTokenExactInput(address tokenIn, address tokenOut, uint256 amountIn, uint256 amountOutMinimum)
433:         internal
434:         returns (uint256 amountOut)
435:     {
436:         address router =
437:             IAddressRegistry(IVault(vault).addressProvider()).getAddress(AddressId.ADDRESS_ID_SHADOW_ROUTER);
438:         IERC20(tokenIn).approve(router, amountIn); // <= FOUND
439: 
440:         amountOut = IShadowSwapRouter(router).exactInputSingle(
441:             IShadowSwapRouter.ExactInputSingleParams({
442:                 tokenIn: tokenIn,
443:                 tokenOut: tokenOut,
444:                 tickSpacing: tickSpacing,
445:                 recipient: address(this),
446:                 deadline: block.timestamp,
447:                 amountIn: amountIn,
448:                 amountOutMinimum: amountOutMinimum,
449:                 sqrtPriceLimitX96: 0
450:             })
451:         );
452:     }

['454']

454:     function _swapTokenExactOutput(address tokenIn, address tokenOut, uint256 amountOut, uint256 amountInMaximum)
455:         internal
456:         returns (uint256 amountIn)
457:     {
458:         address router =
459:             IAddressRegistry(IVault(vault).addressProvider()).getAddress(AddressId.ADDRESS_ID_SHADOW_ROUTER);
460:         IERC20(tokenIn).approve(router, amountInMaximum); // <= FOUND
461: 
462:         amountIn = IShadowSwapRouter(router).exactOutputSingle(
463:             IShadowSwapRouter.ExactOutputSingleParams({
464:                 tokenIn: tokenIn,
465:                 tokenOut: tokenOut,
466:                 tickSpacing: tickSpacing,
467:                 recipient: address(this),
468:                 deadline: block.timestamp,
469:                 amountOut: amountOut,
470:                 amountInMaximum: amountInMaximum,
471:                 sqrtPriceLimitX96: 0
472:             })
473:         );
474:     }

['131']

131:     function setReserveIds(uint256 _token0ReserveId, uint256 _token1ReserveId) external onlyOwner {
132:         address lendingPool = getLendingPool();
133: 
134:         IERC20(token0).approve(lendingPool, type(uint256).max); // <= FOUND
135:         IERC20(token1).approve(lendingPool, type(uint256).max); // <= FOUND
136:         token0ReserveId = _token0ReserveId;
137:         token1ReserveId = _token1ReserveId;
138:     }

[Low-25] Constant decimal values

Resolution

The use of fixed decimal values such as 1e18 or 1e8 in Solidity contracts can lead to inaccuracies, bugs, and vulnerabilities, particularly when interacting with tokens having different decimal configurations. Not all ERC20 tokens follow the standard 18 decimal places, and assumptions about decimal places can lead to miscalculations.

Resolution: Always retrieve and use the decimals() function from the token contract itself when performing calculations involving token amounts. This ensures that your contract correctly handles tokens with any number of decimal places, mitigating the risk of numerical errors or under/overflows that could jeopardize contract integrity and user funds.

Num of instances: 3

Findings

Click to show findings

['80']

80:         return rewardData[rewardToken].rewardPerTokenStored
81:             + (rewardData[rewardToken].rewardRate * dt * 1e18) / totalStaked; // <= FOUND

['93']

93:         return (balanceOf[user] * d) / 1e18 + userRewardsClaimable[user][rewardToken]; // <= FOUND

['118']

118:             rewardData[rewardToken].rewardPerTokenStored +=
119:                 (rewardData[rewardToken].rewardRate * dt * 1e18) / totalStaked; // <= FOUND

[Low-26] Arrays can grow in size without a way to shrink them

Resolution

It's a good practice to maintain control over the size of array state variables in Solidity, especially if they are dynamically updated. If a contract includes a mechanism to push items into an array, it should ideally also provide a mechanism to remove items. This is because Solidity arrays don't automatically shrink when items are deleted - their length needs to be manually adjusted.

Ignoring this can lead to bloated and inefficient contracts. For instance, iterating over a large array can cause your contract to hit the block gas limit. Additionally, if entries are only marked for deletion but never actually removed, you may end up dealing with stale or irrelevant data, which can cause logical errors.

Therefore, implementing a method to 'pop' items from arrays helps manage contract's state, improve efficiency and prevent potential issues related to gas limits or stale data. Always ensure to handle potential underflow conditions when popping elements from the array.

Num of instances: 1

Findings

Click to show findings

['20']

20: address[] public rewardTokens; // <= FOUND

[Low-27] Sending tokens in a for loop

Resolution

Performing token transfers in a loop in a Solidity contract is generally not recommended due to various reasons. One of these reasons is the "Fail-Silently" issue.

In a Solidity loop, if one transfer operation fails, it causes the entire transaction to fail. This issue can be particularly troublesome when you're dealing with multiple transfers in one transaction. For instance, if you're looping through an array of recipients to distribute dividends or rewards, a single failed transfer will prevent all the subsequent recipients from receiving their transfers. This could be due to the recipient contract throwing an exception or due to other issues like a gas limit being exceeded.

Moreover, such a design could also inadvertently lead to a situation where a malicious contract intentionally causes a failure when receiving Ether to prevent other participants from getting their rightful transfers. This could open up avenues for griefing attacks in the system.

Resolution: To mitigate this problem, it's typically recommended to follow the "withdraw pattern" in your contracts instead of pushing payments. In this model, each recipient would be responsible for triggering their own payment. This separates each transfer operation, so a failure in one doesn't impact the others. Additionally, it greatly reduces the chance of malicious interference as the control over fund withdrawal lies with the intended recipient and not with an external loop operation.

Num of instances: 4

Findings

Click to show findings

['196']

196:        for (uint256 i = 0; i < rewardTokens.length; i++) {
197:             address rewardToken = rewardTokens[i];
198:             uint256 claimable = userRewardsClaimable[msg.sender][rewardToken];
199:             if (claimable > 0) {
200:                 userRewardsClaimable[msg.sender][rewardToken] = 0;
201:                 require(IERC20(rewardToken).transfer(msg.sender, claimable), "transfer failed"); // <= FOUND
202:                 emit RewardPaid(msg.sender, rewardToken, claimable);
203:             }
204:         }

['196']

196:         for (uint256 i = 0; i < rewardTokens.length; i++) {
197:             address rewardToken = rewardTokens[i];
198:             uint256 claimable = userRewardsClaimable[msg.sender][rewardToken];
199:             if (claimable > 0) {
200:                 userRewardsClaimable[msg.sender][rewardToken] = 0;
201:                 require(IERC20(rewardToken).transfer(msg.sender, claimable), "transfer failed"); // <= FOUND
202:                 emit RewardPaid(msg.sender, rewardToken, claimable);
203:             }
204:         }

['196']

196:        for (uint256 i = 0; i < rewardTokens.length; i++) {
197:             address rewardToken = rewardTokens[i];
198:             uint256 claimable = userRewardsClaimable[msg.sender][rewardToken];
199:             if (claimable > 0) {
200:                 userRewardsClaimable[msg.sender][rewardToken] = 0;
201:                 require(IERC20(rewardToken).transfer(msg.sender, claimable), "transfer failed"); // <= FOUND
202:                 emit RewardPaid(msg.sender, rewardToken, claimable);
203:             }
204:         }

['196']

196:         for (uint256 i = 0; i < rewardTokens.length; i++) {
197:             address rewardToken = rewardTokens[i];
198:             uint256 claimable = userRewardsClaimable[msg.sender][rewardToken];
199:             if (claimable > 0) {
200:                 userRewardsClaimable[msg.sender][rewardToken] = 0;
201:                 require(IERC20(rewardToken).transfer(msg.sender, claimable), "transfer failed"); // <= FOUND
202:                 emit RewardPaid(msg.sender, rewardToken, claimable);
203:             }
204:         }

[Low-28] Large approvals such as type(uint256).max may not work with some tokens

Resolution

The approve function in ERC-20 tokens allows a user to permit another user or contract to spend tokens on their behalf. Setting the approval to type(uint256).max is often used as a way to grant an indefinite approval, as this value is the maximum possible value of a uint256 variable in Solidity.

However, some tokens may not function as expected when type(uint256).max is used. These tokens may have an atypical implementation of the transferFrom function, which is used in combination with approve. This function might behave differently when confronted with such a high allowance, possibly due to custom logic in the contract that wasn't designed to handle these edge cases.

Moreover, tokens that have a built-in burning or fees mechanism could behave unpredictably when the maximum allowance is set. This can lead to potential vulnerabilities or misinterpretations of contract behavior.

Resolution: It's advisable to be conservative with the approve function and only approve the specific amount of tokens that need to be spent for the specific operation you're performing. If you need to provide an extensive allowance, ensure you've thoroughly analyzed the token contract to understand how it behaves with high allowances. Alternatively, consider implementing a mechanism in your contract to handle token allowances in a more dynamic way, adjusting them as needed for each operation, rather than relying on a single indefinite approval.

Num of instances: 6

Findings

Click to show findings

['51']

51:         IERC20(token0).approve(vault, type(uint256).max); // <= FOUND

['52']

52:         IERC20(token1).approve(vault, type(uint256).max); // <= FOUND

['134']

134:         IERC20(token0).approve(lendingPool, type(uint256).max); // <= FOUND

['135']

135:         IERC20(token1).approve(lendingPool, type(uint256).max); // <= FOUND

['432']

432:     function _swapTokenExactInput(address tokenIn, address tokenOut, uint256 amountIn, uint256 amountOutMinimum) // <= FOUND
433:         internal
434:         returns (uint256 amountOut)
435:     {
436:         address router =
437:             IAddressRegistry(IVault(vault).addressProvider()).getAddress(AddressId.ADDRESS_ID_SHADOW_ROUTER);
438:         IERC20(tokenIn).approve(router, amountIn); // <= FOUND
439: 
440:         amountOut = IShadowSwapRouter(router).exactInputSingle(
441:             IShadowSwapRouter.ExactInputSingleParams({
442:                 tokenIn: tokenIn,
443:                 tokenOut: tokenOut,
444:                 tickSpacing: tickSpacing,
445:                 recipient: address(this),
446:                 deadline: block.timestamp,
447:                 amountIn: amountIn,
448:                 amountOutMinimum: amountOutMinimum,
449:                 sqrtPriceLimitX96: 0
450:             })
451:         );
452:     }

['454']

454:     function _swapTokenExactOutput(address tokenIn, address tokenOut, uint256 amountOut, uint256 amountInMaximum) // <= FOUND
455:         internal
456:         returns (uint256 amountIn)
457:     {
458:         address router =
459:             IAddressRegistry(IVault(vault).addressProvider()).getAddress(AddressId.ADDRESS_ID_SHADOW_ROUTER);
460:         IERC20(tokenIn).approve(router, amountInMaximum); // <= FOUND
461: 
462:         amountIn = IShadowSwapRouter(router).exactOutputSingle(
463:             IShadowSwapRouter.ExactOutputSingleParams({
464:                 tokenIn: tokenIn,
465:                 tokenOut: tokenOut,
466:                 tickSpacing: tickSpacing,
467:                 recipient: address(this),
468:                 deadline: block.timestamp,
469:                 amountOut: amountOut,
470:                 amountInMaximum: amountInMaximum,
471:                 sqrtPriceLimitX96: 0
472:             })
473:         );
474:     }

[Low-29] Revert on Transfer to the Zero Address

Resolution

Many ERC-20 and ERC-721 token contracts implement a safeguard that reverts transactions which attempt to transfer tokens to the zero address. This is because such transfers are often the result of programming errors. The OpenZeppelin library, a popular choice for implementing these standards, includes this safeguard. For token contract developers who want to avoid unintentional transfers to the zero address, it's good practice to include a condition that reverts the transaction if the recipient's address is the zero address.

Num of instances: 4

Findings

Click to show findings

['92']

92:     function transferUnderlyingTo(address target, uint256 amount) // <= FOUND
93:         external
94:         onlyLendingPool
95:         nonReentrant
96:         returns (uint256)
97:     {
98:         IERC20(underlyingAsset).safeTransfer(target, amount);
99:         return amount;
100:     }

['324']

324:     function repay(address onBehalfOf, uint256 debtId, uint256 amount) // <= FOUND
325:         external
326:         notPaused
327:         nonReentrant
328:         returns (uint256)
329:     {
330:         require(borrowingWhiteList[_msgSender()], Errors.VL_BORROWING_CALLER_NOT_IN_WHITELIST);
331: 
332:         DataTypes.DebtPositionData storage debtPosition = debtPositions[debtId];
333:         require(_msgSender() == debtPosition.owner, Errors.VL_INVALID_DEBT_OWNER);
334: 
335:         DataTypes.ReserveData storage reserve = getReserve(debtPosition.reserveId);
336: 
337:         
338:         reserve.updateState(getTreasury());
339:         updateDebtPosition(debtPosition, reserve.borrowingIndex);
340: 
341:         
342:         
343:         
344:         uint256 credit = credits[debtPosition.reserveId][_msgSender()];
345:         credits[debtPosition.reserveId][_msgSender()] = credit.add(amount);
346: 
347:         if (amount > debtPosition.borrowed) {
348:             amount = debtPosition.borrowed;
349:         }
350:         reserve.totalBorrows = reserve.totalBorrows.sub(amount);
351:         debtPosition.borrowed = debtPosition.borrowed.sub(amount);
352: 
353:         
354:         IERC20(reserve.underlyingTokenAddress).safeTransferFrom(_msgSender(), reserve.eTokenAddress, amount); // <= FOUND
355: 
356:         reserve.updateInterestRates();
357: 
358:         emit Repay(debtPosition.reserveId, onBehalfOf, _msgSender(), amount);
359:         return amount;
360:     }

['159']

159:     function withdraw(uint256 amount, address to) external nonReentrant updateReward(msg.sender) { // <= FOUND
160:         require(amount > 0, "amount = 0");
161: 
162:         balanceOf[msg.sender] -= amount;
163:         totalStaked -= amount;
164: 
165:         require(stakedToken.transfer(to, amount), "transfer failed"); // <= FOUND
166: 
167:         emit Withdraw(msg.sender, to, amount);
168:     }

['179']

179:     function withdrawByLendingPool(uint256 amount, address user, address to) // <= FOUND
180:         external
181:         onlyLendingPool
182:         nonReentrant
183:         updateReward(user)
184:     {
185:         require(amount > 0, "amount = 0");
186: 
187:         balanceOf[user] -= amount;
188:         totalStaked -= amount;
189: 
190:         require(stakedToken.transfer(to, amount), "transfer falied"); // <= FOUND
191: 
192:         emit Withdraw(user, to, amount);
193:     }

[Low-30] Return values not checked for approve()

Resolution

The ERC-20 token standard does not dictate that the approve function must return a value. The function signature in the ERC-20 standard is function approve(address spender, uint tokens) public returns (bool success);. However, a well-implemented ERC-20 token contract will typically have approve return a boolean value indicating whether or not the operation was successful.

It's crucial to note that not all token contracts follow this practice. Some might not return a value, or they might return a value in a non-standard way. This inconsistency among token contracts is one reason why it's important to handle token interactions carefully in your smart contracts and to check the return value of approve when possible.

Num of instances: 18

Findings

Click to show findings

['134']

134:         IERC20(getETokenAddress(reserveId)).approve(stakingPool, eTokenAmount); // <= FOUND

['51']

51:         IERC20(token0).approve(vault, type(uint256).max); // <= FOUND

['52']

52:         IERC20(token1).approve(vault, type(uint256).max); // <= FOUND

['109']

109:         IERC20(token0).approve(shadowNonfungiblePositionManager, params.amount0Desired); // <= FOUND

['110']

110:         IERC20(token1).approve(shadowNonfungiblePositionManager, params.amount1Desired); // <= FOUND

['175']

175:             IERC20(token0).approve(vault, currentDebt0); // <= FOUND

['187']

187:             IERC20(token1).approve(vault, currentDebt1); // <= FOUND

['228']

228:             IERC20(token0).approve(vault, token0Left); // <= FOUND

['232']

232:             IERC20(token1).approve(vault, token1Left); // <= FOUND

['367']

367:             IERC20(token0).approve(vault, token0Fees); // <= FOUND

['371']

371:             IERC20(token1).approve(vault, token1Fees); // <= FOUND

['393']

393:                             IERC20(tokens[i]).approve(x33Adapter, balance); // <= FOUND

['396']

396:                             IERC20(x33).approve(vault, balance); // <= FOUND

['400']

400:                         IERC20(tokens[i]).approve(vault, balance); // <= FOUND

['438']

438:         IERC20(tokenIn).approve(router, amountIn); // <= FOUND

['460']

460:         IERC20(tokenIn).approve(router, amountInMaximum); // <= FOUND

['134']

134:         IERC20(token0).approve(lendingPool, type(uint256).max); // <= FOUND

['135']

135:         IERC20(token1).approve(lendingPool, type(uint256).max); // <= FOUND

[Low-31] The function symbol() is not part of the ERC20 standard

Resolution

Assuming that the symbol() function is always present in an ERC20 contract can lead to unexpected issues, as not all ERC20 tokens implement it. Some tokens may omit this function or use a non-standard implementation. If your contract makes an external call to symbol() on a token that does not implement this function, it will result in a runtime error, possibly breaking your contract's functionality. The recommended solution is to use try/catch statements when calling symbol(). This way, your contract can handle cases where the function is missing or implemented differently, ensuring robust interaction with any ERC20 token.

Num of instances: 1

Findings

Click to show findings

['75']

75:         string memory symbol = string(abi.encodePacked("m", ERC20(asset).symbol())); // <= FOUND

[Low-32] Events may be emitted out of order due to code not follow the best practice of check-effects-interaction

Resolution

The "check-effects-interaction" pattern also impacts event ordering. When a contract doesn't adhere to this pattern, events might be emitted in a sequence that doesn't reflect the actual logical flow of operations. This can cause confusion during event tracking, potentially leading to erroneous off-chain interpretations. To rectify this, always ensure that checks are performed first, state modifications come next, and interactions with external contracts or addresses are done last. This will ensure events are emitted in a logical, consistent manner, providing a clear and accurate chronological record of on-chain actions for off-chain systems and observers.

Num of instances: 7

Findings

Click to show findings

['69']

69:     function initReserve(address asset) external onlyOwner notPaused { // <= FOUND
70:         uint256 id = nextReserveId;
71:         nextReserveId += 1;
72: 
73:         
74:         string memory name = string(abi.encodePacked(ERC20(asset).name(), "(MightyFi Interest Bearing Token)")); // <= FOUND
75:         string memory symbol = string(abi.encodePacked("m", ERC20(asset).symbol())); // <= FOUND
76:         uint8 decimals = ERC20(asset).decimals(); // <= FOUND
77: 
78:         address eTokenAddress = ETokenDeployer.deploy(name, symbol, decimals, asset, id);
79: 
80:         DataTypes.ReserveData storage reserveData = reserves[id];
81:         reserveData.setActive(true);
82:         reserveData.setBorrowingEnabled(true);
83: 
84:         initReserve(reserveData, asset, eTokenAddress, type(uint256).max, id);
85: 
86:         createStakingPoolForReserve(id);
87: 
88:         emit InitReserve(asset, eTokenAddress, reserveData.stakingAddress, id); // <= FOUND
89:     }

['123']

123:     function depositAndStake(uint256 reserveId, uint256 amount, address onBehalfOf, uint16 referralCode) // <= FOUND
124:         external
125:         payable
126:         notPaused
127:         nonReentrant
128:         returns (uint256 eTokenAmount)
129:     {
130:         eTokenAmount = _deposit(reserveId, amount, address(this));
131: 
132:         address stakingPool = reserves[reserveId].stakingAddress;
133:         require(stakingPool != address(0), "Address=0");
134:         IERC20(getETokenAddress(reserveId)).approve(stakingPool, eTokenAmount);
135:         IStakingRewards(stakingPool).stake(eTokenAmount, onBehalfOf); // <= FOUND
136: 
137:         
138:         if (msg.value > 0) {
139:             refundETH();
140:         }
141: 
142:         
143:         emit Deposited(reserveId, _msgSender(), onBehalfOf, amount, eTokenAmount, referralCode); // <= FOUND
144:     }

['185']

185:     function unStakeAndWithdraw(uint256 reserveId, uint256 eTokenAmount, address to, bool receiveNativeETH) // <= FOUND
186:         external
187:         payable
188:         notPaused
189:         nonReentrant
190:         avoidUsingNativeEther
191:         returns (uint256)
192:     {
193:         address stakingPool = reserves[reserveId].stakingAddress;
194:         require(stakingPool != address(0), "Address=0");
195: 
196:         IStakingRewards(stakingPool).withdrawByLendingPool(eTokenAmount, _msgSender(), address(this)); // <= FOUND
197: 
198:         uint256 underlyingTokenAmount = _redeem(reserveId, eTokenAmount, to, receiveNativeETH);
199: 
200:         emit Redeemed(reserveId, _msgSender(), to, eTokenAmount, underlyingTokenAmount); // <= FOUND
201: 
202:         return (underlyingTokenAmount);
203:     }

['96']

96:     function setReward(address rewardToken, uint256 startTime, uint256 endTime, uint256 totalRewards) // <= FOUND
97:         public
98:         onlyOwner
99:         nonReentrant
100:         updateReward(address(0))
101:     {
102:         require(startTime < endTime, "start must lt end");
103:         require(rewardData[rewardToken].endTime < block.timestamp, "not end");
104: 
105:         if (!inRewardsTokenList[rewardToken]) {
106:             rewardTokens.push(rewardToken);
107:             inRewardsTokenList[rewardToken] = true;
108:         }
109: 
110:         rewardData[rewardToken].startTime = startTime;
111:         rewardData[rewardToken].endTime = endTime;
112:         rewardData[rewardToken].lastUpdateTime = block.timestamp;
113:         rewardData[rewardToken].rewardRate = totalRewards / (endTime - startTime);
114: 
115:         if (block.timestamp > startTime && totalStaked > 0) {
116:             uint256 dt = block.timestamp - startTime;
117: 
118:             rewardData[rewardToken].rewardPerTokenStored +=
119:                 (rewardData[rewardToken].rewardRate * dt * 1e18) / totalStaked;
120:         }
121: 
122:         if (block.timestamp > startTime && totalStaked == 0) {
123:             
124:             
125:             uint256 dt = block.timestamp - startTime;
126:             totalRewards -= rewardData[rewardToken].rewardRate * dt;
127:         }
128: 
129:         IERC20(rewardToken).safeTransferFrom(msg.sender, address(this), totalRewards); // <= FOUND
130: 
131:         emit RewardsSet(rewardToken, startTime, endTime, totalRewards); // <= FOUND
132:     }

['195']

195:     function claim() external nonReentrant updateReward(msg.sender) { // <= FOUND
196:         for (uint256 i = 0; i < rewardTokens.length; i++) {
197:             address rewardToken = rewardTokens[i];
198:             uint256 claimable = userRewardsClaimable[msg.sender][rewardToken];
199:             if (claimable > 0) {
200:                 userRewardsClaimable[msg.sender][rewardToken] = 0;
201:                 require(IERC20(rewardToken).transfer(msg.sender, claimable), "transfer failed"); // <= FOUND
202:                 emit RewardPaid(msg.sender, rewardToken, claimable); // <= FOUND
203:             }
204:         }
205:     }

['152']

152:     function openPosition(OpenPositionParams calldata params) // <= FOUND
153:         external
154:         payable
155:         nonReentrant
156:         checkDeadline(params.deadline)
157:     {
158:         (uint256 _positionId, address _positionAddress) = _createNewPosition();
159: 
160:         PositionInfo storage positionInfo = positionInfos[_positionId];
161:         positionInfo.owner = msg.sender;
162:         positionInfo.vaultId = vaultId;
163:         positionInfo.positionAddress = _positionAddress;
164:         positionInfo.positionId = _positionId;
165:         positionIds[msg.sender].push(_positionId);
166:         positionAddressToId[_positionAddress] = _positionId;
167: 
168:         
169:         if (params.amount0Principal > 0) {
170:             pay(token0, msg.sender, address(this), params.amount0Principal);
171:         }
172:         if (params.amount1Principal > 0) {
173:             pay(token1, msg.sender, address(this), params.amount1Principal);
174:         }
175: 
176:         
177:         address lendingPool = getLendingPool();
178:         if (params.amount0Borrow > 0) {
179:             positionInfo.token0DebtId = ILendingPool(lendingPool).newDebtPosition(token0ReserveId); // <= FOUND
180: 
181:             ILendingPool(lendingPool).borrow(address(this), positionInfo.token0DebtId, params.amount0Borrow); // <= FOUND
182:         }
183:         if (params.amount1Borrow > 0) {
184:             positionInfo.token1DebtId = ILendingPool(lendingPool).newDebtPosition(token1ReserveId); // <= FOUND
185: 
186:             ILendingPool(lendingPool).borrow(address(this), positionInfo.token1DebtId, params.amount1Borrow); // <= FOUND
187:         }
188: 
189:         require(
190:             getDebtRatioFromAmounts(
191:                 params.amount0Principal, params.amount1Principal, params.amount0Borrow, params.amount1Borrow
192:             ) < liquidationDebtRatio,
193:             "Borrow value is too high"
194:         );
195: 
196:         if (params.amount0Principal > 0 || params.amount0Borrow > 0) {
197:             pay(token0, address(this), positionInfo.positionAddress, params.amount0Principal + params.amount0Borrow);
198:         }
199:         if (params.amount1Principal > 0 || params.amount1Borrow > 0) {
200:             pay(token1, address(this), positionInfo.positionAddress, params.amount1Principal + params.amount1Borrow);
201:         }
202:         
203:         (uint256 tokenId, uint128 liquidity, uint256 amount0, uint256 amount1) = IShadowRangePositionImpl(
204:             positionInfo.positionAddress
205:         ).openPosition(
206:             IShadowRangePositionImpl.OpenPositionParams({
207:                 amount0Desired: params.amount0Desired,
208:                 amount1Desired: params.amount1Desired,
209:                 tickLower: params.tickLower,
210:                 tickUpper: params.tickUpper,
211:                 positionOwner: msg.sender
212:             })
213:         );
214: 
215:         require(getPositionValueByNftId(tokenId) > minPositionSize, "PVL");
216: 
217:         positionInfo.shadowPositionId = tokenId;
218:         positionInfo.tickLower = params.tickLower;
219:         positionInfo.tickUpper = params.tickUpper;
220:         positionInfo.ul = params.ul;
221:         positionInfo.ll = params.ll;
222: 
223:         emit PositionOpened( // <= FOUND
224:             msg.sender,
225:             vaultId,
226:             _positionId,
227:             _positionAddress,
228:             params.amount0Principal,
229:             params.amount1Principal,
230:             amount0,
231:             amount1,
232:             liquidity,
233:             block.timestamp
234:         );
235: 
236:         if (msg.value > 0) {
237:             refundETH();
238:         }
239:         require(getDebtRatio(_positionId) < liquidationDebtRatio, "DRH");
240:     }

['29']

29:     function newVault(address _newVault) external onlyOwner returns (uint256 vaultId) { // <= FOUND
30:         require(!isRegistered[_newVault], "Vault already registered");
31:         vaultId = nextVaultID;
32:         nextVaultID = nextVaultID + 1;
33: 
34:         IVault(_newVault).onReceiveRegisterCallback(vaultId); // <= FOUND
35: 
36:         vaults[vaultId] = _newVault;
37:         isRegistered[_newVault] = true;
38:         emit NewVault(_newVault, vaultId); // <= FOUND
39:     }

[Low-33] Missing zero address check in initializer

Resolution

Initializer functions in contracts often set important parameters or addresses. Failing to check for the zero address (0x0000000000000000000000000000000000000000) in initializers can lead to unintended behavior, as this address typically signifies an unset or default value. Transfers to or interactions with the zero address can result in permanent loss of assets or broken functionality. It's crucial to add checks using require(targetAddress != address(0), "Address cannot be zero") in initializers to prevent accidentally setting important state variables or parameters to this address, ensuring the system's integrity and user asset safety.

Num of instances: 2

Findings

Click to show findings

['18']

18:     function initialize(
19:         address _pyth,
20:         address _oracleManager,
21:         address[] memory initialTokens,
22:         bytes32[] memory priceIds
23:     ) external initializer {
24:         pyth = IPyth(_pyth);
25:         oracleManager = _oracleManager;
26:         require(initialTokens.length == priceIds.length, "Initial tokens and price ids length mismatch");
27:         for (uint256 i = 0; i < initialTokens.length; i++) {
28:             pythPriceIds[initialTokens[i]] = priceIds[i];
29:         }
30: 
31:         maxPriceAge = 1 hours;
32:     }

['83']

83:     function initialize(
84:         address _addressProvider,
85:         address _vaultRegistry,
86:         address _shadowV3PoolAddress,
87:         address _initialPositionImplementation
88:     ) public initializer {
89:         _initializePayments(IAddressRegistry(_addressProvider).getAddress(AddressId.ADDRESS_ID_WETH9));
90:         __ReentrancyGuard_init();
91:         __Ownable2Step_init();
92:         token0 = IShadowV3Pool(_shadowV3PoolAddress).token0();
93:         token1 = IShadowV3Pool(_shadowV3PoolAddress).token1();
94:         token0Decimals = IERC20Metadata(token0).decimals();
95:         token1Decimals = IERC20Metadata(token1).decimals();
96: 
97:         addressProvider = _addressProvider;
98:         vaultRegistry = _vaultRegistry;
99:         nextPositionID = 1;
100:         shadowV3Pool = _shadowV3PoolAddress;
101: 
102:         liquidationDebtRatio = 8600;
103:         liquidationFee = 500;
104:         performanceFee = 3000;
105:         minPositionSize = 100e8;
106: 
107:         positionImplementation = _initialPositionImplementation;
108:     }

[Low-34] Critical functions should have a timelock

Resolution

Critical functions, especially those affecting protocol parameters or user funds, are potential points of failure or exploitation. To mitigate risks, incorporating a timelock on such functions can be beneficial. A timelock requires a waiting period between the time an action is initiated and when it's executed, giving stakeholders time to react, potentially vetoing malicious or erroneous changes. To implement, integrate a smart contract like OpenZeppelin's TimelockController or build a custom mechanism. This ensures governance decisions or administrative changes are transparent and allows for community or multi-signature interventions, enhancing protocol security and trustworthiness.

Num of instances: 18

Findings

Click to show findings

['14']

14:     function setAddress(uint256 id, address _addr) public onlyOwner  // <= FOUND

['559']

559:     function setCreditsOfVault(uint256 vaultId, uint256 reserveId, uint256 credit) external onlyOwner notPaused  // <= FOUND

['602']

602:     function setReserveFeeRate(uint256 reserveId, uint16 _rate) public onlyOwner notPaused  // <= FOUND

['610']

610:     function setBorrowingRateConfig( // <= FOUND
611:         uint256 reserveId,
612:         uint16 utilizationA,
613:         uint16 borrowingRateA,
614:         uint16 utilizationB,
615:         uint16 borrowingRateB,
616:         uint16 maxBorrowingRate
617:     ) public onlyOwner notPaused 

['626']

626:     function setReserveCapacity(uint256 reserveId, uint256 cap) public onlyOwner notPaused  // <= FOUND

['96']

96:     function setReward(address rewardToken, uint256 startTime, uint256 endTime, uint256 totalRewards) // <= FOUND
97:         public
98:         onlyOwner
99:         nonReentrant
100:         updateReward(address(0))
101:     

['110']

110:     function setLiquidator(address _liquidator, bool _isLiquidator) external onlyOwner  // <= FOUND

['114']

114:     function setLiquidationFeeParams(uint256 _liquidationFee, uint256 _liquidationCallerFeeRatio) external onlyOwner  // <= FOUND

['119']

119:     function setOpenLiquidationEnabled(bool _openLiquidationEnabled) external onlyOwner  // <= FOUND

['123']

123:     function setPositionImplementation(address _positionImplementation) external onlyOwner  // <= FOUND

['127']

127:     function setLiquidationDebtRatio(uint256 _liquidationDebtRatio) external onlyOwner  // <= FOUND

['131']

131:     function setReserveIds(uint256 _token0ReserveId, uint256 _token1ReserveId) external onlyOwner  // <= FOUND

['140']

140:     function setMinPositionSize(uint256 _minPositionSize) external onlyOwner  // <= FOUND

['144']

144:     function setPerformanceFee(uint256 _performanceFee) external onlyOwner  // <= FOUND

['148']

148:     function setShadowGauge(address _shadowGauge) external onlyOwner  // <= FOUND

['39']

39:     function setOracleManager(address _oracleManager) external onlyOracleManager  // <= FOUND

['44']

44:     function setPythPriceId(address token, bytes32 priceId) external onlyOracleManager  // <= FOUND

['48']

48:     function setMaxPriceAge(uint256 _maxPriceAge) external onlyOracleManager  // <= FOUND

[Low-35] Unbounded loop may run out of gas

Resolution

Unbounded loops in smart contracts pose a risk because they iterate over an unknown number of elements, potentially consuming all available gas for a transaction. This can result in unintended transaction failures. Gas consumption increases linearly with the number of iterations, and if it surpasses the gas limit, the transaction reverts, wasting the gas spent. To mitigate this, developers should either set a maximum limit on loop iterations.

Num of instances: 7

Findings

Click to show findings

['445']

445:     function getReserveStatus(uint256[] calldata reserveIdArr) // <= FOUND
446:         external
447:         view
448:         returns (ReserveStatus[] memory statusArr)
449:     {
450:         statusArr = new ReserveStatus[](reserveIdArr.length);
451: 
452:         for (uint256 i = 0; i < reserveIdArr.length; i++) { // <= FOUND
453:             statusArr[i].reserveId = reserveIdArr[i];
454:             statusArr[i].underlyingTokenAddress = reserves[reserveIdArr[i]].underlyingTokenAddress;
455:             statusArr[i].eTokenAddress = reserves[reserveIdArr[i]].eTokenAddress;
456:             statusArr[i].stakingAddress = reserves[reserveIdArr[i]].stakingAddress;
457:             (statusArr[i].totalLiquidity, statusArr[i].totalBorrows) =
458:                 reserves[reserveIdArr[i]].totalLiquidityAndBorrows();
459:             statusArr[i].exchangeRate = reserves[reserveIdArr[i]].eTokenToReserveExchangeRate();
460:             statusArr[i].borrowingRate = reserves[reserveIdArr[i]].borrowingRate();
461:         }
462:     }

['464']

464:     function getPositionStatus(uint256[] calldata reserveIdArr, address user) // <= FOUND
465:         external
466:         view
467:         returns (PositionStatus[] memory statusArr)
468:     {
469:         statusArr = new PositionStatus[](reserveIdArr.length);
470: 
471:         for (uint256 i = 0; i < reserveIdArr.length; i++) { // <= FOUND
472:             statusArr[i].reserveId = reserveIdArr[i];
473:             statusArr[i].user = user;
474:             statusArr[i].eTokenStaked = IStakingRewards(reserves[reserveIdArr[i]].stakingAddress).balanceOf(user);
475:             statusArr[i].eTokenUnStaked = IERC20(reserves[reserveIdArr[i]].eTokenAddress).balanceOf(user);
476:             statusArr[i].liquidity = statusArr[i].eTokenStaked.add(statusArr[i].eTokenUnStaked).mul(
477:                 reserves[reserveIdArr[i]].eTokenToReserveExchangeRate()
478:             ).div(Precision.FACTOR1E18);
479:         }
480:     }

['195']

195:     function claim() external nonReentrant updateReward(msg.sender) { // <= FOUND
196:         for (uint256 i = 0; i < rewardTokens.length; i++) { // <= FOUND
197:             address rewardToken = rewardTokens[i];
198:             uint256 claimable = userRewardsClaimable[msg.sender][rewardToken];
199:             if (claimable > 0) {
200:                 userRewardsClaimable[msg.sender][rewardToken] = 0;
201:                 require(IERC20(rewardToken).transfer(msg.sender, claimable), "transfer failed");
202:                 emit RewardPaid(msg.sender, rewardToken, claimable);
203:             }
204:         }
205:     }

['336']

336:     function claimableRewards() external view returns (address[] memory, uint256[] memory) { // <= FOUND
337:         address shadowGauge = IShadowRangeVault(vault).shadowGauge();
338:         if (shadowGauge != address(0)) {
339:             address[] memory tokens = IShadowGaugeV3(shadowGauge).getRewardTokens();
340:             uint256[] memory amounts = new uint256[](tokens.length);
341:             for (uint256 i = 0; i < tokens.length; i++) { // <= FOUND
342:                 amounts[i] = IShadowGaugeV3(shadowGauge).earned(tokens[i], shadowPositionId);
343:             }
344:             return (tokens, amounts);
345:         }
346:         return (new address[](0), new uint256[](0));
347:     }

['376']

376:     function _claimRewards() internal { // <= FOUND
377:         address shadowGauge = IShadowRangeVault(vault).shadowGauge();
378:         if (shadowGauge != address(0)) {
379:             address[] memory tokens = IShadowGaugeV3(shadowGauge).getRewardTokens();
380:             IShadowGaugeV3(shadowGauge).getReward(shadowPositionId, tokens);
381: 
382:             for (uint256 i = 0; i < tokens.length; i++) { // <= FOUND
383:                 uint256 balance = IERC20(tokens[i]).balanceOf(address(this));
384:                 if (balance > 0) {
385:                     if (tokens[i] == xShadow) {
386:                         
387:                         
388:                         
389:                         
390:                         
391:                         if (IShadowX33(x33).isUnlocked()) {
392:                             address x33Adapter = getX33Adapter();
393:                             IERC20(tokens[i]).approve(x33Adapter, balance);
394:                             IERC4626(x33Adapter).deposit(balance, address(this));
395:                             balance = IERC20(x33).balanceOf(address(this));
396:                             IERC20(x33).approve(vault, balance);
397:                             IVault(vault).claimCallback(x33, balance);
398:                         }
399:                     } else {
400:                         IERC20(tokens[i]).approve(vault, balance);
401:                         IVault(vault).claimCallback(tokens[i], balance);
402:                     }
403:                 }
404:             }
405:         }
406:     }

['68']

68:     function getUserPosition(address vault, address owner) public view returns (PositionInfo[] memory) { // <= FOUND
69:         uint256[] memory ids = getPositionIds(vault, owner);
70:         PositionInfo[] memory positions = new PositionInfo[](ids.length);
71:         for (uint256 i = 0; i < ids.length; i++) { // <= FOUND
72:             positions[i].vault = vault;
73:             positions[i].id = ids[i];
74:             positions[i].token0 = IVault(vault).token0();
75:             positions[i].token1 = IVault(vault).token1();
76:             (positions[i].amount0, positions[i].amount1) = IVault(vault).getPositionAmounts(ids[i]);
77:             if (positions[i].amount0 > 0 || positions[i].amount1 > 0) {
78:                 (positions[i].amount0Debt, positions[i].amount1Debt) = IVault(vault).getPositionDebt(ids[i]);
79:                 positions[i].debtRatio = IVault(vault).getDebtRatio(ids[i]);
80:             }
81:             positions[i].price0 = IVault(vault).getTokenPrice(positions[i].token0);
82:             positions[i].price1 = IVault(vault).getTokenPrice(positions[i].token1);
83: 
84:             IVault.PositionInfo memory positionInfo = IVault(vault).getPositionInfos(ids[i]);
85: 
86:             positions[i].positionAddress = positionInfo.positionAddress;
87:             positions[i].positionNftId = positionInfo.shadowPositionId;
88:             positions[i].tickUpper = positionInfo.tickUpper;
89:             positions[i].tickLower = positionInfo.tickLower;
90:             positions[i].ul = positionInfo.ul;
91:             positions[i].ll = positionInfo.ll;
92: 
93:             positions[i].inRange = positions[i].amount0 > 0 && positions[i].amount1 > 0;
94: 
95:             (positions[i].rewards, positions[i].rewardsAmounts) =
96:                 IPositionImpl(positionInfo.positionAddress).claimableRewards();
97: 
98:             positions[i].currentTick = IVault(vault).getCurrentTick();
99: 
100:             (,,,,, positions[i].liquidity,,,,) = INFTPositionManager(
101:                 IAddressRegistry(addressProvider).getAddress(AddressId.ADDRESS_ID_SHADOW_NONFUNGIBLE_POSITION_MANAGER)
102:             ).positions(positionInfo.shadowPositionId);
103:         }
104:         return positions;
105:     }

['107']

107:     function getUserPositions(address[] memory vaults, address owner) public view returns (PositionInfo[][] memory) { // <= FOUND
108:         PositionInfo[][] memory positions = new PositionInfo[][](vaults.length);
109:         for (uint256 i = 0; i < vaults.length; i++) { // <= FOUND
110:             positions[i] = getUserPosition(vaults[i], owner);
111:         }
112:         return positions;
113:     }

[Low-36] Mapping arrays can grow in size without a way to shrink them

Resolution

It's a good practice to maintain control over the size of array mappings in Solidity, especially if they are dynamically updated. If a contract includes a mechanism to push items into an array, it should ideally also provide a mechanism to remove items. This is because Solidity arrays don't automatically shrink when items are deleted - their length needs to be manually adjusted.

Ignoring this can lead to bloated and inefficient contracts. For instance, iterating over a large array can cause your contract to hit the block gas limit. Additionally, if entries are only marked for deletion but never actually removed, you may end up dealing with stale or irrelevant data, which can cause logical errors.

Therefore, implementing a method to 'pop' items from mapping arrays helps manage contract's state, improve efficiency and prevent potential issues related to gas limits or stale data. Always ensure to handle potential underflow conditions when popping elements from the mapping array.

Num of instances: 1

Findings

Click to show findings

['55']

55:  mapping(address => uint256[]) public positionIds; // <= FOUND

[Low-37] Consider implementing two-step procedure for updating protocol addresses

Resolution

Implementing a two-step procedure for updating protocol addresses adds an extra layer of security. In such a system, the first step initiates the change, and the second step, after a predefined delay, confirms and finalizes it. This delay allows stakeholders or monitoring tools to observe and react to unintended or malicious changes. If an unauthorized change is detected, corrective actions can be taken before the change is finalized. To achieve this, introduce a "proposed address" state variable and a "delay period". Upon an update request, set the "proposed address". After the delay, if not contested, the main protocol address can be updated.

Num of instances: 6

Findings

Click to show findings

['110']

110:     function setLiquidator(address _liquidator, bool _isLiquidator) external onlyOwner { // <= FOUND
111:         liquidators[_liquidator] = _isLiquidator;
112:     }

['123']

123:     function setPositionImplementation(address _positionImplementation) external onlyOwner { // <= FOUND
124:         positionImplementation = _positionImplementation;
125:     }

['148']

148:     function setShadowGauge(address _shadowGauge) external onlyOwner { // <= FOUND
149:         shadowGauge = _shadowGauge;
150:     }

['39']

39:     function setOracleManager(address _oracleManager) external onlyOracleManager { // <= FOUND
40:         require(_oracleManager != address(0), "Oracle manager cannot be zero address");
41:         oracleManager = _oracleManager;
42:     }

['44']

44:     function setPythPriceId(address token, bytes32 priceId) external onlyOracleManager { // <= FOUND
45:         pythPriceIds[token] = priceId;
46:     }

['49']

49:     function changeOwner(address newOwner) external onlyOwner { // <= FOUND
50:         require(newOwner != address(0), "Proxy: new Owner is the zero address");
51:         emit OwnerChanged(_owner(), newOwner);
52:         _setOwner(newOwner);
53:     }

[Low-38] The function name() is not part of the ERC20 standard

Resolution

Assuming that the name() function is always present in an ERC20 contract can lead to unexpected issues, as not all ERC20 tokens implement it. Some tokens may omit this function or use a non-standard implementation. If your contract makes an external call to name() on a token that does not implement this function, it will result in a runtime error, possibly breaking your contract's functionality. The recommended solution is to use try/catch statements when calling name(). This way, your contract can handle cases where the function is missing or implemented differently, ensuring robust interaction with any ERC20 token.

Num of instances: 1

Findings

Click to show findings

['74']

74:         
75:         string memory name = string(abi.encodePacked(ERC20(asset).name(), "(MightyFi Interest Bearing Token)")); // <= FOUND

[Low-39] Avoid mutating function parameters

Resolution

Function parameters in Solidity are passed by value, meaning they are essentially local copies. Mutating them can lead to confusion and errors because the changes don't persist outside the function. By keeping function parameters immutable, you ensure clarity in code behavior, preventing unintended side-effects. If you need to modify a value based on a parameter, use a local variable inside the function, leaving the original parameter unaltered. By adhering to this practice, you maintain a clear distinction between input data and the internal processing logic, improving code readability and reducing the potential for bugs.

Num of instances: 4

Findings

Click to show findings

['160']

160:     function redeem(uint256 reserveId, uint256 eTokenAmount, address to, bool receiveNativeETH) // <= FOUND
161:         public
162:         payable
163:         notPaused
164:         nonReentrant
165:         avoidUsingNativeEther
166:         returns (uint256)
167:     {
168:         DataTypes.ReserveData storage reserve = getReserve(reserveId);
169: 
170:         if (eTokenAmount == type(uint256).max) {
171:             eTokenAmount = IExtraInterestBearingToken(reserve.eTokenAddress).balanceOf(_msgSender()); // <= FOUND
172:         }
173:         
174:         IERC20(reserve.eTokenAddress).safeTransferFrom(_msgSender(), address(this), eTokenAmount);
175: 
176:         
177:         uint256 underlyingTokenAmount = _redeem(reserveId, eTokenAmount, to, receiveNativeETH);
178: 
179:         emit Redeemed(reserveId, _msgSender(), to, eTokenAmount, underlyingTokenAmount);
180: 
181:         return (underlyingTokenAmount);
182:     }

['324']

324:     function repay(address onBehalfOf, uint256 debtId, uint256 amount) // <= FOUND
325:         external
326:         notPaused
327:         nonReentrant
328:         returns (uint256)
329:     {
330:         require(borrowingWhiteList[_msgSender()], Errors.VL_BORROWING_CALLER_NOT_IN_WHITELIST);
331: 
332:         DataTypes.DebtPositionData storage debtPosition = debtPositions[debtId];
333:         require(_msgSender() == debtPosition.owner, Errors.VL_INVALID_DEBT_OWNER);
334: 
335:         DataTypes.ReserveData storage reserve = getReserve(debtPosition.reserveId);
336: 
337:         
338:         reserve.updateState(getTreasury());
339:         updateDebtPosition(debtPosition, reserve.borrowingIndex);
340: 
341:         
342:         
343:         
344:         uint256 credit = credits[debtPosition.reserveId][_msgSender()];
345:         credits[debtPosition.reserveId][_msgSender()] = credit.add(amount);
346: 
347:         if (amount > debtPosition.borrowed) {
348:             amount = debtPosition.borrowed; // <= FOUND
349:         }
350:         reserve.totalBorrows = reserve.totalBorrows.sub(amount);
351:         debtPosition.borrowed = debtPosition.borrowed.sub(amount);
352: 
353:         
354:         IERC20(reserve.underlyingTokenAddress).safeTransferFrom(_msgSender(), reserve.eTokenAddress, amount);
355: 
356:         reserve.updateInterestRates();
357: 
358:         emit Repay(debtPosition.reserveId, onBehalfOf, _msgSender(), amount);
359:         return amount;
360:     }

['309']

309:     function repayExact(uint256 positionId, uint256 amount0, uint256 amount1) external payable { // <= FOUND
310:         PositionInfo storage positionInfo = positionInfos[positionId];
311: 
312:         
313:         address lendingPool = getLendingPool();
314:         if (amount0 > 0 && positionInfo.token0DebtId != 0) {
315:             (uint256 currentDebt0,) = ILendingPool(lendingPool).getCurrentDebt(positionInfo.token0DebtId);
316:             if (amount0 > currentDebt0) {
317:                 amount0 = currentDebt0; // <= FOUND
318:             }
319: 
320:             pay(token0, msg.sender, address(this), amount0);
321: 
322:             ILendingPool(lendingPool).repay(address(this), positionInfo.token0DebtId, amount0);
323:         }
324:         if (amount1 > 0 && positionInfo.token1DebtId != 0) {
325:             (uint256 currentDebt1,) = ILendingPool(lendingPool).getCurrentDebt(positionInfo.token1DebtId);
326:             if (amount1 > currentDebt1) {
327:                 amount1 = currentDebt1;
328:             }
329: 
330:             pay(token1, msg.sender, address(this), amount1);
331: 
332:             ILendingPool(lendingPool).repay(address(this), positionInfo.token1DebtId, amount1);
333:         }
334: 
335:         if (msg.value > 0) {
336:             refundETH();
337:         }
338:     }

['96']

96:     function setReward(address rewardToken, uint256 startTime, uint256 endTime, uint256 totalRewards) // <= FOUND
97:         public
98:         onlyOwner
99:         nonReentrant
100:         updateReward(address(0))
101:     {
102:         require(startTime < endTime, "start must lt end");
103:         require(rewardData[rewardToken].endTime < block.timestamp, "not end");
104: 
105:         if (!inRewardsTokenList[rewardToken]) {
106:             rewardTokens.push(rewardToken);
107:             inRewardsTokenList[rewardToken] = true;
108:         }
109: 
110:         rewardData[rewardToken].startTime = startTime;
111:         rewardData[rewardToken].endTime = endTime;
112:         rewardData[rewardToken].lastUpdateTime = block.timestamp;
113:         rewardData[rewardToken].rewardRate = totalRewards / (endTime - startTime);
114: 
115:         if (block.timestamp > startTime && totalStaked > 0) {
116:             uint256 dt = block.timestamp - startTime;
117: 
118:             rewardData[rewardToken].rewardPerTokenStored +=
119:                 (rewardData[rewardToken].rewardRate * dt * 1e18) / totalStaked;
120:         }
121: 
122:         if (block.timestamp > startTime && totalStaked == 0) {
123:             
124:             
125:             uint256 dt = block.timestamp - startTime;
126:             totalRewards -= rewardData[rewardToken].rewardRate * dt; // <= FOUND
127:         }
128: 
129:         IERC20(rewardToken).safeTransferFrom(msg.sender, address(this), totalRewards);
130: 
131:         emit RewardsSet(rewardToken, startTime, endTime, totalRewards);
132:     }

[Low-40] approve will always revert as the IERC20 interface mismatch

Resolution

In Solidity, using the ERC20 approve function can be problematic with tokens like USDT, which may not fully adhere to the standard interface, potentially causing transaction reverts. To avoid issues, it’s crucial to interact directly with the token's specific ABI rather than the generic IERC20 interface. Before integrating any token, thoroughly review its contract to ensure compatibility, especially for the transfer and approve methods.

Num of instances: 11

Findings

Click to show findings

['123']

123:     function depositAndStake(uint256 reserveId, uint256 amount, address onBehalfOf, uint16 referralCode) // <= FOUND
124:         external
125:         payable
126:         notPaused
127:         nonReentrant
128:         returns (uint256 eTokenAmount)
129:     {
130:         eTokenAmount = _deposit(reserveId, amount, address(this));
131: 
132:         address stakingPool = reserves[reserveId].stakingAddress;
133:         require(stakingPool != address(0), "Address=0");
134:         IERC20(getETokenAddress(reserveId)).approve(stakingPool, eTokenAmount); // <= FOUND
135:         IStakingRewards(stakingPool).stake(eTokenAmount, onBehalfOf);
136: 
137:         
138:         if (msg.value > 0) {
139:             refundETH();
140:         }
141: 
142:         
143:         emit Deposited(reserveId, _msgSender(), onBehalfOf, amount, eTokenAmount, referralCode);
144:     }

['41']

41:     function initialize(uint256 _positionId) external { // <= FOUND
42:         require(vault == address(0), "Already initialized");
43:         xShadow = 0x5050bc082FF4A74Fb6B0B04385dEfdDB114b2424;
44:         x33 = 0x3333111A391cC08fa51353E9195526A70b333333;
45:         vault = msg.sender;
46:         positionId = _positionId;
47: 
48:         token0 = IVault(msg.sender).token0();
49:         token1 = IVault(msg.sender).token1();
50: 
51:         IERC20(token0).approve(vault, type(uint256).max); // <= FOUND
52:         IERC20(token1).approve(vault, type(uint256).max);
53: 
54:         shadowV3Pool = IShadowRangeVault(vault).shadowV3Pool();
55:         tickSpacing = IShadowV3Pool(shadowV3Pool).tickSpacing();
56: 
57:         _initializePayments(IPayments(msg.sender).WETH9());
58: 
59:         addressProvider = IVault(msg.sender).addressProvider();
60:     }

['83']

83:     function openPosition(OpenPositionParams memory params) // <= FOUND
84:         external
85:         onlyVault
86:         returns (uint256 tokenId, uint128 liquidity, uint256 amount0, uint256 amount1)
87:     {
88:         uint256 token0Balance = IERC20(token0).balanceOf(address(this));
89:         if (params.amount0Desired > token0Balance) {
90:             _swapTokenExactInput(
91:                 token1,
92:                 token0,
93:                 IERC20(token1).balanceOf(address(this)) - params.amount1Desired,
94:                 params.amount0Desired - token0Balance
95:             );
96:         }
97: 
98:         uint256 token1Balance = IERC20(token1).balanceOf(address(this));
99:         if (params.amount1Desired > token1Balance) {
100:             _swapTokenExactInput(
101:                 token0,
102:                 token1,
103:                 IERC20(token0).balanceOf(address(this)) - params.amount0Desired,
104:                 params.amount1Desired - token1Balance
105:             );
106:         }
107:         address shadowNonfungiblePositionManager = getShadowNonfungiblePositionManager();
108: 
109:         IERC20(token0).approve(shadowNonfungiblePositionManager, params.amount0Desired); // <= FOUND
110:         IERC20(token1).approve(shadowNonfungiblePositionManager, params.amount1Desired);
111: 
112:         (tokenId, liquidity, amount0, amount1) = IShadowNonfungiblePositionManager(shadowNonfungiblePositionManager)
113:             .mint(
114:             IShadowNonfungiblePositionManager.MintParams({
115:                 token0: token0,
116:                 token1: token1,
117:                 tickSpacing: tickSpacing,
118:                 tickLower: params.tickLower,
119:                 tickUpper: params.tickUpper,
120:                 amount0Desired: params.amount0Desired,
121:                 amount1Desired: params.amount1Desired,
122:                 amount0Min: 0,
123:                 amount1Min: 0,
124:                 recipient: address(this),
125:                 deadline: block.timestamp
126:             })
127:         );
128: 
129:         shadowPositionId = tokenId;
130:         positionOwner = params.positionOwner;
131: 
132:         unwrapWETH9(0, positionOwner);
133: 
134:         token0Balance = IERC20(token0).balanceOf(address(this));
135:         token1Balance = IERC20(token1).balanceOf(address(this));
136: 
137:         if (token0Balance > 0) {
138:             pay(token0, address(this), positionOwner, token0Balance);
139:         }
140:         if (token1Balance > 0) {
141:             pay(token1, address(this), positionOwner, token1Balance);
142:         }
143:     }

['145']

145:     function closePosition() // <= FOUND
146:         external
147:         onlyVault
148:         returns (
149:             uint128 liquidity,
150:             uint256 token0Reduced,
151:             uint256 token1Reduced,
152:             uint256 token0Balance,
153:             uint256 token1Balance
154:         )
155:     {
156:         _claimFees();
157:         _claimRewards();
158: 
159:         (,,,,, liquidity,,,,) =
160:             IShadowNonfungiblePositionManager(getShadowNonfungiblePositionManager()).positions(shadowPositionId);
161: 
162:         (token0Reduced, token1Reduced) = _decreasePosition(liquidity);
163: 
164:         (uint256 currentDebt0, uint256 currentDebt1) = IVault(vault).getPositionDebt(positionId);
165: 
166:         if (currentDebt0 > 0) {
167:             
168:             if (currentDebt0 > token0Reduced) {
169:                 
170:                 
171:                 
172:                 uint256 amount1Excess = token1Reduced - currentDebt1;
173:                 _swapTokenExactInput(token1, token0, amount1Excess, currentDebt0 - token0Reduced);
174:             }
175:             IERC20(token0).approve(vault, currentDebt0);
176:         }
177: 
178:         if (currentDebt1 > 0) {
179:             
180:             if (currentDebt1 > token1Reduced) {
181:                 
182:                 
183:                 
184:                 uint256 amount0Excess = token0Reduced - currentDebt0;
185:                 _swapTokenExactInput(token0, token1, amount0Excess, currentDebt1 - token1Reduced);
186:             }
187:             IERC20(token1).approve(vault, currentDebt1);
188:         }
189: 
190:         IVault(vault).repayExact(positionId, currentDebt0, currentDebt1);
191: 
192:         token0Balance = IERC20(token0).balanceOf(address(this));
193:         token1Balance = IERC20(token1).balanceOf(address(this));
194:         if (token0Balance > 0) {
195:             pay(token0, address(this), positionOwner, token0Balance);
196:         }
197:         if (token1Balance > 0) {
198:             pay(token1, address(this), positionOwner, token1Balance);
199:         }
200:     }

['202']

202:     function reducePosition(uint128 reducePercentage, uint256 amount0ToSwap, uint256 amount1ToSwap) // <= FOUND
203:         external
204:         onlyVault
205:         returns (uint128 reduceLiquidity, uint256 token0Reduced, uint256 token1Reduced)
206:     {
207:         _claimFees();
208: 
209:         (,,,,, uint128 liquidity,,,,) =
210:             IShadowNonfungiblePositionManager(getShadowNonfungiblePositionManager()).positions(shadowPositionId);
211: 
212:         reduceLiquidity = liquidity * reducePercentage / 10000;
213: 
214:         (token0Reduced, token1Reduced) = _decreasePosition(reduceLiquidity);
215: 
216:         if (amount0ToSwap > 0) {
217:             _swapTokenExactInput(token0, token1, amount0ToSwap, 0);
218:         }
219: 
220:         if (amount1ToSwap > 0) {
221:             _swapTokenExactInput(token1, token0, amount1ToSwap, 0);
222:         }
223: 
224:         uint256 token0Left = IERC20(token0).balanceOf(address(this));
225:         uint256 token1Left = IERC20(token1).balanceOf(address(this));
226: 
227:         if (token0Left > 0) { // <= FOUND
228:             IERC20(token0).approve(vault, token0Left);
229:         }
230: 
231:         if (token1Left > 0) {
232:             IERC20(token1).approve(vault, token1Left);
233:         }
234: 
235:         IVault(vault).repayExact(positionId, token0Left, token1Left);
236: 
237:         token0Left = IERC20(token0).balanceOf(address(this));
238:         token1Left = IERC20(token1).balanceOf(address(this));
239:         if (token0Left > 0) { // <= FOUND
240:             pay(token0, address(this), positionOwner, token0Left);
241:         }
242:         if (token1Left > 0) {
243:             pay(token1, address(this), positionOwner, token1Left);
244:         }
245:     }

['254']

254:     function liquidatePosition(address caller) // <= FOUND
255:         external
256:         onlyVault
257:         returns (
258:             uint128 liquidity,
259:             uint256 token0Reduced,
260:             uint256 token1Reduced,
261:             uint256 token0Fees,
262:             uint256 token1Fees,
263:             uint256 token0Left,
264:             uint256 token1Left
265:         )
266:     {
267:         _claimFees();
268:         _claimRewards();
269:         (,,,,, liquidity,,,,) =
270:             IShadowNonfungiblePositionManager(getShadowNonfungiblePositionManager()).positions(shadowPositionId);
271: 
272:         (token0Reduced, token1Reduced) = _decreasePosition(liquidity);
273:         (uint256 currentDebt0, uint256 currentDebt1) = IVault(vault).getPositionDebt(positionId);
274: 
275:         LiquidationFeeVars memory vars = LiquidationFeeVars({
276:             liquidationFee: IVault(vault).liquidationFee(),
277:             liquidationCallerFee: IVault(vault).liquidationCallerFee(),
278:             liquidationFeeRecipient: IVault(vault).liquidationFeeRecipient()
279:         });
280:         
281:         if (token0Reduced > 0) {
282:             token0Fees = token0Reduced * vars.liquidationFee / 10000;
283:             pay(token0, address(this), vars.liquidationFeeRecipient, token0Fees);
284: 
285:             uint256 token0CallerFees = token0Fees * vars.liquidationCallerFee / 10000;
286:             if (token0CallerFees > 0) {
287:                 pay(token0, address(this), caller, token0CallerFees);
288:             }
289: 
290:             token0Reduced = token0Reduced - token0Fees - token0CallerFees;
291:         }
292: 
293:         if (token1Reduced > 0) {
294:             token1Fees = token1Reduced * vars.liquidationFee / 10000;
295: 
296:             pay(token1, address(this), vars.liquidationFeeRecipient, token1Fees);
297: 
298:             uint256 token1CallerFees = token1Fees * vars.liquidationCallerFee / 10000;
299:             if (token1CallerFees > 0) {
300:                 pay(token1, address(this), caller, token1CallerFees);
301:             }
302: 
303:             token1Reduced = token1Reduced - token1Fees - token1CallerFees;
304:         }
305: 
306:         if (currentDebt0 > 0) {
307:             
308:             if (currentDebt0 > token0Reduced) {
309:                 uint256 amount1Excess = token1Reduced - currentDebt1;
310:                 _swapTokenExactInput(token1, token0, amount1Excess, currentDebt0 - token0Reduced);
311:             }
312:             IERC20(token0).approve(vault, currentDebt0);
313:         }
314: 
315:         if (currentDebt1 > 0) {
316:             
317:             if (currentDebt1 > token1Reduced) {
318:                 uint256 amount0Excess = token0Reduced - currentDebt0;
319:                 _swapTokenExactInput(token0, token1, amount0Excess, currentDebt1 - token1Reduced);
320:             }
321:             IERC20(token1).approve(vault, currentDebt1);
322:         }
323: 
324:         IVault(vault).repayExact(positionId, currentDebt0, currentDebt1);
325: 
326:         token0Left = IERC20(token0).balanceOf(address(this));
327:         token1Left = IERC20(token1).balanceOf(address(this));
328:         if (token0Left > 0) {
329:             pay(token0, address(this), positionOwner, token0Left);
330:         }
331:         if (token1Left > 0) {
332:             pay(token1, address(this), positionOwner, token1Left);
333:         }
334:     }

['354']

354:     function _claimFees() internal { // <= FOUND
355:         IShadowNonfungiblePositionManager(getShadowNonfungiblePositionManager()).collect(
356:             IShadowNonfungiblePositionManager.CollectParams({
357:                 tokenId: shadowPositionId,
358:                 recipient: address(this),
359:                 amount0Max: type(uint128).max,
360:                 amount1Max: type(uint128).max
361:             })
362:         );
363: 
364:         uint256 token0Fees = IERC20(token0).balanceOf(address(this));
365:         uint256 token1Fees = IERC20(token1).balanceOf(address(this));
366:         if (token0Fees > 0) { // <= FOUND
367:             IERC20(token0).approve(vault, token0Fees);
368:             IVault(vault).claimCallback(token0, token0Fees);
369:         }
370:         if (token1Fees > 0) {
371:             IERC20(token1).approve(vault, token1Fees);
372:             IVault(vault).claimCallback(token1, token1Fees);
373:         }
374:     }

['376']

376:     function _claimRewards() internal { // <= FOUND
377:         address shadowGauge = IShadowRangeVault(vault).shadowGauge();
378:         if (shadowGauge != address(0)) {
379:             address[] memory tokens = IShadowGaugeV3(shadowGauge).getRewardTokens();
380:             IShadowGaugeV3(shadowGauge).getReward(shadowPositionId, tokens);
381: 
382:             for (uint256 i = 0; i < tokens.length; i++) {
383:                 uint256 balance = IERC20(tokens[i]).balanceOf(address(this));
384:                 if (balance > 0) {
385:                     if (tokens[i] == xShadow) {
386:                         
387:                         
388:                         
389:                         
390:                         
391:                         if (IShadowX33(x33).isUnlocked()) {
392:                             address x33Adapter = getX33Adapter();
393:                             IERC20(tokens[i]).approve(x33Adapter, balance); // <= FOUND
394:                             IERC4626(x33Adapter).deposit(balance, address(this));
395:                             balance = IERC20(x33).balanceOf(address(this));
396:                             IERC20(x33).approve(vault, balance);
397:                             IVault(vault).claimCallback(x33, balance);
398:                         }
399:                     } else {
400:                         IERC20(tokens[i]).approve(vault, balance);
401:                         IVault(vault).claimCallback(tokens[i], balance);
402:                     }
403:                 }
404:             }
405:         }
406:     }

['432']

432:     function _swapTokenExactInput(address tokenIn, address tokenOut, uint256 amountIn, uint256 amountOutMinimum) // <= FOUND
433:         internal
434:         returns (uint256 amountOut)
435:     {
436:         address router = // <= FOUND
437:             IAddressRegistry(IVault(vault).addressProvider()).getAddress(AddressId.ADDRESS_ID_SHADOW_ROUTER);
438:         IERC20(tokenIn).approve(router, amountIn);
439: 
440:         amountOut = IShadowSwapRouter(router).exactInputSingle(
441:             IShadowSwapRouter.ExactInputSingleParams({
442:                 tokenIn: tokenIn,
443:                 tokenOut: tokenOut,
444:                 tickSpacing: tickSpacing,
445:                 recipient: address(this),
446:                 deadline: block.timestamp,
447:                 amountIn: amountIn,
448:                 amountOutMinimum: amountOutMinimum,
449:                 sqrtPriceLimitX96: 0
450:             })
451:         );
452:     }

['454']

454:     function _swapTokenExactOutput(address tokenIn, address tokenOut, uint256 amountOut, uint256 amountInMaximum) // <= FOUND
455:         internal
456:         returns (uint256 amountIn)
457:     {
458:         address router = // <= FOUND
459:             IAddressRegistry(IVault(vault).addressProvider()).getAddress(AddressId.ADDRESS_ID_SHADOW_ROUTER);
460:         IERC20(tokenIn).approve(router, amountInMaximum);
461: 
462:         amountIn = IShadowSwapRouter(router).exactOutputSingle(
463:             IShadowSwapRouter.ExactOutputSingleParams({
464:                 tokenIn: tokenIn,
465:                 tokenOut: tokenOut,
466:                 tickSpacing: tickSpacing,
467:                 recipient: address(this),
468:                 deadline: block.timestamp,
469:                 amountOut: amountOut,
470:                 amountInMaximum: amountInMaximum,
471:                 sqrtPriceLimitX96: 0
472:             })
473:         );
474:     }

['131']

131:     function setReserveIds(uint256 _token0ReserveId, uint256 _token1ReserveId) external onlyOwner { // <= FOUND
132:         address lendingPool = getLendingPool();
133: 
134:         IERC20(token0).approve(lendingPool, type(uint256).max); // <= FOUND
135:         IERC20(token1).approve(lendingPool, type(uint256).max);
136:         token0ReserveId = _token0ReserveId;
137:         token1ReserveId = _token1ReserveId;
138:     }

[Low-41] transfer will always revert as the IERC20 interface mismatch

Resolution

In Solidity, using the ERC20 transfer function can be problematic with tokens like USDT, which may not fully adhere to the standard interface, potentially causing transaction reverts. To avoid issues, it’s crucial to interact directly with the token's specific ABI rather than the generic IERC20 interface. Before integrating any token, thoroughly review its contract to ensure compatibility, especially for the transfer method.

Num of instances: 1

Findings

Click to show findings

['195']

195:     function claim() external nonReentrant updateReward(msg.sender) { // <= FOUND
196:         for (uint256 i = 0; i < rewardTokens.length; i++) {
197:             address rewardToken = rewardTokens[i];
198:             uint256 claimable = userRewardsClaimable[msg.sender][rewardToken];
199:             if (claimable > 0) {
200:                 userRewardsClaimable[msg.sender][rewardToken] = 0;
201:                 require(IERC20(rewardToken).transfer(msg.sender, claimable), "transfer failed"); // <= FOUND
202:                 emit RewardPaid(msg.sender, rewardToken, claimable);
203:             }
204:         }
205:     }

[Low-42] No license selected for project

Resolution

Not selecting a license for a software project, particularly in the context of blockchain and smart contracts, poses significant risks and uncertainties. A license explicitly defines how others can use, modify, distribute, and contribute to your code. Without it, the legal status of a project remains unclear, potentially deterring usage and contributions, and raising concerns about intellectual property rights. To mitigate these risks, it is advisable to choose an appropriate open-source license. This fosters a clear understanding of how your project can be utilized and encourages collaboration and adoption within the community, aligning with the open and collaborative nature of blockchain technology.

Num of instances: 1

Findings

Click to show findings

['1']

1: // SPDX-License-Identifier: UNLICENSED // <= FOUND

[Low-43] Constructors missing validation

Resolution

In Solidity, when values are being assigned in constructors to unsigned or integer variables, it's crucial to ensure the provided values adhere to the protocol's specific operational boundaries as laid out in the project specifications and documentation. If the constructors lack appropriate validation checks, there's a risk of setting state variables with values that could cause unexpected and potentially detrimental behavior within the contract's operations, violating the intended logic of the protocol. This can compromise the contract's security and impact the maintainability and reliability of the system. In order to avoid such issues, it is recommended to incorporate rigorous validation checks in constructors. These checks should align with the project's defined rules and constraints, making use of Solidity's built-in require function to enforce these conditions. If the validation checks fail, the require function will cause the transaction to revert, ensuring the integrity and adherence to the protocol's expected behavior.

Num of instances: 3

Findings

Click to show findings

['32']

32:     constructor(string memory name_, string memory symbol_, uint8 decimals_, address underlyingAsset_)
33:         ERC20(name_, symbol_)
34:     {
35:         _decimals = decimals_; // <= FOUND
36: 
37:         require(underlyingAsset_ != address(0), Errors.VL_ADDRESS_CANNOT_ZERO);
38:         underlyingAsset = underlyingAsset_;
39:         lendingPool = msg.sender;
40:     }

['18']

18:     constructor(address _WETH9) {
19:         WETH9 = _WETH9; // <= FOUND
20:     }

['34']

34:     constructor(address _addressProvider) {
35:         addressProvider = _addressProvider; // <= FOUND
36:     }

[Low-44] Contract contains payable functions but no withdraw/sweep function

Resolution

In smart contract development, particularly for Ethereum, having payable functions without a corresponding withdraw or sweep function can lead to potential issues. Payable functions allow the contract to receive Ether, but without a mechanism to withdraw these funds, the Ether can become locked within the contract indefinitely. This situation might be intentional in some cases (like a burn function), but generally, it’s a design oversight. A withdraw or sweep function is necessary to transfer Ether out of the contract to a specific address, typically the owner's or a designated recipient. Without this, the contract lacks flexibility in managing its funds, potentially leading to lost or inaccessible Ether.

Num of instances: 3

Findings

Click to show findings

['9']

9: contract IndividualPosition 

['30']

30: contract ShadowRangeVault is
31:     Initializable,
32:     ReentrancyGuardUpgradeable,
33:     Ownable2StepUpgradeable,
34:     PaymentsUpgradeable,
35:     IVault
36: 

['24']

24: contract LendingPool is ILendingPool, OwnableUpgradeable, PaymentsUpgradeable, ReentrancyGuardUpgradeable 

[Low-45] State variables not capped at reasonable values

Resolution

Setting boundaries on state variables in smart contracts is essential for maintaining system integrity and user protection. Without caps on values, variables could reach extremes that exploit or disrupt contract functionality, leading to potential loss or unintended consequences for users. Implementing checks for minimum and maximum permissible values can prevent such issues, ensuring variables remain within a safe and reasonable range. This practice guards against attacks aimed at destabilizing the contract, such as griefing, where attackers intentionally cause distress by exploiting vulnerabilities. Proper validation promotes contract reliability, user trust, and a healthier ecosystem by mitigating risks associated with unbounded state changes.

Num of instances: 8

Findings

Click to show findings

['131']

131:     function setReserveIds(uint256 _token0ReserveId, uint256 _token1ReserveId) external onlyOwner {
132:         address lendingPool = getLendingPool(); // <= FOUND
133: 
134:         IERC20(token0).approve(lendingPool, type(uint256).max);
135:         IERC20(token1).approve(lendingPool, type(uint256).max);
136:         token0ReserveId = _token0ReserveId;
137:         token1ReserveId = _token1ReserveId;
138:     }

['127']

127:     function setLiquidationDebtRatio(uint256 _liquidationDebtRatio) external onlyOwner {
128:         liquidationDebtRatio = _liquidationDebtRatio; // <= FOUND
129:     }

['114']

114:     function setLiquidationFeeParams(uint256 _liquidationFee, uint256 _liquidationCallerFeeRatio) external onlyOwner {
115:         liquidationFee = _liquidationFee; // <= FOUND
116:         liquidationCallerFee = _liquidationCallerFeeRatio;
117:     }

['144']

144:     function setPerformanceFee(uint256 _performanceFee) external onlyOwner {
145:         performanceFee = _performanceFee; // <= FOUND
146:     }

['140']

140:     function setMinPositionSize(uint256 _minPositionSize) external onlyOwner {
141:         minPositionSize = _minPositionSize; // <= FOUND
142:     }

['626']

626:     function setReserveCapacity(uint256 reserveId, uint256 cap) public onlyOwner notPaused {
627:         DataTypes.ReserveData storage reserve = reserves[reserveId]; // <= FOUND
628: 
629:         reserve.reserveCapacity = cap;
630:         emit SetReserveCapacity(reserveId, cap);
631:     }

['559']

559:     function setCreditsOfVault(uint256 vaultId, uint256 reserveId, uint256 credit) external onlyOwner notPaused {
560:         address vaultAddr = getVault(vaultId);
561:         credits[reserveId][vaultAddr] = credit; // <= FOUND
562:         emit SetCreditsOfVault(vaultId, vaultAddr, reserveId, credit);
563:     }

['96']

96:     function setReward(address rewardToken, uint256 startTime, uint256 endTime, uint256 totalRewards)
97:         public
98:         onlyOwner
99:         nonReentrant
100:         updateReward(address(0))
101:     {
102:         require(startTime < endTime, "start must lt end");
103:         require(rewardData[rewardToken].endTime < block.timestamp, "not end");
104: 
105:         if (!inRewardsTokenList[rewardToken]) { // <= FOUND
106:             rewardTokens.push(rewardToken);
107:             inRewardsTokenList[rewardToken] = true; // <= FOUND
108:         }
109: 
110:         rewardData[rewardToken].startTime = startTime;
111:         rewardData[rewardToken].endTime = endTime;
112:         rewardData[rewardToken].lastUpdateTime = block.timestamp;
113:         rewardData[rewardToken].rewardRate = totalRewards / (endTime - startTime);
114: 
115:         if (block.timestamp > startTime && totalStaked > 0) {
116:             uint256 dt = block.timestamp - startTime;
117: 
118:             rewardData[rewardToken].rewardPerTokenStored +=
119:                 (rewardData[rewardToken].rewardRate * dt * 1e18) / totalStaked;
120:         }
121: 
122:         if (block.timestamp > startTime && totalStaked == 0) {
123:             
124:             
125:             uint256 dt = block.timestamp - startTime;
126:             totalRewards -= rewardData[rewardToken].rewardRate * dt;
127:         }
128: 
129:         IERC20(rewardToken).safeTransferFrom(msg.sender, address(this), totalRewards);
130: 
131:         emit RewardsSet(rewardToken, startTime, endTime, totalRewards);
132:     }

[Low-46] The WETH address should not be assignable post deployment

Resolution

Making the Wrapped Ether (WETH) address non-assignable post-deployment enhances contract security and predictability. Once a contract is deployed with a hardcoded or constructor-set WETH address, it ensures that interactions remain consistent and secure, protecting against the risk of address tampering or errors in updating the address. This immutability prevents unauthorized changes that could redirect funds or break integrations, fostering trust in the contract's operations and its interactions with the WETH token.

Num of instances: 1

Findings

Click to show findings

['11']

11:  address public WETH9;

[Low-47] Use forceApprove in place of approve

Resolution

The forceApprove function is a specialized solution addressing the issues that arise with non-standard ERC-20 tokens, such as USDT, which exhibit non-standard behavior in their approve function. These tokens may necessitate setting the allowance to 0 before changing it, may not return a boolean value from approve calls, or may return false instead of reverting on failure. OpenZeppelin's SafeERC20 library includes a forceApprove method to safely handle these peculiarities, ensuring that smart contracts can interact with such tokens without transaction failures. It rectifies inconsistencies by ensuring that all token interactions, including approvals, adhere to expected standards, even when the underlying tokens do not.

Num of instances: 17

Findings

Click to show findings

['51']

51:         IERC20(token0).approve(vault, type(uint256).max); // <= FOUND

['52']

52:         IERC20(token1).approve(vault, type(uint256).max); // <= FOUND

['109']

109:         IERC20(token0).approve(shadowNonfungiblePositionManager, params.amount0Desired); // <= FOUND

['110']

110:         IERC20(token1).approve(shadowNonfungiblePositionManager, params.amount1Desired); // <= FOUND

['175']

175:             IERC20(token0).approve(vault, currentDebt0); // <= FOUND

['187']

187:             IERC20(token1).approve(vault, currentDebt1); // <= FOUND

['228']

228:             IERC20(token0).approve(vault, token0Left); // <= FOUND

['232']

232:             IERC20(token1).approve(vault, token1Left); // <= FOUND

['367']

367:             IERC20(token0).approve(vault, token0Fees); // <= FOUND

['371']

371:             IERC20(token1).approve(vault, token1Fees); // <= FOUND

['393']

393:                             IERC20(tokens[i]).approve(x33Adapter, balance); // <= FOUND

['396']

396:                             IERC20(x33).approve(vault, balance); // <= FOUND

['400']

400:                         IERC20(tokens[i]).approve(vault, balance); // <= FOUND

['438']

438:         IERC20(tokenIn).approve(router, amountIn); // <= FOUND

['460']

460:         IERC20(tokenIn).approve(router, amountInMaximum); // <= FOUND

['134']

134:         IERC20(token0).approve(lendingPool, type(uint256).max); // <= FOUND

['135']

135:         IERC20(token1).approve(lendingPool, type(uint256).max); // <= FOUND

[Low-48] Lack of support for rebasing tokens

Resolution

Rebasing tokens, such as Aave's Tokens, dynamically adjust their holders' balances over time, meaning that the token balance in a user's account increases periodically. This rebase feature poses a challenge when these tokens are held in contracts since any rewards generated accrue directly to the contract, preventing the original depositor from withdrawing them.\n\nTo solve this issue, a viable approach is to maintain a system of 'shares' corresponding to each deposit. These shares denote the proportion of the total contract balance attributable to each depositor. When a withdrawal is initiated, these shares can be redeemed, ensuring each user receives their fair portion of the current balance at the time of withdrawal. This strategy effectively enables the original depositor to benefit from the accrued rewards, despite the rebasing nature of the tokens.

Num of instances: 3

Findings

Click to show findings

['159']

159:     function withdraw(uint256 amount, address to) external nonReentrant updateReward(msg.sender) { // <= FOUND
160:         require(amount > 0, "amount = 0");
161: 
162:         balanceOf[msg.sender] -= amount;
163:         totalStaked -= amount;
164: 
165:         require(stakedToken.transfer(to, amount), "transfer failed"); // <= FOUND
166: 
167:         emit Withdraw(msg.sender, to, amount);
168:     }

['179']

179:     function withdrawByLendingPool(uint256 amount, address user, address to) // <= FOUND
180:         external
181:         onlyLendingPool
182:         nonReentrant
183:         updateReward(user)
184:     {
185:         require(amount > 0, "amount = 0");
186: 
187:         balanceOf[user] -= amount;
188:         totalStaked -= amount;
189: 
190:         require(stakedToken.transfer(to, amount), "transfer falied"); // <= FOUND
191: 
192:         emit Withdraw(user, to, amount);
193:     }

['96']

96:     function setReward(address rewardToken, uint256 startTime, uint256 endTime, uint256 totalRewards) // <= FOUND
97:         public
98:         onlyOwner
99:         nonReentrant
100:         updateReward(address(0))
101:     {
102:         require(startTime < endTime, "start must lt end");
103:         require(rewardData[rewardToken].endTime < block.timestamp, "not end");
104: 
105:         if (!inRewardsTokenList[rewardToken]) {
106:             rewardTokens.push(rewardToken);
107:             inRewardsTokenList[rewardToken] = true;
108:         }
109: 
110:         rewardData[rewardToken].startTime = startTime;
111:         rewardData[rewardToken].endTime = endTime;
112:         rewardData[rewardToken].lastUpdateTime = block.timestamp;
113:         rewardData[rewardToken].rewardRate = totalRewards / (endTime - startTime);
114: 
115:         if (block.timestamp > startTime && totalStaked > 0) {
116:             uint256 dt = block.timestamp - startTime;
117: 
118:             rewardData[rewardToken].rewardPerTokenStored +=
119:                 (rewardData[rewardToken].rewardRate * dt * 1e18) / totalStaked;
120:         }
121: 
122:         if (block.timestamp > startTime && totalStaked == 0) {
123:             
124:             
125:             uint256 dt = block.timestamp - startTime;
126:             totalRewards -= rewardData[rewardToken].rewardRate * dt;
127:         }
128: 
129:         IERC20(rewardToken).safeTransferFrom(msg.sender, address(this), totalRewards); // <= FOUND
130: 
131:         emit RewardsSet(rewardToken, startTime, endTime, totalRewards);
132:     }

[Low-49] Constructor doesn't set initial owner

Resolution

In recent versions of the OpenZeppelin Ownable library, ownership has to be initialized within the constructor by adding Ownable(<owner_address>) to the constructor declaration. This is only a LOW issue as in older versions of OZ lib this will not cause an issue as the Ownable constructor automatically sets the owner to msg.sender.

Num of instances: 3

Findings

Click to show findings

['10']

10:     constructor(address _weth9) {
11:         setAddress(AddressId.ADDRESS_ID_WETH9, _weth9);
12:     }

['62']

62:     constructor(address _stakingToken) {
63:         stakedToken = IERC20(_stakingToken);
64:         lendingPool = msg.sender;
65:     }

['19']

19:     constructor(address _addressRegistry) {
20:         require(_addressRegistry != address(0), Errors.VL_ADDRESS_CANNOT_ZERO);
21:         addressRegistry = _addressRegistry;
22:         nextVaultID = 1;
23:     }

[Low-50] Functions calling contracts/addresses with transfer hooks are missing reentrancy guards

Resolution

While adherence to the check-effects-interaction pattern is commendable, the absence of a reentrancy guard in functions, especially where transfer hooks might be present, can expose the protocol users to risks of read-only reentrancies. Such reentrancy vulnerabilities can be exploited to execute malicious actions even without altering the contract state. Without a reentrancy guard, the only potential mitigation would be to blocklist the entire protocol - an extreme and disruptive measure. Therefore, incorporating a reentrancy guard into these functions is vital to bolster security, as it helps protect against both traditional reentrancy attacks and read-only reentrancies, ensuring robust and safe protocol operations.

Num of instances: 2

Findings

Click to show findings

['46']

46:     function pay(address token, address payer, address recipient, uint256 value) internal { // <= FOUND
47:         if (token == WETH9 && address(this).balance >= value) {
48:             
49:             IWETH9(WETH9).deposit{value: value}(); 
50:             require(IWETH9(WETH9).transfer(recipient, value), "transfer failed"); // <= FOUND
51:         } else if (payer == address(this)) {
52:             
53:             TransferHelper.safeTransfer(token, recipient, value);
54:         } else {
55:             
56:             TransferHelper.safeTransferFrom(token, payer, recipient, value);
57:         }
58:     }

['46']

46:     function pay(address token, address payer, address recipient, uint256 value) internal {
47:         if (token == WETH9 && address(this).balance >= value) {
48:             
49:             IWETH9(WETH9).deposit{value: value}(); 
50:             require(IWETH9(WETH9).transfer(recipient, value), "transfer failed"); // <= FOUND
51:         } else if (payer == address(this)) {
52:             
53:             TransferHelper.safeTransfer(token, recipient, value);
54:         } else {
55:             
56:             TransferHelper.safeTransferFrom(token, payer, recipient, value);
57:         }
58:     }

[Low-51] Solidity version 0.8.20 won't work on all chains due to PUSH0

Resolution

Solidity version 0.8.20 uses the new Shanghai EVM which introduces the PUSH0 opcode, this may not be implemented on all chains and L2 thus reducing the portability and compatability of the code. Consider using a earlier solidity version.

Num of instances: 2

Findings

Click to show findings

['3']

3: pragma solidity ^0.8.0; // <= FOUND

['3']

3: pragma solidity ^0.8.18; // <= FOUND

[Low-52] Missing events in functions that are either setters, privileged or voting related

Resolution

Sensitive setter functions in smart contracts often alter critical state variables. Without events emitted in these functions, external observers or dApps cannot easily track or react to these state changes. Missing events can obscure contract activity, hampering transparency and making integration more challenging. To resolve this, incorporate appropriate event emissions within these functions. Events offer an efficient way to log crucial changes, aiding in real-time tracking and post-transaction verification.

Num of instances: 32

Findings

Click to show findings

['401']

401:     function setBorrowingRateConfig(
402:         DataTypes.ReserveData storage reserve,
403:         uint16 utilizationA,
404:         uint16 borrowingRateA,
405:         uint16 utilizationB,
406:         uint16 borrowingRateB,
407:         uint16 maxBorrowingRate
408:     ) internal 

['39']

39:     function setOracleManager(address _oracleManager) external onlyOracleManager 

['44']

44:     function setPythPriceId(address token, bytes32 priceId) external onlyOracleManager 

['48']

48:     function setMaxPriceAge(uint256 _maxPriceAge) external onlyOracleManager 

['110']

110:     function setLiquidator(address _liquidator, bool _isLiquidator) external onlyOwner 

['114']

114:     function setLiquidationFeeParams(uint256 _liquidationFee, uint256 _liquidationCallerFeeRatio) external onlyOwner 

['119']

119:     function setOpenLiquidationEnabled(bool _openLiquidationEnabled) external onlyOwner 

['123']

123:     function setPositionImplementation(address _positionImplementation) external onlyOwner 

['127']

127:     function setLiquidationDebtRatio(uint256 _liquidationDebtRatio) external onlyOwner 

['131']

131:     function setReserveIds(uint256 _token0ReserveId, uint256 _token1ReserveId) external onlyOwner 

['140']

140:     function setMinPositionSize(uint256 _minPositionSize) external onlyOwner 

['144']

144:     function setPerformanceFee(uint256 _performanceFee) external onlyOwner 

['148']

148:     function setShadowGauge(address _shadowGauge) external onlyOwner 

['40']

40:     function _setOwner(address newOwner) private 

['393']

393:     function updateDebtPosition(DataTypes.DebtPositionData storage debtPosition, uint256 latestBorrowingIndex)
394:         internal
395:     

['207']

207:     function update() external updateReward(address(0)) onlyOwner 

['401']

401:     function setBorrowingRateConfig( // <= FOUND
402:         DataTypes.ReserveData storage reserve,
403:         uint16 utilizationA,
404:         uint16 borrowingRateA,
405:         uint16 utilizationB,
406:         uint16 borrowingRateB,
407:         uint16 maxBorrowingRate
408:     ) internal {
409:         
410:         reserve.borrowingRateConfig.utilizationA =
411:             uint128(Precision.FACTOR1E18.mul(utilizationA).div(Constants.PERCENT_100));
412: 
413:         reserve.borrowingRateConfig.borrowingRateA =
414:             uint128(Precision.FACTOR1E18.mul(borrowingRateA).div(Constants.PERCENT_100));
415:         reserve.borrowingRateConfig.utilizationB =
416:             uint128(Precision.FACTOR1E18.mul(utilizationB).div(Constants.PERCENT_100));
417:         reserve.borrowingRateConfig.borrowingRateB =
418:             uint128(Precision.FACTOR1E18.mul(borrowingRateB).div(Constants.PERCENT_100));
419:         reserve.borrowingRateConfig.maxBorrowingRate =
420:             uint128(Precision.FACTOR1E18.mul(maxBorrowingRate).div(Constants.PERCENT_100));
421:     }

['39']

39:     function setOracleManager(address _oracleManager) external onlyOracleManager { // <= FOUND
40:         require(_oracleManager != address(0), "Oracle manager cannot be zero address");
41:         oracleManager = _oracleManager;
42:     }

['44']

44:     function setPythPriceId(address token, bytes32 priceId) external onlyOracleManager { // <= FOUND
45:         pythPriceIds[token] = priceId;
46:     }

['48']

48:     function setMaxPriceAge(uint256 _maxPriceAge) external onlyOracleManager { // <= FOUND
49:         maxPriceAge = _maxPriceAge;
50:     }

['110']

110:     function setLiquidator(address _liquidator, bool _isLiquidator) external onlyOwner { // <= FOUND
111:         liquidators[_liquidator] = _isLiquidator;
112:     }

['114']

114:     function setLiquidationFeeParams(uint256 _liquidationFee, uint256 _liquidationCallerFeeRatio) external onlyOwner { // <= FOUND
115:         liquidationFee = _liquidationFee;
116:         liquidationCallerFee = _liquidationCallerFeeRatio;
117:     }

['119']

119:     function setOpenLiquidationEnabled(bool _openLiquidationEnabled) external onlyOwner { // <= FOUND
120:         openLiquidationEnabled = _openLiquidationEnabled;
121:     }

['123']

123:     function setPositionImplementation(address _positionImplementation) external onlyOwner { // <= FOUND
124:         positionImplementation = _positionImplementation;
125:     }

['127']

127:     function setLiquidationDebtRatio(uint256 _liquidationDebtRatio) external onlyOwner { // <= FOUND
128:         liquidationDebtRatio = _liquidationDebtRatio;
129:     }

['131']

131:     function setReserveIds(uint256 _token0ReserveId, uint256 _token1ReserveId) external onlyOwner { // <= FOUND
132:         address lendingPool = getLendingPool();
133: 
134:         IERC20(token0).approve(lendingPool, type(uint256).max);
135:         IERC20(token1).approve(lendingPool, type(uint256).max);
136:         token0ReserveId = _token0ReserveId;
137:         token1ReserveId = _token1ReserveId;
138:     }

['140']

140:     function setMinPositionSize(uint256 _minPositionSize) external onlyOwner { // <= FOUND
141:         minPositionSize = _minPositionSize;
142:     }

['144']

144:     function setPerformanceFee(uint256 _performanceFee) external onlyOwner { // <= FOUND
145:         performanceFee = _performanceFee;
146:     }

['148']

148:     function setShadowGauge(address _shadowGauge) external onlyOwner { // <= FOUND
149:         shadowGauge = _shadowGauge;
150:     }

['40']

40:     function _setOwner(address newOwner) private { // <= FOUND
41:         bytes32 slot = _ADMIN_SLOT;
42: 
43:         
44:         assembly {
45:             sstore(slot, newOwner)
46:         }
47:     }

['393']

393:     function updateDebtPosition(DataTypes.DebtPositionData storage debtPosition, uint256 latestBorrowingIndex) // <= FOUND
394:         internal
395:     {
396:         debtPosition.borrowed = debtPosition.borrowed.mul(latestBorrowingIndex).div(debtPosition.borrowedIndex);
397: 
398:         debtPosition.borrowedIndex = latestBorrowingIndex;
399:     }

['207']

207:     function update() external updateReward(address(0)) onlyOwner {} // <= FOUND

[Low-53] Unsafe use of transfer()/transferFrom() with IERC20

Resolution

SafeTransfer should be used in place of Transfer for Solidity contracts to ensure robust security and error handling. Unlike the basic Transfer function, SafeTransfer incorporates safeguards against potential smart contract vulnerabilities, such as reentrancy attacks and unexpected token loss. By automatically validating the recipient's ability to receive tokens and reverting transactions in case of failures,

Num of instances: 4

Findings

Click to show findings

['46']

46:     function pay(address token, address payer, address recipient, uint256 value) internal { // <= FOUND
47:         if (token == WETH9 && address(this).balance >= value) {
48:             
49:             IWETH9(WETH9).deposit{value: value}(); 
50:             require(IWETH9(WETH9).transfer(recipient, value), "transfer failed"); // <= FOUND
51:         } else if (payer == address(this)) {
52:             
53:             TransferHelper.safeTransfer(token, recipient, value);
54:         } else {
55:             
56:             TransferHelper.safeTransferFrom(token, payer, recipient, value);
57:         }
58:     }

['159']

159:     function withdraw(uint256 amount, address to) external nonReentrant updateReward(msg.sender) { // <= FOUND
160:         require(amount > 0, "amount = 0");
161: 
162:         balanceOf[msg.sender] -= amount;
163:         totalStaked -= amount;
164: 
165:         require(stakedToken.transfer(to, amount), "transfer failed"); // <= FOUND
166: 
167:         emit Withdraw(msg.sender, to, amount);
168:     }

['179']

179:     function withdrawByLendingPool(uint256 amount, address user, address to) // <= FOUND
180:         external
181:         onlyLendingPool
182:         nonReentrant
183:         updateReward(user)
184:     {
185:         require(amount > 0, "amount = 0");
186: 
187:         balanceOf[user] -= amount;
188:         totalStaked -= amount;
189: 
190:         require(stakedToken.transfer(to, amount), "transfer falied"); // <= FOUND
191: 
192:         emit Withdraw(user, to, amount);
193:     }

['195']

195:     function claim() external nonReentrant updateReward(msg.sender) { // <= FOUND
196:         for (uint256 i = 0; i < rewardTokens.length; i++) {
197:             address rewardToken = rewardTokens[i];
198:             uint256 claimable = userRewardsClaimable[msg.sender][rewardToken];
199:             if (claimable > 0) {
200:                 userRewardsClaimable[msg.sender][rewardToken] = 0;
201:                 require(IERC20(rewardToken).transfer(msg.sender, claimable), "transfer failed"); // <= FOUND
202:                 emit RewardPaid(msg.sender, rewardToken, claimable);
203:             }
204:         }
205:     }

[Low-54] Avoid floating pragma in non interface/library files

Resolution

Avoid using floating pragmas in non-interface and non-library files to ensure contract compatibility and security. Floating pragmas like pragma solidity ^0.8.0; allow any compiler version that matches the specified range. While this can provide flexibility, it risks introducing unexpected behavior or vulnerabilities from future compiler versions. Instead, specify a fixed pragma version, such as pragma solidity 0.8.0;, to guarantee consistent behavior and security across deployments. This practice ensures that your contracts are tested and verified against a specific compiler version, reducing the risk of unforeseen issues and maintaining code reliability.

Num of instances: 15

Findings

Click to show findings

[]

7: contract AddressRegistry is IAddressRegistry, Ownable 

[]

9: contract IndividualPosition 

[]

19: contract ExtraInterestBearingToken is IExtraInterestBearingToken, ReentrancyGuard, ERC20 

[]

24: contract LendingPool is ILendingPool, OwnableUpgradeable, PaymentsUpgradeable, ReentrancyGuardUpgradeable 

[]

13: contract StakingRewards is Ownable, IStakingRewards 

[]

7: contract MightyTimelockController is TimelockController 

[]

10: contract PrimaryPriceOracle is IPriceOracle, Initializable 

[]

10: contract ShadowPositionValueCalculator 

[]

26: contract ShadowRangePositionImpl is IShadowRangePositionImpl, PaymentsUpgradeable 

[]

30: contract ShadowRangeVault is
31:     Initializable,
32:     ReentrancyGuardUpgradeable,
33:     Ownable2StepUpgradeable,
34:     PaymentsUpgradeable,
35:     IVault
36: 

[]

10: contract SwapXPositionValueCalculator 

[]

11: contract VaultRegistry is Ownable, IVaultFactory 

[]

31: contract VaultPositionViewer 

[]

10: abstract contract Payments 

[]

10: abstract contract PaymentsUpgradeable 

[Low-55] Upgradeable contract uses SafeERC20 rather than SafeERC20Upgradeable thus isn't upgrade safe as SafeERC20 utilizes Address.sol which uses delegatecall

Resolution

It has been noted by OpenZeppelin that SafeERC20 isn't upgrade safe as it 'does depend on Address, which contains non-upgrade-safe functions' - OZ, as such it has been advised to use SafeERC20Upgradeable instead which utilizes AddressUpgradable which is upgrade safe.

Num of instances: 1

Findings

Click to show findings

['24']

24: contract LendingPool is ILendingPool, OwnableUpgradeable, PaymentsUpgradeable, ReentrancyGuardUpgradeable 

[Low-56] Read only reentrancy risk detected

Resolution

A read-only reentrancy vulnerability arises when a contract's view or pure function is re-entered during its execution, potentially leading to inconsistent state readings or unintended behaviors. Although these functions don't modify the contract's state, they can still be exploited if they depend on external calls that may re-enter the contract. This can result in the function returning outdated or manipulated data, which, when relied upon by other contracts or systems, can cause incorrect operations or financial discrepancies.

To mitigate this risk, it's essential to implement reentrancy guards even in read-only functions, especially when they involve external calls. Additionally, adhering to the checks-effects-interactions pattern and minimizing external calls within view functions can further reduce the potential for such vulnerabilities. Regular security audits and thorough testing are also crucial to identify and address any reentrancy issues before deployment.

Num of instances: 15

Findings

Click to show findings

['433']

433:     function getVault(uint256 vaultId) internal view returns (address vaultAddress) {
434:         address vaultFactory = AddressRegistry(addressRegistry).getAddress(AddressId.ADDRESS_ID_VAULT_FACTORY); // <= FOUND
435: 
436:         vaultAddress = IVaultFactory(vaultFactory).vaults(vaultId);
437:         require(vaultAddress != address(0), "Invalid VaultId");
438:     }

['432']

432:     function _swapTokenExactInput(address tokenIn, address tokenOut, uint256 amountIn, uint256 amountOutMinimum)
433:         internal
434:         returns (uint256 amountOut)
435:     {
436:         address router =
437:             IAddressRegistry(IVault(vault).addressProvider()).getAddress(AddressId.ADDRESS_ID_SHADOW_ROUTER); // <= FOUND
438:         IERC20(tokenIn).approve(router, amountIn);
439: 
440:         amountOut = IShadowSwapRouter(router).exactInputSingle(
441:             IShadowSwapRouter.ExactInputSingleParams({
442:                 tokenIn: tokenIn,
443:                 tokenOut: tokenOut,
444:                 tickSpacing: tickSpacing,
445:                 recipient: address(this),
446:                 deadline: block.timestamp,
447:                 amountIn: amountIn,
448:                 amountOutMinimum: amountOutMinimum,
449:                 sqrtPriceLimitX96: 0
450:             })
451:         );
452:     }

['454']

454:     function _swapTokenExactOutput(address tokenIn, address tokenOut, uint256 amountOut, uint256 amountInMaximum)
455:         internal
456:         returns (uint256 amountIn)
457:     {
458:         address router =
459:             IAddressRegistry(IVault(vault).addressProvider()).getAddress(AddressId.ADDRESS_ID_SHADOW_ROUTER); // <= FOUND
460:         IERC20(tokenIn).approve(router, amountInMaximum);
461: 
462:         amountIn = IShadowSwapRouter(router).exactOutputSingle(
463:             IShadowSwapRouter.ExactOutputSingleParams({
464:                 tokenIn: tokenIn,
465:                 tokenOut: tokenOut,
466:                 tickSpacing: tickSpacing,
467:                 recipient: address(this),
468:                 deadline: block.timestamp,
469:                 amountOut: amountOut,
470:                 amountInMaximum: amountInMaximum,
471:                 sqrtPriceLimitX96: 0
472:             })
473:         );
474:     }

['83']

83:     function initialize(
84:         address _addressProvider,
85:         address _vaultRegistry,
86:         address _shadowV3PoolAddress,
87:         address _initialPositionImplementation
88:     ) public initializer {
89:         _initializePayments(IAddressRegistry(_addressProvider).getAddress(AddressId.ADDRESS_ID_WETH9)); // <= FOUND
90:         __ReentrancyGuard_init();
91:         __Ownable2Step_init();
92:         token0 = IShadowV3Pool(_shadowV3PoolAddress).token0();
93:         token1 = IShadowV3Pool(_shadowV3PoolAddress).token1();
94:         token0Decimals = IERC20Metadata(token0).decimals(); // <= FOUND
95:         token1Decimals = IERC20Metadata(token1).decimals(); // <= FOUND
96: 
97:         addressProvider = _addressProvider;
98:         vaultRegistry = _vaultRegistry;
99:         nextPositionID = 1;
100:         shadowV3Pool = _shadowV3PoolAddress;
101: 
102:         liquidationDebtRatio = 8600;
103:         liquidationFee = 500;
104:         performanceFee = 3000;
105:         minPositionSize = 100e8;
106: 
107:         positionImplementation = _initialPositionImplementation;
108:     }

['69']

69:     function initReserve(address asset) external onlyOwner notPaused {
70:         uint256 id = nextReserveId;
71:         nextReserveId += 1;
72: 
73:         
74:         string memory name = string(abi.encodePacked(ERC20(asset).name(), "(MightyFi Interest Bearing Token)"));
75:         string memory symbol = string(abi.encodePacked("m", ERC20(asset).symbol()));
76:         uint8 decimals = ERC20(asset).decimals(); // <= FOUND
77: 
78:         address eTokenAddress = ETokenDeployer.deploy(name, symbol, decimals, asset, id);
79: 
80:         DataTypes.ReserveData storage reserveData = reserves[id];
81:         reserveData.setActive(true);
82:         reserveData.setBorrowingEnabled(true);
83: 
84:         initReserve(reserveData, asset, eTokenAddress, type(uint256).max, id);
85: 
86:         createStakingPoolForReserve(id);
87: 
88:         emit InitReserve(asset, eTokenAddress, reserveData.stakingAddress, id);
89:     }

['309']

309:     function repayExact(uint256 positionId, uint256 amount0, uint256 amount1) external payable {
310:         PositionInfo storage positionInfo = positionInfos[positionId];
311: 
312:         
313:         address lendingPool = getLendingPool();
314:         if (amount0 > 0 && positionInfo.token0DebtId != 0) {
315:             (uint256 currentDebt0,) = ILendingPool(lendingPool).getCurrentDebt(positionInfo.token0DebtId); // <= FOUND
316:             if (amount0 > currentDebt0) {
317:                 amount0 = currentDebt0;
318:             }
319: 
320:             pay(token0, msg.sender, address(this), amount0);
321: 
322:             ILendingPool(lendingPool).repay(address(this), positionInfo.token0DebtId, amount0);
323:         }
324:         if (amount1 > 0 && positionInfo.token1DebtId != 0) {
325:             (uint256 currentDebt1,) = ILendingPool(lendingPool).getCurrentDebt(positionInfo.token1DebtId); // <= FOUND
326:             if (amount1 > currentDebt1) {
327:                 amount1 = currentDebt1;
328:             }
329: 
330:             pay(token1, msg.sender, address(this), amount1);
331: 
332:             ILendingPool(lendingPool).repay(address(this), positionInfo.token1DebtId, amount1);
333:         }
334: 
335:         if (msg.value > 0) {
336:             refundETH();
337:         }
338:     }

['437']

437:     function getPositionDebt(uint256 positionId) public view returns (uint256 amount0Debt, uint256 amount1Debt) {
438:         address lendingPool = getLendingPool();
439:         if (positionInfos[positionId].token0DebtId != 0) {
440:             (amount0Debt,) = ILendingPool(lendingPool).getCurrentDebt(positionInfos[positionId].token0DebtId); // <= FOUND
441:         }
442:         if (positionInfos[positionId].token1DebtId != 0) {
443:             (amount1Debt,) = ILendingPool(lendingPool).getCurrentDebt(positionInfos[positionId].token1DebtId); // <= FOUND
444:         }
445:     }

['68']

68:     function getUserPosition(address vault, address owner) public view returns (PositionInfo[] memory) {
69:         uint256[] memory ids = getPositionIds(vault, owner);
70:         PositionInfo[] memory positions = new PositionInfo[](ids.length);
71:         for (uint256 i = 0; i < ids.length; i++) {
72:             positions[i].vault = vault;
73:             positions[i].id = ids[i];
74:             positions[i].token0 = IVault(vault).token0();
75:             positions[i].token1 = IVault(vault).token1();
76:             (positions[i].amount0, positions[i].amount1) = IVault(vault).getPositionAmounts(ids[i]);
77:             if (positions[i].amount0 > 0 || positions[i].amount1 > 0) {
78:                 (positions[i].amount0Debt, positions[i].amount1Debt) = IVault(vault).getPositionDebt(ids[i]); // <= FOUND
79:                 positions[i].debtRatio = IVault(vault).getDebtRatio(ids[i]);
80:             }
81:             positions[i].price0 = IVault(vault).getTokenPrice(positions[i].token0); // <= FOUND
82:             positions[i].price1 = IVault(vault).getTokenPrice(positions[i].token1); // <= FOUND
83: 
84:             IVault.PositionInfo memory positionInfo = IVault(vault).getPositionInfos(ids[i]);
85: 
86:             positions[i].positionAddress = positionInfo.positionAddress;
87:             positions[i].positionNftId = positionInfo.shadowPositionId;
88:             positions[i].tickUpper = positionInfo.tickUpper;
89:             positions[i].tickLower = positionInfo.tickLower;
90:             positions[i].ul = positionInfo.ul;
91:             positions[i].ll = positionInfo.ll;
92: 
93:             positions[i].inRange = positions[i].amount0 > 0 && positions[i].amount1 > 0;
94: 
95:             (positions[i].rewards, positions[i].rewardsAmounts) =
96:                 IPositionImpl(positionInfo.positionAddress).claimableRewards();
97: 
98:             positions[i].currentTick = IVault(vault).getCurrentTick();
99: 
100:             (,,,,, positions[i].liquidity,,,,) = INFTPositionManager(
101:                 IAddressRegistry(addressProvider).getAddress(AddressId.ADDRESS_ID_SHADOW_NONFUNGIBLE_POSITION_MANAGER) // <= FOUND
102:             ).positions(positionInfo.shadowPositionId);
103:         }
104:         return positions;
105:     }

['145']

145:     function closePosition()
146:         external
147:         onlyVault
148:         returns (
149:             uint128 liquidity,
150:             uint256 token0Reduced,
151:             uint256 token1Reduced,
152:             uint256 token0Balance,
153:             uint256 token1Balance
154:         )
155:     {
156:         _claimFees();
157:         _claimRewards();
158: 
159:         (,,,,, liquidity,,,,) =
160:             IShadowNonfungiblePositionManager(getShadowNonfungiblePositionManager()).positions(shadowPositionId);
161: 
162:         (token0Reduced, token1Reduced) = _decreasePosition(liquidity);
163: 
164:         (uint256 currentDebt0, uint256 currentDebt1) = IVault(vault).getPositionDebt(positionId); // <= FOUND
165: 
166:         if (currentDebt0 > 0) {
167:             
168:             if (currentDebt0 > token0Reduced) {
169:                 
170:                 
171:                 
172:                 uint256 amount1Excess = token1Reduced - currentDebt1;
173:                 _swapTokenExactInput(token1, token0, amount1Excess, currentDebt0 - token0Reduced);
174:             }
175:             IERC20(token0).approve(vault, currentDebt0);
176:         }
177: 
178:         if (currentDebt1 > 0) {
179:             
180:             if (currentDebt1 > token1Reduced) {
181:                 
182:                 
183:                 
184:                 uint256 amount0Excess = token0Reduced - currentDebt0;
185:                 _swapTokenExactInput(token0, token1, amount0Excess, currentDebt1 - token1Reduced);
186:             }
187:             IERC20(token1).approve(vault, currentDebt1);
188:         }
189: 
190:         IVault(vault).repayExact(positionId, currentDebt0, currentDebt1); // <= FOUND
191: 
192:         token0Balance = IERC20(token0).balanceOf(address(this));
193:         token1Balance = IERC20(token1).balanceOf(address(this));
194:         if (token0Balance > 0) {
195:             pay(token0, address(this), positionOwner, token0Balance);
196:         }
197:         if (token1Balance > 0) {
198:             pay(token1, address(this), positionOwner, token1Balance);
199:         }
200:     }

['254']

254:     function liquidatePosition(address caller)
255:         external
256:         onlyVault
257:         returns (
258:             uint128 liquidity,
259:             uint256 token0Reduced,
260:             uint256 token1Reduced,
261:             uint256 token0Fees,
262:             uint256 token1Fees,
263:             uint256 token0Left,
264:             uint256 token1Left
265:         )
266:     {
267:         _claimFees();
268:         _claimRewards();
269:         (,,,,, liquidity,,,,) =
270:             IShadowNonfungiblePositionManager(getShadowNonfungiblePositionManager()).positions(shadowPositionId);
271: 
272:         (token0Reduced, token1Reduced) = _decreasePosition(liquidity);
273:         (uint256 currentDebt0, uint256 currentDebt1) = IVault(vault).getPositionDebt(positionId); // <= FOUND
274: 
275:         LiquidationFeeVars memory vars = LiquidationFeeVars({
276:             liquidationFee: IVault(vault).liquidationFee(),
277:             liquidationCallerFee: IVault(vault).liquidationCallerFee(),
278:             liquidationFeeRecipient: IVault(vault).liquidationFeeRecipient()
279:         });
280:         
281:         if (token0Reduced > 0) {
282:             token0Fees = token0Reduced * vars.liquidationFee / 10000;
283:             pay(token0, address(this), vars.liquidationFeeRecipient, token0Fees);
284: 
285:             uint256 token0CallerFees = token0Fees * vars.liquidationCallerFee / 10000;
286:             if (token0CallerFees > 0) {
287:                 pay(token0, address(this), caller, token0CallerFees);
288:             }
289: 
290:             token0Reduced = token0Reduced - token0Fees - token0CallerFees;
291:         }
292: 
293:         if (token1Reduced > 0) {
294:             token1Fees = token1Reduced * vars.liquidationFee / 10000;
295: 
296:             pay(token1, address(this), vars.liquidationFeeRecipient, token1Fees);
297: 
298:             uint256 token1CallerFees = token1Fees * vars.liquidationCallerFee / 10000;
299:             if (token1CallerFees > 0) {
300:                 pay(token1, address(this), caller, token1CallerFees);
301:             }
302: 
303:             token1Reduced = token1Reduced - token1Fees - token1CallerFees;
304:         }
305: 
306:         if (currentDebt0 > 0) {
307:             
308:             if (currentDebt0 > token0Reduced) {
309:                 uint256 amount1Excess = token1Reduced - currentDebt1;
310:                 _swapTokenExactInput(token1, token0, amount1Excess, currentDebt0 - token0Reduced);
311:             }
312:             IERC20(token0).approve(vault, currentDebt0);
313:         }
314: 
315:         if (currentDebt1 > 0) {
316:             
317:             if (currentDebt1 > token1Reduced) {
318:                 uint256 amount0Excess = token0Reduced - currentDebt0;
319:                 _swapTokenExactInput(token0, token1, amount0Excess, currentDebt1 - token1Reduced);
320:             }
321:             IERC20(token1).approve(vault, currentDebt1);
322:         }
323: 
324:         IVault(vault).repayExact(positionId, currentDebt0, currentDebt1); // <= FOUND
325: 
326:         token0Left = IERC20(token0).balanceOf(address(this));
327:         token1Left = IERC20(token1).balanceOf(address(this));
328:         if (token0Left > 0) {
329:             pay(token0, address(this), positionOwner, token0Left);
330:         }
331:         if (token1Left > 0) {
332:             pay(token1, address(this), positionOwner, token1Left);
333:         }
334:     }

['83']

83:     function openPosition(OpenPositionParams memory params)
84:         external
85:         onlyVault
86:         returns (uint256 tokenId, uint128 liquidity, uint256 amount0, uint256 amount1)
87:     {
88:         uint256 token0Balance = IERC20(token0).balanceOf(address(this));
89:         if (params.amount0Desired > token0Balance) {
90:             _swapTokenExactInput(
91:                 token1,
92:                 token0,
93:                 IERC20(token1).balanceOf(address(this)) - params.amount1Desired,
94:                 params.amount0Desired - token0Balance
95:             );
96:         }
97: 
98:         uint256 token1Balance = IERC20(token1).balanceOf(address(this));
99:         if (params.amount1Desired > token1Balance) {
100:             _swapTokenExactInput(
101:                 token0,
102:                 token1,
103:                 IERC20(token0).balanceOf(address(this)) - params.amount0Desired,
104:                 params.amount1Desired - token1Balance
105:             );
106:         }
107:         address shadowNonfungiblePositionManager = getShadowNonfungiblePositionManager();
108: 
109:         IERC20(token0).approve(shadowNonfungiblePositionManager, params.amount0Desired);
110:         IERC20(token1).approve(shadowNonfungiblePositionManager, params.amount1Desired);
111: 
112:         (tokenId, liquidity, amount0, amount1) = IShadowNonfungiblePositionManager(shadowNonfungiblePositionManager)
113:             .mint( // <= FOUND
114:             IShadowNonfungiblePositionManager.MintParams({
115:                 token0: token0,
116:                 token1: token1,
117:                 tickSpacing: tickSpacing,
118:                 tickLower: params.tickLower,
119:                 tickUpper: params.tickUpper,
120:                 amount0Desired: params.amount0Desired,
121:                 amount1Desired: params.amount1Desired,
122:                 amount0Min: 0,
123:                 amount1Min: 0,
124:                 recipient: address(this),
125:                 deadline: block.timestamp
126:             })
127:         );
128: 
129:         shadowPositionId = tokenId;
130:         positionOwner = params.positionOwner;
131: 
132:         unwrapWETH9(0, positionOwner);
133: 
134:         token0Balance = IERC20(token0).balanceOf(address(this));
135:         token1Balance = IERC20(token1).balanceOf(address(this));
136: 
137:         if (token0Balance > 0) {
138:             pay(token0, address(this), positionOwner, token0Balance);
139:         }
140:         if (token1Balance > 0) {
141:             pay(token1, address(this), positionOwner, token1Balance);
142:         }
143:     }

['376']

376:     function _claimRewards() internal {
377:         address shadowGauge = IShadowRangeVault(vault).shadowGauge();
378:         if (shadowGauge != address(0)) {
379:             address[] memory tokens = IShadowGaugeV3(shadowGauge).getRewardTokens();
380:             IShadowGaugeV3(shadowGauge).getReward(shadowPositionId, tokens);
381: 
382:             for (uint256 i = 0; i < tokens.length; i++) {
383:                 uint256 balance = IERC20(tokens[i]).balanceOf(address(this));
384:                 if (balance > 0) {
385:                     if (tokens[i] == xShadow) {
386:                         
387:                         
388:                         
389:                         
390:                         
391:                         if (IShadowX33(x33).isUnlocked()) {
392:                             address x33Adapter = getX33Adapter();
393:                             IERC20(tokens[i]).approve(x33Adapter, balance);
394:                             IERC4626(x33Adapter).deposit(balance, address(this)); // <= FOUND
395:                             balance = IERC20(x33).balanceOf(address(this));
396:                             IERC20(x33).approve(vault, balance);
397:                             IVault(vault).claimCallback(x33, balance);
398:                         }
399:                     } else {
400:                         IERC20(tokens[i]).approve(vault, balance);
401:                         IVault(vault).claimCallback(tokens[i], balance);
402:                     }
403:                 }
404:             }
405:         }
406:     }

['271']

271:     function _closePosition(uint256 positionId) internal returns (uint256, uint256) {
272:         PositionInfo memory positionInfo = positionInfos[positionId];
273: 
274:         (uint128 liquidity, uint256 token0Reduced, uint256 token1Reduced, uint256 token0Balance, uint256 token1Balance)
275:         = IShadowRangePositionImpl(positionInfo.positionAddress).closePosition(); // <= FOUND
276: 
277:         (uint256 currentDebt0, uint256 currentDebt1) = getPositionDebt(positionId);
278:         require(currentDebt0 == 0 && currentDebt1 == 0, "Still debt");
279: 
280:         emit PositionClosed(
281:             positionInfo.owner,
282:             vaultId,
283:             positionId,
284:             positionInfo.positionAddress,
285:             token0Reduced,
286:             token1Reduced,
287:             liquidity,
288:             block.timestamp
289:         );
290: 
291:         return (token0Balance, token1Balance);
292:     }

['29']

29:     function newVault(address _newVault) external onlyOwner returns (uint256 vaultId) {
30:         require(!isRegistered[_newVault], "Vault already registered");
31:         vaultId = nextVaultID;
32:         nextVaultID = nextVaultID + 1;
33: 
34:         IVault(_newVault).onReceiveRegisterCallback(vaultId); // <= FOUND
35: 
36:         vaults[vaultId] = _newVault;
37:         isRegistered[_newVault] = true;
38:         emit NewVault(_newVault, vaultId);
39:     }

['202']

202:     function reducePosition(uint128 reducePercentage, uint256 amount0ToSwap, uint256 amount1ToSwap)
203:         external
204:         onlyVault
205:         returns (uint128 reduceLiquidity, uint256 token0Reduced, uint256 token1Reduced)
206:     {
207:         _claimFees();
208: 
209:         (,,,,, uint128 liquidity,,,,) =
210:             IShadowNonfungiblePositionManager(getShadowNonfungiblePositionManager()).positions(shadowPositionId);
211: 
212:         reduceLiquidity = liquidity * reducePercentage / 10000;
213: 
214:         (token0Reduced, token1Reduced) = _decreasePosition(reduceLiquidity);
215: 
216:         if (amount0ToSwap > 0) {
217:             _swapTokenExactInput(token0, token1, amount0ToSwap, 0);
218:         }
219: 
220:         if (amount1ToSwap > 0) {
221:             _swapTokenExactInput(token1, token0, amount1ToSwap, 0);
222:         }
223: 
224:         uint256 token0Left = IERC20(token0).balanceOf(address(this));
225:         uint256 token1Left = IERC20(token1).balanceOf(address(this));
226: 
227:         if (token0Left > 0) {
228:             IERC20(token0).approve(vault, token0Left);
229:         }
230: 
231:         if (token1Left > 0) {
232:             IERC20(token1).approve(vault, token1Left);
233:         }
234: 
235:         IVault(vault).repayExact(positionId, token0Left, token1Left); // <= FOUND
236: 
237:         token0Left = IERC20(token0).balanceOf(address(this));
238:         token1Left = IERC20(token1).balanceOf(address(this));
239:         if (token0Left > 0) {
240:             pay(token0, address(this), positionOwner, token0Left);
241:         }
242:         if (token1Left > 0) {
243:             pay(token1, address(this), positionOwner, token1Left);
244:         }
245:     }

[Low-57] Common tokens such as WETH9 work differently on chains such a Blast which isn't taken into account during transfer calls.

Resolution

There is a difference on chains such as Blast on how WETH9 is implemented. On most chains the WETH9 contract contains handling for the case where src == msg.sender, however on chains such as Blast, Arbitrum and Fantom. This isn’t the case. Failing to take this discrepancy into account can results in the protocol not functioning as intended on these chains which can have drastic results. Particularly in cases where the contract interacts with it’s own WETH allowance through transferFrom calls, in such cases these can fail if the contract fails to approve itself to use it’s WETH balance which normally isn't done as most WETH contracts do not require approval for instances where src is msg.sender.

Num of instances: 2

Findings

Click to show findings

['96']

96:     function setReward(address rewardToken, uint256 startTime, uint256 endTime, uint256 totalRewards) // <= FOUND
97:         public
98:         onlyOwner
99:         nonReentrant
100:         updateReward(address(0))
101:     {
102:         require(startTime < endTime, "start must lt end");
103:         require(rewardData[rewardToken].endTime < block.timestamp, "not end");
104: 
105:         if (!inRewardsTokenList[rewardToken]) {
106:             rewardTokens.push(rewardToken);
107:             inRewardsTokenList[rewardToken] = true;
108:         }
109: 
110:         rewardData[rewardToken].startTime = startTime;
111:         rewardData[rewardToken].endTime = endTime;
112:         rewardData[rewardToken].lastUpdateTime = block.timestamp;
113:         rewardData[rewardToken].rewardRate = totalRewards / (endTime - startTime);
114: 
115:         if (block.timestamp > startTime && totalStaked > 0) {
116:             uint256 dt = block.timestamp - startTime;
117: 
118:             rewardData[rewardToken].rewardPerTokenStored +=
119:                 (rewardData[rewardToken].rewardRate * dt * 1e18) / totalStaked;
120:         }
121: 
122:         if (block.timestamp > startTime && totalStaked == 0) {
123:             
124:             
125:             uint256 dt = block.timestamp - startTime;
126:             totalRewards -= rewardData[rewardToken].rewardRate * dt;
127:         }
128: 
129:         IERC20(rewardToken).safeTransferFrom(msg.sender, address(this), totalRewards); // <= FOUND
130: 
131:         emit RewardsSet(rewardToken, startTime, endTime, totalRewards);
132:     }

['46']

46:     function pay(address token, address payer, address recipient, uint256 value) internal { // <= FOUND
47:         if (token == WETH9 && address(this).balance >= value) {
48:             
49:             IWETH9(WETH9).deposit{value: value}(); 
50:             require(IWETH9(WETH9).transfer(recipient, value), "transfer failed");
51:         } else if (payer == address(this)) {
52:             
53:             TransferHelper.safeTransfer(token, recipient, value);
54:         } else {
55:             
56:             TransferHelper.safeTransferFrom(token, payer, recipient, value); // <= FOUND
57:         }
58:     }

[Low-58] Pyth Oracle getPriceUnsafe used instead of getPrice or getPriceNoOlderThan. Thus introducing stale price risks

Resolution

Pyth oracles offer a getPriceUnsafe function that retrieves the latest price without verifying its freshness. Using this function can lead to reliance on outdated data, potentially causing significant inaccuracies in applications where timely information is critical. To mitigate this risk, it's advisable to use the getPriceNoOlderThan function, which allows developers to specify a maximum acceptable age for the price data, ensuring that only sufficiently recent information is utilized. This practice enhances data reliability and aligns with recommended best practices for integrating Pyth price feeds into decentralized applications.

Num of instances: 1

Findings

Click to show findings

['56']

56:     function getPythPrice(address token) public view returns (uint256) { // <= FOUND
57:         bytes32 priceId = pythPriceIds[token];
58:         if (priceId == bytes32(0)) {
59:             revert("PriceId not set");
60:         }
61:         PythStructs.Price memory priceStruct = pyth.getPriceUnsafe(priceId); // <= FOUND
62: 
63:         require(priceStruct.publishTime + maxPriceAge > block.timestamp, "Price is too old"); // <= FOUND
64: 
65:         uint256 price = uint256(uint64(priceStruct.price)); // <= FOUND
66: 
67:         return price;
68:     }

[Low-59] Function transfer's amount value differs from amount approved by user in same flow

Resolution

When working with ERC20 tokens, it’s crucial that the allowance check performed before a transferFrom operation accurately reflects the amount intended to be transferred. If the allowance is compared against a value that doesn’t match the actual transfer amount—either due to a miscalculation or incorrect logic—this can result in unintended behavior, such as unauthorized transfers or unnecessary reverts. For instance, checking if allowance >= fee and then transferring amount + fee would result in failure unless the full sum was approved. These types of mismatches can break expected flows and leave users confused or unable to complete their actions. Another issue is where the transferred amount is less than the approval amount, this is problematic as the protocol is unneccesarly approving more than neccessary. Always ensure the value checked against the allowance matches the actual transferFrom amount being deducted, especially when including fees, taxes, or slippage adjustments.

Num of instances: 3

Findings

Click to show findings

['83']

83:     function openPosition(OpenPositionParams memory params) // <= FOUND
84:         external
85:         onlyVault
86:         returns (uint256 tokenId, uint128 liquidity, uint256 amount0, uint256 amount1)
87:     {
88:         uint256 token0Balance = IERC20(token0).balanceOf(address(this));
89:         if (params.amount0Desired > token0Balance) { // <= FOUND
90:             _swapTokenExactInput(
91:                 token1,
92:                 token0,
93:                 IERC20(token1).balanceOf(address(this)) - params.amount1Desired,
94:                 params.amount0Desired - token0Balance
95:             );
96:         }
97: 
98:         uint256 token1Balance = IERC20(token1).balanceOf(address(this));
99:         if (params.amount1Desired > token1Balance) {
100:             _swapTokenExactInput(
101:                 token0,
102:                 token1,
103:                 IERC20(token0).balanceOf(address(this)) - params.amount0Desired,
104:                 params.amount1Desired - token1Balance
105:             );
106:         }
107:         address shadowNonfungiblePositionManager = getShadowNonfungiblePositionManager();
108: 
109:         IERC20(token0).approve(shadowNonfungiblePositionManager, params.amount0Desired);
110:         IERC20(token1).approve(shadowNonfungiblePositionManager, params.amount1Desired);
111: 
112:         (tokenId, liquidity, amount0, amount1) = IShadowNonfungiblePositionManager(shadowNonfungiblePositionManager)
113:             .mint(
114:             IShadowNonfungiblePositionManager.MintParams({
115:                 token0: token0,
116:                 token1: token1,
117:                 tickSpacing: tickSpacing,
118:                 tickLower: params.tickLower,
119:                 tickUpper: params.tickUpper,
120:                 amount0Desired: params.amount0Desired,
121:                 amount1Desired: params.amount1Desired,
122:                 amount0Min: 0,
123:                 amount1Min: 0,
124:                 recipient: address(this),
125:                 deadline: block.timestamp
126:             })
127:         );
128: 
129:         shadowPositionId = tokenId;
130:         positionOwner = params.positionOwner;
131: 
132:         unwrapWETH9(0, positionOwner);
133: 
134:         token0Balance = IERC20(token0).balanceOf(address(this));
135:         token1Balance = IERC20(token1).balanceOf(address(this));
136: 
137:         if (token0Balance > 0) {
138:             pay(token0, address(this), positionOwner, token0Balance);
139:         }
140:         if (token1Balance > 0) {
141:             pay(token1, address(this), positionOwner, token1Balance);
142:         }
143:     }

['145']

145:     function closePosition() // <= FOUND
146:         external
147:         onlyVault
148:         returns (
149:             uint128 liquidity,
150:             uint256 token0Reduced,
151:             uint256 token1Reduced,
152:             uint256 token0Balance,
153:             uint256 token1Balance
154:         )
155:     {
156:         _claimFees();
157:         _claimRewards();
158: 
159:         (,,,,, liquidity,,,,) =
160:             IShadowNonfungiblePositionManager(getShadowNonfungiblePositionManager()).positions(shadowPositionId);
161: 
162:         (token0Reduced, token1Reduced) = _decreasePosition(liquidity);
163: 
164:         (uint256 currentDebt0, uint256 currentDebt1) = IVault(vault).getPositionDebt(positionId);
165: 
166:         if (currentDebt0 > 0) {
167:             
168:             if (currentDebt0 > token0Reduced) {
169:                 
170:                 
171:                 
172:                 uint256 amount1Excess = token1Reduced - currentDebt1;
173:                 _swapTokenExactInput(token1, token0, amount1Excess, currentDebt0 - token0Reduced);
174:             }
175:             IERC20(token0).approve(vault, currentDebt0);
176:         }
177: 
178:         if (currentDebt1 > 0) {
179:             
180:             if (currentDebt1 > token1Reduced) {
181:                 
182:                 
183:                 
184:                 uint256 amount0Excess = token0Reduced - currentDebt0;
185:                 _swapTokenExactInput(token0, token1, amount0Excess, currentDebt1 - token1Reduced);
186:             }
187:             IERC20(token1).approve(vault, currentDebt1);
188:         }
189: 
190:         IVault(vault).repayExact(positionId, currentDebt0, currentDebt1);
191: 
192:         token0Balance = IERC20(token0).balanceOf(address(this));
193:         token1Balance = IERC20(token1).balanceOf(address(this));
194:         if (token0Balance > 0) {
195:             pay(token0, address(this), positionOwner, token0Balance); // <= FOUND
196:         }
197:         if (token1Balance > 0) {
198:             pay(token1, address(this), positionOwner, token1Balance);
199:         }
200:     }

['254']

254:     function liquidatePosition(address caller) // <= FOUND
255:         external
256:         onlyVault
257:         returns (
258:             uint128 liquidity,
259:             uint256 token0Reduced,
260:             uint256 token1Reduced,
261:             uint256 token0Fees,
262:             uint256 token1Fees,
263:             uint256 token0Left,
264:             uint256 token1Left
265:         )
266:     {
267:         _claimFees();
268:         _claimRewards();
269:         (,,,,, liquidity,,,,) =
270:             IShadowNonfungiblePositionManager(getShadowNonfungiblePositionManager()).positions(shadowPositionId);
271: 
272:         (token0Reduced, token1Reduced) = _decreasePosition(liquidity);
273:         (uint256 currentDebt0, uint256 currentDebt1) = IVault(vault).getPositionDebt(positionId);
274: 
275:         LiquidationFeeVars memory vars = LiquidationFeeVars({
276:             liquidationFee: IVault(vault).liquidationFee(),
277:             liquidationCallerFee: IVault(vault).liquidationCallerFee(),
278:             liquidationFeeRecipient: IVault(vault).liquidationFeeRecipient()
279:         });
280:         
281:         if (token0Reduced > 0) {
282:             token0Fees = token0Reduced * vars.liquidationFee / 10000;
283:             pay(token0, address(this), vars.liquidationFeeRecipient, token0Fees); // <= FOUND
284: 
285:             uint256 token0CallerFees = token0Fees * vars.liquidationCallerFee / 10000;
286:             if (token0CallerFees > 0) {
287:                 pay(token0, address(this), caller, token0CallerFees);
288:             }
289: 
290:             token0Reduced = token0Reduced - token0Fees - token0CallerFees;
291:         }
292: 
293:         if (token1Reduced > 0) {
294:             token1Fees = token1Reduced * vars.liquidationFee / 10000;
295: 
296:             pay(token1, address(this), vars.liquidationFeeRecipient, token1Fees);
297: 
298:             uint256 token1CallerFees = token1Fees * vars.liquidationCallerFee / 10000;
299:             if (token1CallerFees > 0) {
300:                 pay(token1, address(this), caller, token1CallerFees);
301:             }
302: 
303:             token1Reduced = token1Reduced - token1Fees - token1CallerFees;
304:         }
305: 
306:         if (currentDebt0 > 0) {
307:             
308:             if (currentDebt0 > token0Reduced) {
309:                 uint256 amount1Excess = token1Reduced - currentDebt1;
310:                 _swapTokenExactInput(token1, token0, amount1Excess, currentDebt0 - token0Reduced);
311:             }
312:             IERC20(token0).approve(vault, currentDebt0);
313:         }
314: 
315:         if (currentDebt1 > 0) {
316:             
317:             if (currentDebt1 > token1Reduced) {
318:                 uint256 amount0Excess = token0Reduced - currentDebt0;
319:                 _swapTokenExactInput(token0, token1, amount0Excess, currentDebt1 - token1Reduced);
320:             }
321:             IERC20(token1).approve(vault, currentDebt1);
322:         }
323: 
324:         IVault(vault).repayExact(positionId, currentDebt0, currentDebt1);
325: 
326:         token0Left = IERC20(token0).balanceOf(address(this));
327:         token1Left = IERC20(token1).balanceOf(address(this));
328:         if (token0Left > 0) {
329:             pay(token0, address(this), positionOwner, token0Left);
330:         }
331:         if (token1Left > 0) {
332:             pay(token1, address(this), positionOwner, token1Left);
333:         }
334:     }

[Low-60] deposit/redeem functions found which implement accounting arithmetic vulnerable to the donation attack

Num of instances: 5

Findings

Click to show findings

['464']

464:     function getPositionStatus(uint256[] calldata reserveIdArr, address user) // <= FOUND
465:         external
466:         view
467:         returns (PositionStatus[] memory statusArr)
468:     {
469:         statusArr = new PositionStatus[](reserveIdArr.length);
470: 
471:         for (uint256 i = 0; i < reserveIdArr.length; i++) {
472:             statusArr[i].reserveId = reserveIdArr[i];
473:             statusArr[i].user = user;
474:             statusArr[i].eTokenStaked = IStakingRewards(reserves[reserveIdArr[i]].stakingAddress).balanceOf(user);
475:             statusArr[i].eTokenUnStaked = IERC20(reserves[reserveIdArr[i]].eTokenAddress).balanceOf(user);
476:             statusArr[i].liquidity = statusArr[i].eTokenStaked.add(statusArr[i].eTokenUnStaked).mul( // <= FOUND
477:                 reserves[reserveIdArr[i]].eTokenToReserveExchangeRate()
478:             ).div(Precision.FACTOR1E18);
479:         }
480:     }

['145']

145:     function closePosition()
146:         external
147:         onlyVault
148:         returns (
149:             uint128 liquidity,
150:             uint256 token0Reduced,
151:             uint256 token1Reduced,
152:             uint256 token0Balance,
153:             uint256 token1Balance
154:         )
155:     {
156:         _claimFees();
157:         _claimRewards();
158: 
159:         (,,,,, liquidity,,,,) =
160:             IShadowNonfungiblePositionManager(getShadowNonfungiblePositionManager()).positions(shadowPositionId);
161: 
162:         (token0Reduced, token1Reduced) = _decreasePosition(liquidity);
163: 
164:         (uint256 currentDebt0, uint256 currentDebt1) = IVault(vault).getPositionDebt(positionId);
165: 
166:         if (currentDebt0 > 0) {
167:             
168:             if (currentDebt0 > token0Reduced) {
169:                 
170:                 
171:                 
172:                 uint256 amount1Excess = token1Reduced - currentDebt1;
173:                 _swapTokenExactInput(token1, token0, amount1Excess, currentDebt0 - token0Reduced);
174:             }
175:             IERC20(token0).approve(vault, currentDebt0);
176:         }
177: 
178:         if (currentDebt1 > 0) {
179:             
180:             if (currentDebt1 > token1Reduced) {
181:                 
182:                 
183:                 
184:                 uint256 amount0Excess = token0Reduced - currentDebt0;
185:                 _swapTokenExactInput(token0, token1, amount0Excess, currentDebt1 - token1Reduced);
186:             }
187:             IERC20(token1).approve(vault, currentDebt1);
188:         }
189: 
190:         IVault(vault).repayExact(positionId, currentDebt0, currentDebt1);
191: 
192:         token0Balance = IERC20(token0).balanceOf(address(this));
193:         token1Balance = IERC20(token1).balanceOf(address(this));
194:         if (token0Balance > 0) {
195:             pay(token0, address(this), positionOwner, token0Balance);
196:         }
197:         if (token1Balance > 0) {
198:             pay(token1, address(this), positionOwner, token1Balance);
199:         }
200:     }

['254']

254:     function liquidatePosition(address caller) // <= FOUND
255:         external
256:         onlyVault
257:         returns (
258:             uint128 liquidity,
259:             uint256 token0Reduced,
260:             uint256 token1Reduced,
261:             uint256 token0Fees,
262:             uint256 token1Fees,
263:             uint256 token0Left,
264:             uint256 token1Left
265:         )
266:     {
267:         _claimFees();
268:         _claimRewards();
269:         (,,,,, liquidity,,,,) =
270:             IShadowNonfungiblePositionManager(getShadowNonfungiblePositionManager()).positions(shadowPositionId);
271: 
272:         (token0Reduced, token1Reduced) = _decreasePosition(liquidity);
273:         (uint256 currentDebt0, uint256 currentDebt1) = IVault(vault).getPositionDebt(positionId);
274: 
275:         LiquidationFeeVars memory vars = LiquidationFeeVars({
276:             liquidationFee: IVault(vault).liquidationFee(),
277:             liquidationCallerFee: IVault(vault).liquidationCallerFee(),
278:             liquidationFeeRecipient: IVault(vault).liquidationFeeRecipient()
279:         });
280:         
281:         if (token0Reduced > 0) {
282:             token0Fees = token0Reduced * vars.liquidationFee / 10000;
283:             pay(token0, address(this), vars.liquidationFeeRecipient, token0Fees);
284: 
285:             uint256 token0CallerFees = token0Fees * vars.liquidationCallerFee / 10000;
286:             if (token0CallerFees > 0) {
287:                 pay(token0, address(this), caller, token0CallerFees);
288:             }
289: 
290:             token0Reduced = token0Reduced - token0Fees - token0CallerFees; // <= FOUND
291:         }
292: 
293:         if (token1Reduced > 0) {
294:             token1Fees = token1Reduced * vars.liquidationFee / 10000;
295: 
296:             pay(token1, address(this), vars.liquidationFeeRecipient, token1Fees);
297: 
298:             uint256 token1CallerFees = token1Fees * vars.liquidationCallerFee / 10000;
299:             if (token1CallerFees > 0) {
300:                 pay(token1, address(this), caller, token1CallerFees);
301:             }
302: 
303:             token1Reduced = token1Reduced - token1Fees - token1CallerFees;
304:         }
305: 
306:         if (currentDebt0 > 0) {
307:             
308:             if (currentDebt0 > token0Reduced) {
309:                 uint256 amount1Excess = token1Reduced - currentDebt1;
310:                 _swapTokenExactInput(token1, token0, amount1Excess, currentDebt0 - token0Reduced);
311:             }
312:             IERC20(token0).approve(vault, currentDebt0);
313:         }
314: 
315:         if (currentDebt1 > 0) {
316:             
317:             if (currentDebt1 > token1Reduced) {
318:                 uint256 amount0Excess = token0Reduced - currentDebt0;
319:                 _swapTokenExactInput(token0, token1, amount0Excess, currentDebt1 - token1Reduced);
320:             }
321:             IERC20(token1).approve(vault, currentDebt1);
322:         }
323: 
324:         IVault(vault).repayExact(positionId, currentDebt0, currentDebt1);
325: 
326:         token0Left = IERC20(token0).balanceOf(address(this));
327:         token1Left = IERC20(token1).balanceOf(address(this));
328:         if (token0Left > 0) {
329:             pay(token0, address(this), positionOwner, token0Left);
330:         }
331:         if (token1Left > 0) {
332:             pay(token1, address(this), positionOwner, token1Left);
333:         }
334:     }

['349']

349:     function claimRewards() external onlyVault {
350:         _claimFees();
351:         _claimRewards();
352:     }

['376']

376:     function _claimRewards() internal { // <= FOUND
377:         address shadowGauge = IShadowRangeVault(vault).shadowGauge();
378:         if (shadowGauge != address(0)) {
379:             address[] memory tokens = IShadowGaugeV3(shadowGauge).getRewardTokens();
380:             IShadowGaugeV3(shadowGauge).getReward(shadowPositionId, tokens);
381: 
382:             for (uint256 i = 0; i < tokens.length; i++) {
383:                 uint256 balance = IERC20(tokens[i]).balanceOf(address(this));
384:                 if (balance > 0) {
385:                     if (tokens[i] == xShadow) {
386:                         
387:                         
388:                         
389:                         
390:                         
391:                         if (IShadowX33(x33).isUnlocked()) {
392:                             address x33Adapter = getX33Adapter();
393:                             IERC20(tokens[i]).approve(x33Adapter, balance);
394:                             IERC4626(x33Adapter).deposit(balance, address(this));
395:                             balance = IERC20(x33).balanceOf(address(this)); // <= FOUND
396:                             IERC20(x33).approve(vault, balance);
397:                             IVault(vault).claimCallback(x33, balance);
398:                         }
399:                     } else {
400:                         IERC20(tokens[i]).approve(vault, balance);
401:                         IVault(vault).claimCallback(tokens[i], balance);
402:                     }
403:                 }
404:             }
405:         }
406:     }

[Low-61] return used in yul assembly block which results in post code from not being executed as this halts the performance of logic even outside of the yul block

Resolution

If there are operations outside an assembly block which contains a conditional or non conditional return ,there can be instances where not all intended operations are performed. This is likely intentional but has been flagged to be reviewed to confirm.

Num of instances: 1

Findings

Click to show findings

['55']

55:     function _delegate() internal { // <= FOUND
56:         address impl = IPositionImplGetter(_owner()).positionImplementation();
57:         require(impl != address(0));
58:         assembly {
59:             
60:             
61:             
62:             calldatacopy(0, 0, calldatasize())
63: 
64:             
65:             
66:             let result := delegatecall(gas(), impl, 0, calldatasize(), 0, 0)
67: 
68:             
69:             returndatacopy(0, 0, returndatasize())
70: 
71:             switch result
72:             
73:             case 0 { revert(0, returndatasize()) }
74:             default { return(0, returndatasize()) }
75:         }
76:     }

[Low-62] Contract's initialization function can fail due to out of GAS error on certain chains due to iteration which can prevent accurate intended deployment

Resolution

Initialization functions that rely on gas-heavy operations, such as iterating over large arrays, risk exceeding the block gas limit. This can make deployment or initialization of the contract impossible, particularly when handling large datasets. For example, if an initialization function processes a lengthy array for setup, the operation could consume more gas than allowed, causing the transaction to revert. This effectively prevents the contract from being deployed or initialized successfully. To mitigate this, consider splitting heavy initialization tasks into smaller, manageable batches or leveraging external scripts for pre-processing. This ensures gas efficiency while maintaining the contract's functionality and deployability.

Num of instances: 1

Findings

Click to show findings

['18']

18:     function initialize(
19:         address _pyth,
20:         address _oracleManager,
21:         address[] memory initialTokens,
22:         bytes32[] memory priceIds
23:     ) external initializer {
24:         pyth = IPyth(_pyth);
25:         oracleManager = _oracleManager;
26:         require(initialTokens.length == priceIds.length, "Initial tokens and price ids length mismatch"); // <= FOUND
27:         for (uint256 i = 0; i < initialTokens.length; i++) { // <= FOUND
28:             pythPriceIds[initialTokens[i]] = priceIds[i];
29:         }
30: 
31:         maxPriceAge = 1 hours;
32:     }

[Low-63] Transfers which take place within iteration do not compare the amounts[i] value against zero which can result in the transfer chain reverting

Resolution

A downside of performing multiple transfers using iteration is that if one of these transfers fails, they all do, one way this can happen is if 0 amount is used within a transfer as many tokens revert on zero transfer. As such it is advisable to check all amounts values against zero prior to performing the transfer loop, ideally when the amounts value is pushed to the amounts array.

Num of instances: 2

Findings

Click to show findings

['196']

196:        for (uint256 i = 0; i < rewardTokens.length; i++) { // <= FOUND
197:             address rewardToken = rewardTokens[i];
198:             uint256 claimable = userRewardsClaimable[msg.sender][rewardToken];
199:             if (claimable > 0) {
200:                 userRewardsClaimable[msg.sender][rewardToken] = 0;
201:                 require(IERC20(rewardToken).transfer(msg.sender, claimable), "transfer failed"); // <= FOUND
202:                 emit RewardPaid(msg.sender, rewardToken, claimable);
203:             }
204:         }

['196']

196:         for (uint256 i = 0; i < rewardTokens.length; i++) { // <= FOUND
197:             address rewardToken = rewardTokens[i];
198:             uint256 claimable = userRewardsClaimable[msg.sender][rewardToken];
199:             if (claimable > 0) {
200:                 userRewardsClaimable[msg.sender][rewardToken] = 0;
201:                 require(IERC20(rewardToken).transfer(msg.sender, claimable), "transfer failed"); // <= FOUND
202:                 emit RewardPaid(msg.sender, rewardToken, claimable);
203:             }
204:         }

[Low-64] Consider a uptime feed on L2 deployments to prevent issues caused by downtime

Resolution

In L2 deployments, incorporating an uptime feed is crucial to mitigate issues arising from sequencer downtime. Downtime can disrupt services, leading to transaction failures or incorrect data readings, affecting overall system reliability. By integrating an uptime feed, you gain insight into the operational status of the L2 network, enabling proactive measures like halting sensitive operations or alerting users. This approach ensures that your contract behaves predictably and securely during network outages, enhancing the robustness and reliability of your decentralized application, which is especially important in mission-critical or high-stakes environments.

Num of instances: 15

Findings

Click to show findings

['7']

7: contract AddressRegistry is IAddressRegistry, Ownable  // <= FOUND

['9']

9: contract IndividualPosition  // <= FOUND

['19']

19: contract ExtraInterestBearingToken is IExtraInterestBearingToken, ReentrancyGuard, ERC20  // <= FOUND

['24']

24: contract LendingPool is ILendingPool, OwnableUpgradeable, PaymentsUpgradeable, ReentrancyGuardUpgradeable  // <= FOUND

['13']

13: contract StakingRewards is Ownable, IStakingRewards  // <= FOUND

['7']

7: contract MightyTimelockController is TimelockController  // <= FOUND

['10']

10: contract PrimaryPriceOracle is IPriceOracle, Initializable  // <= FOUND

['10']

10: contract ShadowPositionValueCalculator  // <= FOUND

['26']

26: contract ShadowRangePositionImpl is IShadowRangePositionImpl, PaymentsUpgradeable  // <= FOUND

['30']

30: contract ShadowRangeVault is // <= FOUND
31:     Initializable,
32:     ReentrancyGuardUpgradeable,
33:     Ownable2StepUpgradeable,
34:     PaymentsUpgradeable,
35:     IVault
36: 

['10']

10: contract SwapXPositionValueCalculator  // <= FOUND

['11']

11: contract VaultRegistry is Ownable, IVaultFactory  // <= FOUND

['31']

31: contract VaultPositionViewer  // <= FOUND

['10']

10: abstract contract Payments  // <= FOUND

['10']

10: abstract contract PaymentsUpgradeable  // <= FOUND

[NonCritical-1] Subtraction may underflow if multiplication is too large

Resolution

In arithmetic operations involving subtraction and multiplication, an underflow may occur if a subtraction result is negative, or if a multiplication result exceeds the maximum value representable in the data type. For instance, if a large multiplication precedes a subtraction, it may create a value too large to subtract from, causing an underflow. This could lead to unexpected and incorrect results in the calculation.

Num of instances: 1

Findings

Click to show findings

['96']

96:     function setReward(address rewardToken, uint256 startTime, uint256 endTime, uint256 totalRewards) // <= FOUND
97:         public
98:         onlyOwner
99:         nonReentrant
100:         updateReward(address(0))
101:     {
102:         require(startTime < endTime, "start must lt end");
103:         require(rewardData[rewardToken].endTime < block.timestamp, "not end");
104: 
105:         if (!inRewardsTokenList[rewardToken]) {
106:             rewardTokens.push(rewardToken);
107:             inRewardsTokenList[rewardToken] = true;
108:         }
109: 
110:         rewardData[rewardToken].startTime = startTime;
111:         rewardData[rewardToken].endTime = endTime;
112:         rewardData[rewardToken].lastUpdateTime = block.timestamp;
113:         rewardData[rewardToken].rewardRate = totalRewards / (endTime - startTime);
114: 
115:         if (block.timestamp > startTime && totalStaked > 0) {
116:             uint256 dt = block.timestamp - startTime;
117: 
118:             rewardData[rewardToken].rewardPerTokenStored +=
119:                 (rewardData[rewardToken].rewardRate * dt * 1e18) / totalStaked;
120:         }
121: 
122:         if (block.timestamp > startTime && totalStaked == 0) {
123:             
124:             
125:             uint256 dt = block.timestamp - startTime;
126:             totalRewards -= rewardData[rewardToken].rewardRate * dt; // <= FOUND
127:         }
128: 
129:         IERC20(rewardToken).safeTransferFrom(msg.sender, address(this), totalRewards);
130: 
131:         emit RewardsSet(rewardToken, startTime, endTime, totalRewards);
132:     }

[NonCritical-2] Local variable shadowing

Resolution

Local variable shadowing in Solidity creates confusion by allowing a local variable within a function to share the same name as a state variable or another local variable in an outer scope. This confusion can lead to errors in code interpretation and execution. It's especially problematic when maintaining or modifying the code, as it may inadvertently introduce bugs. To resolve this issue, developers should avoid variable shadowing by using unique and descriptive names for variables. Leveraging development tools and linters that warn about shadowing can also be helpful. By paying careful attention to variable naming and being aware of shadowing, developers can create more readable and robust code.

Num of instances: 10

Findings

Click to show findings

['381']

381:     function claimCallback(address reward, uint256 amount) external {
382:         uint256 positionId = positionAddressToId[msg.sender]; // <= FOUND
383:         require(positionId != 0, "Not a position");
384:         uint256 feeAmount = amount * performanceFee / 10000;
385:         pay(reward, msg.sender, positionInfos[positionId].owner, amount - feeAmount);
386:         pay(reward, msg.sender, performanceFeeRecipient(), feeAmount);
387: 
388:         emit RewardClaimed(positionInfos[positionId].owner, vaultId, positionId, reward, amount - feeAmount, feeAmount);
389:     }

['309']

309:     function repayExact(uint256 positionId, uint256 amount0, uint256 amount1) external payable { // <= FOUND
310:         PositionInfo storage positionInfo = positionInfos[positionId];
311: 
312:         
313:         address lendingPool = getLendingPool(); // <= FOUND
314:         if (amount0 > 0 && positionInfo.token0DebtId != 0) {
315:             (uint256 currentDebt0,) = ILendingPool(lendingPool).getCurrentDebt(positionInfo.token0DebtId);
316:             if (amount0 > currentDebt0) {
317:                 amount0 = currentDebt0;
318:             }
319: 
320:             pay(token0, msg.sender, address(this), amount0);
321: 
322:             ILendingPool(lendingPool).repay(address(this), positionInfo.token0DebtId, amount0);
323:         }
324:         if (amount1 > 0 && positionInfo.token1DebtId != 0) {
325:             (uint256 currentDebt1,) = ILendingPool(lendingPool).getCurrentDebt(positionInfo.token1DebtId);
326:             if (amount1 > currentDebt1) {
327:                 amount1 = currentDebt1;
328:             }
329: 
330:             pay(token1, msg.sender, address(this), amount1);
331: 
332:             ILendingPool(lendingPool).repay(address(this), positionInfo.token1DebtId, amount1);
333:         }
334: 
335:         if (msg.value > 0) {
336:             refundETH();
337:         }
338:     }

['559']

559:     function setCreditsOfVault(uint256 vaultId, uint256 reserveId, uint256 credit) external onlyOwner notPaused { // <= FOUND
560:         address vaultAddr = getVault(vaultId);
561:         credits[reserveId][vaultAddr] = credit;
562:         emit SetCreditsOfVault(vaultId, vaultAddr, reserveId, credit);
563:     }

['131']

131:     function setReserveIds(uint256 _token0ReserveId, uint256 _token1ReserveId) external onlyOwner {
132:         address lendingPool = getLendingPool(); // <= FOUND
133: 
134:         IERC20(token0).approve(lendingPool, type(uint256).max);
135:         IERC20(token1).approve(lendingPool, type(uint256).max);
136:         token0ReserveId = _token0ReserveId;
137:         token1ReserveId = _token1ReserveId;
138:     }

['152']

152:     function openPosition(OpenPositionParams calldata params)
153:         external
154:         payable
155:         nonReentrant
156:         checkDeadline(params.deadline)
157:     {
158:         (uint256 _positionId, address _positionAddress) = _createNewPosition();
159: 
160:         PositionInfo storage positionInfo = positionInfos[_positionId];
161:         positionInfo.owner = msg.sender;
162:         positionInfo.vaultId = vaultId;
163:         positionInfo.positionAddress = _positionAddress;
164:         positionInfo.positionId = _positionId;
165:         positionIds[msg.sender].push(_positionId);
166:         positionAddressToId[_positionAddress] = _positionId;
167: 
168:         
169:         if (params.amount0Principal > 0) {
170:             pay(token0, msg.sender, address(this), params.amount0Principal);
171:         }
172:         if (params.amount1Principal > 0) {
173:             pay(token1, msg.sender, address(this), params.amount1Principal);
174:         }
175: 
176:         
177:         address lendingPool = getLendingPool(); // <= FOUND
178:         if (params.amount0Borrow > 0) {
179:             positionInfo.token0DebtId = ILendingPool(lendingPool).newDebtPosition(token0ReserveId);
180: 
181:             ILendingPool(lendingPool).borrow(address(this), positionInfo.token0DebtId, params.amount0Borrow);
182:         }
183:         if (params.amount1Borrow > 0) {
184:             positionInfo.token1DebtId = ILendingPool(lendingPool).newDebtPosition(token1ReserveId);
185: 
186:             ILendingPool(lendingPool).borrow(address(this), positionInfo.token1DebtId, params.amount1Borrow);
187:         }
188: 
189:         require(
190:             getDebtRatioFromAmounts(
191:                 params.amount0Principal, params.amount1Principal, params.amount0Borrow, params.amount1Borrow
192:             ) < liquidationDebtRatio,
193:             "Borrow value is too high"
194:         );
195: 
196:         if (params.amount0Principal > 0 || params.amount0Borrow > 0) {
197:             pay(token0, address(this), positionInfo.positionAddress, params.amount0Principal + params.amount0Borrow);
198:         }
199:         if (params.amount1Principal > 0 || params.amount1Borrow > 0) {
200:             pay(token1, address(this), positionInfo.positionAddress, params.amount1Principal + params.amount1Borrow);
201:         }
202:         
203:         (uint256 tokenId, uint128 liquidity, uint256 amount0, uint256 amount1) = IShadowRangePositionImpl(
204:             positionInfo.positionAddress
205:         ).openPosition(
206:             IShadowRangePositionImpl.OpenPositionParams({
207:                 amount0Desired: params.amount0Desired,
208:                 amount1Desired: params.amount1Desired,
209:                 tickLower: params.tickLower,
210:                 tickUpper: params.tickUpper,
211:                 positionOwner: msg.sender
212:             })
213:         );
214: 
215:         require(getPositionValueByNftId(tokenId) > minPositionSize, "PVL");
216: 
217:         positionInfo.shadowPositionId = tokenId;
218:         positionInfo.tickLower = params.tickLower;
219:         positionInfo.tickUpper = params.tickUpper;
220:         positionInfo.ul = params.ul;
221:         positionInfo.ll = params.ll;
222: 
223:         emit PositionOpened(
224:             msg.sender,
225:             vaultId,
226:             _positionId,
227:             _positionAddress,
228:             params.amount0Principal,
229:             params.amount1Principal,
230:             amount0,
231:             amount1,
232:             liquidity,
233:             block.timestamp
234:         );
235: 
236:         if (msg.value > 0) {
237:             refundETH();
238:         }
239:         require(getDebtRatio(_positionId) < liquidationDebtRatio, "DRH");
240:     }

['437']

437:     function getPositionDebt(uint256 positionId) public view returns (uint256 amount0Debt, uint256 amount1Debt) {
438:         address lendingPool = getLendingPool(); // <= FOUND
439:         if (positionInfos[positionId].token0DebtId != 0) {
440:             (amount0Debt,) = ILendingPool(lendingPool).getCurrentDebt(positionInfos[positionId].token0DebtId);
441:         }
442:         if (positionInfos[positionId].token1DebtId != 0) {
443:             (amount1Debt,) = ILendingPool(lendingPool).getCurrentDebt(positionInfos[positionId].token1DebtId);
444:         }
445:     }

['336']

336:     function claimableRewards() external view returns (address[] memory, uint256[] memory) {
337:         address shadowGauge = IShadowRangeVault(vault).shadowGauge(); // <= FOUND
338:         if (shadowGauge != address(0)) {
339:             address[] memory tokens = IShadowGaugeV3(shadowGauge).getRewardTokens();
340:             uint256[] memory amounts = new uint256[](tokens.length);
341:             for (uint256 i = 0; i < tokens.length; i++) {
342:                 amounts[i] = IShadowGaugeV3(shadowGauge).earned(tokens[i], shadowPositionId);
343:             }
344:             return (tokens, amounts);
345:         }
346:         return (new address[](0), new uint256[](0));
347:     }

['376']

376:     function _claimRewards() internal {
377:         address shadowGauge = IShadowRangeVault(vault).shadowGauge(); // <= FOUND
378:         if (shadowGauge != address(0)) {
379:             address[] memory tokens = IShadowGaugeV3(shadowGauge).getRewardTokens();
380:             IShadowGaugeV3(shadowGauge).getReward(shadowPositionId, tokens);
381: 
382:             for (uint256 i = 0; i < tokens.length; i++) {
383:                 uint256 balance = IERC20(tokens[i]).balanceOf(address(this));
384:                 if (balance > 0) {
385:                     if (tokens[i] == xShadow) {
386:                         
387:                         
388:                         
389:                         
390:                         
391:                         if (IShadowX33(x33).isUnlocked()) {
392:                             address x33Adapter = getX33Adapter();
393:                             IERC20(tokens[i]).approve(x33Adapter, balance);
394:                             IERC4626(x33Adapter).deposit(balance, address(this));
395:                             balance = IERC20(x33).balanceOf(address(this));
396:                             IERC20(x33).approve(vault, balance);
397:                             IVault(vault).claimCallback(x33, balance);
398:                         }
399:                     } else {
400:                         IERC20(tokens[i]).approve(vault, balance);
401:                         IVault(vault).claimCallback(tokens[i], balance);
402:                     }
403:                 }
404:             }
405:         }
406:     }

['38']

38:     function getPositionIds(address vault, address owner) public view returns (uint256[] memory) { // <= FOUND
39:         return IVault(vault).getPositionIds(owner);
40:     }

['68']

68:     function getUserPosition(address vault, address owner) public view returns (PositionInfo[] memory) { // <= FOUND
69:         uint256[] memory ids = getPositionIds(vault, owner);
70:         PositionInfo[] memory positions = new PositionInfo[](ids.length);
71:         for (uint256 i = 0; i < ids.length; i++) {
72:             positions[i].vault = vault;
73:             positions[i].id = ids[i];
74:             positions[i].token0 = IVault(vault).token0();
75:             positions[i].token1 = IVault(vault).token1();
76:             (positions[i].amount0, positions[i].amount1) = IVault(vault).getPositionAmounts(ids[i]);
77:             if (positions[i].amount0 > 0 || positions[i].amount1 > 0) {
78:                 (positions[i].amount0Debt, positions[i].amount1Debt) = IVault(vault).getPositionDebt(ids[i]);
79:                 positions[i].debtRatio = IVault(vault).getDebtRatio(ids[i]);
80:             }
81:             positions[i].price0 = IVault(vault).getTokenPrice(positions[i].token0);
82:             positions[i].price1 = IVault(vault).getTokenPrice(positions[i].token1);
83: 
84:             IVault.PositionInfo memory positionInfo = IVault(vault).getPositionInfos(ids[i]);
85: 
86:             positions[i].positionAddress = positionInfo.positionAddress;
87:             positions[i].positionNftId = positionInfo.shadowPositionId;
88:             positions[i].tickUpper = positionInfo.tickUpper;
89:             positions[i].tickLower = positionInfo.tickLower;
90:             positions[i].ul = positionInfo.ul;
91:             positions[i].ll = positionInfo.ll;
92: 
93:             positions[i].inRange = positions[i].amount0 > 0 && positions[i].amount1 > 0;
94: 
95:             (positions[i].rewards, positions[i].rewardsAmounts) =
96:                 IPositionImpl(positionInfo.positionAddress).claimableRewards();
97: 
98:             positions[i].currentTick = IVault(vault).getCurrentTick();
99: 
100:             (,,,,, positions[i].liquidity,,,,) = INFTPositionManager(
101:                 IAddressRegistry(addressProvider).getAddress(AddressId.ADDRESS_ID_SHADOW_NONFUNGIBLE_POSITION_MANAGER)
102:             ).positions(positionInfo.shadowPositionId);
103:         }
104:         return positions;
105:     }

[NonCritical-3] Greater than comparisons made on state uints that can be set to max

Resolution

When state variables (uints) that can be set to their maximum value (type(uint256).max for example) are used in "greater than" comparisons, it introduces a risk of logic errors. If the state variable ever reaches this max value, any comparison expecting it to increment further will fail. This can halt or disrupt contract functionality. To avoid this, implement checks to ensure that the state variable doesn't exceed a certain threshold below the max value.

Num of instances: 6

Findings

Click to show findings

['340']

340:     function liquidatePosition(uint256 positionId) external nonReentrant {
341:         PositionInfo memory positionInfo = positionInfos[positionId];
342:         require(positionInfo.owner != address(0), "Position does not exist");
343:         require(getDebtRatio(positionId) > liquidationDebtRatio, "Debt ratio is too low"); // <= FOUND
344: 
345:         if (!openLiquidationEnabled) {
346:             require(liquidators[msg.sender], "Not a liquidator");
347:         }
348: 
349:         (
350:             ,
351:             uint256 token0Reduced,
352:             uint256 token1Reduced,
353:             uint256 token0Fees,
354:             uint256 token1Fees,
355:             uint256 token0Left,
356:             uint256 token1Left
357:         ) = IShadowRangePositionImpl(positionInfo.positionAddress).liquidatePosition(msg.sender);
358: 
359:         emit PositionLiquidated(
360:             positionInfo.owner,
361:             vaultId,
362:             positionId,
363:             token0Reduced,
364:             token1Reduced,
365:             token0Fees,
366:             token1Fees,
367:             token0Left,
368:             token1Left,
369:             block.timestamp
370:         );
371:         (uint256 currentDebt0, uint256 currentDebt1) = getPositionDebt(positionId);
372:         require(currentDebt0 == 0 && currentDebt1 == 0, "Still debt");
373:     }

['152']

152:     function openPosition(OpenPositionParams calldata params)
153:         external
154:         payable
155:         nonReentrant
156:         checkDeadline(params.deadline)
157:     {
158:         (uint256 _positionId, address _positionAddress) = _createNewPosition();
159: 
160:         PositionInfo storage positionInfo = positionInfos[_positionId];
161:         positionInfo.owner = msg.sender;
162:         positionInfo.vaultId = vaultId;
163:         positionInfo.positionAddress = _positionAddress;
164:         positionInfo.positionId = _positionId;
165:         positionIds[msg.sender].push(_positionId);
166:         positionAddressToId[_positionAddress] = _positionId;
167: 
168:         
169:         if (params.amount0Principal > 0) {
170:             pay(token0, msg.sender, address(this), params.amount0Principal);
171:         }
172:         if (params.amount1Principal > 0) {
173:             pay(token1, msg.sender, address(this), params.amount1Principal);
174:         }
175: 
176:         
177:         address lendingPool = getLendingPool();
178:         if (params.amount0Borrow > 0) {
179:             positionInfo.token0DebtId = ILendingPool(lendingPool).newDebtPosition(token0ReserveId);
180: 
181:             ILendingPool(lendingPool).borrow(address(this), positionInfo.token0DebtId, params.amount0Borrow);
182:         }
183:         if (params.amount1Borrow > 0) {
184:             positionInfo.token1DebtId = ILendingPool(lendingPool).newDebtPosition(token1ReserveId);
185: 
186:             ILendingPool(lendingPool).borrow(address(this), positionInfo.token1DebtId, params.amount1Borrow);
187:         }
188: 
189:         require(
190:             getDebtRatioFromAmounts(
191:                 params.amount0Principal, params.amount1Principal, params.amount0Borrow, params.amount1Borrow
192:             ) < liquidationDebtRatio,
193:             "Borrow value is too high"
194:         );
195: 
196:         if (params.amount0Principal > 0 || params.amount0Borrow > 0) {
197:             pay(token0, address(this), positionInfo.positionAddress, params.amount0Principal + params.amount0Borrow);
198:         }
199:         if (params.amount1Principal > 0 || params.amount1Borrow > 0) {
200:             pay(token1, address(this), positionInfo.positionAddress, params.amount1Principal + params.amount1Borrow);
201:         }
202:         
203:         (uint256 tokenId, uint128 liquidity, uint256 amount0, uint256 amount1) = IShadowRangePositionImpl(
204:             positionInfo.positionAddress
205:         ).openPosition(
206:             IShadowRangePositionImpl.OpenPositionParams({
207:                 amount0Desired: params.amount0Desired,
208:                 amount1Desired: params.amount1Desired,
209:                 tickLower: params.tickLower,
210:                 tickUpper: params.tickUpper,
211:                 positionOwner: msg.sender
212:             })
213:         );
214: 
215:         require(getPositionValueByNftId(tokenId) > minPositionSize, "PVL"); // <= FOUND
216: 
217:         positionInfo.shadowPositionId = tokenId;
218:         positionInfo.tickLower = params.tickLower;
219:         positionInfo.tickUpper = params.tickUpper;
220:         positionInfo.ul = params.ul;
221:         positionInfo.ll = params.ll;
222: 
223:         emit PositionOpened(
224:             msg.sender,
225:             vaultId,
226:             _positionId,
227:             _positionAddress,
228:             params.amount0Principal,
229:             params.amount1Principal,
230:             amount0,
231:             amount1,
232:             liquidity,
233:             block.timestamp
234:         );
235: 
236:         if (msg.value > 0) {
237:             refundETH();
238:         }
239:         require(getDebtRatio(_positionId) < liquidationDebtRatio, "DRH");
240:     }

['294']

294:     function reducePosition(ReducePositionParams calldata params) external nonReentrant {
295:         PositionInfo memory positionInfo = positionInfos[params.positionId];
296:         require(positionInfo.owner == msg.sender, "Not the owner");
297:         require(params.reducePercentage > 0 && params.reducePercentage < 10000, "Invalid reduce percentage");
298:         require(getDebtRatio(params.positionId) < liquidationDebtRatio, "Debt ratio is too high");
299: 
300:         (uint128 reduceLiquidity,,) = IShadowRangePositionImpl(positionInfo.positionAddress).reducePosition(
301:             params.reducePercentage, params.amount0ToSwap, params.amount1ToSwap
302:         );
303: 
304:         require(getPositionValueByNftId(positionInfo.shadowPositionId) > minPositionSize, "PVL"); // <= FOUND
305: 
306:         emit PositionReduced(positionInfo.owner, vaultId, params.positionId, reduceLiquidity, block.timestamp);
307:     }

['83']

83:     function initialize(
84:         address _addressProvider,
85:         address _vaultRegistry,
86:         address _shadowV3PoolAddress,
87:         address _initialPositionImplementation
88:     ) public initializer {
89:         _initializePayments(IAddressRegistry(_addressProvider).getAddress(AddressId.ADDRESS_ID_WETH9));
90:         __ReentrancyGuard_init();
91:         __Ownable2Step_init();
92:         token0 = IShadowV3Pool(_shadowV3PoolAddress).token0();
93:         token1 = IShadowV3Pool(_shadowV3PoolAddress).token1();
94:         token0Decimals = IERC20Metadata(token0).decimals();
95:         token1Decimals = IERC20Metadata(token1).decimals();
96: 
97:         addressProvider = _addressProvider;
98:         vaultRegistry = _vaultRegistry;
99:         nextPositionID = 1;
100:         shadowV3Pool = _shadowV3PoolAddress;
101: 
102:         liquidationDebtRatio = 8600; // <= FOUND
103:         liquidationFee = 500;
104:         performanceFee = 3000;
105:         minPositionSize = 100e8; // <= FOUND
106: 
107:         positionImplementation = _initialPositionImplementation;
108:     }

['127']

127:     function setLiquidationDebtRatio(uint256 _liquidationDebtRatio) external onlyOwner {
128:         liquidationDebtRatio = _liquidationDebtRatio; // <= FOUND
129:     }

['140']

140:     function setMinPositionSize(uint256 _minPositionSize) external onlyOwner {
141:         minPositionSize = _minPositionSize; // <= FOUND
142:     }

[NonCritical-4] Floating pragma should be avoided

Num of instances: 2

Findings

Click to show findings

['3']

3: pragma solidity ^0.8.0; // <= FOUND

['3']

3: pragma solidity ^0.8.18; // <= FOUND

[NonCritical-5] Interfaces should be declared in a separate file

Resolution

It is general standard to declare interfaces on files separate from regular contract declarations

Num of instances: 2

Findings

Click to show findings

['5']

5: interface IPositionImplGetter  // <= FOUND

['9']

9: interface IPositionImpl  // <= FOUND

[NonCritical-6] Require statements should have error string

Resolution

Adding error strings to require statements in Solidity contracts, although not mandatory, enhances error handling, debugging, and overall contract maintainability. Providing a descriptive error message with each require statement helps identify the specific reason for a transaction failure, making it easier for developers to troubleshoot issues and for users to understand the cause of a revert. Including error strings improves code readability and fosters transparency, as the logic and conditions behind each requirement are clearly communicated

Num of instances: 4

Findings

Click to show findings

['18']

18: require(success);

['24']

24: require(msg.sender == _owner());

['57']

57: require(impl != address(0));

['42']

42: require(lendingPool == msg.sender);

[NonCritical-7] In functions which accept an address as a parameter, there should be a zero address check to prevent bugs

Resolution

In smart contract development, especially with Solidity, it's crucial to validate inputs to functions. When a function accepts an Ethereum address as a parameter, implementing a zero address check (i.e., ensuring the address is not 0x0) is a best practice to prevent potential bugs and vulnerabilities. The zero address (0x0) is a default value and generally indicates an uninitialized or invalid state. Passing the zero address to certain functions can lead to unintended behaviors, like funds getting locked permanently or transactions failing silently. By checking for and rejecting the zero address, developers can ensure that the function operates as intended and interacts only with valid Ethereum addresses. This check enhances the contract's robustness and security.

Num of instances: 39

Findings

Click to show findings

['14']

14:     function setAddress(uint256 id, address _addr) public onlyOwner 

['40']

40:     function _setOwner(address newOwner) private 

['47']

47:     function mint(address user, uint256 amount) external onlyLendingPool nonReentrant 

['61']

61:     function burn(address receiverOfUnderlying, uint256 eTokenAmount, uint256 underlyingTokenAmount)
62:         external
63:         onlyLendingPool
64:         nonReentrant
65:     

['92']

92:     function transferUnderlyingTo(address target, uint256 amount)
93:         external
94:         onlyLendingPool
95:         nonReentrant
96:         returns (uint256)
97:     

['104']

104:     function deposit(uint256 reserveId, uint256 amount, address onBehalfOf, uint16 referralCode)
105:         public
106:         payable
107:         notPaused
108:         nonReentrant
109:         returns (uint256 eTokenAmount)
110:     

['160']

160:     function redeem(uint256 reserveId, uint256 eTokenAmount, address to, bool receiveNativeETH)
161:         public
162:         payable
163:         notPaused
164:         nonReentrant
165:         avoidUsingNativeEther
166:         returns (uint256)
167:     

['362']

362:     function initReserve(
363:         DataTypes.ReserveData storage reserveData,
364:         address underlyingTokenAddress,
365:         address eTokenAddress,
366:         uint256 reserveCapacity,
367:         uint256 id
368:     ) internal 

['464']

464:     function getPositionStatus(uint256[] calldata reserveIdArr, address user)
465:         external
466:         view
467:         returns (PositionStatus[] memory statusArr)
468:     

['26']

26:     function unwrapWETH9(uint256 amountMinimum, address recipient) internal 

['46']

46:     function pay(address token, address payer, address recipient, uint256 value) internal 

['67']

67:     function rewardPerToken(address rewardToken) public view returns (uint256) 

['84']

84:     function earned(address user, address rewardToken) public view returns (uint256) 

['90']

90:     function earned(address user, address rewardToken, uint256 curRewardPerToken) internal view returns (uint256) 

['96']

96:     function setReward(address rewardToken, uint256 startTime, uint256 endTime, uint256 totalRewards)
97:         public
98:         onlyOwner
99:         nonReentrant
100:         updateReward(address(0))
101:     

['141']

141:     function stake(uint256 amount, address onBehalfOf) external nonReentrant updateReward(onBehalfOf) 

['159']

159:     function withdraw(uint256 amount, address to) external nonReentrant updateReward(msg.sender) 

['179']

179:     function withdrawByLendingPool(uint256 amount, address user, address to)
180:         external
181:         onlyLendingPool
182:         nonReentrant
183:         updateReward(user)
184:     

['18']

18:     function _initializePayments(address _WETH9) internal 

['18']

18:     function initialize(
19:         address _pyth,
20:         address _oracleManager,
21:         address[] memory initialTokens,
22:         bytes32[] memory priceIds
23:     ) external initializer 

['44']

44:     function setPythPriceId(address token, bytes32 priceId) external onlyOracleManager 

['398']

398:     function getTokenPrice(address token) public view returns (uint256) 

['56']

56:     function getPythPrice(address token) public view returns (uint256) 

['18']

18:     function principal(IShadowNonfungiblePositionManager positionManager, uint256 tokenId, address pool)
19:         public
20:         view
21:         returns (uint256 amount0, uint256 amount1)
22:     

['33']

33:     function getCurrentTick(address v3Pool) public view returns (int24) 

['432']

432:     function _swapTokenExactInput(address tokenIn, address tokenOut, uint256 amountIn, uint256 amountOutMinimum)
433:         internal
434:         returns (uint256 amountOut)
435:     

['454']

454:     function _swapTokenExactOutput(address tokenIn, address tokenOut, uint256 amountOut, uint256 amountInMaximum)
455:         internal
456:         returns (uint256 amountIn)
457:     

['83']

83:     function initialize(
84:         address _addressProvider,
85:         address _vaultRegistry,
86:         address _shadowV3PoolAddress,
87:         address _initialPositionImplementation
88:     ) public initializer 

['110']

110:     function setLiquidator(address _liquidator, bool _isLiquidator) external onlyOwner 

['123']

123:     function setPositionImplementation(address _positionImplementation) external onlyOwner 

['148']

148:     function setShadowGauge(address _shadowGauge) external onlyOwner 

['381']

381:     function claimCallback(address reward, uint256 amount) external 

['398']

398:     function getTokenPrice(address token) public view returns (uint256) 

['468']

468:     function getPositionIds(address owner) public view returns (uint256[] memory) 

['18']

18:     function principal(ISwapXNonfungiblePositionManager positionManager, uint256 tokenId, address pool)
19:         public
20:         view
21:         returns (uint256 amount0, uint256 amount1)
22:     

['33']

33:     function getCurrentTick(address v3Pool) public view returns (int24) 

['29']

29:     function newVault(address _newVault) external onlyOwner returns (uint256 vaultId) 

['38']

38:     function getPositionIds(address vault, address owner) public view returns (uint256[] memory) 

['107']

107:     function getUserPositions(address[] memory vaults, address owner) public view returns (PositionInfo[][] memory) 

[NonCritical-8] Default bool values are manually set

Resolution

In instances where a new variable is defined, there is no need to set it to it's default value.

Num of instances: 1

Findings

Click to show findings

['50']

50:     bool public paused = false; // <= FOUND

[NonCritical-9] Default int values are manually set

Resolution

In instances where a new variable is defined, there is no need to set it to it's default value.

Num of instances: 6

Findings

Click to show findings

['452']

452:         for (uint256 i = 0; i < reserveIdArr.length; i++) { // <= FOUND

['196']

196:         for (uint256 i = 0; i < rewardTokens.length; i++) { // <= FOUND

['27']

27:         for (uint256 i = 0; i < initialTokens.length; i++) { // <= FOUND

['341']

341:             for (uint256 i = 0; i < tokens.length; i++) { // <= FOUND

['71']

71:         for (uint256 i = 0; i < ids.length; i++) { // <= FOUND

['109']

109:         for (uint256 i = 0; i < vaults.length; i++) { // <= FOUND

[NonCritical-10] Ownable2Step should be used in place of Ownable

Resolution

Ownable2Step further prevents risks posed by centralised privileges as there is a smaller likelihood of the owner being wrongfully changed

Num of instances: 4

Findings

Click to show findings

['7']

7: contract AddressRegistry is IAddressRegistry, Ownable  // <= FOUND

['13']

13: contract StakingRewards is Ownable, IStakingRewards  // <= FOUND

['11']

11: contract VaultRegistry is Ownable, IVaultFactory  // <= FOUND

['24']

24: contract LendingPool is ILendingPool, OwnableUpgradeable, PaymentsUpgradeable, ReentrancyGuardUpgradeable  // <= FOUND

[NonCritical-11] Revert statements within external and public functions can be used to perform DOS attacks

Resolution

In Solidity, 'revert' statements are used to undo changes and throw an exception when certain conditions are not met. However, in public and external functions, improper use of revert can be exploited for Denial of Service (DoS) attacks. An attacker can intentionally trigger these 'revert' conditions, causing legitimate transactions to consistently fail. For example, if a function relies on specific conditions from user input or contract state, an attacker could manipulate these to continually force reverts, blocking the function's execution. Therefore, it's crucial to design contract logic to handle exceptions properly and avoid scenarios where revert can be predictably triggered by malicious actors. This includes careful input validation and considering alternative design patterns that are less susceptible to such abuses.

Num of instances: 2

Findings

Click to show findings

['253']

253:     function fullfillLimitOrder(uint256 positionId) external nonReentrant {
254:         PositionInfo memory positionInfo = positionInfos[positionId];
255:         int24 currentTick = IPositionValueCalculator(
256:             IAddressRegistry(addressProvider).getAddress(AddressId.ADDRESS_ID_SHADOW_POSITION_VALUE_CALCULATOR)
257:         ).getCurrentTick(shadowV3Pool);
258: 
259:         if (
260:             (positionInfo.ll != 0 && currentTick < positionInfo.ll)
261:                 || (positionInfo.ul != 0 && currentTick > positionInfo.ul)
262:         ) {
263:             _closePosition(positionId);
264:         } else {
265:             revert("Current tick is not in the position range"); // <= FOUND
266:         }
267: 
268:         require(getDebtRatio(positionId) < liquidationDebtRatio, "DRH");
269:     }

['56']

56:     function getPythPrice(address token) public view returns (uint256) {
57:         bytes32 priceId = pythPriceIds[token];
58:         if (priceId == bytes32(0)) {
59:             revert("PriceId not set"); // <= FOUND
60:         }
61:         PythStructs.Price memory priceStruct = pyth.getPriceUnsafe(priceId);
62: 
63:         require(priceStruct.publishTime + maxPriceAge > block.timestamp, "Price is too old");
64: 
65:         uint256 price = uint256(uint64(priceStruct.price));
66: 
67:         return price;
68:     }

[NonCritical-12] Reverts should use custom errors instead of strings

Resolution

Custom error codes should be used in place of strings for revert statements in Solidity contracts to enhance efficiency and reduce gas costs. String-based error messages consume more bytecode and storage, increasing the overall gas consumption during contract deployment and execution

Num of instances: 2

Findings

Click to show findings

['59']

59:             revert("PriceId not set"); // <= FOUND

['265']

265:             revert("Current tick is not in the position range"); // <= FOUND

[NonCritical-13] Functions which are either private or internal should have a preceding _ in their name

Resolution

Add a preceding underscore to the function name, take care to refactor where there functions are called

Num of instances: 11

Findings

Click to show findings

['362']

362:     function initReserve(
363:         DataTypes.ReserveData storage reserveData,
364:         address underlyingTokenAddress,
365:         address eTokenAddress,
366:         uint256 reserveCapacity,
367:         uint256 id
368:     ) internal 

['384']

384:     function createStakingPoolForReserve(uint256 reserveId) internal 

['393']

393:     function updateDebtPosition(DataTypes.DebtPositionData storage debtPosition, uint256 latestBorrowingIndex)
394:         internal
395:     

['401']

401:     function setBorrowingRateConfig(
402:         DataTypes.ReserveData storage reserve,
403:         uint16 utilizationA,
404:         uint16 borrowingRateA,
405:         uint16 utilizationB,
406:         uint16 borrowingRateB,
407:         uint16 maxBorrowingRate
408:     ) internal 

['423']

423:     function getReserve(uint256 reserveId) internal view returns (DataTypes.ReserveData storage reserve) 

['428']

428:     function getTreasury() internal view returns (address treasury) 

['433']

433:     function getVault(uint256 vaultId) internal view returns (address vaultAddress) 

['26']

26:     function unwrapWETH9(uint256 amountMinimum, address recipient) internal 

['36']

36:     function refundETH() internal 

['46']

46:     function pay(address token, address payer, address recipient, uint256 value) internal 

['90']

90:     function earned(address user, address rewardToken, uint256 curRewardPerToken) internal view returns (uint256) 

[NonCritical-14] Private and internal state variables should have a preceding _ in their name unless they are constants

Resolution

Add a preceding underscore to the state variable name, take care to refactor where there variables are read/wrote

Num of instances: 1

Findings

Click to show findings

['32']

32:  address addressProvider;

[NonCritical-15] Contract lines should not be longer than 120 characters for readability

Resolution

Consider spreading these lines over multiple lines to aid in readability and the support of VIM users everywhere.

Num of instances: 2

Findings

Click to show findings

['240']

240:         require(underlyingTokenAmount <= reserve.availableLiquidity(), Errors.VL_CURRENT_AVAILABLE_LIQUIDITY_NOT_ENOUGH); // <= FOUND

['27']

27:  * @notice User interacts this contract when open, close, reduce, repay leveraged position. Liquidator also use this contract. // <= FOUND

[NonCritical-16] Avoid updating storage when the value hasn't changed

Resolution

In Solidity, manipulating contract storage comes with significant gas costs. One can optimize gas usage by preventing unnecessary storage updates when the new value is the same as the existing one. If an existing value is the same as the new one, not reassigning it to the storage could potentially save substantial amounts of gas, notably 2900 gas for a 'Gsreset'. This saving may come at the expense of a cold storage load operation ('Gcoldsload'), which costs 2100 gas, or a warm storage access operation ('Gwarmaccess'), which costs 100 gas. Therefore, the gas efficiency of your contract can be significantly improved by adding a check that compares the new value with the current one before any storage update operation. If the values are the same, you can bypass the storage operation, thereby saving gas.

Num of instances: 17

Findings

Click to show findings

['14']

14:     function setAddress(uint256 id, address _addr) public onlyOwner {
15:         libraryAndContractAddresses[id] = _addr;
16:         emit SetAddress(_msgSender(), id, _addr);
17:     }

['559']

559:     function setCreditsOfVault(uint256 vaultId, uint256 reserveId, uint256 credit) external onlyOwner notPaused {
560:         address vaultAddr = getVault(vaultId);
561:         credits[reserveId][vaultAddr] = credit;
562:         emit SetCreditsOfVault(vaultId, vaultAddr, reserveId, credit);
563:     }

['602']

602:     function setReserveFeeRate(uint256 reserveId, uint16 _rate) public onlyOwner notPaused {
603:         require(_rate <= Constants.PERCENT_100, "invalid percent");
604:         DataTypes.ReserveData storage reserve = reserves[reserveId];
605:         reserve.reserveFeeRate = _rate;
606: 
607:         emit SetReserveFeeRate(reserveId, _rate);
608:     }

['610']

610:     function setBorrowingRateConfig(
611:         uint256 reserveId,
612:         uint16 utilizationA,
613:         uint16 borrowingRateA,
614:         uint16 utilizationB,
615:         uint16 borrowingRateB,
616:         uint16 maxBorrowingRate
617:     ) public onlyOwner notPaused {
618:         DataTypes.ReserveData storage reserve = reserves[reserveId];
619:         setBorrowingRateConfig(reserve, utilizationA, borrowingRateA, utilizationB, borrowingRateB, maxBorrowingRate);
620: 
621:         emit SetInterestRateConfig(
622:             reserveId, utilizationA, borrowingRateA, utilizationB, borrowingRateB, maxBorrowingRate
623:         );
624:     }

['626']

626:     function setReserveCapacity(uint256 reserveId, uint256 cap) public onlyOwner notPaused {
627:         DataTypes.ReserveData storage reserve = reserves[reserveId];
628: 
629:         reserve.reserveCapacity = cap;
630:         emit SetReserveCapacity(reserveId, cap);
631:     }

['110']

110:     function setLiquidator(address _liquidator, bool _isLiquidator) external onlyOwner {
111:         liquidators[_liquidator] = _isLiquidator;
112:     }

['114']

114:     function setLiquidationFeeParams(uint256 _liquidationFee, uint256 _liquidationCallerFeeRatio) external onlyOwner {
115:         liquidationFee = _liquidationFee;
116:         liquidationCallerFee = _liquidationCallerFeeRatio;
117:     }

['119']

119:     function setOpenLiquidationEnabled(bool _openLiquidationEnabled) external onlyOwner {
120:         openLiquidationEnabled = _openLiquidationEnabled;
121:     }

['123']

123:     function setPositionImplementation(address _positionImplementation) external onlyOwner {
124:         positionImplementation = _positionImplementation;
125:     }

['127']

127:     function setLiquidationDebtRatio(uint256 _liquidationDebtRatio) external onlyOwner {
128:         liquidationDebtRatio = _liquidationDebtRatio;
129:     }

['131']

131:     function setReserveIds(uint256 _token0ReserveId, uint256 _token1ReserveId) external onlyOwner {
132:         address lendingPool = getLendingPool();
133: 
134:         IERC20(token0).approve(lendingPool, type(uint256).max);
135:         IERC20(token1).approve(lendingPool, type(uint256).max);
136:         token0ReserveId = _token0ReserveId;
137:         token1ReserveId = _token1ReserveId;
138:     }

['140']

140:     function setMinPositionSize(uint256 _minPositionSize) external onlyOwner {
141:         minPositionSize = _minPositionSize;
142:     }

['144']

144:     function setPerformanceFee(uint256 _performanceFee) external onlyOwner {
145:         performanceFee = _performanceFee;
146:     }

['148']

148:     function setShadowGauge(address _shadowGauge) external onlyOwner {
149:         shadowGauge = _shadowGauge;
150:     }

['44']

44:     function setPythPriceId(address token, bytes32 priceId) external onlyOracleManager {
45:         pythPriceIds[token] = priceId;
46:     }

['48']

48:     function setMaxPriceAge(uint256 _maxPriceAge) external onlyOracleManager {
49:         maxPriceAge = _maxPriceAge;
50:     }

['207']

207:     function update() external updateReward(address(0)) onlyOwner {}

[NonCritical-17] Specific imports should be used where possible so only used code is imported

Resolution

In many cases only some functionality is used from an import. In such cases it makes more sense to use {} to specify what to import and thus save gas whilst improving readability

Num of instances: 32

Findings

Click to show findings

['4']

4: import "@openzeppelin/contracts/access/Ownable.sol";

['6']

6: import "./interfaces/IAddressRegistry.sol";

['5']

5: import "@openzeppelin/contracts/utils/math/SafeMath.sol";

['6']

6: import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

['7']

7: import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

['7']

7: import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

['8']

8: import "@openzeppelin/contracts/security/ReentrancyGuard.sol";

['10']

10: import "../interfaces/IExtraInterestBearingToken.sol";

['12']

12: import "../interfaces/ILendingPool.sol";

['8']

8: import "../libraries/helpers/Errors.sol";

['9']

9: import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";

['10']

10: import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol";

['13']

13: import "../interfaces/IVaultFactory.sol";

['15']

15: import "../libraries/types/DataTypes.sol";

['16']

16: import "../libraries/logic/ReserveLogic.sol";

['17']

17: import "../libraries/logic/ReserveKey.sol";

['18']

18: import "../libraries/logic/ETokenDeployer.sol";

['19']

19: import "../libraries/logic/StakingRewardsDeployer.sol";

['21']

21: import "../AddressRegistry.sol";

['22']

22: import "../PaymentsUpgradeable.sol";

['5']

5: import "../interfaces/IWETH9.sol";

['6']

6: import "../libraries/TransferHelper.sol";

['8']

8: import "@openzeppelin/contracts/utils/math/Math.sol";

['11']

11: import "../interfaces/IStakingRewards.sol";

['5']

5: import "@openzeppelin/contracts/governance/TimelockController.sol";

['5']

5: import "./interfaces/IWETH9.sol";

['6']

6: import "./libraries/TransferHelper.sol";

['9']

9: import "./libraries/helpers/Errors.sol";

['7']

7: import "@pythnetwork/pyth-sdk-solidity/IPyth.sol";

['8']

8: import "@pythnetwork/pyth-sdk-solidity/PythStructs.sol";

['7']

7: import "./interfaces/IVault.sol";

['8']

8: import "./interfaces/IVaultFactory.sol";

[NonCritical-18] Old Solidity version

Resolution

Using outdated Solidity versions can lead to security risks and inefficiencies. It's recommended to adopt newer versions, ideally the latest, which as of now is 0.8.24. This ensures access to the latest bug fixes, features, and optimizations, particularly crucial for Layer 2 deployments. Regular updates to versions like 0.8.19 or later, up to 0.8.24, enhance contract security and performance.

Num of instances: 2

Findings

Click to show findings

['3']

3: pragma solidity ^0.8.0; // <= FOUND

['3']

3: pragma solidity ^0.8.18;

[NonCritical-19] Not all event definitions are utilizing indexed variables.

Resolution

Try to index as much as three variables in event declarations as this is more gas efficient when done on value type variables (uint, address etc) however not for bytes and string variables

Num of instances: 1

Findings

Click to show findings

['21']

21: event OwnerChanged(address previousOwner, address newOwner); // <= FOUND

[NonCritical-20] Explicitly define visibility of state variables to prevent misconceptions on what can access the variable

Resolution

Such state variables should be marked as private as this is the default visibility

Num of instances: 1

Findings

Click to show findings

['32']

32:  address addressProvider; // <= FOUND

[NonCritical-21] Contracts should have all public/external functions exposed by interfaces

Resolution

Contracts should expose all public and external functions through interfaces. This practice ensures a clear and consistent definition of how the contract can be interacted with, promoting better transparency and integration.

Num of instances: 111

Findings

Click to show findings

['19']

19:     function getAddress(uint256 id) external view returns (address) 

['28']

28:     function Owner() external view returns (address) 

['49']

49:     function changeOwner(address newOwner) external onlyOwner 

['47']

47:     function mint(address user, uint256 amount) external onlyLendingPool nonReentrant 

['61']

61:     function burn(address receiverOfUnderlying, uint256 eTokenAmount, uint256 underlyingTokenAmount)
62:         external
63:         onlyLendingPool
64:         nonReentrant
65:     

['78']

78:     function mintToTreasury(address treasury, uint256 amount) external onlyLendingPool nonReentrant 

['92']

92:     function transferUnderlyingTo(address target, uint256 amount)
93:         external
94:         onlyLendingPool
95:         nonReentrant
96:         returns (uint256)
97:     

['69']

69:     function initReserve(address asset) external onlyOwner notPaused 

['123']

123:     function depositAndStake(uint256 reserveId, uint256 amount, address onBehalfOf, uint16 referralCode)
124:         external
125:         payable
126:         notPaused
127:         nonReentrant
128:         returns (uint256 eTokenAmount)
129:     

['185']

185:     function unStakeAndWithdraw(uint256 reserveId, uint256 eTokenAmount, address to, bool receiveNativeETH)
186:         external
187:         payable
188:         notPaused
189:         nonReentrant
190:         avoidUsingNativeEther
191:         returns (uint256)
192:     

['256']

256:     function newDebtPosition(uint256 reserveId) external notPaused nonReentrant returns (uint256 debtId) 

['282']

282:     function borrow(address onBehalfOf, uint256 debtId, uint256 amount) external notPaused nonReentrant 

['324']

324:     function repay(address onBehalfOf, uint256 debtId, uint256 amount)
325:         external
326:         notPaused
327:         nonReentrant
328:         returns (uint256)
329:     

['445']

445:     function getReserveStatus(uint256[] calldata reserveIdArr)
446:         external
447:         view
448:         returns (ReserveStatus[] memory statusArr)
449:     

['464']

464:     function getPositionStatus(uint256[] calldata reserveIdArr, address user)
465:         external
466:         view
467:         returns (PositionStatus[] memory statusArr)
468:     

['482']

482:     function getCurrentDebt(uint256 debtId) external view returns (uint256 currentDebt, uint256 latestBorrowingIndex) 

['535']

535:     function emergencyPauseAll() external onlyOwner 

['540']

540:     function unPauseAll() external onlyOwner 

['545']

545:     function enableVaultToBorrow(uint256 vaultId) external onlyOwner notPaused 

['552']

552:     function disableVaultToBorrow(uint256 vaultId) external onlyOwner notPaused 

['559']

559:     function setCreditsOfVault(uint256 vaultId, uint256 reserveId, uint256 credit) external onlyOwner notPaused 

['141']

141:     function stake(uint256 amount, address onBehalfOf) external nonReentrant updateReward(onBehalfOf) 

['159']

159:     function withdraw(uint256 amount, address to) external nonReentrant updateReward(msg.sender) 

['179']

179:     function withdrawByLendingPool(uint256 amount, address user, address to)
180:         external
181:         onlyLendingPool
182:         nonReentrant
183:         updateReward(user)
184:     

['195']

195:     function claim() external nonReentrant updateReward(msg.sender) 

['207']

207:     function update() external updateReward(address(0)) onlyOwner 

['209']

209:     function rewardsTokenListLength() external view returns (uint256) 

['18']

18:     function initialize(
19:         address _pyth,
20:         address _oracleManager,
21:         address[] memory initialTokens,
22:         bytes32[] memory priceIds
23:     ) external initializer 

['39']

39:     function setOracleManager(address _oracleManager) external onlyOracleManager 

['44']

44:     function setPythPriceId(address token, bytes32 priceId) external onlyOracleManager 

['48']

48:     function setMaxPriceAge(uint256 _maxPriceAge) external onlyOracleManager 

['41']

41:     function initialize(uint256 _positionId) external 

['67']

67:     function version() external pure returns (uint256) 

['83']

83:     function openPosition(OpenPositionParams memory params)
84:         external
85:         onlyVault
86:         returns (uint256 tokenId, uint128 liquidity, uint256 amount0, uint256 amount1)
87:     

['145']

145:     function closePosition()
146:         external
147:         onlyVault
148:         returns (
149:             uint128 liquidity,
150:             uint256 token0Reduced,
151:             uint256 token1Reduced,
152:             uint256 token0Balance,
153:             uint256 token1Balance
154:         )
155:     

['202']

202:     function reducePosition(uint128 reducePercentage, uint256 amount0ToSwap, uint256 amount1ToSwap)
203:         external
204:         onlyVault
205:         returns (uint128 reduceLiquidity, uint256 token0Reduced, uint256 token1Reduced)
206:     

['254']

254:     function liquidatePosition(address caller)
255:         external
256:         onlyVault
257:         returns (
258:             uint128 liquidity,
259:             uint256 token0Reduced,
260:             uint256 token1Reduced,
261:             uint256 token0Fees,
262:             uint256 token1Fees,
263:             uint256 token0Left,
264:             uint256 token1Left
265:         )
266:     

['336']

336:     function claimableRewards() external view returns (address[] memory, uint256[] memory) 

['349']

349:     function claimRewards() external onlyVault 

['77']

77:     function onReceiveRegisterCallback(uint256 _vaultId) external 

['110']

110:     function setLiquidator(address _liquidator, bool _isLiquidator) external onlyOwner 

['114']

114:     function setLiquidationFeeParams(uint256 _liquidationFee, uint256 _liquidationCallerFeeRatio) external onlyOwner 

['119']

119:     function setOpenLiquidationEnabled(bool _openLiquidationEnabled) external onlyOwner 

['123']

123:     function setPositionImplementation(address _positionImplementation) external onlyOwner 

['127']

127:     function setLiquidationDebtRatio(uint256 _liquidationDebtRatio) external onlyOwner 

['131']

131:     function setReserveIds(uint256 _token0ReserveId, uint256 _token1ReserveId) external onlyOwner 

['140']

140:     function setMinPositionSize(uint256 _minPositionSize) external onlyOwner 

['144']

144:     function setPerformanceFee(uint256 _performanceFee) external onlyOwner 

['148']

148:     function setShadowGauge(address _shadowGauge) external onlyOwner 

['152']

152:     function openPosition(OpenPositionParams calldata params)
153:         external
154:         payable
155:         nonReentrant
156:         checkDeadline(params.deadline)
157:     

['242']

242:     function closePosition(uint256 positionId)
243:         external
244:         nonReentrant
245:         returns (uint256 token0Balance, uint256 token1Balance)
246:     

['253']

253:     function fullfillLimitOrder(uint256 positionId) external nonReentrant 

['294']

294:     function reducePosition(ReducePositionParams calldata params) external nonReentrant 

['309']

309:     function repayExact(uint256 positionId, uint256 amount0, uint256 amount1) external payable 

['340']

340:     function liquidatePosition(uint256 positionId) external nonReentrant 

['375']

375:     function claimRewards(uint256 positionId) external nonReentrant 

['381']

381:     function claimCallback(address reward, uint256 amount) external 

['29']

29:     function newVault(address _newVault) external onlyOwner returns (uint256 vaultId) 

['14']

14:     function setAddress(uint256 id, address _addr) public onlyOwner 

['57']

57:     function initialize(address _addressRegistry, address _WETH9) public initializer 

['104']

104:     function deposit(uint256 reserveId, uint256 amount, address onBehalfOf, uint16 referralCode)
105:         public
106:         payable
107:         notPaused
108:         nonReentrant
109:         returns (uint256 eTokenAmount)
110:     

['160']

160:     function redeem(uint256 reserveId, uint256 eTokenAmount, address to, bool receiveNativeETH)
161:         public
162:         payable
163:         notPaused
164:         nonReentrant
165:         avoidUsingNativeEther
166:         returns (uint256)
167:     

['490']

490:     function getReserveIdOfDebt(uint256 debtId) public view returns (uint256) 

['494']

494:     function getUnderlyingTokenAddress(uint256 reserveId) public view returns (address) 

['499']

499:     function getETokenAddress(uint256 reserveId) public view returns (address) 

['504']

504:     function getStakingAddress(uint256 reserveId) public view returns (address) 

['509']

509:     function exchangeRateOfReserve(uint256 reserveId) public view returns (uint256) 

['514']

514:     function utilizationRateOfReserve(uint256 reserveId) public view returns (uint256) 

['519']

519:     function borrowingRateOfReserve(uint256 reserveId) public view returns (uint256) 

['524']

524:     function totalLiquidityOfReserve(uint256 reserveId) public view returns (uint256 totalLiquidity) 

['529']

529:     function totalBorrowsOfReserve(uint256 reserveId) public view returns (uint256 totalBorrows) 

['565']

565:     function activateReserve(uint256 reserveId) public onlyOwner notPaused 

['572']

572:     function deActivateReserve(uint256 reserveId) public onlyOwner notPaused 

['578']

578:     function freezeReserve(uint256 reserveId) public onlyOwner notPaused 

['584']

584:     function unFreezeReserve(uint256 reserveId) public onlyOwner notPaused 

['590']

590:     function enableBorrowing(uint256 reserveId) public onlyOwner notPaused 

['596']

596:     function disableBorrowing(uint256 reserveId) public onlyOwner notPaused 

['602']

602:     function setReserveFeeRate(uint256 reserveId, uint16 _rate) public onlyOwner notPaused 

['610']

610:     function setBorrowingRateConfig(
611:         uint256 reserveId,
612:         uint16 utilizationA,
613:         uint16 borrowingRateA,
614:         uint16 utilizationB,
615:         uint16 borrowingRateB,
616:         uint16 maxBorrowingRate
617:     ) public onlyOwner notPaused 

['626']

626:     function setReserveCapacity(uint256 reserveId, uint256 cap) public onlyOwner notPaused 

['67']

67:     function rewardPerToken(address rewardToken) public view returns (uint256) 

['84']

84:     function earned(address user, address rewardToken) public view returns (uint256) 

['96']

96:     function setReward(address rewardToken, uint256 startTime, uint256 endTime, uint256 totalRewards)
97:         public
98:         onlyOwner
99:         nonReentrant
100:         updateReward(address(0))
101:     

['398']

398:     function getTokenPrice(address token) public view returns (uint256) 

['56']

56:     function getPythPrice(address token) public view returns (uint256) 

['18']

18:     function principal(IShadowNonfungiblePositionManager positionManager, uint256 tokenId, address pool)
19:         public
20:         view
21:         returns (uint256 amount0, uint256 amount1)
22:     

['33']

33:     function getCurrentTick(address v3Pool) public view returns (int24) 

['71']

71:     function getX33Adapter() public view returns (address) 

['75']

75:     function getShadowNonfungiblePositionManager() public view returns (address) 

['79']

79:     function getSwapRouter() public view returns (address) 

['83']

83:     function initialize(
84:         address _addressProvider,
85:         address _vaultRegistry,
86:         address _shadowV3PoolAddress,
87:         address _initialPositionImplementation
88:     ) public initializer 

['398']

398:     function getTokenPrice(address token) public view returns (uint256) 

['403']

403:     function getPositionAmounts(uint256 positionId) public view returns (uint256 amount0, uint256 amount1) 

['407']

407:     function getPositionAmountsByNftId(uint256 nftId) public view returns (uint256 amount0, uint256 amount1) 

['419']

419:     function getPositionValue(uint256 positionId) public view returns (uint256 value) 

['425']

425:     function getPositionValueByNftId(uint256 nftId) public view returns (uint256 value) 

['431']

431:     function getDebtValue(uint256 positionId) public view returns (uint256 value) 

['437']

437:     function getPositionDebt(uint256 positionId) public view returns (uint256 amount0Debt, uint256 amount1Debt) 

['447']

447:     function getDebtRatioFromAmounts(
448:         uint256 amount0Principal,
449:         uint256 amount1Principal,
450:         uint256 amount0Borrow,
451:         uint256 amount1Borrow
452:     ) public view returns (uint256 debtRatio) 

['460']

460:     function getDebtRatio(uint256 positionId) public view returns (uint256 debtRatio) 

['464']

464:     function getLendingPool() public view returns (address) 

['468']

468:     function getPositionIds(address owner) public view returns (uint256[] memory) 

['472']

472:     function getPositionInfos(uint256 positionId) public view returns (PositionInfo memory) 

['476']

476:     function liquidationFeeRecipient() public view returns (address) 

['480']

480:     function performanceFeeRecipient() public view returns (address) 

['484']

484:     function getCurrentTick() public view returns (int24) 

['18']

18:     function principal(ISwapXNonfungiblePositionManager positionManager, uint256 tokenId, address pool)
19:         public
20:         view
21:         returns (uint256 amount0, uint256 amount1)
22:     

['33']

33:     function getCurrentTick(address v3Pool) public view returns (int24) 

['38']

38:     function getPositionIds(address vault, address owner) public view returns (uint256[] memory) 

['68']

68:     function getUserPosition(address vault, address owner) public view returns (PositionInfo[] memory) 

['107']

107:     function getUserPositions(address[] memory vaults, address owner) public view returns (PositionInfo[][] memory) 

[NonCritical-22] Functions within contracts are not ordered according to the solidity style guide

Resolution

The following order should be used within contracts

constructor

receive function (if exists)

fallback function (if exists)

external

public

internal

private

Rearrange the contract functions and contructors to fit this ordering

Num of instances: 7

Findings

Click to show findings

['7']

7: contract AddressRegistry is IAddressRegistry, Ownable  // <= FOUND

['9']

9: contract IndividualPosition  // <= FOUND

['24']

24: contract LendingPool is ILendingPool, OwnableUpgradeable, PaymentsUpgradeable, ReentrancyGuardUpgradeable  // <= FOUND

['13']

13: contract StakingRewards is Ownable, IStakingRewards  // <= FOUND

['26']

26: contract ShadowRangePositionImpl is IShadowRangePositionImpl, PaymentsUpgradeable  // <= FOUND

[]

30: contract ShadowRangeVault is
31:     Initializable,
32:     ReentrancyGuardUpgradeable,
33:     Ownable2StepUpgradeable,
34:     PaymentsUpgradeable,
35:     IVault
36: 

['10']

10: abstract contract PaymentsUpgradeable  // <= FOUND

[NonCritical-23] Double type casts create complexity within the code

Resolution

Double type casting should be avoided in Solidity contracts to prevent unintended consequences and ensure accurate data representation. Performing multiple type casts in succession can lead to unexpected truncation, rounding errors, or loss of precision, potentially compromising the contract's functionality and reliability. Furthermore, double type casting can make the code less readable and harder to maintain, increasing the likelihood of errors and misunderstandings during development and debugging. To ensure precise and consistent data handling, developers should use appropriate data types and avoid unnecessary or excessive type casting, promoting a more robust and dependable contract execution.

Num of instances: 1

Findings

Click to show findings

['13']

13:         assert(_ADMIN_SLOT == bytes32(uint256(keccak256("eip1967.proxy.admin")) - 1)); // <= FOUND

[NonCritical-24] Emits without msg.sender parameter

Resolution

In Solidity, when msg.sender plays a crucial role in a function's logic, it's important for transparency and auditability that any events emitted by this function include msg.sender as a parameter. This practice enhances the traceability and accountability of transactions, allowing users and external observers to easily track who initiated a particular action. Including msg.sender in event logs helps in creating a clear and verifiable record of interactions with the contract, thereby increasing user trust and facilitating easier debugging and analysis of contract behavior. It's a key aspect of writing clear, transparent, and user-friendly smart contracts.

Num of instances: 4

Findings

Click to show findings

['96']

96:     function setReward(address rewardToken, uint256 startTime, uint256 endTime, uint256 totalRewards)
97:         public
98:         onlyOwner
99:         nonReentrant
100:         updateReward(address(0))
101:     {
102:         require(startTime < endTime, "start must lt end");
103:         require(rewardData[rewardToken].endTime < block.timestamp, "not end");
104: 
105:         if (!inRewardsTokenList[rewardToken]) {
106:             rewardTokens.push(rewardToken);
107:             inRewardsTokenList[rewardToken] = true;
108:         }
109: 
110:         rewardData[rewardToken].startTime = startTime;
111:         rewardData[rewardToken].endTime = endTime;
112:         rewardData[rewardToken].lastUpdateTime = block.timestamp;
113:         rewardData[rewardToken].rewardRate = totalRewards / (endTime - startTime);
114: 
115:         if (block.timestamp > startTime && totalStaked > 0) {
116:             uint256 dt = block.timestamp - startTime;
117: 
118:             rewardData[rewardToken].rewardPerTokenStored +=
119:                 (rewardData[rewardToken].rewardRate * dt * 1e18) / totalStaked;
120:         }
121: 
122:         if (block.timestamp > startTime && totalStaked == 0) {
123:             
124:             
125:             uint256 dt = block.timestamp - startTime;
126:             totalRewards -= rewardData[rewardToken].rewardRate * dt;
127:         }
128: 
129:         IERC20(rewardToken).safeTransferFrom(msg.sender, address(this), totalRewards); // <= FOUND
130: 
131:         emit RewardsSet(rewardToken, startTime, endTime, totalRewards);
132:     }

['294']

294:     function reducePosition(ReducePositionParams calldata params) external nonReentrant {
295:         PositionInfo memory positionInfo = positionInfos[params.positionId];
296:         require(positionInfo.owner == msg.sender, "Not the owner"); // <= FOUND
297:         require(params.reducePercentage > 0 && params.reducePercentage < 10000, "Invalid reduce percentage");
298:         require(getDebtRatio(params.positionId) < liquidationDebtRatio, "Debt ratio is too high");
299: 
300:         (uint128 reduceLiquidity,,) = IShadowRangePositionImpl(positionInfo.positionAddress).reducePosition(
301:             params.reducePercentage, params.amount0ToSwap, params.amount1ToSwap
302:         );
303: 
304:         require(getPositionValueByNftId(positionInfo.shadowPositionId) > minPositionSize, "PVL");
305: 
306:         emit PositionReduced(positionInfo.owner, vaultId, params.positionId, reduceLiquidity, block.timestamp);
307:     }

['340']

340:     function liquidatePosition(uint256 positionId) external nonReentrant {
341:         PositionInfo memory positionInfo = positionInfos[positionId];
342:         require(positionInfo.owner != address(0), "Position does not exist");
343:         require(getDebtRatio(positionId) > liquidationDebtRatio, "Debt ratio is too low");
344: 
345:         if (!openLiquidationEnabled) {
346:             require(liquidators[msg.sender], "Not a liquidator"); // <= FOUND
347:         }
348: 
349:         (
350:             ,
351:             uint256 token0Reduced,
352:             uint256 token1Reduced,
353:             uint256 token0Fees,
354:             uint256 token1Fees,
355:             uint256 token0Left,
356:             uint256 token1Left
357:         ) = IShadowRangePositionImpl(positionInfo.positionAddress).liquidatePosition(msg.sender); // <= FOUND
358: 
359:         emit PositionLiquidated(
360:             positionInfo.owner,
361:             vaultId,
362:             positionId,
363:             token0Reduced,
364:             token1Reduced,
365:             token0Fees,
366:             token1Fees,
367:             token0Left,
368:             token1Left,
369:             block.timestamp
370:         );
371:         (uint256 currentDebt0, uint256 currentDebt1) = getPositionDebt(positionId);
372:         require(currentDebt0 == 0 && currentDebt1 == 0, "Still debt");
373:     }

['381']

381:     function claimCallback(address reward, uint256 amount) external {
382:         uint256 positionId = positionAddressToId[msg.sender]; // <= FOUND
383:         require(positionId != 0, "Not a position");
384:         uint256 feeAmount = amount * performanceFee / 10000;
385:         pay(reward, msg.sender, positionInfos[positionId].owner, amount - feeAmount); // <= FOUND
386:         pay(reward, msg.sender, performanceFeeRecipient(), feeAmount); // <= FOUND
387: 
388:         emit RewardClaimed(positionInfos[positionId].owner, vaultId, positionId, reward, amount - feeAmount, feeAmount);
389:     }

[NonCritical-25] A function which defines named returns in it's declaration doesn't need to use return

Resolution

Refacter the code to assign to the named return variables rather than using a return statement

Num of instances: 9

Findings

Click to show findings

['18']

18:     function principal(IShadowNonfungiblePositionManager positionManager, uint256 tokenId, address pool)
19:         public
20:         view
21:         returns (uint256 amount0, uint256 amount1)
22:     {
23:         IShadowV3Pool v3Pool = IShadowV3Pool(pool);
24:         (uint160 sqrtPriceX96,,,,,,) = v3Pool.slot0();
25: 
26:         (,,, int24 tickLower, int24 tickUpper, uint128 liquidity,,,,) = positionManager.positions(tokenId);
27: 
28:         return LiquidityAmounts.getAmountsForLiquidity( // <= FOUND
29:             sqrtPriceX96, TickMath.getSqrtRatioAtTick(tickLower), TickMath.getSqrtRatioAtTick(tickUpper), liquidity
30:         );
31:     }

['403']

403:     function getPositionAmounts(uint256 positionId) public view returns (uint256 amount0, uint256 amount1) {
404:         return getPositionAmountsByNftId(positionInfos[positionId].shadowPositionId); // <= FOUND
405:     }

['407']

407:     function getPositionAmountsByNftId(uint256 nftId) public view returns (uint256 amount0, uint256 amount1) {
408:         return IPositionValueCalculator( // <= FOUND
409:             IAddressRegistry(addressProvider).getAddress(AddressId.ADDRESS_ID_SHADOW_POSITION_VALUE_CALCULATOR)
410:         ).principal(
411:             IShadowNonfungiblePositionManager(
412:                 IAddressRegistry(addressProvider).getAddress(AddressId.ADDRESS_ID_SHADOW_NONFUNGIBLE_POSITION_MANAGER)
413:             ),
414:             nftId,
415:             shadowV3Pool
416:         );
417:     }

['419']

419:     function getPositionValue(uint256 positionId) public view returns (uint256 value) {
420:         (uint256 amount0, uint256 amount1) = getPositionAmounts(positionId);
421:         return amount0 * getTokenPrice(token0) / 10 ** token0Decimals // <= FOUND
422:             + amount1 * getTokenPrice(token1) / 10 ** token1Decimals;
423:     }

['425']

425:     function getPositionValueByNftId(uint256 nftId) public view returns (uint256 value) {
426:         (uint256 amount0, uint256 amount1) = getPositionAmountsByNftId(nftId);
427:         return amount0 * getTokenPrice(token0) / 10 ** token0Decimals // <= FOUND
428:             + amount1 * getTokenPrice(token1) / 10 ** token1Decimals;
429:     }

['431']

431:     function getDebtValue(uint256 positionId) public view returns (uint256 value) {
432:         (uint256 amount0Debt, uint256 amount1Debt) = getPositionDebt(positionId);
433:         return amount0Debt * getTokenPrice(token0) / 10 ** token0Decimals // <= FOUND
434:             + amount1Debt * getTokenPrice(token1) / 10 ** token1Decimals;
435:     }

['447']

447:     function getDebtRatioFromAmounts(
448:         uint256 amount0Principal,
449:         uint256 amount1Principal,
450:         uint256 amount0Borrow,
451:         uint256 amount1Borrow
452:     ) public view returns (uint256 debtRatio) {
453:         uint256 principalValue = amount0Principal * getTokenPrice(token0) / 10 ** token0Decimals
454:             + amount1Principal * getTokenPrice(token1) / 10 ** token1Decimals;
455:         uint256 borrowValue = amount0Borrow * getTokenPrice(token0) / 10 ** token0Decimals
456:             + amount1Borrow * getTokenPrice(token1) / 10 ** token1Decimals;
457:         return borrowValue * 10000 / (principalValue + borrowValue); // <= FOUND
458:     }

['460']

460:     function getDebtRatio(uint256 positionId) public view returns (uint256 debtRatio) {
461:         return getDebtValue(positionId) * 10000 / getPositionValue(positionId); // <= FOUND
462:     }

['18']

18:     function principal(ISwapXNonfungiblePositionManager positionManager, uint256 tokenId, address pool)
19:         public
20:         view
21:         returns (uint256 amount0, uint256 amount1)
22:     {
23:         (uint160 sqrtPriceX96,,,,,) = ISwapXV3Pool(pool).globalState();
24: 
25:         (,,,, int24 tickLower, int24 tickUpper, uint128 liquidity,,,,) = positionManager.positions(tokenId);
26: 
27:         return LiquidityAmounts.getAmountsForLiquidity( // <= FOUND
28:             sqrtPriceX96, TickMath.getSqrtRatioAtTick(tickLower), TickMath.getSqrtRatioAtTick(tickUpper), liquidity
29:         );
30:     }

[NonCritical-26] Keccak hashes which never change can be made into a constant state variable

Resolution

There is no point in using GAS to calculate a kaccak hash which never changes

Num of instances: 1

Findings

Click to show findings

['13']

13:         assert(_ADMIN_SLOT == bytes32(uint256(keccak256("eip1967.proxy.admin")) - 1));

[NonCritical-27] Constants should be on the left side of the comparison

Resolution

Putting constants on the left side of a comparison operator like == or < is a best practice known as "Yoda conditions", which can help prevent accidental assignment instead of comparison. In some programming languages, if a variable is mistakenly put on the left with a single = instead of ==, it assigns the constant's value to the variable without any compiler error. However, doing this with the constant on the left would generate an error, as constants cannot be assigned values. Although Solidity's static typing system prevents accidental assignments within conditionals, adopting this practice enhances code readability and consistency, especially when developers are working across multiple languages that support this convention.

Num of instances: 35

Findings

Click to show findings

['76']

76:         if (dt == 0 || totalStaked == 0)  // <= FOUND

['122']

122:         if (block.timestamp > startTime && totalStaked == 0)  // <= FOUND

['259']

259:         if (
260:             (positionInfo.ll != 0 && currentTick < positionInfo.ll) // <= FOUND
261:                 || (positionInfo.ul != 0 && currentTick > positionInfo.ul) // <= FOUND
262:         ) 

['314']

314:         if (amount0 > 0 && positionInfo.token0DebtId != 0)  // <= FOUND

['439']

439:         if (positionInfos[positionId].token0DebtId != 0)  // <= FOUND

['442']

442:         if (positionInfos[positionId].token1DebtId != 0)  // <= FOUND

['324']

324:         if (amount1 > 0 && positionInfo.token1DebtId != 0)  // <= FOUND

['114']

114:         if (msg.value > 0)  // <= FOUND

['30']

30:         if (balanceWETH9 > 0)  // <= FOUND

['37']

37:        if (address(this).balance > 0)  // <= FOUND

['115']

115:         if (block.timestamp > startTime && totalStaked > 0)  // <= FOUND

['137']

137:         if (token0Balance > 0)  // <= FOUND

['140']

140:         if (token1Balance > 0)  // <= FOUND

['166']

166:         if (currentDebt0 > 0)  // <= FOUND

['216']

216:         if (amount0ToSwap > 0)  // <= FOUND

['220']

220:         if (amount1ToSwap > 0)  // <= FOUND

['227']

227:         if (token0Left > 0)  // <= FOUND

['231']

231:         if (token1Left > 0)  // <= FOUND

['227']

227:         if (token0Left > 0)  // <= FOUND

['231']

231:         if (token1Left > 0)  // <= FOUND

['286']

286:             if (token0CallerFees > 0)  // <= FOUND

['299']

299:             if (token1CallerFees > 0)  // <= FOUND

['366']

366:         if (token0Fees > 0)  // <= FOUND

['370']

370:         if (token1Fees > 0)  // <= FOUND

['169']

169:         if (params.amount0Principal > 0)  // <= FOUND

['172']

172:         if (params.amount1Principal > 0)  // <= FOUND

['178']

178:         if (params.amount0Borrow > 0)  // <= FOUND

['183']

183:         if (params.amount1Borrow > 0)  // <= FOUND

['196']

196:         if (params.amount0Principal > 0 || params.amount0Borrow > 0)  // <= FOUND

['199']

199:         if (params.amount1Principal > 0 || params.amount1Borrow > 0)  // <= FOUND

['178']

178:         if (currentDebt1 > 0)  // <= FOUND

['281']

281:         if (token0Reduced > 0)  // <= FOUND

['293']

293:         if (token1Reduced > 0)  // <= FOUND

['166']

166:         if (currentDebt0 > 0)  // <= FOUND

['178']

178:         if (currentDebt1 > 0)  // <= FOUND

[NonCritical-28] Defined named returns not used within function

Resolution

Such instances can be replaced with unnamed returns

Num of instances: 9

Findings

Click to show findings

['18']

18:     function principal(IShadowNonfungiblePositionManager positionManager, uint256 tokenId, address pool) // <= FOUND
19:         public
20:         view
21:         returns (uint256 amount0, uint256 amount1) // <= FOUND
22:     {
23:         IShadowV3Pool v3Pool = IShadowV3Pool(pool);
24:         (uint160 sqrtPriceX96,,,,,,) = v3Pool.slot0();
25: 
26:         (,,, int24 tickLower, int24 tickUpper, uint128 liquidity,,,,) = positionManager.positions(tokenId);
27: 
28:         return LiquidityAmounts.getAmountsForLiquidity(
29:             sqrtPriceX96, TickMath.getSqrtRatioAtTick(tickLower), TickMath.getSqrtRatioAtTick(tickUpper), liquidity
30:         );
31:     }

['18']

18:     function principal(ISwapXNonfungiblePositionManager positionManager, uint256 tokenId, address pool) // <= FOUND
19:         public
20:         view
21:         returns (uint256 amount0, uint256 amount1) // <= FOUND
22:     {
23:         (uint160 sqrtPriceX96,,,,,) = ISwapXV3Pool(pool).globalState();
24: 
25:         (,,,, int24 tickLower, int24 tickUpper, uint128 liquidity,,,,) = positionManager.positions(tokenId);
26: 
27:         return LiquidityAmounts.getAmountsForLiquidity(
28:             sqrtPriceX96, TickMath.getSqrtRatioAtTick(tickLower), TickMath.getSqrtRatioAtTick(tickUpper), liquidity
29:         );
30:     }

['403']

403:     function getPositionAmounts(uint256 positionId) public view returns (uint256 amount0, uint256 amount1) { // <= FOUND
404:         return getPositionAmountsByNftId(positionInfos[positionId].shadowPositionId);
405:     }

['407']

407:     function getPositionAmountsByNftId(uint256 nftId) public view returns (uint256 amount0, uint256 amount1) { // <= FOUND
408:         return IPositionValueCalculator(
409:             IAddressRegistry(addressProvider).getAddress(AddressId.ADDRESS_ID_SHADOW_POSITION_VALUE_CALCULATOR)
410:         ).principal(
411:             IShadowNonfungiblePositionManager(
412:                 IAddressRegistry(addressProvider).getAddress(AddressId.ADDRESS_ID_SHADOW_NONFUNGIBLE_POSITION_MANAGER)
413:             ),
414:             nftId,
415:             shadowV3Pool
416:         );
417:     }

['419']

419:     function getPositionValue(uint256 positionId) public view returns (uint256 value) { // <= FOUND
420:         (uint256 amount0, uint256 amount1) = getPositionAmounts(positionId);
421:         return amount0 * getTokenPrice(token0) / 10 ** token0Decimals
422:             + amount1 * getTokenPrice(token1) / 10 ** token1Decimals;
423:     }

['425']

425:     function getPositionValueByNftId(uint256 nftId) public view returns (uint256 value) { // <= FOUND
426:         (uint256 amount0, uint256 amount1) = getPositionAmountsByNftId(nftId);
427:         return amount0 * getTokenPrice(token0) / 10 ** token0Decimals
428:             + amount1 * getTokenPrice(token1) / 10 ** token1Decimals;
429:     }

['431']

431:     function getDebtValue(uint256 positionId) public view returns (uint256 value) { // <= FOUND
432:         (uint256 amount0Debt, uint256 amount1Debt) = getPositionDebt(positionId);
433:         return amount0Debt * getTokenPrice(token0) / 10 ** token0Decimals
434:             + amount1Debt * getTokenPrice(token1) / 10 ** token1Decimals;
435:     }

['447']

447:     function getDebtRatioFromAmounts( // <= FOUND
448:         uint256 amount0Principal,
449:         uint256 amount1Principal,
450:         uint256 amount0Borrow,
451:         uint256 amount1Borrow
452:     ) public view returns (uint256 debtRatio) { // <= FOUND
453:         uint256 principalValue = amount0Principal * getTokenPrice(token0) / 10 ** token0Decimals
454:             + amount1Principal * getTokenPrice(token1) / 10 ** token1Decimals;
455:         uint256 borrowValue = amount0Borrow * getTokenPrice(token0) / 10 ** token0Decimals
456:             + amount1Borrow * getTokenPrice(token1) / 10 ** token1Decimals;
457:         return borrowValue * 10000 / (principalValue + borrowValue);
458:     }

['460']

460:     function getDebtRatio(uint256 positionId) public view returns (uint256 debtRatio) { // <= FOUND
461:         return getDebtValue(positionId) * 10000 / getPositionValue(positionId);
462:     }

[NonCritical-29] Initialize functions do not emit an event

Resolution

Emitting an event within initializer functions in Solidity is a best practice for providing transparency and traceability. Initializer functions set the initial state and values of an upgradeable contract. Emitting an event during initialization allows anyone to verify and audit the initial state of the contract via the transaction logs. This can be particularly useful for verifying the parameters set during initialization, tracking the contract's deployment, and troubleshooting or debugging. Therefore, developers should include an event emission in their initializer functions, providing a clear record of the contract's initialization and enhancing the contract's transparency and security.

Num of instances: 4

Findings

Click to show findings

['57']

57:     function initialize(address _addressRegistry, address _WETH9) public initializer { // <= FOUND
58:         require(_addressRegistry != address(0), Errors.VL_ADDRESS_CANNOT_ZERO);
59:         require(_WETH9 != address(0), Errors.VL_ADDRESS_CANNOT_ZERO);
60:         addressRegistry = _addressRegistry;
61:         __Ownable_init();
62:         __ReentrancyGuard_init();
63:         _initializePayments(_WETH9);
64:         nextReserveId = 1;
65:         nextDebtPositionId = 1;
66:     }

['18']

18:     function initialize( // <= FOUND
19:         address _pyth,
20:         address _oracleManager,
21:         address[] memory initialTokens,
22:         bytes32[] memory priceIds
23:     ) external initializer {
24:         pyth = IPyth(_pyth);
25:         oracleManager = _oracleManager;
26:         require(initialTokens.length == priceIds.length, "Initial tokens and price ids length mismatch");
27:         for (uint256 i = 0; i < initialTokens.length; i++) {
28:             pythPriceIds[initialTokens[i]] = priceIds[i];
29:         }
30: 
31:         maxPriceAge = 1 hours;
32:     }

['41']

41:     function initialize(uint256 _positionId) external { // <= FOUND
42:         require(vault == address(0), "Already initialized");
43:         xShadow = 0x5050bc082FF4A74Fb6B0B04385dEfdDB114b2424;
44:         x33 = 0x3333111A391cC08fa51353E9195526A70b333333;
45:         vault = msg.sender;
46:         positionId = _positionId;
47: 
48:         token0 = IVault(msg.sender).token0();
49:         token1 = IVault(msg.sender).token1();
50: 
51:         IERC20(token0).approve(vault, type(uint256).max);
52:         IERC20(token1).approve(vault, type(uint256).max);
53: 
54:         shadowV3Pool = IShadowRangeVault(vault).shadowV3Pool();
55:         tickSpacing = IShadowV3Pool(shadowV3Pool).tickSpacing();
56: 
57:         _initializePayments(IPayments(msg.sender).WETH9());
58: 
59:         addressProvider = IVault(msg.sender).addressProvider();
60:     }

['83']

83:     function initialize( // <= FOUND
84:         address _addressProvider,
85:         address _vaultRegistry,
86:         address _shadowV3PoolAddress,
87:         address _initialPositionImplementation
88:     ) public initializer {
89:         _initializePayments(IAddressRegistry(_addressProvider).getAddress(AddressId.ADDRESS_ID_WETH9));
90:         __ReentrancyGuard_init();
91:         __Ownable2Step_init();
92:         token0 = IShadowV3Pool(_shadowV3PoolAddress).token0();
93:         token1 = IShadowV3Pool(_shadowV3PoolAddress).token1();
94:         token0Decimals = IERC20Metadata(token0).decimals();
95:         token1Decimals = IERC20Metadata(token1).decimals();
96: 
97:         addressProvider = _addressProvider;
98:         vaultRegistry = _vaultRegistry;
99:         nextPositionID = 1;
100:         shadowV3Pool = _shadowV3PoolAddress;
101: 
102:         liquidationDebtRatio = 8600;
103:         liquidationFee = 500;
104:         performanceFee = 3000;
105:         minPositionSize = 100e8;
106: 
107:         positionImplementation = _initialPositionImplementation;
108:     }

[NonCritical-30] Both immutable and constant state variables should be CONSTANT_CASE

Resolution

Make found instants CAPITAL_CASE

Num of instances: 4

Findings

Click to show findings

['17']

17: IERC20 public immutable stakedToken; // <= FOUND

['17']

17: address public immutable addressRegistry; // <= FOUND

['18']

18: address public immutable lendingPool; // <= FOUND

['23']

23: address public immutable underlyingAsset; // <= FOUND

[NonCritical-31] Overly complicated arithmetic

Resolution

To maintain readability in code, particularly in Solidity which can involve complex mathematical operations, it is often recommended to limit the number of arithmetic operations to a maximum of 2-3 per line. Too many operations in a single line can make the code difficult to read and understand, increase the likelihood of mistakes, and complicate the process of debugging and reviewing the code. Consider splitting such operations over more than one line, take special care when dealing with division however. Try to limit the number of arithmetic operations to a maximum of 3 per line.

Num of instances: 5

Findings

Click to show findings

['80']

80:         return rewardData[rewardToken].rewardPerTokenStored
81:             + (rewardData[rewardToken].rewardRate * dt * 1e18) / totalStaked; // <= FOUND

['421']

421:         return amount0 * getTokenPrice(token0) / 10 ** token0Decimals
422:             + amount1 * getTokenPrice(token1) / 10 ** token1Decimals; // <= FOUND

['433']

433:         return amount0Debt * getTokenPrice(token0) / 10 ** token0Decimals
434:             + amount1Debt * getTokenPrice(token1) / 10 ** token1Decimals; // <= FOUND

['453']

453:         uint256 principalValue = amount0Principal * getTokenPrice(token0) / 10 ** token0Decimals
454:             + amount1Principal * getTokenPrice(token1) / 10 ** token1Decimals; // <= FOUND

['455']

455:         uint256 borrowValue = amount0Borrow * getTokenPrice(token0) / 10 ** token0Decimals
456:             + amount1Borrow * getTokenPrice(token1) / 10 ** token1Decimals; // <= FOUND

[NonCritical-32] Using zero as a parameter

Resolution

Taking 0 as a valid argument in Solidity without checks can lead to severe security issues. A historical example is the infamous 0x0 address bug where numerous tokens were lost. This happens because '0' can be interpreted as an uninitialized address, leading to transfers to the '0x0' address, effectively burning tokens. Moreover, 0 as a denominator in division operations would cause a runtime exception. It's also often indicative of a logical error in the caller's code. It's important to always validate input and handle edge cases like 0 appropriately. Use require() statements to enforce conditions and provide clear error messages to facilitate debugging and safer code.

Num of instances: 3

Findings

Click to show findings

['83']

83:     function openPosition(OpenPositionParams memory params) // <= FOUND
84:         external
85:         onlyVault
86:         returns (uint256 tokenId, uint128 liquidity, uint256 amount0, uint256 amount1)
87:     {
88:         uint256 token0Balance = IERC20(token0).balanceOf(address(this));
89:         if (params.amount0Desired > token0Balance) {
90:             _swapTokenExactInput(
91:                 token1,
92:                 token0,
93:                 IERC20(token1).balanceOf(address(this)) - params.amount1Desired,
94:                 params.amount0Desired - token0Balance
95:             );
96:         }
97: 
98:         uint256 token1Balance = IERC20(token1).balanceOf(address(this));
99:         if (params.amount1Desired > token1Balance) {
100:             _swapTokenExactInput(
101:                 token0,
102:                 token1,
103:                 IERC20(token0).balanceOf(address(this)) - params.amount0Desired,
104:                 params.amount1Desired - token1Balance
105:             );
106:         }
107:         address shadowNonfungiblePositionManager = getShadowNonfungiblePositionManager();
108: 
109:         IERC20(token0).approve(shadowNonfungiblePositionManager, params.amount0Desired);
110:         IERC20(token1).approve(shadowNonfungiblePositionManager, params.amount1Desired);
111: 
112:         (tokenId, liquidity, amount0, amount1) = IShadowNonfungiblePositionManager(shadowNonfungiblePositionManager)
113:             .mint(
114:             IShadowNonfungiblePositionManager.MintParams({
115:                 token0: token0, // <= FOUND
116:                 token1: token1,
117:                 tickSpacing: tickSpacing,
118:                 tickLower: params.tickLower,
119:                 tickUpper: params.tickUpper,
120:                 amount0Desired: params.amount0Desired,
121:                 amount1Desired: params.amount1Desired,
122:                 amount0Min: 0,
123:                 amount1Min: 0,
124:                 recipient: address(this),
125:                 deadline: block.timestamp
126:             })
127:         );
128: 
129:         shadowPositionId = tokenId;
130:         positionOwner = params.positionOwner;
131: 
132:         unwrapWETH9(0, positionOwner);
133: 
134:         token0Balance = IERC20(token0).balanceOf(address(this));
135:         token1Balance = IERC20(token1).balanceOf(address(this));
136: 
137:         if (token0Balance > 0) {
138:             pay(token0, address(this), positionOwner, token0Balance);
139:         }
140:         if (token1Balance > 0) {
141:             pay(token1, address(this), positionOwner, token1Balance);
142:         }
143:     }

['202']

202:     function reducePosition(uint128 reducePercentage, uint256 amount0ToSwap, uint256 amount1ToSwap) // <= FOUND
203:         external
204:         onlyVault
205:         returns (uint128 reduceLiquidity, uint256 token0Reduced, uint256 token1Reduced)
206:     {
207:         _claimFees();
208: 
209:         (,,,,, uint128 liquidity,,,,) =
210:             IShadowNonfungiblePositionManager(getShadowNonfungiblePositionManager()).positions(shadowPositionId);
211: 
212:         reduceLiquidity = liquidity * reducePercentage / 10000;
213: 
214:         (token0Reduced, token1Reduced) = _decreasePosition(reduceLiquidity);
215: 
216:         if (amount0ToSwap > 0) {
217:             _swapTokenExactInput(token0, token1, amount0ToSwap, 0); // <= FOUND
218:         }
219: 
220:         if (amount1ToSwap > 0) {
221:             _swapTokenExactInput(token1, token0, amount1ToSwap, 0);
222:         }
223: 
224:         uint256 token0Left = IERC20(token0).balanceOf(address(this));
225:         uint256 token1Left = IERC20(token1).balanceOf(address(this));
226: 
227:         if (token0Left > 0) {
228:             IERC20(token0).approve(vault, token0Left);
229:         }
230: 
231:         if (token1Left > 0) {
232:             IERC20(token1).approve(vault, token1Left);
233:         }
234: 
235:         IVault(vault).repayExact(positionId, token0Left, token1Left);
236: 
237:         token0Left = IERC20(token0).balanceOf(address(this));
238:         token1Left = IERC20(token1).balanceOf(address(this));
239:         if (token0Left > 0) {
240:             pay(token0, address(this), positionOwner, token0Left);
241:         }
242:         if (token1Left > 0) {
243:             pay(token1, address(this), positionOwner, token1Left);
244:         }
245:     }

['408']

408:     function _decreasePosition(uint128 liquidity) internal returns (uint256 amount0, uint256 amount1) { // <= FOUND
409:         IShadowNonfungiblePositionManager(getShadowNonfungiblePositionManager()).decreaseLiquidity(
410:             IShadowNonfungiblePositionManager.DecreaseLiquidityParams({
411:                 tokenId: shadowPositionId, // <= FOUND
412:                 liquidity: liquidity,
413:                 amount0Min: 0,
414:                 amount1Min: 0,
415:                 deadline: block.timestamp
416:             })
417:         );
418: 
419:         IShadowNonfungiblePositionManager(getShadowNonfungiblePositionManager()).collect(
420:             IShadowNonfungiblePositionManager.CollectParams({
421:                 tokenId: shadowPositionId, // <= FOUND
422:                 recipient: address(this),
423:                 amount0Max: type(uint128).max,
424:                 amount1Max: type(uint128).max
425:             })
426:         );
427: 
428:         amount0 = IERC20(token0).balanceOf(address(this));
429:         amount1 = IERC20(token1).balanceOf(address(this));
430:     }

[NonCritical-33] Use of non-named numeric constants

Resolution

Magic numbers should be avoided in Solidity code to enhance readability, maintainability, and reduce the likelihood of errors. Magic numbers are hard-coded values with no clear meaning or context, which can create confusion and make the code harder to understand for developers. Using well-defined constants or variables with descriptive names instead of magic numbers not only clarifies the purpose and significance of the value but also simplifies code updates and modifications.

Num of instances: 9

Findings

Click to show findings

['381']

381:         
382:         
383:         setBorrowingRateConfig(reserveData, 8000, 2000, 9000, 5000, 15000);

['212']

212:         reduceLiquidity = liquidity * reducePercentage / 10000;

['282']

282:             token0Fees = token0Reduced * vars.liquidationFee / 10000;

['285']

285:             uint256 token0CallerFees = token0Fees * vars.liquidationCallerFee / 10000;

['294']

294:             token1Fees = token1Reduced * vars.liquidationFee / 10000;

['298']

298:             uint256 token1CallerFees = token1Fees * vars.liquidationCallerFee / 10000;

['384']

384:         uint256 feeAmount = amount * performanceFee / 10000;

['457']

457:         return borrowValue * 10000 / (principalValue + borrowValue);

['461']

461:         return getDebtValue(positionId) * 10000 / getPositionValue(positionId);

[NonCritical-34] Long powers of ten should use scientific notation 1eX

Resolution

A large number such as 1000000 is far more readable as 1e6, this will help prevent unintended bugs in the code

Num of instances: 8

Findings

Click to show findings

['212']

212:         reduceLiquidity = liquidity * reducePercentage / 10000; // <= FOUND

['282']

282:             token0Fees = token0Reduced * vars.liquidationFee / 10000; // <= FOUND

['285']

285:             uint256 token0CallerFees = token0Fees * vars.liquidationCallerFee / 10000; // <= FOUND

['294']

294:             token1Fees = token1Reduced * vars.liquidationFee / 10000; // <= FOUND

['298']

298:             uint256 token1CallerFees = token1Fees * vars.liquidationCallerFee / 10000; // <= FOUND

['384']

384:         uint256 feeAmount = amount * performanceFee / 10000; // <= FOUND

['457']

457:         return borrowValue * 10000 / (principalValue + borrowValue); // <= FOUND

['461']

461:         return getDebtValue(positionId) * 10000 / getPositionValue(positionId); // <= FOUND

[NonCritical-35] Event emit should emit a parameter

Resolution

Events in Solidity offer valuable insight into the contract's execution as they log specific instances of state changes or value transfers. However, if events do not include any parameters, their usefulness can be significantly reduced. Events without parameters can provide limited information, mainly signaling that a specific operation occurred, but lacking the context of what exactly changed. It's generally recommended to include relevant parameters, such as state changes or value modifications, in the emitted events.

Num of instances: 2

Findings

Click to show findings

['537']

537:         emit Paused(); // <= FOUND

['542']

542:         emit UnPaused(); // <= FOUND

[NonCritical-36] Two or more functions contain the exact same code

Resolution

In Solidity programming, duplicate code in multiple functions introduces potential security risks and maintainability issues. It magnifies the impact of any bugs or vulnerabilities, since developers must fix these in every location where the code is replicated. Overlooking any instance of replicated code could leave vulnerabilities intact. Furthermore, code duplication leads to contract bloating, diminishing the readability and manageability of the code. Future logic changes would need to be applied in every location where the code is replicated, increasing the complexity of updates. To resolve this, it is recommended to consolidate duplicate code into a single internal function, and replace the duplicate instances with calls to this new function. This approach streamlines the code, reducing the attack surface and making it easier to maintain and update.

Num of instances: 2

Findings

Click to show findings

['26']

26:     function unwrapWETH9(uint256 amountMinimum, address recipient) internal {
27:         uint256 balanceWETH9 = IWETH9(WETH9).balanceOf(address(this)); // <= FOUND
28:         require(balanceWETH9 >= amountMinimum, Errors.VL_INSUFFICIENT_WETH9);
29: 
30:         if (balanceWETH9 > 0) {
31:             IWETH9(WETH9).withdraw(balanceWETH9);
32:             TransferHelper.safeTransferETH(recipient, balanceWETH9);
33:         }
34:     }

['46']

46:     function pay(address token, address payer, address recipient, uint256 value) internal {
47:         if (token == WETH9 && address(this).balance >= value) {
48:             
49:             IWETH9(WETH9).deposit{value: value}();  // <= FOUND
50:             require(IWETH9(WETH9).transfer(recipient, value), "transfer failed");
51:         } else if (payer == address(this)) {
52:             
53:             TransferHelper.safeTransfer(token, recipient, value);
54:         } else {
55:             
56:             TransferHelper.safeTransferFrom(token, payer, recipient, value);
57:         }
58:     }

[NonCritical-37] Empty bytes check is missing

Resolution

When developing smart contracts in Solidity, it's crucial to validate the inputs of your functions. This includes ensuring that the bytes parameters are not empty, especially when they represent crucial data such as addresses, identifiers, or raw data that the contract needs to process.

Missing empty bytes checks can lead to unexpected behaviour in your contract. For instance, certain operations might fail, produce incorrect results, or consume unnecessary gas when performed with empty bytes. Moreover, missing input validation can potentially expose your contract to malicious activity, including exploitation of unhandled edge cases.

To mitigate these issues, always validate that bytes parameters are not empty when the logic of your contract requires it.

Num of instances: 1

Findings

Click to show findings

['44']

44:     function setPythPriceId(address token, bytes32 priceId) external onlyOracleManager {
45:         pythPriceIds[token] = priceId;
46:     }

[NonCritical-38] Use OpenZeppelin's ReentrancyGuard/ReentrancyGuardUpgradeable rather than writing your own

Resolution

Leveraging OpenZeppelin's ReentrancyGuard or ReentrancyGuardUpgradeable is generally recommended over creating your own versions. The OpenZeppelin modules have undergone rigorous optimization for gas efficiency and have been comprehensively tested for potential security vulnerabilities. As a result, they provide a high degree of reliability and performance. Writing your own reentrancy guard mechanism might introduce unseen bugs or security loopholes, and likely be less gas efficient. Therefore, it's beneficial to rely on the tried-and-tested solutions offered by OpenZeppelin's established and widely-accepted smart contract library.

Num of instances: 1

Findings

Click to show findings

['34']

34:     modifier nonReentrant() { // <= FOUND
35:         require(_unlocked == 1, "reentrant call");
36:         _unlocked = 2;
37:         _;
38:         _unlocked = 1;
39:     }

[NonCritical-39] Cyclomatic complexity in functions

Resolution

Cyclomatic complexity is a software metric used to measure the complexity of a program. It quantifies the number of linearly independent paths through a program's source code, giving an idea of how complex the control flow is. High cyclomatic complexity may indicate a higher risk of defects and can make the code harder to understand, test, and maintain. It often suggests that a function or method is trying to do too much, and a refactor might be needed. By breaking down complex functions into smaller, more focused pieces, you can improve readability, ease of testing, and overall maintainability.

Num of instances: 8

Findings

Click to show findings

[]

96:     function setReward(address rewardToken, uint256 startTime, uint256 endTime, uint256 totalRewards)
97:         public
98:         onlyOwner
99:         nonReentrant
100:         updateReward(address(0))
101:     {
102:         require(startTime < endTime, "start must lt end");
103:         require(rewardData[rewardToken].endTime < block.timestamp, "not end");
104: 
105:         if (!inRewardsTokenList[rewardToken]) {
106:             rewardTokens.push(rewardToken);
107:             inRewardsTokenList[rewardToken] = true;
108:         }
109: 
110:         rewardData[rewardToken].startTime = startTime;
111:         rewardData[rewardToken].endTime = endTime;
112:         rewardData[rewardToken].lastUpdateTime = block.timestamp;
113:         rewardData[rewardToken].rewardRate = totalRewards / (endTime - startTime);
114: 
115:         if (block.timestamp > startTime && totalStaked > 0) {
116:             uint256 dt = block.timestamp - startTime;
117: 
118:             rewardData[rewardToken].rewardPerTokenStored +=
119:                 (rewardData[rewardToken].rewardRate * dt * 1e18) / totalStaked;
120:         }
121: 
122:         if (block.timestamp > startTime && totalStaked == 0) {
123:             
124:             
125:             uint256 dt = block.timestamp - startTime;
126:             totalRewards -= rewardData[rewardToken].rewardRate * dt;
127:         }
128: 
129:         IERC20(rewardToken).safeTransferFrom(msg.sender, address(this), totalRewards);
130: 
131:         emit RewardsSet(rewardToken, startTime, endTime, totalRewards);
132:     }

[]

83:     function openPosition(OpenPositionParams memory params)
84:         external
85:         onlyVault
86:         returns (uint256 tokenId, uint128 liquidity, uint256 amount0, uint256 amount1)
87:     {
88:         uint256 token0Balance = IERC20(token0).balanceOf(address(this));
89:         if (params.amount0Desired > token0Balance) {
90:             _swapTokenExactInput(
91:                 token1,
92:                 token0,
93:                 IERC20(token1).balanceOf(address(this)) - params.amount1Desired,
94:                 params.amount0Desired - token0Balance
95:             );
96:         }
97: 
98:         uint256 token1Balance = IERC20(token1).balanceOf(address(this));
99:         if (params.amount1Desired > token1Balance) {
100:             _swapTokenExactInput(
101:                 token0,
102:                 token1,
103:                 IERC20(token0).balanceOf(address(this)) - params.amount0Desired,
104:                 params.amount1Desired - token1Balance
105:             );
106:         }
107:         address shadowNonfungiblePositionManager = getShadowNonfungiblePositionManager();
108: 
109:         IERC20(token0).approve(shadowNonfungiblePositionManager, params.amount0Desired);
110:         IERC20(token1).approve(shadowNonfungiblePositionManager, params.amount1Desired);
111: 
112:         (tokenId, liquidity, amount0, amount1) = IShadowNonfungiblePositionManager(shadowNonfungiblePositionManager)
113:             .mint(
114:             IShadowNonfungiblePositionManager.MintParams({
115:                 token0: token0,
116:                 token1: token1,
117:                 tickSpacing: tickSpacing,
118:                 tickLower: params.tickLower,
119:                 tickUpper: params.tickUpper,
120:                 amount0Desired: params.amount0Desired,
121:                 amount1Desired: params.amount1Desired,
122:                 amount0Min: 0,
123:                 amount1Min: 0,
124:                 recipient: address(this),
125:                 deadline: block.timestamp
126:             })
127:         );
128: 
129:         shadowPositionId = tokenId;
130:         positionOwner = params.positionOwner;
131: 
132:         unwrapWETH9(0, positionOwner);
133: 
134:         token0Balance = IERC20(token0).balanceOf(address(this));
135:         token1Balance = IERC20(token1).balanceOf(address(this));
136: 
137:         if (token0Balance > 0) {
138:             pay(token0, address(this), positionOwner, token0Balance);
139:         }
140:         if (token1Balance > 0) {
141:             pay(token1, address(this), positionOwner, token1Balance);
142:         }
143:     }

[]

145:     function closePosition()
146:         external
147:         onlyVault
148:         returns (
149:             uint128 liquidity,
150:             uint256 token0Reduced,
151:             uint256 token1Reduced,
152:             uint256 token0Balance,
153:             uint256 token1Balance
154:         )
155:     {
156:         _claimFees();
157:         _claimRewards();
158: 
159:         (,,,,, liquidity,,,,) =
160:             IShadowNonfungiblePositionManager(getShadowNonfungiblePositionManager()).positions(shadowPositionId);
161: 
162:         (token0Reduced, token1Reduced) = _decreasePosition(liquidity);
163: 
164:         (uint256 currentDebt0, uint256 currentDebt1) = IVault(vault).getPositionDebt(positionId);
165: 
166:         if (currentDebt0 > 0) {
167:             
168:             if (currentDebt0 > token0Reduced) {
169:                 
170:                 
171:                 
172:                 uint256 amount1Excess = token1Reduced - currentDebt1;
173:                 _swapTokenExactInput(token1, token0, amount1Excess, currentDebt0 - token0Reduced);
174:             }
175:             IERC20(token0).approve(vault, currentDebt0);
176:         }
177: 
178:         if (currentDebt1 > 0) {
179:             
180:             if (currentDebt1 > token1Reduced) {
181:                 
182:                 
183:                 
184:                 uint256 amount0Excess = token0Reduced - currentDebt0;
185:                 _swapTokenExactInput(token0, token1, amount0Excess, currentDebt1 - token1Reduced);
186:             }
187:             IERC20(token1).approve(vault, currentDebt1);
188:         }
189: 
190:         IVault(vault).repayExact(positionId, currentDebt0, currentDebt1);
191: 
192:         token0Balance = IERC20(token0).balanceOf(address(this));
193:         token1Balance = IERC20(token1).balanceOf(address(this));
194:         if (token0Balance > 0) {
195:             pay(token0, address(this), positionOwner, token0Balance);
196:         }
197:         if (token1Balance > 0) {
198:             pay(token1, address(this), positionOwner, token1Balance);
199:         }
200:     }

[]

202:     function reducePosition(uint128 reducePercentage, uint256 amount0ToSwap, uint256 amount1ToSwap)
203:         external
204:         onlyVault
205:         returns (uint128 reduceLiquidity, uint256 token0Reduced, uint256 token1Reduced)
206:     {
207:         _claimFees();
208: 
209:         (,,,,, uint128 liquidity,,,,) =
210:             IShadowNonfungiblePositionManager(getShadowNonfungiblePositionManager()).positions(shadowPositionId);
211: 
212:         reduceLiquidity = liquidity * reducePercentage / 10000;
213: 
214:         (token0Reduced, token1Reduced) = _decreasePosition(reduceLiquidity);
215: 
216:         if (amount0ToSwap > 0) {
217:             _swapTokenExactInput(token0, token1, amount0ToSwap, 0);
218:         }
219: 
220:         if (amount1ToSwap > 0) {
221:             _swapTokenExactInput(token1, token0, amount1ToSwap, 0);
222:         }
223: 
224:         uint256 token0Left = IERC20(token0).balanceOf(address(this));
225:         uint256 token1Left = IERC20(token1).balanceOf(address(this));
226: 
227:         if (token0Left > 0) {
228:             IERC20(token0).approve(vault, token0Left);
229:         }
230: 
231:         if (token1Left > 0) {
232:             IERC20(token1).approve(vault, token1Left);
233:         }
234: 
235:         IVault(vault).repayExact(positionId, token0Left, token1Left);
236: 
237:         token0Left = IERC20(token0).balanceOf(address(this));
238:         token1Left = IERC20(token1).balanceOf(address(this));
239:         if (token0Left > 0) {
240:             pay(token0, address(this), positionOwner, token0Left);
241:         }
242:         if (token1Left > 0) {
243:             pay(token1, address(this), positionOwner, token1Left);
244:         }
245:     }

[]

254:     function liquidatePosition(address caller)
255:         external
256:         onlyVault
257:         returns (
258:             uint128 liquidity,
259:             uint256 token0Reduced,
260:             uint256 token1Reduced,
261:             uint256 token0Fees,
262:             uint256 token1Fees,
263:             uint256 token0Left,
264:             uint256 token1Left
265:         )
266:     {
267:         _claimFees();
268:         _claimRewards();
269:         (,,,,, liquidity,,,,) =
270:             IShadowNonfungiblePositionManager(getShadowNonfungiblePositionManager()).positions(shadowPositionId);
271: 
272:         (token0Reduced, token1Reduced) = _decreasePosition(liquidity);
273:         (uint256 currentDebt0, uint256 currentDebt1) = IVault(vault).getPositionDebt(positionId);
274: 
275:         LiquidationFeeVars memory vars = LiquidationFeeVars({
276:             liquidationFee: IVault(vault).liquidationFee(),
277:             liquidationCallerFee: IVault(vault).liquidationCallerFee(),
278:             liquidationFeeRecipient: IVault(vault).liquidationFeeRecipient()
279:         });
280:         
281:         if (token0Reduced > 0) {
282:             token0Fees = token0Reduced * vars.liquidationFee / 10000;
283:             pay(token0, address(this), vars.liquidationFeeRecipient, token0Fees);
284: 
285:             uint256 token0CallerFees = token0Fees * vars.liquidationCallerFee / 10000;
286:             if (token0CallerFees > 0) {
287:                 pay(token0, address(this), caller, token0CallerFees);
288:             }
289: 
290:             token0Reduced = token0Reduced - token0Fees - token0CallerFees;
291:         }
292: 
293:         if (token1Reduced > 0) {
294:             token1Fees = token1Reduced * vars.liquidationFee / 10000;
295: 
296:             pay(token1, address(this), vars.liquidationFeeRecipient, token1Fees);
297: 
298:             uint256 token1CallerFees = token1Fees * vars.liquidationCallerFee / 10000;
299:             if (token1CallerFees > 0) {
300:                 pay(token1, address(this), caller, token1CallerFees);
301:             }
302: 
303:             token1Reduced = token1Reduced - token1Fees - token1CallerFees;
304:         }
305: 
306:         if (currentDebt0 > 0) {
307:             
308:             if (currentDebt0 > token0Reduced) {
309:                 uint256 amount1Excess = token1Reduced - currentDebt1;
310:                 _swapTokenExactInput(token1, token0, amount1Excess, currentDebt0 - token0Reduced);
311:             }
312:             IERC20(token0).approve(vault, currentDebt0);
313:         }
314: 
315:         if (currentDebt1 > 0) {
316:             
317:             if (currentDebt1 > token1Reduced) {
318:                 uint256 amount0Excess = token0Reduced - currentDebt0;
319:                 _swapTokenExactInput(token0, token1, amount0Excess, currentDebt1 - token1Reduced);
320:             }
321:             IERC20(token1).approve(vault, currentDebt1);
322:         }
323: 
324:         IVault(vault).repayExact(positionId, currentDebt0, currentDebt1);
325: 
326:         token0Left = IERC20(token0).balanceOf(address(this));
327:         token1Left = IERC20(token1).balanceOf(address(this));
328:         if (token0Left > 0) {
329:             pay(token0, address(this), positionOwner, token0Left);
330:         }
331:         if (token1Left > 0) {
332:             pay(token1, address(this), positionOwner, token1Left);
333:         }
334:     }

['376']

376:     function _claimRewards() internal { // <= FOUND
377:         address shadowGauge = IShadowRangeVault(vault).shadowGauge();
378:         if (shadowGauge != address(0)) {
379:             address[] memory tokens = IShadowGaugeV3(shadowGauge).getRewardTokens();
380:             IShadowGaugeV3(shadowGauge).getReward(shadowPositionId, tokens);
381: 
382:             for (uint256 i = 0; i < tokens.length; i++) {
383:                 uint256 balance = IERC20(tokens[i]).balanceOf(address(this));
384:                 if (balance > 0) {
385:                     if (tokens[i] == xShadow) {
386:                         
387:                         
388:                         
389:                         
390:                         
391:                         if (IShadowX33(x33).isUnlocked()) {
392:                             address x33Adapter = getX33Adapter();
393:                             IERC20(tokens[i]).approve(x33Adapter, balance);
394:                             IERC4626(x33Adapter).deposit(balance, address(this));
395:                             balance = IERC20(x33).balanceOf(address(this));
396:                             IERC20(x33).approve(vault, balance);
397:                             IVault(vault).claimCallback(x33, balance);
398:                         }
399:                     } else {
400:                         IERC20(tokens[i]).approve(vault, balance);
401:                         IVault(vault).claimCallback(tokens[i], balance);
402:                     }
403:                 }
404:             }
405:         }
406:     }

[]

152:     function openPosition(OpenPositionParams calldata params)
153:         external
154:         payable
155:         nonReentrant
156:         checkDeadline(params.deadline)
157:     {
158:         (uint256 _positionId, address _positionAddress) = _createNewPosition();
159: 
160:         PositionInfo storage positionInfo = positionInfos[_positionId];
161:         positionInfo.owner = msg.sender;
162:         positionInfo.vaultId = vaultId;
163:         positionInfo.positionAddress = _positionAddress;
164:         positionInfo.positionId = _positionId;
165:         positionIds[msg.sender].push(_positionId);
166:         positionAddressToId[_positionAddress] = _positionId;
167: 
168:         
169:         if (params.amount0Principal > 0) {
170:             pay(token0, msg.sender, address(this), params.amount0Principal);
171:         }
172:         if (params.amount1Principal > 0) {
173:             pay(token1, msg.sender, address(this), params.amount1Principal);
174:         }
175: 
176:         
177:         address lendingPool = getLendingPool();
178:         if (params.amount0Borrow > 0) {
179:             positionInfo.token0DebtId = ILendingPool(lendingPool).newDebtPosition(token0ReserveId);
180: 
181:             ILendingPool(lendingPool).borrow(address(this), positionInfo.token0DebtId, params.amount0Borrow);
182:         }
183:         if (params.amount1Borrow > 0) {
184:             positionInfo.token1DebtId = ILendingPool(lendingPool).newDebtPosition(token1ReserveId);
185: 
186:             ILendingPool(lendingPool).borrow(address(this), positionInfo.token1DebtId, params.amount1Borrow);
187:         }
188: 
189:         require(
190:             getDebtRatioFromAmounts(
191:                 params.amount0Principal, params.amount1Principal, params.amount0Borrow, params.amount1Borrow
192:             ) < liquidationDebtRatio,
193:             "Borrow value is too high"
194:         );
195: 
196:         if (params.amount0Principal > 0 || params.amount0Borrow > 0) {
197:             pay(token0, address(this), positionInfo.positionAddress, params.amount0Principal + params.amount0Borrow);
198:         }
199:         if (params.amount1Principal > 0 || params.amount1Borrow > 0) {
200:             pay(token1, address(this), positionInfo.positionAddress, params.amount1Principal + params.amount1Borrow);
201:         }
202:         
203:         (uint256 tokenId, uint128 liquidity, uint256 amount0, uint256 amount1) = IShadowRangePositionImpl(
204:             positionInfo.positionAddress
205:         ).openPosition(
206:             IShadowRangePositionImpl.OpenPositionParams({
207:                 amount0Desired: params.amount0Desired,
208:                 amount1Desired: params.amount1Desired,
209:                 tickLower: params.tickLower,
210:                 tickUpper: params.tickUpper,
211:                 positionOwner: msg.sender
212:             })
213:         );
214: 
215:         require(getPositionValueByNftId(tokenId) > minPositionSize, "PVL");
216: 
217:         positionInfo.shadowPositionId = tokenId;
218:         positionInfo.tickLower = params.tickLower;
219:         positionInfo.tickUpper = params.tickUpper;
220:         positionInfo.ul = params.ul;
221:         positionInfo.ll = params.ll;
222: 
223:         emit PositionOpened(
224:             msg.sender,
225:             vaultId,
226:             _positionId,
227:             _positionAddress,
228:             params.amount0Principal,
229:             params.amount1Principal,
230:             amount0,
231:             amount1,
232:             liquidity,
233:             block.timestamp
234:         );
235: 
236:         if (msg.value > 0) {
237:             refundETH();
238:         }
239:         require(getDebtRatio(_positionId) < liquidationDebtRatio, "DRH");
240:     }

['309']

309:     function repayExact(uint256 positionId, uint256 amount0, uint256 amount1) external payable { // <= FOUND
310:         PositionInfo storage positionInfo = positionInfos[positionId];
311: 
312:         
313:         address lendingPool = getLendingPool();
314:         if (amount0 > 0 && positionInfo.token0DebtId != 0) {
315:             (uint256 currentDebt0,) = ILendingPool(lendingPool).getCurrentDebt(positionInfo.token0DebtId);
316:             if (amount0 > currentDebt0) {
317:                 amount0 = currentDebt0;
318:             }
319: 
320:             pay(token0, msg.sender, address(this), amount0);
321: 
322:             ILendingPool(lendingPool).repay(address(this), positionInfo.token0DebtId, amount0);
323:         }
324:         if (amount1 > 0 && positionInfo.token1DebtId != 0) {
325:             (uint256 currentDebt1,) = ILendingPool(lendingPool).getCurrentDebt(positionInfo.token1DebtId);
326:             if (amount1 > currentDebt1) {
327:                 amount1 = currentDebt1;
328:             }
329: 
330:             pay(token1, msg.sender, address(this), amount1);
331: 
332:             ILendingPool(lendingPool).repay(address(this), positionInfo.token1DebtId, amount1);
333:         }
334: 
335:         if (msg.value > 0) {
336:             refundETH();
337:         }
338:     }

[NonCritical-40] Incorrect withdraw declaration

Resolution

In Solidity, it's essential for clarity and interoperability to correctly specify return types in function declarations. If the withdraw function is expected to return a bool to indicate success or failure, its omission could lead to ambiguity or unexpected behavior when interacting with or calling this function from other contracts or off-chain systems. Missing return values can mislead developers and potentially lead to contract integrations built on incorrect assumptions. To resolve this, the function declaration for withdraw should be modified to explicitly include the bool return type, ensuring clarity and correctness in contract interactions.

Num of instances: 1

Findings

Click to show findings

['159']

159:     function withdraw(uint256 amount, address to) external nonReentrant updateReward(msg.sender)  // <= FOUND

[NonCritical-41] Contract does not follow the Solidity style guide's suggested layout ordering

Resolution

Contracts should define structs before modifiers and modifiers before functions.

Num of instances: 3

Findings

Click to show findings

['10']

10: contract PrimaryPriceOracle is IPriceOracle, Initializable  // <= FOUND

['26']

26: contract ShadowRangePositionImpl is IShadowRangePositionImpl, PaymentsUpgradeable  // <= FOUND

['31']

31: contract VaultPositionViewer  // <= FOUND

[NonCritical-42] Don't only depend on Solidity's arithmetic ordering.

Resolution

Relying on Solidity's default arithmetic operator precedence can lead to misunderstood or overlooked nuances in code execution. Misinterpretation risks can be significant, especially when different developers or auditors review the code. To ensure clarity and minimize potential errors, it's recommended to use parentheses. These not only override the default precedence when needed but also emphasize the intended order of operations. By deliberately showing the intended calculation sequence using parentheses, developers make the code's logic explicit, reducing the risk of unintended behaviors and making the codebase more maintainable and understandable for all stakeholders.

Num of instances: 5

Findings

Click to show findings

['212']

212:         reduceLiquidity = liquidity * reducePercentage / 10000;

['282']

282:             token0Fees = token0Reduced * vars.liquidationFee / 10000;

['285']

285:             uint256 token0CallerFees = token0Fees * vars.liquidationCallerFee / 10000;

['294']

294:             token1Fees = token1Reduced * vars.liquidationFee / 10000;

['298']

298:             uint256 token1CallerFees = token1Fees * vars.liquidationCallerFee / 10000;

[NonCritical-43] Contracts with only unimplemented functions can be labeled as abstract

Resolution

In Solidity, a contract that's not meant to be deployed on its own but is intended to be inherited by other contracts should be marked abstract. This ensures that developers recognize the contract's incomplete or intended-to-be-extended nature. If a contract has unimplemented functions or is designed with the intention that another contract should extend its functionality, it should be explicitly labeled as abstract. This helps prevent inadvertent deployments and clearly communicates the contract's purpose to other developers. Resolution: Review the contract, and if it's not supposed to function standalone, mark it as abstract to make the intention clear.

Num of instances: 6

Findings

Click to show findings

['24']

24: contract LendingPool is ILendingPool, OwnableUpgradeable, PaymentsUpgradeable, ReentrancyGuardUpgradeable 

['10']

10: contract PrimaryPriceOracle is IPriceOracle, Initializable 

['10']

10: contract ShadowPositionValueCalculator 

['26']

26: contract ShadowRangePositionImpl is IShadowRangePositionImpl, PaymentsUpgradeable 

['30']

30: contract ShadowRangeVault is
31:     Initializable,
32:     ReentrancyGuardUpgradeable,
33:     Ownable2StepUpgradeable,
34:     PaymentsUpgradeable,
35:     IVault
36: 

['10']

10: contract SwapXPositionValueCalculator 

[NonCritical-44] A event should be emitted if a non immutable state variable is set in a constructor

Num of instances: 4

Findings

Click to show findings

['32']

32:     constructor(string memory name_, string memory symbol_, uint8 decimals_, address underlyingAsset_)
33:         ERC20(name_, symbol_)
34:     {
35:         _decimals = decimals_; // <= FOUND
36: 
37:         require(underlyingAsset_ != address(0), Errors.VL_ADDRESS_CANNOT_ZERO);
38:         underlyingAsset = underlyingAsset_;
39:         lendingPool = msg.sender;
40:     }

['19']

19:     constructor(address _addressRegistry) {
20:         require(_addressRegistry != address(0), Errors.VL_ADDRESS_CANNOT_ZERO);
21:         addressRegistry = _addressRegistry; // <= FOUND
22:         nextVaultID = 1;
23:     }

['34']

34:     constructor(address _addressProvider) {
35:         addressProvider = _addressProvider; // <= FOUND
36:     }

['18']

18:     constructor(address _WETH9) {
19:         WETH9 = _WETH9; // <= FOUND
20:     }

[NonCritical-45] No limit when setting fees in initializer

Num of instances: 1

Findings

Click to show findings

['83']

83:     function initialize(
84:         address _addressProvider,
85:         address _vaultRegistry,
86:         address _shadowV3PoolAddress,
87:         address _initialPositionImplementation
88:     ) public initializer {
89:         _initializePayments(IAddressRegistry(_addressProvider).getAddress(AddressId.ADDRESS_ID_WETH9));
90:         __ReentrancyGuard_init();
91:         __Ownable2Step_init();
92:         token0 = IShadowV3Pool(_shadowV3PoolAddress).token0();
93:         token1 = IShadowV3Pool(_shadowV3PoolAddress).token1();
94:         token0Decimals = IERC20Metadata(token0).decimals();
95:         token1Decimals = IERC20Metadata(token1).decimals();
96: 
97:         addressProvider = _addressProvider;
98:         vaultRegistry = _vaultRegistry;
99:         nextPositionID = 1;
100:         shadowV3Pool = _shadowV3PoolAddress;
101: 
102:         liquidationDebtRatio = 8600;
103:         liquidationFee = 500; // <= FOUND
104:         performanceFee = 3000; // <= FOUND
105:         minPositionSize = 100e8;
106: 
107:         positionImplementation = _initialPositionImplementation;
108:     }

[NonCritical-46] Non constant/immutable state variables are missing a setter post deployment

Resolution

Non-constant or non-immutable state variables lacking a setter function can create inflexibility in contract operations. If there's no way to update these variables post-deployment, the contract might not adapt to changing conditions or requirements, which can be a significant drawback, especially in upgradable or long-lived contracts. To resolve this, implement setter functions guarded by appropriate access controls, like onlyOwner or similar modifiers, so that these variables can be updated as required while maintaining security. This enables smoother contract maintenance and feature upgrades.

Num of instances: 1

Findings

Click to show findings

['32']

32:  uint256 internal _unlocked = 1;

[NonCritical-47] Contracts use both += 1 and ++ (-- and -= 1)

Resolution

For consistency consider only using one of these incrementing methods.

Num of instances: 1

Findings

Click to show findings

['24']

24: contract LendingPool is ILendingPool, OwnableUpgradeable, PaymentsUpgradeable, ReentrancyGuardUpgradeable {
25:     using SafeMath for uint256;
26:     using SafeERC20 for IERC20;
71:         nextReserveId += 1; // <= FOUND

[NonCritical-48] Long numbers should include underscores to improve readability and prevent typos

Resolution

A large number such as 2000000 is far more readable as 2_000_000, this will help prevent unintended bugs in the code

Num of instances: 10

Findings

Click to show findings

['381']

381:         
382:         
383:         setBorrowingRateConfig(reserveData, 8000, 2000, 9000, 5000, 15000); // <= FOUND

['212']

212:         reduceLiquidity = liquidity * reducePercentage / 10000; // <= FOUND

['282']

282:             token0Fees = token0Reduced * vars.liquidationFee / 10000; // <= FOUND

['285']

285:             uint256 token0CallerFees = token0Fees * vars.liquidationCallerFee / 10000; // <= FOUND

['294']

294:             token1Fees = token1Reduced * vars.liquidationFee / 10000; // <= FOUND

['298']

298:             uint256 token1CallerFees = token1Fees * vars.liquidationCallerFee / 10000; // <= FOUND

['297']

297:         require(params.reducePercentage > 0 && params.reducePercentage < 10000, "Invalid reduce percentage"); // <= FOUND

['384']

384:         uint256 feeAmount = amount * performanceFee / 10000; // <= FOUND

['457']

457:         return borrowValue * 10000 / (principalValue + borrowValue); // <= FOUND

['461']

461:         return getDebtValue(positionId) * 10000 / getPositionValue(positionId); // <= FOUND

[NonCritical-49] Inconsistent checks of address params against address(0)

Resolution

Only some address parameters are checked against address(0), to ensure consistency ensure all address parameters are checked.

Num of instances: 2

Findings

Click to show findings

['123']

123:     function depositAndStake(uint256 reserveId, uint256 amount, address onBehalfOf, uint16 referralCode) // <= FOUND
124:         external
125:         payable
126:         notPaused
127:         nonReentrant
128:         returns (uint256 eTokenAmount)
129:     {
130:         eTokenAmount = _deposit(reserveId, amount, address(this));
131: 
132:         address stakingPool = reserves[reserveId].stakingAddress;
133:         require(stakingPool != address(0), "Address=0");
134:         IERC20(getETokenAddress(reserveId)).approve(stakingPool, eTokenAmount);
135:         IStakingRewards(stakingPool).stake(eTokenAmount, onBehalfOf);
136: 
137:         
138:         if (msg.value > 0) {
139:             refundETH();
140:         }
141: 
142:         
143:         emit Deposited(reserveId, _msgSender(), onBehalfOf, amount, eTokenAmount, referralCode);
144:     }

['185']

185:     function unStakeAndWithdraw(uint256 reserveId, uint256 eTokenAmount, address to, bool receiveNativeETH) // <= FOUND
186:         external
187:         payable
188:         notPaused
189:         nonReentrant
190:         avoidUsingNativeEther
191:         returns (uint256)
192:     {
193:         address stakingPool = reserves[reserveId].stakingAddress;
194:         require(stakingPool != address(0), "Address=0");
195: 
196:         IStakingRewards(stakingPool).withdrawByLendingPool(eTokenAmount, _msgSender(), address(this));
197: 
198:         uint256 underlyingTokenAmount = _redeem(reserveId, eTokenAmount, to, receiveNativeETH);
199: 
200:         emit Redeemed(reserveId, _msgSender(), to, eTokenAmount, underlyingTokenAmount);
201: 
202:         return (underlyingTokenAmount);
203:     }

[NonCritical-50] Avoid declaring variables with the names of defined functions within the project

Resolution

Having such variables can create confusion in both developers and in users of the project. Consider renaming these variables to improve code clarity.

Num of instances: 1

Findings

Click to show findings

['251']

251:         address liquidationFeeRecipient; // <= FOUND

[NonCritical-51] Constructors should emit an event

Resolution

Emitting an event in a constructor of a smart contract provides transparency and traceability in blockchain applications. This event logs the contract’s creation, aiding in monitoring and verifying contract deployment. Although constructors are executed only once, the emitted event ensures the contract's initialization is recorded on the blockchain.

Num of instances: 7

Findings

Click to show findings

['10']

10:     constructor(address _weth9) { // <= FOUND
11:         setAddress(AddressId.ADDRESS_ID_WETH9, _weth9);
12:     }

['12']

12:     constructor(uint256 _positionId) { // <= FOUND
13:         assert(_ADMIN_SLOT == bytes32(uint256(keccak256("eip1967.proxy.admin")) - 1));
14:         _setOwner(msg.sender);
15: 
16:         address impl = IPositionImplGetter(_owner()).positionImplementation();
17:         (bool success,) = impl.delegatecall(abi.encodeWithSignature("initialize(uint256)", _positionId));
18:         require(success);
19:     }

['32']

32:     constructor(string memory name_, string memory symbol_, uint8 decimals_, address underlyingAsset_) // <= FOUND
33:         ERC20(name_, symbol_)
34:     {
35:         _decimals = decimals_;
36: 
37:         require(underlyingAsset_ != address(0), Errors.VL_ADDRESS_CANNOT_ZERO);
38:         underlyingAsset = underlyingAsset_;
39:         lendingPool = msg.sender;
40:     }

['18']

18:     constructor(address _WETH9) { // <= FOUND
19:         WETH9 = _WETH9;
20:     }

['62']

62:     constructor(address _stakingToken) { // <= FOUND
63:         stakedToken = IERC20(_stakingToken);
64:         lendingPool = msg.sender;
65:     }

['19']

19:     constructor(address _addressRegistry) { // <= FOUND
20:         require(_addressRegistry != address(0), Errors.VL_ADDRESS_CANNOT_ZERO);
21:         addressRegistry = _addressRegistry;
22:         nextVaultID = 1;
23:     }

['34']

34:     constructor(address _addressProvider) { // <= FOUND
35:         addressProvider = _addressProvider;
36:     }

[NonCritical-52] Contract and Abstract files should have a fixed compiler version

Resolution

Using a fixed compiler version in Solidity contract and abstract files ensures consistency and predictability in smart contract behavior. A fixed version prevents unforeseen issues arising from compiler updates, which might introduce breaking changes or new bugs. It guarantees that the contract's behavior remains stable and consistent with the version used during its development and testing.

Num of instances: 17

Findings

Click to show findings

[]

7: contract AddressRegistry is IAddressRegistry, Ownable 

[]

9: contract IndividualPosition 

[]

19: contract ExtraInterestBearingToken is IExtraInterestBearingToken, ReentrancyGuard, ERC20 

[]

24: contract LendingPool is ILendingPool, OwnableUpgradeable, PaymentsUpgradeable, ReentrancyGuardUpgradeable 

[]

13: contract StakingRewards is Ownable, IStakingRewards 

[]

7: contract MightyTimelockController is TimelockController 

[]

10: contract PrimaryPriceOracle is IPriceOracle, Initializable 

[]

10: contract ShadowPositionValueCalculator 

[]

26: contract ShadowRangePositionImpl is IShadowRangePositionImpl, PaymentsUpgradeable 

[]

30: contract ShadowRangeVault is
31:     Initializable,
32:     ReentrancyGuardUpgradeable,
33:     Ownable2StepUpgradeable,
34:     PaymentsUpgradeable,
35:     IVault
36: 

[]

10: contract SwapXPositionValueCalculator 

[]

11: contract VaultRegistry is Ownable, IVaultFactory 

[]

31: contract VaultPositionViewer 

[]

10: abstract contract Payments 

[]

10: abstract contract PaymentsUpgradeable 

['3']

3: pragma solidity ^0.8.0; // <= FOUND

['3']

3: pragma solidity ^0.8.18; // <= FOUND

[NonCritical-53] Function call in event emit

Resolution

Emits are designed to make users aware of state variable changes. As such the event declaration should be clear on what it will output, by passing in function calls this can affect the readability of a emit declaration. As such it is advisable to make function calls outside of the event emit and pass the return value into the emit instead.

Num of instances: 1

Findings

Click to show findings

['49']

49:     function changeOwner(address newOwner) external onlyOwner { // <= FOUND
50:         require(newOwner != address(0), "Proxy: new Owner is the zero address");
51:         emit OwnerChanged(_owner(), newOwner); // <= FOUND
52:         _setOwner(newOwner);
53:     }

[NonCritical-54] For loop iterates on arrays not indexed

Resolution

Ensuring matching input array lengths in functions is crucial to prevent logical errors and inconsistencies in smart contracts. Mismatched array lengths can lead to incomplete operations or incorrect data handling, particularly if one array's length is assumed to reflect another's. For instance, iterating over a shorter array than intended could result in unprocessed elements from a longer array, potentially causing unintended consequences in contract execution. Validating that all input arrays have intended lengths before performing operations helps safeguard against such errors, ensuring that all elements are appropriately processed and the contract behaves as expected.

Num of instances: 4

Findings

Click to show findings

['471']

471:        for (uint256 i = 0; i < reserveIdArr.length; i++) { // <= FOUND
472:             statusArr[i].reserveId = reserveIdArr[i];
473:             statusArr[i].user = user; // <= FOUND
474:             statusArr[i].eTokenStaked = IStakingRewards(reserves[reserveIdArr[i]].stakingAddress).balanceOf(user);
475:             statusArr[i].eTokenUnStaked = IERC20(reserves[reserveIdArr[i]].eTokenAddress).balanceOf(user);
476:             statusArr[i].liquidity = statusArr[i].eTokenStaked.add(statusArr[i].eTokenUnStaked).mul(
477:                 reserves[reserveIdArr[i]].eTokenToReserveExchangeRate()
478:             ).div(Precision.FACTOR1E18);
479:         }

['471']

471:         for (uint256 i = 0; i < reserveIdArr.length; i++) { // <= FOUND
472:             statusArr[i].reserveId = reserveIdArr[i];
473:             statusArr[i].user = user; // <= FOUND
474:             statusArr[i].eTokenStaked = IStakingRewards(reserves[reserveIdArr[i]].stakingAddress).balanceOf(user);
475:             statusArr[i].eTokenUnStaked = IERC20(reserves[reserveIdArr[i]].eTokenAddress).balanceOf(user);
476:             statusArr[i].liquidity = statusArr[i].eTokenStaked.add(statusArr[i].eTokenUnStaked).mul(
477:                 reserves[reserveIdArr[i]].eTokenToReserveExchangeRate()
478:             ).div(Precision.FACTOR1E18);
479:         }

['71']

71:        for (uint256 i = 0; i < ids.length; i++) { // <= FOUND
72:             positions[i].vault = vault; // <= FOUND
73:             positions[i].id = ids[i];
74:             positions[i].token0 = IVault(vault).token0();
75:             positions[i].token1 = IVault(vault).token1();
76:             (positions[i].amount0, positions[i].amount1) = IVault(vault).getPositionAmounts(ids[i]);
77:             if (positions[i].amount0 > 0 || positions[i].amount1 > 0) {
78:                 (positions[i].amount0Debt, positions[i].amount1Debt) = IVault(vault).getPositionDebt(ids[i]);
79:                 positions[i].debtRatio = IVault(vault).getDebtRatio(ids[i]);
80:             }
81:             positions[i].price0 = IVault(vault).getTokenPrice(positions[i].token0);
82:             positions[i].price1 = IVault(vault).getTokenPrice(positions[i].token1);
83: 
84:             IVault.PositionInfo memory positionInfo = IVault(vault).getPositionInfos(ids[i]);
85: 
86:             positions[i].positionAddress = positionInfo.positionAddress;
87:             positions[i].positionNftId = positionInfo.shadowPositionId;
88:             positions[i].tickUpper = positionInfo.tickUpper;
89:             positions[i].tickLower = positionInfo.tickLower;
90:             positions[i].ul = positionInfo.ul;
91:             positions[i].ll = positionInfo.ll;
92: 
93:             positions[i].inRange = positions[i].amount0 > 0 && positions[i].amount1 > 0;
94: 
95:             (positions[i].rewards, positions[i].rewardsAmounts) =
96:                 IPositionImpl(positionInfo.positionAddress).claimableRewards();
97: 
98:             positions[i].currentTick = IVault(vault).getCurrentTick();
99: 
100:             (,,,,, positions[i].liquidity,,,,) = INFTPositionManager(
101:                 IAddressRegistry(addressProvider).getAddress(AddressId.ADDRESS_ID_SHADOW_NONFUNGIBLE_POSITION_MANAGER)
102:             ).positions(positionInfo.shadowPositionId);
103:         }

['71']

71:         for (uint256 i = 0; i < ids.length; i++) { // <= FOUND
72:             positions[i].vault = vault; // <= FOUND
73:             positions[i].id = ids[i];
74:             positions[i].token0 = IVault(vault).token0();
75:             positions[i].token1 = IVault(vault).token1();
76:             (positions[i].amount0, positions[i].amount1) = IVault(vault).getPositionAmounts(ids[i]);
77:             if (positions[i].amount0 > 0 || positions[i].amount1 > 0) {
78:                 (positions[i].amount0Debt, positions[i].amount1Debt) = IVault(vault).getPositionDebt(ids[i]);
79:                 positions[i].debtRatio = IVault(vault).getDebtRatio(ids[i]);
80:             }
81:             positions[i].price0 = IVault(vault).getTokenPrice(positions[i].token0);
82:             positions[i].price1 = IVault(vault).getTokenPrice(positions[i].token1);
83: 
84:             IVault.PositionInfo memory positionInfo = IVault(vault).getPositionInfos(ids[i]);
85: 
86:             positions[i].positionAddress = positionInfo.positionAddress;
87:             positions[i].positionNftId = positionInfo.shadowPositionId;
88:             positions[i].tickUpper = positionInfo.tickUpper;
89:             positions[i].tickLower = positionInfo.tickLower;
90:             positions[i].ul = positionInfo.ul;
91:             positions[i].ll = positionInfo.ll;
92: 
93:             positions[i].inRange = positions[i].amount0 > 0 && positions[i].amount1 > 0;
94: 
95:             (positions[i].rewards, positions[i].rewardsAmounts) =
96:                 IPositionImpl(positionInfo.positionAddress).claimableRewards();
97: 
98:             positions[i].currentTick = IVault(vault).getCurrentTick();
99: 
100:             (,,,,, positions[i].liquidity,,,,) = INFTPositionManager(
101:                 IAddressRegistry(addressProvider).getAddress(AddressId.ADDRESS_ID_SHADOW_NONFUNGIBLE_POSITION_MANAGER)
102:             ).positions(positionInfo.shadowPositionId);
103:         }

[NonCritical-55] Constructor with array/string/bytes parameters has no empty array checks

Resolution

Failing to validate for empty array inputs in constructors can lead to vulnerabilities or logical errors in smart contracts. Constructors often initialize key contract settings, and arrays may represent crucial data like access controls, initial states, or configuration parameters. Without empty array checks, a contract might be deployed in an unintended state, lacking necessary data for its operations or security measures.

Num of instances: 2

Findings

Click to show findings

['8']

8:     constructor(uint256 minDelay, address[] memory proposers, address[] memory executors, address admin)
9:         TimelockController(minDelay, proposers, executors, admin)
10:     {
11:         
12:     }

['32']

32:     constructor(string memory name_, string memory symbol_, uint8 decimals_, address underlyingAsset_)
33:         ERC20(name_, symbol_)
34:     {
35:         _decimals = decimals_;
36: 
37:         require(underlyingAsset_ != address(0), Errors.VL_ADDRESS_CANNOT_ZERO);
38:         underlyingAsset = underlyingAsset_;
39:         lendingPool = msg.sender;
40:     }

[NonCritical-56] Avoid external calls in modifiers

Resolution

Including external calls in modifiers is generally discouraged in Solidity programming because modifiers are primarily intended for checks and assertions. External calls increase complexity and can obscure the flow of transactions, making it harder for reviewers to identify potentially risky interactions. To maintain clarity and security, it's advisable to encapsulate external calls within internal functions. These functions can then be invoked within the main function body, rather than in modifiers, ensuring that external interactions are explicit and easier to audit, thus enhancing contract safety and readability.

Num of instances: 2

Findings

Click to show findings

['23']

23:     modifier onlyOwner() { // <= FOUND
24:         require(msg.sender == _owner()); // <= FOUND
25:         _;
26:     }

['46']

46:     modifier updateReward(address user) { // <= FOUND
47:         for (uint256 i; i < rewardTokens.length; i++) {
48:             address rewardToken = rewardTokens[i];
49:             rewardData[rewardToken].rewardPerTokenStored = rewardPerToken(rewardToken); // <= FOUND
50:             rewardData[rewardToken].lastUpdateTime = Math.min(rewardData[rewardToken].endTime, block.timestamp);
51: 
52:             if (user != address(0)) {
53:                 userRewardsClaimable[user][rewardToken] =
54:                     earned(user, rewardToken, rewardData[rewardToken].rewardPerTokenStored);
55:                 userRewardPerTokenPaid[user][rewardToken] = rewardData[rewardToken].rewardPerTokenStored;
56:             }
57:         }
58:         _;
59:     }

[NonCritical-57] Avoid using 'owner' or '_owner' as a parameter name

Resolution

Using 'owner' or '_owner' as a parameter name in functions, especially in contracts inheriting from or interacting with OpenZeppelin's Ownable contract, can lead to confusion and potential bugs. These contracts often have a state variable named owner, managed by the Ownable pattern for access control. Using the same name for function parameters may obscure the contract's owner state variable, complicating code readability and maintenance. Prefer alternative descriptive names for parameters to maintain clarity and prevent conflicts with established ownership patterns.

Num of instances: 4

Findings

Click to show findings

['468']

468:     function getPositionIds(address owner) public view returns (uint256[] memory)  // <= FOUND

['38']

38:     function getPositionIds(address vault, address owner) public view returns (uint256[] memory)  // <= FOUND

['68']

68:     function getUserPosition(address vault, address owner) public view returns (PositionInfo[] memory)  // <= FOUND

['107']

107:     function getUserPositions(address[] memory vaults, address owner) public view returns (PositionInfo[][] memory)  // <= FOUND

[NonCritical-58] Unnecessary struct attribute prefix

Resolution

In struct definitions, using redundant prefixes for attributes is unnecessary. For instance, in a struct named Employee, attributes like employeeName, employeeID, and employeeEmail can be simplified to name, ID, and email respectively, since they are already inherently associated with Employee. By removing these repetitive prefixes, the code becomes more concise and easier to read, maintaining its contextual clarity.

Num of instances: 2

Findings

Click to show findings

['248']

248:     struct LiquidationFeeVars { // <= FOUND
249:         uint256 liquidationFee; // <= FOUND
250:         uint256 liquidationCallerFee;
251:         address liquidationFeeRecipient;
252:     }

['42']

42:     struct PositionInfo { // <= FOUND
43:         address vault;
44:         uint256 id;
45:         address token0;
46:         address token1;
47:         uint256 amount0;
48:         uint256 amount1;
49:         uint256 amount0Debt;
50:         uint256 amount1Debt;
51:         uint256 debtRatio;
52:         uint256 price0;
53:         uint256 price1;
54:         
55:         address positionAddress; // <= FOUND
56:         uint256 positionNftId;
57:         uint128 liquidity;
58:         int24 tickUpper;
59:         int24 tickLower;
60:         int24 currentTick;
61:         int24 ul;
62:         int24 ll;
63:         bool inRange;
64:         address[] rewards;
65:         uint256[] rewardsAmounts;
66:     }

[NonCritical-59] Redundant function returns constant

Resolution

Functions which only return a predefined constant have a similar redundancy as empty code blocks. If these blocks are not used to satisfy interface enforcements, or similar rulings, they should be removed.

Num of instances: 2

Findings

Click to show findings

['71']

71:     function getX33Adapter() public view returns (address) {
72:         return 0x9710E10A8f6FbA8C391606fee18614885684548d; // <= FOUND
73:     }

['67']

67:     function version() external pure returns (uint256) {
68:         return 1; // <= FOUND
69:     }

[NonCritical-60] ERC777 tokens can introduce reentrancy risks

Resolution

ERC777 is an advanced token standard that introduces hooks, allowing operators to execute additional logic during transfers. While this feature offers greater flexibility, it also opens up the possibility of reentrancy attacks. Specifically, when tokens are sent, the receiving contract's tokensReceived hook gets called, and this external call can execute arbitrary code. An attacker can exploit this feature to re-enter the original function, potentially leading to double-spending or other types of financial manipulation.

To mitigate reentrancy risks with ERC777, it's crucial to adopt established security measures, such as utilizing reentrancy guards or following the check-effects-interactions pattern. Some developers opt to stick with the simpler ERC20 standard, which does not have these hooks, to minimize this risk. If you do choose to use ERC777, extreme caution and thorough auditing are advised to secure against potential reentrancy vulnerabilities.

Num of instances: 1

Findings

Click to show findings

['46']

46:     function pay(address token, address payer, address recipient, uint256 value) internal { // <= FOUND
47:         if (token == WETH9 && address(this).balance >= value) {
48:             
49:             IWETH9(WETH9).deposit{value: value}(); 
50:             require(IWETH9(WETH9).transfer(recipient, value), "transfer failed");
51:         } else if (payer == address(this)) {
52:             
53:             TransferHelper.safeTransfer(token, recipient, value);
54:         } else {
55:             
56:             TransferHelper.safeTransferFrom(token, payer, recipient, value); // <= FOUND
57:         }
58:     }

[NonCritical-61] Avoid assembly in view or pure functions

Resolution

Using assembly in view or pure functions is risky because it bypasses Solidity's safeguards, potentially allowing unintended state reads or writes when interacting with storage. view and pure functions are meant to ensure no state changes occur, but improper use of assembly can violate this, leading to logical errors or undefined behavior.

For instance, directly accessing storage slots in assembly within a view function could inadvertently modify state due to mismanagement of pointers or memory allocation. This undermines the trust and guarantees associated with view and pure functions. To ensure integrity, use high-level Solidity constructs for state access, as they enforce read/write restrictions and maintain consistency.

Num of instances: 2

Findings

Click to show findings

[]

28:     function Owner() external view returns (address) {
29:         return _owner();
30:     }

['32']

32:     function _owner() internal view returns (address adm) {
33:         bytes32 slot = _ADMIN_SLOT;
34:         
35:         assembly { // <= FOUND
36:             adm := sload(slot)
37:         }
38:     }

[Gas-1] The usage of SafeMath is useless in solidity versions 0.8.0 and above hence wasting gas

Resolution

Remove the library and refactor where it's used to regular arithmetic

Num of instances: 2

Findings

Click to show findings

['24']

24: contract LendingPool is ILendingPool, OwnableUpgradeable, PaymentsUpgradeable, ReentrancyGuardUpgradeable {
25:     using SafeMath for uint256; // <= FOUND
26:     using SafeERC20 for IERC20;
27:     using ReserveLogic for DataTypes.ReserveData;
28: 
30:     mapping(uint256 => DataTypes.ReserveData) public reserves;
31:     uint256 public nextReserveId;
32: 
38:     mapping(uint256 => mapping(address => uint256)) public credits;
39: 
42:     mapping(address => bool) public borrowingWhiteList;
43: 
44:     address public addressRegistry;
45: 
47:     mapping(uint256 => DataTypes.DebtPositionData) public debtPositions;
48:     uint256 public nextDebtPositionId;
49: 
50:     bool public paused = false;
51: 
196: }

['13']

13: contract StakingRewards is Ownable, IStakingRewards {
14:     using SafeMath for uint256; // <= FOUND
15:     using SafeERC20 for IERC20;
16: 
17:     IERC20 public immutable stakedToken;
18:     address public immutable lendingPool;
19: 
20:     address[] public rewardTokens;
21: 
22:     mapping(address => bool) public inRewardsTokenList;
23: 
24:     uint256 public totalStaked;
25: 
26:     mapping(address => Reward) public rewardData;
27:     mapping(address => uint256) public balanceOf;
28: 
29:     mapping(address => mapping(address => uint256)) public userRewardPerTokenPaid;
30:     mapping(address => mapping(address => uint256)) public userRewardsClaimable;
31: 
32:     uint256 internal _unlocked = 1;
33: 
85: }

[Gas-2] Mappings are cheaper than arrays for state variable iteration

Resolution

Where possible use mappings in place of arrays as they are far cheaper during iterations and are easier to use. There are instances such as struct arrays where arrays make more sense.

Num of instances: 4

Findings

Click to show findings

['47']

47:        for (uint256 i; i < rewardTokens.length; i++) {
48:             address rewardToken = rewardTokens[i]; // <= FOUND
49:             rewardData[rewardToken].rewardPerTokenStored = rewardPerToken(rewardToken);
50:             rewardData[rewardToken].lastUpdateTime = Math.min(rewardData[rewardToken].endTime, block.timestamp);
51: 
52:             if (user != address(0)) {
53:                 userRewardsClaimable[user][rewardToken] =
54:                     earned(user, rewardToken, rewardData[rewardToken].rewardPerTokenStored);
55:                 userRewardPerTokenPaid[user][rewardToken] = rewardData[rewardToken].rewardPerTokenStored;
56:             }
57:         }

['196']

196:        for (uint256 i = 0; i < rewardTokens.length; i++) {
197:             address rewardToken = rewardTokens[i]; // <= FOUND
198:             uint256 claimable = userRewardsClaimable[msg.sender][rewardToken];
199:             if (claimable > 0) {
200:                 userRewardsClaimable[msg.sender][rewardToken] = 0;
201:                 require(IERC20(rewardToken).transfer(msg.sender, claimable), "transfer failed");
202:                 emit RewardPaid(msg.sender, rewardToken, claimable);
203:             }
204:         }

['47']

47:         for (uint256 i; i < rewardTokens.length; i++) {
48:             address rewardToken = rewardTokens[i]; // <= FOUND
49:             rewardData[rewardToken].rewardPerTokenStored = rewardPerToken(rewardToken);
50:             rewardData[rewardToken].lastUpdateTime = Math.min(rewardData[rewardToken].endTime, block.timestamp);
51: 
52:             if (user != address(0)) {
53:                 userRewardsClaimable[user][rewardToken] =
54:                     earned(user, rewardToken, rewardData[rewardToken].rewardPerTokenStored);
55:                 userRewardPerTokenPaid[user][rewardToken] = rewardData[rewardToken].rewardPerTokenStored;
56:             }
57:         }

['196']

196:         for (uint256 i = 0; i < rewardTokens.length; i++) {
197:             address rewardToken = rewardTokens[i]; // <= FOUND
198:             uint256 claimable = userRewardsClaimable[msg.sender][rewardToken];
199:             if (claimable > 0) {
200:                 userRewardsClaimable[msg.sender][rewardToken] = 0;
201:                 require(IERC20(rewardToken).transfer(msg.sender, claimable), "transfer failed");
202:                 emit RewardPaid(msg.sender, rewardToken, claimable);
203:             }
204:         }

[Gas-3] State variables used within a function more than once should be cached to save gas

Resolution

Cache such variables and perform operations on them, if operations include modifications to the state variable(s) then remember to equate the state variable to it's cached counterpart at the end

Num of instances: 2

Findings

Click to show findings

['407']

407:     function getPositionAmountsByNftId(uint256 nftId) public view returns (uint256 amount0, uint256 amount1) { // <= FOUND 'function getPositionAmountsByNftId'
408:         return IPositionValueCalculator(
409:             IAddressRegistry(addressProvider).getAddress(AddressId.ADDRESS_ID_SHADOW_POSITION_VALUE_CALCULATOR) // <= FOUND 'addressProvider'
410:         ).principal(
411:             IShadowNonfungiblePositionManager(
412:                 IAddressRegistry(addressProvider).getAddress(AddressId.ADDRESS_ID_SHADOW_NONFUNGIBLE_POSITION_MANAGER) // <= FOUND 'addressProvider'
413:             ),
414:             nftId,
415:             shadowV3Pool
416:         );
417:     }

['152']

152:     function openPosition(OpenPositionParams calldata params) // <= FOUND 'function openPosition'
153:         external
154:         payable
155:         nonReentrant
156:         checkDeadline(params.deadline)
157:     {
158:         (uint256 _positionId, address _positionAddress) = _createNewPosition();
159: 
160:         PositionInfo storage positionInfo = positionInfos[_positionId];
161:         positionInfo.owner = msg.sender;
162:         positionInfo.vaultId = vaultId;
163:         positionInfo.positionAddress = _positionAddress;
164:         positionInfo.positionId = _positionId;
165:         positionIds[msg.sender].push(_positionId);
166:         positionAddressToId[_positionAddress] = _positionId;
167: 
168:         
169:         if (params.amount0Principal > 0) {
170:             pay(token0, msg.sender, address(this), params.amount0Principal);
171:         }
172:         if (params.amount1Principal > 0) {
173:             pay(token1, msg.sender, address(this), params.amount1Principal);
174:         }
175: 
176:         
177:         address lendingPool = getLendingPool();
178:         if (params.amount0Borrow > 0) {
179:             positionInfo.token0DebtId = ILendingPool(lendingPool).newDebtPosition(token0ReserveId);
180: 
181:             ILendingPool(lendingPool).borrow(address(this), positionInfo.token0DebtId, params.amount0Borrow);
182:         }
183:         if (params.amount1Borrow > 0) {
184:             positionInfo.token1DebtId = ILendingPool(lendingPool).newDebtPosition(token1ReserveId);
185: 
186:             ILendingPool(lendingPool).borrow(address(this), positionInfo.token1DebtId, params.amount1Borrow);
187:         }
188: 
189:         require(
190:             getDebtRatioFromAmounts(
191:                 params.amount0Principal, params.amount1Principal, params.amount0Borrow, params.amount1Borrow
192:             ) < liquidationDebtRatio, // <= FOUND 'liquidationDebtRatio'
193:             "Borrow value is too high"
194:         );
195: 
196:         if (params.amount0Principal > 0 || params.amount0Borrow > 0) {
197:             pay(token0, address(this), positionInfo.positionAddress, params.amount0Principal + params.amount0Borrow);
198:         }
199:         if (params.amount1Principal > 0 || params.amount1Borrow > 0) {
200:             pay(token1, address(this), positionInfo.positionAddress, params.amount1Principal + params.amount1Borrow);
201:         }
202:         
203:         (uint256 tokenId, uint128 liquidity, uint256 amount0, uint256 amount1) = IShadowRangePositionImpl(
204:             positionInfo.positionAddress
205:         ).openPosition(
206:             IShadowRangePositionImpl.OpenPositionParams({
207:                 amount0Desired: params.amount0Desired,
208:                 amount1Desired: params.amount1Desired,
209:                 tickLower: params.tickLower,
210:                 tickUpper: params.tickUpper,
211:                 positionOwner: msg.sender
212:             })
213:         );
214: 
215:         require(getPositionValueByNftId(tokenId) > minPositionSize, "PVL");
216: 
217:         positionInfo.shadowPositionId = tokenId;
218:         positionInfo.tickLower = params.tickLower;
219:         positionInfo.tickUpper = params.tickUpper;
220:         positionInfo.ul = params.ul;
221:         positionInfo.ll = params.ll;
222: 
223:         emit PositionOpened(
224:             msg.sender,
225:             vaultId,
226:             _positionId,
227:             _positionAddress,
228:             params.amount0Principal,
229:             params.amount1Principal,
230:             amount0,
231:             amount1,
232:             liquidity,
233:             block.timestamp
234:         );
235: 
236:         if (msg.value > 0) {
237:             refundETH();
238:         }
239:         require(getDebtRatio(_positionId) < liquidationDebtRatio, "DRH"); // <= FOUND 'liquidationDebtRatio'
240:     }

[Gas-4] Avoid updating storage when the value hasn't changed

Num of instances: 2

Findings

Click to show findings

['362']

362:     function initReserve( // <= FOUND
363:         DataTypes.ReserveData storage reserveData,
364:         address underlyingTokenAddress,
365:         address eTokenAddress,
366:         uint256 reserveCapacity,
367:         uint256 id
368:     ) internal {
369:         reserveData.underlyingTokenAddress = underlyingTokenAddress; // <= FOUND
370:         reserveData.eTokenAddress = eTokenAddress;
371:         reserveData.reserveCapacity = reserveCapacity;
372:         reserveData.id = id;
373: 
374:         reserveData.lastUpdateTimestamp = uint128(block.timestamp);
375:         reserveData.borrowingIndex = Precision.FACTOR1E18;
376: 
377:         reserveData.reserveFeeRate = 1500; 
378: 
379:         
380:         
381:         setBorrowingRateConfig(reserveData, 8000, 2000, 9000, 5000, 15000);
382:     }

['393']

393:     function updateDebtPosition(DataTypes.DebtPositionData storage debtPosition, uint256 latestBorrowingIndex) // <= FOUND
394:         internal
395:     {
396:         debtPosition.borrowed = debtPosition.borrowed.mul(latestBorrowingIndex).div(debtPosition.borrowedIndex); // <= FOUND
397: 
398:         debtPosition.borrowedIndex = latestBorrowingIndex;
399:     }

[Gas-5] Using constants directly, rather than caching the value, saves gas

Resolution

In smart contract development, especially within Ethereum, gas optimization is crucial. Utilizing constants directly, instead of assigning them to a variable (caching) before use, can save gas as reading from a constant is cheaper than reading from storage or a variable. Constants in Solidity are replaced by their actual value in the EVM bytecode, eliminating the need for a SLOAD operation, which costs more gas. Hence, if a value will not change throughout the contract's life, defining it as a constant and using it directly in expressions/functions can be a gas-efficient practice.

Num of instances: 1

Findings

Click to show findings

['33']

33:         bytes32 slot = _ADMIN_SLOT; // <= FOUND

[Gas-6] Shortcircuit rules can be be used to optimize some gas usage

Resolution

Reading state variables takes alot of gas to do, so if you have a conditional statement regarding a state varaible alongide others which are not related to state vars, it is advisable to have conditions which do not involve state vars be declared first in the overall conditional so unnecessary state variable lookups can be avoided.

Num of instances: 1

Findings

Click to show findings

['242']

242:         if (reserve.underlyingTokenAddress == WETH9 && receiveNativeETH) {

[Gas-7] x + y is more efficient than using += for state variables (likewise for -=)

Resolution

In instances found where either += or -= are used against state variables use x = x + y instead

Num of instances: 4

Findings

Click to show findings

['69']

69:     function initReserve(address asset) external onlyOwner notPaused {
70:         uint256 id = nextReserveId;
71:         nextReserveId += 1; // <= FOUND
72: 
73:         
74:         string memory name = string(abi.encodePacked(ERC20(asset).name(), "(MightyFi Interest Bearing Token)"));
75:         string memory symbol = string(abi.encodePacked("m", ERC20(asset).symbol()));
76:         uint8 decimals = ERC20(asset).decimals();
77: 
78:         address eTokenAddress = ETokenDeployer.deploy(name, symbol, decimals, asset, id);
79: 
80:         DataTypes.ReserveData storage reserveData = reserves[id];
81:         reserveData.setActive(true);
82:         reserveData.setBorrowingEnabled(true);
83: 
84:         initReserve(reserveData, asset, eTokenAddress, type(uint256).max, id);
85: 
86:         createStakingPoolForReserve(id);
87: 
88:         emit InitReserve(asset, eTokenAddress, reserveData.stakingAddress, id);
89:     }

['141']

141:     function stake(uint256 amount, address onBehalfOf) external nonReentrant updateReward(onBehalfOf) {
142:         require(amount > 0, "amount = 0");
143: 
144:         stakedToken.safeTransferFrom(msg.sender, address(this), amount);
145: 
146:         balanceOf[onBehalfOf] += amount;
147:         totalStaked += amount; // <= FOUND
148: 
149:         emit Staked(msg.sender, onBehalfOf, amount);
150:     }

['159']

159:     function withdraw(uint256 amount, address to) external nonReentrant updateReward(msg.sender) {
160:         require(amount > 0, "amount = 0");
161: 
162:         balanceOf[msg.sender] -= amount;
163:         totalStaked -= amount; // <= FOUND
164: 
165:         require(stakedToken.transfer(to, amount), "transfer failed");
166: 
167:         emit Withdraw(msg.sender, to, amount);
168:     }

['179']

179:     function withdrawByLendingPool(uint256 amount, address user, address to)
180:         external
181:         onlyLendingPool
182:         nonReentrant
183:         updateReward(user)
184:     {
185:         require(amount > 0, "amount = 0");
186: 
187:         balanceOf[user] -= amount;
188:         totalStaked -= amount; // <= FOUND
189: 
190:         require(stakedToken.transfer(to, amount), "transfer falied");
191: 
192:         emit Withdraw(user, to, amount);
193:     }

[Gas-8] There is a 32 byte length threshold for error strings, strings longer than this consume more gas

Resolution

In require statements found which are longer than 32 characters, shorten these to 32 character or less

Num of instances: 3

Findings

Click to show findings

['49']

49:     function changeOwner(address newOwner) external onlyOwner {
50:         require(newOwner != address(0), "Proxy: new Owner is the zero address"); // <= FOUND
51:         emit OwnerChanged(_owner(), newOwner);
52:         _setOwner(newOwner);
53:     }

['18']

18:     function initialize(
19:         address _pyth,
20:         address _oracleManager,
21:         address[] memory initialTokens,
22:         bytes32[] memory priceIds
23:     ) external initializer {
24:         pyth = IPyth(_pyth);
25:         oracleManager = _oracleManager;
26:         require(initialTokens.length == priceIds.length, "Initial tokens and price ids length mismatch"); // <= FOUND
27:         for (uint256 i = 0; i < initialTokens.length; i++) {
28:             pythPriceIds[initialTokens[i]] = priceIds[i];
29:         }
30: 
31:         maxPriceAge = 1 hours;
32:     }

['39']

39:     function setOracleManager(address _oracleManager) external onlyOracleManager {
40:         require(_oracleManager != address(0), "Oracle manager cannot be zero address"); // <= FOUND
41:         oracleManager = _oracleManager;
42:     }

[Gas-9] Public functions not used internally can be marked as external to save gas

Resolution

Public functions that aren't used internally in Solidity contracts should be made external to optimize gas usage and improve contract efficiency. External functions can only be called from outside the contract, and their arguments are directly read from the calldata, which is more gas-efficient than loading them into memory, as is the case for public functions. By using external visibility, developers can reduce gas consumption for external calls and ensure that the contract operates more cost-effectively for users. Moreover, setting the appropriate visibility level for functions also enhances code readability and maintainability, promoting a more secure and well-structured contract design.

Num of instances: 19

Findings

Click to show findings

['490']

490:     function getReserveIdOfDebt(uint256 debtId) public view returns (uint256) 

['494']

494:     function getUnderlyingTokenAddress(uint256 reserveId) public view returns (address) 

['504']

504:     function getStakingAddress(uint256 reserveId) public view returns (address) 

['509']

509:     function exchangeRateOfReserve(uint256 reserveId) public view returns (uint256) 

['514']

514:     function utilizationRateOfReserve(uint256 reserveId) public view returns (uint256) 

['519']

519:     function borrowingRateOfReserve(uint256 reserveId) public view returns (uint256) 

['524']

524:     function totalLiquidityOfReserve(uint256 reserveId) public view returns (uint256 totalLiquidity) 

['529']

529:     function totalBorrowsOfReserve(uint256 reserveId) public view returns (uint256 totalBorrows) 

['565']

565:     function activateReserve(uint256 reserveId) public onlyOwner notPaused 

['572']

572:     function deActivateReserve(uint256 reserveId) public onlyOwner notPaused 

['578']

578:     function freezeReserve(uint256 reserveId) public onlyOwner notPaused 

['584']

584:     function unFreezeReserve(uint256 reserveId) public onlyOwner notPaused 

['590']

590:     function enableBorrowing(uint256 reserveId) public onlyOwner notPaused 

['596']

596:     function disableBorrowing(uint256 reserveId) public onlyOwner notPaused 

['602']

602:     function setReserveFeeRate(uint256 reserveId, uint16 _rate) public onlyOwner notPaused 

['626']

626:     function setReserveCapacity(uint256 reserveId, uint256 cap) public onlyOwner notPaused 

['96']

96:     function setReward(address rewardToken, uint256 startTime, uint256 endTime, uint256 totalRewards)
97:         public
98:         onlyOwner
99:         nonReentrant
100:         updateReward(address(0))
101:     

['79']

79:     function getSwapRouter() public view returns (address) 

['107']

107:     function getUserPositions(address[] memory vaults, address owner) public view returns (PositionInfo[][] memory) 

[Gas-10] Calldata should be used in place of memory function parameters when not mutated

Resolution

In Solidity, calldata should be used in place of memory for function parameters when the function is external. This is because calldata is a non-modifiable, non-persistent area where function arguments are stored, and it's cheaper in terms of gas than memory. It's especially efficient for arrays and complex data types. calldata provides a gas-efficient way to pass data in external function calls, whereas memory is a temporary space that's erased between external function calls. This distinction is crucial for optimizing smart contracts for gas usage and performance.

Num of instances: 1

Findings

Click to show findings

['18']

18:     function initialize( // <= FOUND
19:         address _pyth,
20:         address _oracleManager,
21:         address[] memory initialTokens,
22:         bytes32[] memory priceIds // <= FOUND
23:     ) external initializer {
24:         pyth = IPyth(_pyth);
25:         oracleManager = _oracleManager;
26:         require(initialTokens.length == priceIds.length, "Initial tokens and price ids length mismatch");
27:         for (uint256 i = 0; i < initialTokens.length; i++) {
28:             pythPriceIds[initialTokens[i]] = priceIds[i];
29:         }
30: 
31:         maxPriceAge = 1 hours;
32:     }

[Gas-11] Usage of smaller uint/int types causes overhead

Resolution

When using a smaller int/uint type it first needs to be converted to it's 256 bit counterpart to be operated, this increases the gas cost and thus should be avoided. However it does make sense to use smaller int/uint values within structs provided you pack the struct properly.

Num of instances: 38

Findings

Click to show findings

['25']

25:     uint8 private _decimals; // <= FOUND

['32']

32:     constructor(string memory name_, string memory symbol_, uint8 decimals_, address underlyingAsset_) // <= FOUND
33:         ERC20(name_, symbol_)
34:     {

['76']

76:         uint8 decimals = ERC20(asset).decimals(); // <= FOUND

['46']

46:     uint8 public token0Decimals;  // <= FOUND

['47']

47:     uint8 public token1Decimals;  // <= FOUND

['25']

25: uint8 private _decimals; // <= FOUND

['46']

46: uint8 public token0Decimals;  // <= FOUND

['47']

47: uint8 public token1Decimals;  // <= FOUND

['104']

104:     
117:     function deposit(uint256 reserveId, uint256 amount, address onBehalfOf, uint16 referralCode) // <= FOUND
118:         public
119:         payable
120:         notPaused
121:         nonReentrant
122:         returns (uint256 eTokenAmount)
123:     {

['123']

123:     
124:     function depositAndStake(uint256 reserveId, uint256 amount, address onBehalfOf, uint16 referralCode) // <= FOUND
125:         external
126:         payable
127:         notPaused
128:         nonReentrant
129:         returns (uint256 eTokenAmount)
130:     {

['401']

401:     function setBorrowingRateConfig(
402:         DataTypes.ReserveData storage reserve,
403:         uint16 utilizationA, // <= FOUND
404:         uint16 borrowingRateA, // <= FOUND
405:         uint16 utilizationB, // <= FOUND
406:         uint16 borrowingRateB, // <= FOUND
407:         uint16 maxBorrowingRate // <= FOUND
408:     ) internal {

['602']

602:     function setReserveFeeRate(uint256 reserveId, uint16 _rate) public onlyOwner notPaused { // <= FOUND

['610']

610:     function setBorrowingRateConfig(
611:         uint256 reserveId,
612:         uint16 utilizationA, // <= FOUND
613:         uint16 borrowingRateA, // <= FOUND
614:         uint16 utilizationB, // <= FOUND
615:         uint16 borrowingRateB, // <= FOUND
616:         uint16 maxBorrowingRate // <= FOUND
617:     ) public onlyOwner notPaused {

['26']

26:         (,,, int24 tickLower, int24 tickUpper, uint128 liquidity,,,,) = positionManager.positions(tokenId); // <= FOUND

['30']

30:     int24 public tickSpacing; // <= FOUND

['255']

255:         int24 currentTick = IPositionValueCalculator( // <= FOUND
256:             IAddressRegistry(addressProvider).getAddress(AddressId.ADDRESS_ID_SHADOW_POSITION_VALUE_CALCULATOR)
257:         ).getCurrentTick(shadowV3Pool);

['25']

25:         (,,,, int24 tickLower, int24 tickUpper, uint128 liquidity,,,,) = positionManager.positions(tokenId); // <= FOUND

['33']

33:         (, int24 tick,,,,) = ISwapXV3Pool(v3Pool).globalState(); // <= FOUND

['14']

14:     function positions(uint256 tokenId)
15:         external
16:         view
17:         returns (
18:             address token0,
19:             address token1,
20:             int24 tickSpacing, // <= FOUND
21:             int24 tickLower, // <= FOUND
22:             int24 tickUpper, // <= FOUND
23:             uint128 liquidity, // <= FOUND
24:             uint256 feeGrowthInside0LastX128,
25:             uint256 feeGrowthInside1LastX128,
26:             uint128 tokensOwed0, // <= FOUND
27:             uint128 tokensOwed1 // <= FOUND
28:         );

['58']

58:         int24 tickUpper; // <= FOUND

['59']

59:         int24 tickLower; // <= FOUND

['60']

60:         int24 currentTick; // <= FOUND

['61']

61:         int24 ul; // <= FOUND

['62']

62:         int24 ll; // <= FOUND

['30']

30: int24 public tickSpacing; // <= FOUND

['83']

83:     function openPosition(OpenPositionParams memory params)
84:         external
85:         onlyVault
86:         returns (uint256 tokenId, uint128 liquidity, uint256 amount0, uint256 amount1) // <= FOUND
87:     {

['145']

145:     function closePosition()
146:         external
147:         onlyVault
148:         returns (
149:             uint128 liquidity, // <= FOUND
150:             uint256 token0Reduced,
151:             uint256 token1Reduced,
152:             uint256 token0Balance,
153:             uint256 token1Balance
154:         )
155:     {

['202']

202:     function reducePosition(uint128 reducePercentage, uint256 amount0ToSwap, uint256 amount1ToSwap) // <= FOUND
203:         external
204:         onlyVault
205:         returns (uint128 reduceLiquidity, uint256 token0Reduced, uint256 token1Reduced) // <= FOUND
206:     {

['209']

209:         (,,,,, uint128 liquidity,,,,) = // <= FOUND
210:             IShadowNonfungiblePositionManager(getShadowNonfungiblePositionManager()).positions(shadowPositionId);

['254']

254:     function liquidatePosition(address caller)
255:         external
256:         onlyVault
257:         returns (
258:             uint128 liquidity, // <= FOUND
259:             uint256 token0Reduced,
260:             uint256 token1Reduced,
261:             uint256 token0Fees,
262:             uint256 token1Fees,
263:             uint256 token0Left,
264:             uint256 token1Left
265:         )
266:     {

['408']

408:     function _decreasePosition(uint128 liquidity) internal returns (uint256 amount0, uint256 amount1) { // <= FOUND

['203']

203:         
204:         (uint256 tokenId, uint128 liquidity, uint256 amount0, uint256 amount1) = IShadowRangePositionImpl( // <= FOUND
205:             positionInfo.positionAddress
206:         ).openPosition(
207:             IShadowRangePositionImpl.OpenPositionParams({
208:                 amount0Desired: params.amount0Desired,
209:                 amount1Desired: params.amount1Desired,
210:                 tickLower: params.tickLower,
211:                 tickUpper: params.tickUpper,
212:                 positionOwner: msg.sender
213:             })

['274']

274:         (uint128 liquidity, uint256 token0Reduced, uint256 token1Reduced, uint256 token0Balance, uint256 token1Balance) // <= FOUND
275:         = IShadowRangePositionImpl(positionInfo.positionAddress).closePosition();

['300']

300:         (uint128 reduceLiquidity,,) = IShadowRangePositionImpl(positionInfo.positionAddress).reducePosition( // <= FOUND
301:             params.reducePercentage, params.amount0ToSwap, params.amount1ToSwap
302:         );

['57']

57:         uint128 liquidity; // <= FOUND

['24']

24:         (uint160 sqrtPriceX96,,,,,,) = v3Pool.slot0(); // <= FOUND

['34']

34:         (uint160 sqrtPriceX96,,,,,,) = IShadowV3Pool(v3Pool).slot0(); // <= FOUND

['23']

23:         (uint160 sqrtPriceX96,,,,,) = ISwapXV3Pool(pool).globalState(); // <= FOUND

[Gas-12] Use != 0 instead of > 0

Resolution

Replace spotted instances with != 0 for uints as this uses less gas

Num of instances: 33

Findings

Click to show findings

['114']

114:         
115:         if (msg.value > 0) { // <= FOUND

['30']

30:         if (balanceWETH9 > 0) { // <= FOUND

['37']

37:         if (address(this).balance > 0) { // <= FOUND

['115']

115:         if (block.timestamp > startTime && totalStaked > 0) { // <= FOUND

['142']

142:         require(amount > 0, "amount = 0"); // <= FOUND

['199']

199:             if (claimable > 0) { // <= FOUND

['137']

137:         if (token0Balance > 0) { // <= FOUND

['140']

140:         if (token1Balance > 0) { // <= FOUND

['166']

166:         if (currentDebt0 > 0) { // <= FOUND

['178']

178:         if (currentDebt1 > 0) { // <= FOUND

['216']

216:         if (amount0ToSwap > 0) { // <= FOUND

['220']

220:         if (amount1ToSwap > 0) { // <= FOUND

['227']

227:         if (token0Left > 0) { // <= FOUND

['231']

231:         if (token1Left > 0) { // <= FOUND

['281']

281:         
282:         if (token0Reduced > 0) { // <= FOUND

['286']

286:             if (token0CallerFees > 0) { // <= FOUND

['293']

293:         if (token1Reduced > 0) { // <= FOUND

['299']

299:             if (token1CallerFees > 0) { // <= FOUND

['366']

366:         if (token0Fees > 0) { // <= FOUND

['370']

370:         if (token1Fees > 0) { // <= FOUND

['384']

384:                 if (balance > 0) { // <= FOUND

['169']

169:         
170:         if (params.amount0Principal > 0) { // <= FOUND

['172']

172:         if (params.amount1Principal > 0) { // <= FOUND

['178']

178:         if (params.amount0Borrow > 0) { // <= FOUND

['183']

183:         if (params.amount1Borrow > 0) { // <= FOUND

['196']

196:         if (params.amount0Principal > 0 || params.amount0Borrow > 0) { // <= FOUND

['199']

199:         if (params.amount1Principal > 0 || params.amount1Borrow > 0) { // <= FOUND

['114']

114:         if (msg.value > 0) { // <= FOUND

['297']

297:         require(params.reducePercentage > 0 && params.reducePercentage < 10000, "Invalid reduce percentage"); // <= FOUND

['314']

314:         if (amount0 > 0 && positionInfo.token0DebtId != 0) { // <= FOUND

['324']

324:         if (amount1 > 0 && positionInfo.token1DebtId != 0) { // <= FOUND

['77']

77:             if (positions[i].amount0 > 0 || positions[i].amount1 > 0) { // <= FOUND

['93']

93:             positions[i].inRange = positions[i].amount0 > 0 && positions[i].amount1 > 0; // <= FOUND

[Gas-13] Default bool values are manually reset

Resolution

Using .delete is better than resetting a Solidity variable to its default value manually because it frees up storage space on the Ethereum blockchain, resulting in gas cost savings.

Num of instances: 2

Findings

Click to show findings

['540']

540:     function unPauseAll() external onlyOwner { // <= FOUND
541:         paused = false; // <= FOUND
542:         emit UnPaused();
543:     }

['552']

552:     function disableVaultToBorrow(uint256 vaultId) external onlyOwner notPaused { // <= FOUND
553:         address vaultAddr = getVault(vaultId);
554: 
555:         borrowingWhiteList[vaultAddr] = false; // <= FOUND
556:         emit DisableVaultToBorrow(vaultId, vaultAddr);
557:     }

[Gas-14] Default int values are manually reset

Resolution

Using .delete is better than resetting a Solidity variable to its default value manually because it frees up storage space on the Ethereum blockchain, resulting in gas cost savings.

Num of instances: 1

Findings

Click to show findings

['200']

200:                 userRewardsClaimable[msg.sender][rewardToken] = 0;

[Gas-15] Function calls within for loops

Resolution

Making function calls or external calls within loops in Solidity can lead to inefficient gas usage, potential bottlenecks, and increased vulnerability to attacks. Each function call or external call consumes gas, and when executed within a loop, the gas cost multiplies, potentially causing the transaction to run out of gas or exceed block gas limits. This can result in transaction failure or unpredictable behavior.

Num of instances: 8

Findings

Click to show findings

['71']

71:        for (uint256 i = 0; i < ids.length; i++) {
72:             positions[i].vault = vault;
73:             positions[i].id = ids[i];
74:             positions[i].token0 = IVault(vault).token0();
75:             positions[i].token1 = IVault(vault).token1();
76:             (positions[i].amount0, positions[i].amount1) = IVault(vault).getPositionAmounts(ids[i]);
77:             if (positions[i].amount0 > 0 || positions[i].amount1 > 0) {
78:                 (positions[i].amount0Debt, positions[i].amount1Debt) = IVault(vault).getPositionDebt(ids[i]);
79:                 positions[i].debtRatio = IVault(vault).getDebtRatio(ids[i]);
80:             }
81:             positions[i].price0 = IVault(vault).getTokenPrice(positions[i].token0);
82:             positions[i].price1 = IVault(vault).getTokenPrice(positions[i].token1);
83: 
84:             IVault.PositionInfo memory positionInfo = IVault(vault).getPositionInfos(ids[i]);
85: 
86:             positions[i].positionAddress = positionInfo.positionAddress;
87:             positions[i].positionNftId = positionInfo.shadowPositionId;
88:             positions[i].tickUpper = positionInfo.tickUpper;
89:             positions[i].tickLower = positionInfo.tickLower;
90:             positions[i].ul = positionInfo.ul;
91:             positions[i].ll = positionInfo.ll;
92: 
93:             positions[i].inRange = positions[i].amount0 > 0 && positions[i].amount1 > 0;
94: 
95:             (positions[i].rewards, positions[i].rewardsAmounts) =
96:                 IPositionImpl(positionInfo.positionAddress).claimableRewards();
97: 
98:             positions[i].currentTick = IVault(vault).getCurrentTick();
99: 
100:             (,,,,, positions[i].liquidity,,,,) = INFTPositionManager(
101:                 IAddressRegistry(addressProvider).getAddress(AddressId.ADDRESS_ID_SHADOW_NONFUNGIBLE_POSITION_MANAGER) // <= FOUND
102:             ).positions(positionInfo.shadowPositionId);
103:         }

['71']

71:         for (uint256 i = 0; i < ids.length; i++) {
72:             positions[i].vault = vault;
73:             positions[i].id = ids[i];
74:             positions[i].token0 = IVault(vault).token0();
75:             positions[i].token1 = IVault(vault).token1();
76:             (positions[i].amount0, positions[i].amount1) = IVault(vault).getPositionAmounts(ids[i]);
77:             if (positions[i].amount0 > 0 || positions[i].amount1 > 0) {
78:                 (positions[i].amount0Debt, positions[i].amount1Debt) = IVault(vault).getPositionDebt(ids[i]);
79:                 positions[i].debtRatio = IVault(vault).getDebtRatio(ids[i]);
80:             }
81:             positions[i].price0 = IVault(vault).getTokenPrice(positions[i].token0);
82:             positions[i].price1 = IVault(vault).getTokenPrice(positions[i].token1);
83: 
84:             IVault.PositionInfo memory positionInfo = IVault(vault).getPositionInfos(ids[i]);
85: 
86:             positions[i].positionAddress = positionInfo.positionAddress;
87:             positions[i].positionNftId = positionInfo.shadowPositionId;
88:             positions[i].tickUpper = positionInfo.tickUpper;
89:             positions[i].tickLower = positionInfo.tickLower;
90:             positions[i].ul = positionInfo.ul;
91:             positions[i].ll = positionInfo.ll;
92: 
93:             positions[i].inRange = positions[i].amount0 > 0 && positions[i].amount1 > 0;
94: 
95:             (positions[i].rewards, positions[i].rewardsAmounts) =
96:                 IPositionImpl(positionInfo.positionAddress).claimableRewards();
97: 
98:             positions[i].currentTick = IVault(vault).getCurrentTick();
99: 
100:             (,,,,, positions[i].liquidity,,,,) = INFTPositionManager(
101:                 IAddressRegistry(addressProvider).getAddress(AddressId.ADDRESS_ID_SHADOW_NONFUNGIBLE_POSITION_MANAGER) // <= FOUND
102:             ).positions(positionInfo.shadowPositionId);
103:         }

['382']

382:             for (uint256 i = 0; i < tokens.length; i++) {
383:                 uint256 balance = IERC20(tokens[i]).balanceOf(address(this));
384:                 if (balance > 0) {
385:                     if (tokens[i] == xShadow) {
386:                         
387:                         
388:                         
389:                         
390:                         
391:                         if (IShadowX33(x33).isUnlocked()) {
392:                             address x33Adapter = getX33Adapter();
393:                             IERC20(tokens[i]).approve(x33Adapter, balance);
394:                             IERC4626(x33Adapter).deposit(balance, address(this)); // <= FOUND
395:                             balance = IERC20(x33).balanceOf(address(this));
396:                             IERC20(x33).approve(vault, balance);
397:                             IVault(vault).claimCallback(x33, balance);
398:                         }
399:                     } else {
400:                         IERC20(tokens[i]).approve(vault, balance);
401:                         IVault(vault).claimCallback(tokens[i], balance);
402:                     }
403:                 }
404:             }

['47']

47:        for (uint256 i; i < rewardTokens.length; i++) {
48:             address rewardToken = rewardTokens[i];
49:             rewardData[rewardToken].rewardPerTokenStored = rewardPerToken(rewardToken); // <= FOUND
50:             rewardData[rewardToken].lastUpdateTime = Math.min(rewardData[rewardToken].endTime, block.timestamp);
51: 
52:             if (user != address(0)) {
53:                 userRewardsClaimable[user][rewardToken] =
54:                     earned(user, rewardToken, rewardData[rewardToken].rewardPerTokenStored); // <= FOUND
55:                 userRewardPerTokenPaid[user][rewardToken] = rewardData[rewardToken].rewardPerTokenStored;
56:             }
57:         }

['47']

47:         for (uint256 i; i < rewardTokens.length; i++) {
48:             address rewardToken = rewardTokens[i];
49:             rewardData[rewardToken].rewardPerTokenStored = rewardPerToken(rewardToken); // <= FOUND
50:             rewardData[rewardToken].lastUpdateTime = Math.min(rewardData[rewardToken].endTime, block.timestamp);
51: 
52:             if (user != address(0)) {
53:                 userRewardsClaimable[user][rewardToken] =
54:                     earned(user, rewardToken, rewardData[rewardToken].rewardPerTokenStored); // <= FOUND
55:                 userRewardPerTokenPaid[user][rewardToken] = rewardData[rewardToken].rewardPerTokenStored;
56:             }
57:         }

['341']

341:             for (uint256 i = 0; i < tokens.length; i++) {
342:                 amounts[i] = IShadowGaugeV3(shadowGauge).earned(tokens[i], shadowPositionId); // <= FOUND
343:             }

['109']

109:        for (uint256 i = 0; i < vaults.length; i++) {
110:             positions[i] = getUserPosition(vaults[i], owner); // <= FOUND
111:         }

['109']

109:         for (uint256 i = 0; i < vaults.length; i++) {
110:             positions[i] = getUserPosition(vaults[i], owner); // <= FOUND
111:         }

[Gas-16] For loops in public or external functions should be avoided due to high gas costs and possible DOS

Resolution

In Solidity, for loops can potentially cause Denial of Service (DoS) attacks if not handled carefully. DoS attacks can occur when an attacker intentionally exploits the gas cost of a function, causing it to run out of gas or making it too expensive for other users to call. Below are some scenarios where for loops can lead to DoS attacks: Nested for loops can become exceptionally gas expensive and should be used sparingly

Num of instances: 2

Findings

Click to show findings

['195']

195:     function claim() external nonReentrant updateReward(msg.sender) {
196:         for (uint256 i = 0; i < rewardTokens.length; i++) { // <= FOUND
197:             address rewardToken = rewardTokens[i];
198:             uint256 claimable = userRewardsClaimable[msg.sender][rewardToken];
199:             if (claimable > 0) {
200:                 userRewardsClaimable[msg.sender][rewardToken] = 0;
201:                 require(IERC20(rewardToken).transfer(msg.sender, claimable), "transfer failed");
202:                 emit RewardPaid(msg.sender, rewardToken, claimable);
203:             }
204:         }
205:     }

['18']

18:     function initialize(
19:         address _pyth,
20:         address _oracleManager,
21:         address[] memory initialTokens,
22:         bytes32[] memory priceIds
23:     ) external initializer {
24:         pyth = IPyth(_pyth);
25:         oracleManager = _oracleManager;
26:         require(initialTokens.length == priceIds.length, "Initial tokens and price ids length mismatch");
27:         for (uint256 i = 0; i < initialTokens.length; i++) { // <= FOUND
28:             pythPriceIds[initialTokens[i]] = priceIds[i];
29:         }
30: 
31:         maxPriceAge = 1 hours;
32:     }

[Gas-17] Use assembly to check for the zero address

Resolution

Using assembly for address comparisons in Solidity can save gas because it allows for more direct access to the Ethereum Virtual Machine (EVM), reducing the overhead of higher-level operations. Solidity's high-level abstraction simplifies coding but can introduce additional gas costs. Using assembly for simple operations like address comparisons can be more gas-efficient.

Num of instances: 13

Findings

Click to show findings

['41']

41:     function initialize(uint256 _positionId) external {
42:         require(vault == address(0), "Already initialized"); // <= FOUND
43:         xShadow = 0x5050bc082FF4A74Fb6B0B04385dEfdDB114b2424;
44:         x33 = 0x3333111A391cC08fa51353E9195526A70b333333;
45:         vault = msg.sender;
46:         positionId = _positionId;
47: 
48:         token0 = IVault(msg.sender).token0();
49:         token1 = IVault(msg.sender).token1();
50: 
51:         IERC20(token0).approve(vault, type(uint256).max);
52:         IERC20(token1).approve(vault, type(uint256).max);
53: 
54:         shadowV3Pool = IShadowRangeVault(vault).shadowV3Pool();
55:         tickSpacing = IShadowV3Pool(shadowV3Pool).tickSpacing();
56: 
57:         _initializePayments(IPayments(msg.sender).WETH9());
58: 
59:         addressProvider = IVault(msg.sender).addressProvider();
60:     }

['49']

49:     function changeOwner(address newOwner) external onlyOwner {
50:         require(newOwner != address(0), "Proxy: new Owner is the zero address"); // <= FOUND
51:         emit OwnerChanged(_owner(), newOwner);
52:         _setOwner(newOwner);
53:     }

['78']

78:     function mintToTreasury(address treasury, uint256 amount) external onlyLendingPool nonReentrant {
79:         require(treasury != address(0), "zero address"); // <= FOUND
80:         _mint(treasury, amount);
81:         emit MintToTreasury(treasury, amount);
82:     }

['57']

57:     function initialize(address _addressRegistry, address _WETH9) public initializer {
58:         require(_addressRegistry != address(0), Errors.VL_ADDRESS_CANNOT_ZERO); // <= FOUND
59:         require(_WETH9 != address(0), Errors.VL_ADDRESS_CANNOT_ZERO); // <= FOUND
60:         addressRegistry = _addressRegistry;
61:         __Ownable_init();
62:         __ReentrancyGuard_init();
63:         _initializePayments(_WETH9);
64:         nextReserveId = 1;
65:         nextDebtPositionId = 1;
66:     }

['123']

123:     function depositAndStake(uint256 reserveId, uint256 amount, address onBehalfOf, uint16 referralCode)
124:         external
125:         payable
126:         notPaused
127:         nonReentrant
128:         returns (uint256 eTokenAmount)
129:     {
130:         eTokenAmount = _deposit(reserveId, amount, address(this));
131: 
132:         address stakingPool = reserves[reserveId].stakingAddress;
133:         require(stakingPool != address(0), "Address=0"); // <= FOUND
134:         IERC20(getETokenAddress(reserveId)).approve(stakingPool, eTokenAmount);
135:         IStakingRewards(stakingPool).stake(eTokenAmount, onBehalfOf);
136: 
137:         
138:         if (msg.value > 0) {
139:             refundETH();
140:         }
141: 
142:         
143:         emit Deposited(reserveId, _msgSender(), onBehalfOf, amount, eTokenAmount, referralCode);
144:     }

['185']

185:     function unStakeAndWithdraw(uint256 reserveId, uint256 eTokenAmount, address to, bool receiveNativeETH)
186:         external
187:         payable
188:         notPaused
189:         nonReentrant
190:         avoidUsingNativeEther
191:         returns (uint256)
192:     {
193:         address stakingPool = reserves[reserveId].stakingAddress;
194:         require(stakingPool != address(0), "Address=0"); // <= FOUND
195: 
196:         IStakingRewards(stakingPool).withdrawByLendingPool(eTokenAmount, _msgSender(), address(this));
197: 
198:         uint256 underlyingTokenAmount = _redeem(reserveId, eTokenAmount, to, receiveNativeETH);
199: 
200:         emit Redeemed(reserveId, _msgSender(), to, eTokenAmount, underlyingTokenAmount);
201: 
202:         return (underlyingTokenAmount);
203:     }

['384']

384:     function createStakingPoolForReserve(uint256 reserveId) internal {
385:         address eTokenAddress = reserves[reserveId].eTokenAddress;
386:         require(eTokenAddress != address(0), "Address=0"); // <= FOUND
387: 
388:         reserves[reserveId].stakingAddress = StakingRewardsDeployer.deploy(eTokenAddress);
389: 
390:         Ownable(reserves[reserveId].stakingAddress).transferOwnership(owner());
391:     }

['428']

428:     function getTreasury() internal view returns (address treasury) {
429:         treasury = AddressRegistry(addressRegistry).getAddress(AddressId.ADDRESS_ID_TREASURY);
430:         require(treasury != address(0), Errors.VL_TREASURY_ADDRESS_NOT_SET); // <= FOUND
431:     }

['433']

433:     function getVault(uint256 vaultId) internal view returns (address vaultAddress) {
434:         address vaultFactory = AddressRegistry(addressRegistry).getAddress(AddressId.ADDRESS_ID_VAULT_FACTORY);
435: 
436:         vaultAddress = IVaultFactory(vaultFactory).vaults(vaultId);
437:         require(vaultAddress != address(0), "Invalid VaultId"); // <= FOUND
438:     }

['39']

39:     function setOracleManager(address _oracleManager) external onlyOracleManager {
40:         require(_oracleManager != address(0), "Oracle manager cannot be zero address"); // <= FOUND
41:         oracleManager = _oracleManager;
42:     }

['336']

336:     function claimableRewards() external view returns (address[] memory, uint256[] memory) {
337:         address shadowGauge = IShadowRangeVault(vault).shadowGauge();
338:         if (shadowGauge != address(0)) { // <= FOUND
339:             address[] memory tokens = IShadowGaugeV3(shadowGauge).getRewardTokens();
340:             uint256[] memory amounts = new uint256[](tokens.length);
341:             for (uint256 i = 0; i < tokens.length; i++) {
342:                 amounts[i] = IShadowGaugeV3(shadowGauge).earned(tokens[i], shadowPositionId);
343:             }
344:             return (tokens, amounts);
345:         }
346:         return (new address[](0), new uint256[](0));
347:     }

['376']

376:     function _claimRewards() internal {
377:         address shadowGauge = IShadowRangeVault(vault).shadowGauge();
378:         if (shadowGauge != address(0)) { // <= FOUND
379:             address[] memory tokens = IShadowGaugeV3(shadowGauge).getRewardTokens();
380:             IShadowGaugeV3(shadowGauge).getReward(shadowPositionId, tokens);
381: 
382:             for (uint256 i = 0; i < tokens.length; i++) {
383:                 uint256 balance = IERC20(tokens[i]).balanceOf(address(this));
384:                 if (balance > 0) {
385:                     if (tokens[i] == xShadow) {
386:                         
387:                         
388:                         
389:                         
390:                         
391:                         if (IShadowX33(x33).isUnlocked()) {
392:                             address x33Adapter = getX33Adapter();
393:                             IERC20(tokens[i]).approve(x33Adapter, balance);
394:                             IERC4626(x33Adapter).deposit(balance, address(this));
395:                             balance = IERC20(x33).balanceOf(address(this));
396:                             IERC20(x33).approve(vault, balance);
397:                             IVault(vault).claimCallback(x33, balance);
398:                         }
399:                     } else {
400:                         IERC20(tokens[i]).approve(vault, balance);
401:                         IVault(vault).claimCallback(tokens[i], balance);
402:                     }
403:                 }
404:             }
405:         }
406:     }

['340']

340:     function liquidatePosition(uint256 positionId) external nonReentrant {
341:         PositionInfo memory positionInfo = positionInfos[positionId];
342:         require(positionInfo.owner != address(0), "Position does not exist"); // <= FOUND
343:         require(getDebtRatio(positionId) > liquidationDebtRatio, "Debt ratio is too low");
344: 
345:         if (!openLiquidationEnabled) {
346:             require(liquidators[msg.sender], "Not a liquidator");
347:         }
348: 
349:         (
350:             ,
351:             uint256 token0Reduced,
352:             uint256 token1Reduced,
353:             uint256 token0Fees,
354:             uint256 token1Fees,
355:             uint256 token0Left,
356:             uint256 token1Left
357:         ) = IShadowRangePositionImpl(positionInfo.positionAddress).liquidatePosition(msg.sender);
358: 
359:         emit PositionLiquidated(
360:             positionInfo.owner,
361:             vaultId,
362:             positionId,
363:             token0Reduced,
364:             token1Reduced,
365:             token0Fees,
366:             token1Fees,
367:             token0Left,
368:             token1Left,
369:             block.timestamp
370:         );
371:         (uint256 currentDebt0, uint256 currentDebt1) = getPositionDebt(positionId);
372:         require(currentDebt0 == 0 && currentDebt1 == 0, "Still debt");
373:     }

[Gas-18] Some error strings are not descriptive

Resolution

Consider adding more detail to these error strings

Num of instances: 16

Findings

Click to show findings

['55']

55:     function _delegate() internal {
56:         address impl = IPositionImplGetter(_owner()).positionImplementation();
57:         require(impl != address(0)); // <= FOUND
58:         assembly {
59:             
60:             
61:             
62:             calldatacopy(0, 0, calldatasize())
63: 
64:             
65:             
66:             let result := delegatecall(gas(), impl, 0, calldatasize(), 0, 0)
67: 
68:             
69:             returndatacopy(0, 0, returndatasize())
70: 
71:             switch result
72:             
73:             case 0 { revert(0, returndatasize()) }
74:             default { return(0, returndatasize()) }
75:         }
76:     }

['57']

57:     function initialize(address _addressRegistry, address _WETH9) public initializer {
58:         require(_addressRegistry != address(0), Errors.VL_ADDRESS_CANNOT_ZERO); // <= FOUND
59:         require(_WETH9 != address(0), Errors.VL_ADDRESS_CANNOT_ZERO);
60:         addressRegistry = _addressRegistry;
61:         __Ownable_init();
62:         __ReentrancyGuard_init();
63:         _initializePayments(_WETH9);
64:         nextReserveId = 1;
65:         nextDebtPositionId = 1;
66:     }

['205']

205:     function _deposit(uint256 reserveId, uint256 amount, address onBehalfOf) internal returns (uint256 eTokenAmount) {
206:         DataTypes.ReserveData storage reserve = getReserve(reserveId);
207:         require(!reserve.getFrozen(), Errors.VL_RESERVE_FROZEN); // <= FOUND
208:         
209:         reserve.updateState(getTreasury());
210: 
211:         
212:         reserve.checkCapacity(amount);
213: 
214:         uint256 exchangeRate = reserve.reserveToETokenExchangeRate();
215: 
216:         
217:         pay(reserve.underlyingTokenAddress, _msgSender(), reserve.eTokenAddress, amount);
218: 
219:         
220:         eTokenAmount = amount.mul(exchangeRate).div(Precision.FACTOR1E18);
221: 
222:         IExtraInterestBearingToken(reserve.eTokenAddress).mint(onBehalfOf, eTokenAmount);
223: 
224:         
225:         reserve.updateInterestRates();
226:     }

['256']

256:     function newDebtPosition(uint256 reserveId) external notPaused nonReentrant returns (uint256 debtId) {
257:         DataTypes.ReserveData storage reserve = getReserve(reserveId);
258:         require(!reserve.getFrozen(), Errors.VL_RESERVE_FROZEN); // <= FOUND
259:         require(reserve.getBorrowingEnabled(), Errors.VL_BORROWING_NOT_ENABLED);
260: 
261:         debtId = nextDebtPositionId;
262:         nextDebtPositionId = nextDebtPositionId + 1;
263:         DataTypes.DebtPositionData storage newPosition = debtPositions[debtId];
264:         newPosition.owner = _msgSender();
265: 
266:         reserve.updateState(getTreasury());
267:         reserve.updateInterestRates();
268: 
269:         newPosition.reserveId = reserveId;
270:         newPosition.borrowedIndex = reserve.borrowingIndex;
271:     }

['282']

282:     function borrow(address onBehalfOf, uint256 debtId, uint256 amount) external notPaused nonReentrant {
283:         require(borrowingWhiteList[_msgSender()], Errors.VL_BORROWING_CALLER_NOT_IN_WHITELIST); // <= FOUND
284: 
285:         DataTypes.DebtPositionData storage debtPosition = debtPositions[debtId];
286:         require(_msgSender() == debtPosition.owner, Errors.VL_INVALID_DEBT_OWNER);
287: 
288:         DataTypes.ReserveData storage reserve = getReserve(debtPosition.reserveId);
289:         require(!reserve.getFrozen(), Errors.VL_RESERVE_FROZEN); // <= FOUND
290:         require(reserve.getBorrowingEnabled(), Errors.VL_BORROWING_NOT_ENABLED);
291: 
292:         
293:         reserve.updateState(getTreasury());
294:         updateDebtPosition(debtPosition, reserve.borrowingIndex);
295: 
296:         
297:         
298:         
299:         uint256 credit = credits[debtPosition.reserveId][_msgSender()];
300:         require(amount <= credit, Errors.VL_OUT_OF_CREDITS);
301:         credits[debtPosition.reserveId][_msgSender()] = credit.sub(amount);
302: 
303:         require(amount <= reserve.availableLiquidity(), Errors.LP_NOT_ENOUGH_LIQUIDITY_TO_BORROW);
304:         reserve.totalBorrows += amount;
305:         debtPosition.borrowed += amount;
306:         
307:         IExtraInterestBearingToken(reserve.eTokenAddress).transferUnderlyingTo(_msgSender(), amount);
308: 
309:         reserve.updateInterestRates();
310: 
311:         emit Borrow(debtPosition.reserveId, _msgSender(), onBehalfOf, amount);
312:     }

['228']

228:     function _redeem(uint256 reserveId, uint256 eTokenAmount, address to, bool receiveNativeETH)
229:         internal
230:         returns (uint256)
231:     {
232:         DataTypes.ReserveData storage reserve = getReserve(reserveId);
233:         
234:         reserve.updateState(getTreasury());
235: 
236:         
237:         uint256 underlyingTokenAmount =
238:             reserve.eTokenToReserveExchangeRate().mul(eTokenAmount).div(Precision.FACTOR1E18);
239: 
240:         require(underlyingTokenAmount <= reserve.availableLiquidity(), Errors.VL_CURRENT_AVAILABLE_LIQUIDITY_NOT_ENOUGH); // <= FOUND
241: 
242:         if (reserve.underlyingTokenAddress == WETH9 && receiveNativeETH) {
243:             IExtraInterestBearingToken(reserve.eTokenAddress).burn(address(this), eTokenAmount, underlyingTokenAmount);
244:             unwrapWETH9(underlyingTokenAmount, to);
245:         } else {
246:             
247:             IExtraInterestBearingToken(reserve.eTokenAddress).burn(to, eTokenAmount, underlyingTokenAmount);
248:         }
249: 
250:         
251:         reserve.updateInterestRates();
252: 
253:         return (underlyingTokenAmount);
254:     }

['324']

324:     function repay(address onBehalfOf, uint256 debtId, uint256 amount)
325:         external
326:         notPaused
327:         nonReentrant
328:         returns (uint256)
329:     {
330:         require(borrowingWhiteList[_msgSender()], Errors.VL_BORROWING_CALLER_NOT_IN_WHITELIST); // <= FOUND
331: 
332:         DataTypes.DebtPositionData storage debtPosition = debtPositions[debtId];
333:         require(_msgSender() == debtPosition.owner, Errors.VL_INVALID_DEBT_OWNER);
334: 
335:         DataTypes.ReserveData storage reserve = getReserve(debtPosition.reserveId);
336: 
337:         
338:         reserve.updateState(getTreasury());
339:         updateDebtPosition(debtPosition, reserve.borrowingIndex);
340: 
341:         
342:         
343:         
344:         uint256 credit = credits[debtPosition.reserveId][_msgSender()];
345:         credits[debtPosition.reserveId][_msgSender()] = credit.add(amount);
346: 
347:         if (amount > debtPosition.borrowed) {
348:             amount = debtPosition.borrowed;
349:         }
350:         reserve.totalBorrows = reserve.totalBorrows.sub(amount);
351:         debtPosition.borrowed = debtPosition.borrowed.sub(amount);
352: 
353:         
354:         IERC20(reserve.underlyingTokenAddress).safeTransferFrom(_msgSender(), reserve.eTokenAddress, amount);
355: 
356:         reserve.updateInterestRates();
357: 
358:         emit Repay(debtPosition.reserveId, onBehalfOf, _msgSender(), amount);
359:         return amount;
360:     }

['423']

423:     function getReserve(uint256 reserveId) internal view returns (DataTypes.ReserveData storage reserve) {
424:         reserve = reserves[reserveId];
425:         require(reserve.getActive(), Errors.VL_NO_ACTIVE_RESERVE); // <= FOUND
426:     }

['428']

428:     function getTreasury() internal view returns (address treasury) {
429:         treasury = AddressRegistry(addressRegistry).getAddress(AddressId.ADDRESS_ID_TREASURY);
430:         require(treasury != address(0), Errors.VL_TREASURY_ADDRESS_NOT_SET); // <= FOUND
431:     }

['26']

26:     function unwrapWETH9(uint256 amountMinimum, address recipient) internal {
27:         uint256 balanceWETH9 = IWETH9(WETH9).balanceOf(address(this));
28:         require(balanceWETH9 >= amountMinimum, Errors.VL_INSUFFICIENT_WETH9); // <= FOUND
29: 
30:         if (balanceWETH9 > 0) {
31:             IWETH9(WETH9).withdraw(balanceWETH9);
32:             TransferHelper.safeTransferETH(recipient, balanceWETH9);
33:         }
34:     }

['96']

96:     function setReward(address rewardToken, uint256 startTime, uint256 endTime, uint256 totalRewards)
97:         public
98:         onlyOwner
99:         nonReentrant
100:         updateReward(address(0))
101:     {
102:         require(startTime < endTime, "start must lt end");
103:         require(rewardData[rewardToken].endTime < block.timestamp, "not end"); // <= FOUND
104: 
105:         if (!inRewardsTokenList[rewardToken]) {
106:             rewardTokens.push(rewardToken);
107:             inRewardsTokenList[rewardToken] = true;
108:         }
109: 
110:         rewardData[rewardToken].startTime = startTime;
111:         rewardData[rewardToken].endTime = endTime;
112:         rewardData[rewardToken].lastUpdateTime = block.timestamp;
113:         rewardData[rewardToken].rewardRate = totalRewards / (endTime - startTime);
114: 
115:         if (block.timestamp > startTime && totalStaked > 0) {
116:             uint256 dt = block.timestamp - startTime;
117: 
118:             rewardData[rewardToken].rewardPerTokenStored +=
119:                 (rewardData[rewardToken].rewardRate * dt * 1e18) / totalStaked;
120:         }
121: 
122:         if (block.timestamp > startTime && totalStaked == 0) {
123:             
124:             
125:             uint256 dt = block.timestamp - startTime;
126:             totalRewards -= rewardData[rewardToken].rewardRate * dt;
127:         }
128: 
129:         IERC20(rewardToken).safeTransferFrom(msg.sender, address(this), totalRewards);
130: 
131:         emit RewardsSet(rewardToken, startTime, endTime, totalRewards);
132:     }

['77']

77:     function onReceiveRegisterCallback(uint256 _vaultId) external {
78:         require(msg.sender == vaultRegistry, "IS"); // <= FOUND
79:         require(vaultId == 0, "VAR");
80:         vaultId = _vaultId;
81:     }

['152']

152:     function openPosition(OpenPositionParams calldata params)
153:         external
154:         payable
155:         nonReentrant
156:         checkDeadline(params.deadline)
157:     {
158:         (uint256 _positionId, address _positionAddress) = _createNewPosition();
159: 
160:         PositionInfo storage positionInfo = positionInfos[_positionId];
161:         positionInfo.owner = msg.sender;
162:         positionInfo.vaultId = vaultId;
163:         positionInfo.positionAddress = _positionAddress;
164:         positionInfo.positionId = _positionId;
165:         positionIds[msg.sender].push(_positionId);
166:         positionAddressToId[_positionAddress] = _positionId;
167: 
168:         
169:         if (params.amount0Principal > 0) {
170:             pay(token0, msg.sender, address(this), params.amount0Principal);
171:         }
172:         if (params.amount1Principal > 0) {
173:             pay(token1, msg.sender, address(this), params.amount1Principal);
174:         }
175: 
176:         
177:         address lendingPool = getLendingPool();
178:         if (params.amount0Borrow > 0) {
179:             positionInfo.token0DebtId = ILendingPool(lendingPool).newDebtPosition(token0ReserveId);
180: 
181:             ILendingPool(lendingPool).borrow(address(this), positionInfo.token0DebtId, params.amount0Borrow);
182:         }
183:         if (params.amount1Borrow > 0) {
184:             positionInfo.token1DebtId = ILendingPool(lendingPool).newDebtPosition(token1ReserveId);
185: 
186:             ILendingPool(lendingPool).borrow(address(this), positionInfo.token1DebtId, params.amount1Borrow);
187:         }
188: 
189:         require(
190:             getDebtRatioFromAmounts(
191:                 params.amount0Principal, params.amount1Principal, params.amount0Borrow, params.amount1Borrow
192:             ) < liquidationDebtRatio,
193:             "Borrow value is too high"
194:         );
195: 
196:         if (params.amount0Principal > 0 || params.amount0Borrow > 0) {
197:             pay(token0, address(this), positionInfo.positionAddress, params.amount0Principal + params.amount0Borrow);
198:         }
199:         if (params.amount1Principal > 0 || params.amount1Borrow > 0) {
200:             pay(token1, address(this), positionInfo.positionAddress, params.amount1Principal + params.amount1Borrow);
201:         }
202:         
203:         (uint256 tokenId, uint128 liquidity, uint256 amount0, uint256 amount1) = IShadowRangePositionImpl(
204:             positionInfo.positionAddress
205:         ).openPosition(
206:             IShadowRangePositionImpl.OpenPositionParams({
207:                 amount0Desired: params.amount0Desired,
208:                 amount1Desired: params.amount1Desired,
209:                 tickLower: params.tickLower,
210:                 tickUpper: params.tickUpper,
211:                 positionOwner: msg.sender
212:             })
213:         );
214: 
215:         require(getPositionValueByNftId(tokenId) > minPositionSize, "PVL"); // <= FOUND
216: 
217:         positionInfo.shadowPositionId = tokenId;
218:         positionInfo.tickLower = params.tickLower;
219:         positionInfo.tickUpper = params.tickUpper;
220:         positionInfo.ul = params.ul;
221:         positionInfo.ll = params.ll;
222: 
223:         emit PositionOpened(
224:             msg.sender,
225:             vaultId,
226:             _positionId,
227:             _positionAddress,
228:             params.amount0Principal,
229:             params.amount1Principal,
230:             amount0,
231:             amount1,
232:             liquidity,
233:             block.timestamp
234:         );
235: 
236:         if (msg.value > 0) {
237:             refundETH();
238:         }
239:         require(getDebtRatio(_positionId) < liquidationDebtRatio, "DRH");
240:     }

['242']

242:     function closePosition(uint256 positionId)
243:         external
244:         nonReentrant
245:         returns (uint256 token0Balance, uint256 token1Balance)
246:     {
247:         PositionInfo memory positionInfo = positionInfos[positionId];
248:         require(positionInfo.owner == msg.sender, "NO"); // <= FOUND
249:         require(getDebtRatio(positionId) < liquidationDebtRatio, "DRH"); // <= FOUND
250:         (token0Balance, token1Balance) = _closePosition(positionId);
251:     }

['253']

253:     function fullfillLimitOrder(uint256 positionId) external nonReentrant {
254:         PositionInfo memory positionInfo = positionInfos[positionId];
255:         int24 currentTick = IPositionValueCalculator(
256:             IAddressRegistry(addressProvider).getAddress(AddressId.ADDRESS_ID_SHADOW_POSITION_VALUE_CALCULATOR)
257:         ).getCurrentTick(shadowV3Pool);
258: 
259:         if (
260:             (positionInfo.ll != 0 && currentTick < positionInfo.ll)
261:                 || (positionInfo.ul != 0 && currentTick > positionInfo.ul)
262:         ) {
263:             _closePosition(positionId);
264:         } else {
265:             revert("Current tick is not in the position range");
266:         }
267: 
268:         require(getDebtRatio(positionId) < liquidationDebtRatio, "DRH"); // <= FOUND
269:     }

['294']

294:     function reducePosition(ReducePositionParams calldata params) external nonReentrant {
295:         PositionInfo memory positionInfo = positionInfos[params.positionId];
296:         require(positionInfo.owner == msg.sender, "Not the owner");
297:         require(params.reducePercentage > 0 && params.reducePercentage < 10000, "Invalid reduce percentage");
298:         require(getDebtRatio(params.positionId) < liquidationDebtRatio, "Debt ratio is too high");
299: 
300:         (uint128 reduceLiquidity,,) = IShadowRangePositionImpl(positionInfo.positionAddress).reducePosition(
301:             params.reducePercentage, params.amount0ToSwap, params.amount1ToSwap
302:         );
303: 
304:         require(getPositionValueByNftId(positionInfo.shadowPositionId) > minPositionSize, "PVL"); // <= FOUND
305: 
306:         emit PositionReduced(positionInfo.owner, vaultId, params.positionId, reduceLiquidity, block.timestamp);
307:     }

[Gas-19] Can transfer 0

Resolution

In Solidity, performing unnecessary operations can consume more gas than needed, leading to cost inefficiencies. For instance, if a transfer function doesn't have a zero amount check and someone calls it with a zero amount, unnecessary gas will be consumed in executing the function, even though the state of the contract remains the same. By implementing a zero amount check, such unnecessary function calls can be avoided, thereby saving gas and making the contract more efficient.

Num of instances: 1

Findings

Click to show findings

['46']

46:     function pay(address token, address payer, address recipient, uint256 value) internal {
47:         if (token == WETH9 && address(this).balance >= value) {
48:             
49:             IWETH9(WETH9).deposit{value: value}(); 
50:             require(IWETH9(WETH9).transfer(recipient, value), "transfer failed"); // <= FOUND
51:         } else if (payer == address(this)) {
52:             
53:             TransferHelper.safeTransfer(token, recipient, value);
54:         } else {
55:             
56:             TransferHelper.safeTransferFrom(token, payer, recipient, value);
57:         }
58:     }

[Gas-20] State variables which are not modified within functions should be set as constants or immutable for values set at deployment

Resolution

Set state variables listed below as constant or immutable for values set at deployment. Ensure it is safe to do so

Num of instances: 1

Findings

Click to show findings

['25']

25:  uint8 private _decimals;

[Gas-21] Structs can be packed into fewer storage slots

Resolution

In Solidity, each storage slot has a size of 32 bytes. If a struct contains multiple uint values, it's efficient to pack these into as few storage slots as possible to optimize gas usage. The EVM (Ethereum Virtual Machine) charges gas for each storage operation, so minimizing the number of slots used can result in substantial gas savings. This can be achieved by ordering struct fields according to their size or by using smaller data types where possible. However, developers must balance these optimizations with the need for code clarity and the precision requirements of their application. Always ensure that data packing does not compromise the functionality or security of the contract.

Num of instances: 2

Findings

Click to show findings

['248']

248:     struct LiquidationFeeVars { // <= FOUND
249:         uint256 liquidationFee; // <= FOUND
250:         uint256 liquidationCallerFee; // <= FOUND
251:         address liquidationFeeRecipient;
252:     }

['42']

42:     struct PositionInfo { // <= FOUND
43:         address vault;
44:         uint256 id; // <= FOUND
45:         address token0;
46:         address token1;
47:         uint256 amount0; // <= FOUND
48:         uint256 amount1; // <= FOUND
49:         uint256 amount0Debt; // <= FOUND
50:         uint256 amount1Debt; // <= FOUND
51:         uint256 debtRatio; // <= FOUND
52:         uint256 price0; // <= FOUND
53:         uint256 price1; // <= FOUND
54:         
55:         address positionAddress;
56:         uint256 positionNftId; // <= FOUND
57:         uint128 liquidity;
58:         int24 tickUpper;
59:         int24 tickLower;
60:         int24 currentTick;
61:         int24 ul;
62:         int24 ll;
63:         bool inRange;
64:         address[] rewards;
65:         uint256[] rewardsAmounts;
66:     }

[Gas-22] Don't use _msgSender() if not supporting EIP-2771

Resolution

From a gas efficiency perspective, using _msgSender() in a contract not intended to support EIP-2771 could add unnecessary overhead. The _msgSender() function includes checks to determine if the transaction was forwarded, which involves extra function calls that consume more gas than a simple msg.sender.

If a contract doesn't require EIP-2771 meta-transaction support, using msg.sender directly is more gas efficient. msg.sender is a globally accessible variable in Solidity that doesn't require an extra function call, making it a less costly choice.

In the context of Ethereum, where every operation has a gas cost, it's crucial to eliminate unnecessary computations to optimize contract execution and minimize transaction fees. Therefore, if EIP-2771 support isn't necessary, it's recommended to use msg.sender instead of _msgSender().

Num of instances: 17

Findings

Click to show findings

['16']

16:         emit SetAddress(_msgSender(), id, _addr); // <= FOUND

['119']

119:         
120:         emit Deposited(reserveId, _msgSender(), onBehalfOf, amount, eTokenAmount, referralCode); // <= FOUND

['171']

171:             eTokenAmount = IExtraInterestBearingToken(reserve.eTokenAddress).balanceOf(_msgSender()); // <= FOUND

['174']

174:         
175:         IERC20(reserve.eTokenAddress).safeTransferFrom(_msgSender(), address(this), eTokenAmount); // <= FOUND

['179']

179:         emit Redeemed(reserveId, _msgSender(), to, eTokenAmount, underlyingTokenAmount); // <= FOUND

['196']

196:         IStakingRewards(stakingPool).withdrawByLendingPool(eTokenAmount, _msgSender(), address(this)); // <= FOUND

['217']

217:         
218:         pay(reserve.underlyingTokenAddress, _msgSender(), reserve.eTokenAddress, amount); // <= FOUND

['264']

264:         newPosition.owner = _msgSender(); // <= FOUND

['283']

283:         require(borrowingWhiteList[_msgSender()], Errors.VL_BORROWING_CALLER_NOT_IN_WHITELIST); // <= FOUND

['286']

286:         require(_msgSender() == debtPosition.owner, Errors.VL_INVALID_DEBT_OWNER); // <= FOUND

['299']

299:         
300:         
301:         
302:         uint256 credit = credits[debtPosition.reserveId][_msgSender()]; // <= FOUND

['301']

301:         credits[debtPosition.reserveId][_msgSender()] = credit.sub(amount); // <= FOUND

['307']

307:         
308:         IExtraInterestBearingToken(reserve.eTokenAddress).transferUnderlyingTo(_msgSender(), amount); // <= FOUND

['311']

311:         emit Borrow(debtPosition.reserveId, _msgSender(), onBehalfOf, amount); // <= FOUND

['345']

345:         credits[debtPosition.reserveId][_msgSender()] = credit.add(amount); // <= FOUND

['354']

354:         
355:         IERC20(reserve.underlyingTokenAddress).safeTransferFrom(_msgSender(), reserve.eTokenAddress, amount); // <= FOUND

['358']

358:         emit Repay(debtPosition.reserveId, onBehalfOf, _msgSender(), amount); // <= FOUND

[Gas-23] Consider using OZ EnumerateSet in place of nested mappings

Resolution

Nested mappings and multi-dimensional arrays in Solidity operate through a process of double hashing, wherein the original storage slot and the first key are concatenated and hashed, and then this hash is again concatenated with the second key and hashed. This process can be quite gas expensive due to the double-hashing operation and subsequent storage operation (sstore).

A possible optimization involves manually concatenating the keys followed by a single hash operation and an sstore. However, this technique introduces the risk of storage collision, especially when there are other nested hash maps in the contract that use the same key types. Because Solidity is unaware of the number and structure of nested hash maps in a contract, it follows a conservative approach in computing the storage slot to avoid possible collisions.

OpenZeppelin's EnumerableSet provides a potential solution to this problem. It creates a data structure that combines the benefits of set operations with the ability to enumerate stored elements, which is not natively available in Solidity. EnumerableSet handles the element uniqueness internally and can therefore provide a more gas-efficient and collision-resistant alternative to nested mappings or multi-dimensional arrays in certain scenarios.

Num of instances: 3

Findings

Click to show findings

['38']

38:  mapping(uint256 => mapping(address => uint256)) public credits; // <= FOUND

['29']

29:  mapping(address => mapping(address => uint256)) public userRewardPerTokenPaid; // <= FOUND

['30']

30:  mapping(address => mapping(address => uint256)) public userRewardsClaimable; // <= FOUND

[Gas-24] Use selfBalance() in place of address(this).balance

Resolution

In Solidity, using selfBalance instead of address(this).balance is advantageous for gas optimization. address(this).balance performs an external call to retrieve the contract's balance, which costs extra gas. As a resolution, defining a selfBalance state variable that's updated accordingly provides a more gas-efficient approach. Using selfBalance instead of address(this).balance can save around 800 gas per call. This is because address(this).balance makes an EXTBAL operation, which costs 700 gas, and additionally, the operation is a SLOAD, which costs around 800 gas. Please note that these estimates are based on the Ethereum gas schedule and might vary depending on the Ethereum network state and its future upgrades.

Num of instances: 3

Findings

Click to show findings

['37']

37:         if (address(this).balance > 0) { // <= FOUND

['38']

38:             TransferHelper.safeTransferETH(msg.sender, address(this).balance); // <= FOUND

['47']

47:         if (token == WETH9 && address(this).balance >= value) { // <= FOUND

[Gas-25] Use assembly to emit events

Resolution

With the use of inline assembly in Solidity, we can take advantage of low-level features like scratch space and the free memory pointer, offering more gas-efficient ways of emitting events. The scratch space is a certain area of memory where we can temporarily store data, and the free memory pointer indicates the next available memory slot. Using these, we can efficiently assemble event data without incurring additional memory expansion costs. However, safety is paramount: to avoid overwriting or leakage, we must cache the free memory pointer before use and restore it afterward, ensuring that it points to the correct memory location post-operation.

Num of instances: 35

Findings

Click to show findings

['16']

16:         emit SetAddress(_msgSender(), id, _addr); // <= FOUND

['51']

51:         emit OwnerChanged(_owner(), newOwner); // <= FOUND

['49']

49:         emit Mint(user, amount); // <= FOUND

['70']

70:         emit Burn(msg.sender, receiverOfUnderlying, eTokenAmount, underlyingTokenAmount); // <= FOUND

['81']

81:         emit MintToTreasury(treasury, amount); // <= FOUND

['88']

88:         emit InitReserve(asset, eTokenAddress, reserveData.stakingAddress, id); // <= FOUND

['119']

119:         
120:         emit Deposited(reserveId, _msgSender(), onBehalfOf, amount, eTokenAmount, referralCode); // <= FOUND

['179']

179:         emit Redeemed(reserveId, _msgSender(), to, eTokenAmount, underlyingTokenAmount); // <= FOUND

['311']

311:         emit Borrow(debtPosition.reserveId, _msgSender(), onBehalfOf, amount); // <= FOUND

['358']

358:         emit Repay(debtPosition.reserveId, onBehalfOf, _msgSender(), amount); // <= FOUND

['537']

537:         emit Paused(); // <= FOUND

['542']

542:         emit UnPaused(); // <= FOUND

['549']

549:         emit EnableVaultToBorrow(vaultId, vaultAddr); // <= FOUND

['556']

556:         emit DisableVaultToBorrow(vaultId, vaultAddr); // <= FOUND

['562']

562:         emit SetCreditsOfVault(vaultId, vaultAddr, reserveId, credit); // <= FOUND

['569']

569:         emit ReserveActivated(reserveId); // <= FOUND

['575']

575:         emit ReserveDeActivated(reserveId); // <= FOUND

['581']

581:         emit ReserveFrozen(reserveId); // <= FOUND

['587']

587:         emit ReserveUnFreeze(reserveId); // <= FOUND

['593']

593:         emit ReserveBorrowEnabled(reserveId); // <= FOUND

['599']

599:         emit ReserveBorrowDisabled(reserveId); // <= FOUND

['607']

607:         emit SetReserveFeeRate(reserveId, _rate); // <= FOUND

['621']

621:         emit SetInterestRateConfig( // <= FOUND
622:             reserveId, utilizationA, borrowingRateA, utilizationB, borrowingRateB, maxBorrowingRate
623:         );

['630']

630:         emit SetReserveCapacity(reserveId, cap); // <= FOUND

['131']

131:         emit RewardsSet(rewardToken, startTime, endTime, totalRewards); // <= FOUND

['149']

149:         emit Staked(msg.sender, onBehalfOf, amount); // <= FOUND

['167']

167:         emit Withdraw(msg.sender, to, amount); // <= FOUND

['192']

192:         emit Withdraw(user, to, amount); // <= FOUND

['202']

202:                 emit RewardPaid(msg.sender, rewardToken, claimable); // <= FOUND

['223']

223:         emit PositionOpened( // <= FOUND
224:             msg.sender,
225:             vaultId,
226:             _positionId,
227:             _positionAddress,
228:             params.amount0Principal,
229:             params.amount1Principal,
230:             amount0,
231:             amount1,
232:             liquidity,
233:             block.timestamp
234:         );

['280']

280:         emit PositionClosed( // <= FOUND
281:             positionInfo.owner,
282:             vaultId,
283:             positionId,
284:             positionInfo.positionAddress,
285:             token0Reduced,
286:             token1Reduced,
287:             liquidity,
288:             block.timestamp
289:         );

['306']

306:         emit PositionReduced(positionInfo.owner, vaultId, params.positionId, reduceLiquidity, block.timestamp); // <= FOUND

['359']

359:         emit PositionLiquidated( // <= FOUND
360:             positionInfo.owner,
361:             vaultId,
362:             positionId,
363:             token0Reduced,
364:             token1Reduced,
365:             token0Fees,
366:             token1Fees,
367:             token0Left,
368:             token1Left,
369:             block.timestamp
370:         );

['388']

388:         emit RewardClaimed(positionInfos[positionId].owner, vaultId, positionId, reward, amount - feeAmount, feeAmount); // <= FOUND

['38']

38:         emit NewVault(_newVault, vaultId); // <= FOUND

[Gas-26] Use solady library where possible to save gas

Resolution

The following OpenZeppelin imports have a Solady equivalent, as such they can be used to save GAS as Solady modules have been specifically designed to be as GAS efficient as possible

Num of instances: 8

Findings

Click to show findings

['4']

4: import "@openzeppelin/contracts/access/Ownable.sol"; // <= FOUND

['9']

9: import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; // <= FOUND

['7']

7: import {Ownable2StepUpgradeable} from "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol"; // <= FOUND

['6']

6: import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; // <= FOUND

['7']

7: import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; // <= FOUND

['7']

7: import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; // <= FOUND

['8']

8: import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; // <= FOUND

['9']

9: import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; // <= FOUND

[Gas-27] Identical Deployments Should be Replaced with Clone

Resolution

In the context of smart contracts, deploying multiple identical contracts can lead to inefficient use of gas and unnecessarily duplicate code on the blockchain. A more gas-efficient approach is to use a "clone" pattern. By deploying a master contract and then creating clones of it, only the differences between the instances are stored for each clone. This approach leverages the EIP-1167 standard, which defines a minimal proxy contract that points to the implementation contract. Clones can be far cheaper to deploy compared to full instances. So, the resolution is to replace identical deployments with clones, saving on gas and storage space.

Num of instances: 1

Findings

Click to show findings

['434']

434:         address vaultFactory = AddressRegistry(addressRegistry).getAddress(AddressId.ADDRESS_ID_VAULT_FACTORY); // <= FOUND

[Gas-28] Mark Functions That Revert For Normal Users As payable

Resolution

In Solidity, marking functions as payable allows them to accept Ether. If a function is known to revert for regular users (non-admin or specific roles) but needs to be accessible to others, marking it as payable can be beneficial. This ensures that even if a regular user accidentally sends Ether to the function, the Ether won't be trapped, as the function reverts, returning the funds. This can save gas by avoiding unnecessary failure handling in the function itself. Resolution: Carefully assess the roles and access patterns, and mark functions that should revert for regular users as payable to handle accidental Ether transfers.

Num of instances: 32

Findings

Click to show findings

['14']

14:     function setAddress(uint256 id, address _addr) public onlyOwner {
15:         libraryAndContractAddresses[id] = _addr;
16:         emit SetAddress(_msgSender(), id, _addr);
17:     }

['49']

49:     function changeOwner(address newOwner) external onlyOwner {
50:         require(newOwner != address(0), "Proxy: new Owner is the zero address");
51:         emit OwnerChanged(_owner(), newOwner);
52:         _setOwner(newOwner);
53:     }

['69']

69:     function initReserve(address asset) external onlyOwner notPaused {
70:         uint256 id = nextReserveId;
71:         nextReserveId += 1;
72: 
73:         
74:         string memory name = string(abi.encodePacked(ERC20(asset).name(), "(MightyFi Interest Bearing Token)"));
75:         string memory symbol = string(abi.encodePacked("m", ERC20(asset).symbol()));
76:         uint8 decimals = ERC20(asset).decimals();
77: 
78:         address eTokenAddress = ETokenDeployer.deploy(name, symbol, decimals, asset, id);
79: 
80:         DataTypes.ReserveData storage reserveData = reserves[id];
81:         reserveData.setActive(true);
82:         reserveData.setBorrowingEnabled(true);
83: 
84:         initReserve(reserveData, asset, eTokenAddress, type(uint256).max, id);
85: 
86:         createStakingPoolForReserve(id);
87: 
88:         emit InitReserve(asset, eTokenAddress, reserveData.stakingAddress, id);
89:     }

['535']

535:     function emergencyPauseAll() external onlyOwner {
536:         paused = true;
537:         emit Paused();
538:     }

['540']

540:     function unPauseAll() external onlyOwner {
541:         paused = false;
542:         emit UnPaused();
543:     }

['545']

545:     function enableVaultToBorrow(uint256 vaultId) external onlyOwner notPaused {
546:         address vaultAddr = getVault(vaultId);
547: 
548:         borrowingWhiteList[vaultAddr] = true;
549:         emit EnableVaultToBorrow(vaultId, vaultAddr);
550:     }

['552']

552:     function disableVaultToBorrow(uint256 vaultId) external onlyOwner notPaused {
553:         address vaultAddr = getVault(vaultId);
554: 
555:         borrowingWhiteList[vaultAddr] = false;
556:         emit DisableVaultToBorrow(vaultId, vaultAddr);
557:     }

['559']

559:     function setCreditsOfVault(uint256 vaultId, uint256 reserveId, uint256 credit) external onlyOwner notPaused {
560:         address vaultAddr = getVault(vaultId);
561:         credits[reserveId][vaultAddr] = credit;
562:         emit SetCreditsOfVault(vaultId, vaultAddr, reserveId, credit);
563:     }

['565']

565:     function activateReserve(uint256 reserveId) public onlyOwner notPaused {
566:         DataTypes.ReserveData storage reserve = reserves[reserveId];
567:         reserve.setActive(true);
568: 
569:         emit ReserveActivated(reserveId);
570:     }

['572']

572:     function deActivateReserve(uint256 reserveId) public onlyOwner notPaused {
573:         DataTypes.ReserveData storage reserve = reserves[reserveId];
574:         reserve.setActive(false);
575:         emit ReserveDeActivated(reserveId);
576:     }

['578']

578:     function freezeReserve(uint256 reserveId) public onlyOwner notPaused {
579:         DataTypes.ReserveData storage reserve = reserves[reserveId];
580:         reserve.setFrozen(true);
581:         emit ReserveFrozen(reserveId);
582:     }

['584']

584:     function unFreezeReserve(uint256 reserveId) public onlyOwner notPaused {
585:         DataTypes.ReserveData storage reserve = reserves[reserveId];
586:         reserve.setFrozen(false);
587:         emit ReserveUnFreeze(reserveId);
588:     }

['590']

590:     function enableBorrowing(uint256 reserveId) public onlyOwner notPaused {
591:         DataTypes.ReserveData storage reserve = reserves[reserveId];
592:         reserve.setBorrowingEnabled(true);
593:         emit ReserveBorrowEnabled(reserveId);
594:     }

['596']

596:     function disableBorrowing(uint256 reserveId) public onlyOwner notPaused {
597:         DataTypes.ReserveData storage reserve = reserves[reserveId];
598:         reserve.setBorrowingEnabled(false);
599:         emit ReserveBorrowDisabled(reserveId);
600:     }

['602']

602:     function setReserveFeeRate(uint256 reserveId, uint16 _rate) public onlyOwner notPaused {
603:         require(_rate <= Constants.PERCENT_100, "invalid percent");
604:         DataTypes.ReserveData storage reserve = reserves[reserveId];
605:         reserve.reserveFeeRate = _rate;
606: 
607:         emit SetReserveFeeRate(reserveId, _rate);
608:     }

['610']

610:     function setBorrowingRateConfig(
611:         uint256 reserveId,
612:         uint16 utilizationA,
613:         uint16 borrowingRateA,
614:         uint16 utilizationB,
615:         uint16 borrowingRateB,
616:         uint16 maxBorrowingRate
617:     ) public onlyOwner notPaused {
618:         DataTypes.ReserveData storage reserve = reserves[reserveId];
619:         setBorrowingRateConfig(reserve, utilizationA, borrowingRateA, utilizationB, borrowingRateB, maxBorrowingRate);
620: 
621:         emit SetInterestRateConfig(
622:             reserveId, utilizationA, borrowingRateA, utilizationB, borrowingRateB, maxBorrowingRate
623:         );
624:     }

['626']

626:     function setReserveCapacity(uint256 reserveId, uint256 cap) public onlyOwner notPaused {
627:         DataTypes.ReserveData storage reserve = reserves[reserveId];
628: 
629:         reserve.reserveCapacity = cap;
630:         emit SetReserveCapacity(reserveId, cap);
631:     }

['96']

96:     function setReward(address rewardToken, uint256 startTime, uint256 endTime, uint256 totalRewards)
97:         public
98:         onlyOwner
99:         nonReentrant
100:         updateReward(address(0))
101:     {
102:         require(startTime < endTime, "start must lt end");
103:         require(rewardData[rewardToken].endTime < block.timestamp, "not end");
104: 
105:         if (!inRewardsTokenList[rewardToken]) {
106:             rewardTokens.push(rewardToken);
107:             inRewardsTokenList[rewardToken] = true;
108:         }
109: 
110:         rewardData[rewardToken].startTime = startTime;
111:         rewardData[rewardToken].endTime = endTime;
112:         rewardData[rewardToken].lastUpdateTime = block.timestamp;
113:         rewardData[rewardToken].rewardRate = totalRewards / (endTime - startTime);
114: 
115:         if (block.timestamp > startTime && totalStaked > 0) {
116:             uint256 dt = block.timestamp - startTime;
117: 
118:             rewardData[rewardToken].rewardPerTokenStored +=
119:                 (rewardData[rewardToken].rewardRate * dt * 1e18) / totalStaked;
120:         }
121: 
122:         if (block.timestamp > startTime && totalStaked == 0) {
123:             
124:             
125:             uint256 dt = block.timestamp - startTime;
126:             totalRewards -= rewardData[rewardToken].rewardRate * dt;
127:         }
128: 
129:         IERC20(rewardToken).safeTransferFrom(msg.sender, address(this), totalRewards);
130: 
131:         emit RewardsSet(rewardToken, startTime, endTime, totalRewards);
132:     }

['207']

207:     function update() external updateReward(address(0)) onlyOwner {}

['110']

110:     function setLiquidator(address _liquidator, bool _isLiquidator) external onlyOwner {
111:         liquidators[_liquidator] = _isLiquidator;
112:     }

['114']

114:     function setLiquidationFeeParams(uint256 _liquidationFee, uint256 _liquidationCallerFeeRatio) external onlyOwner {
115:         liquidationFee = _liquidationFee;
116:         liquidationCallerFee = _liquidationCallerFeeRatio;
117:     }

['119']

119:     function setOpenLiquidationEnabled(bool _openLiquidationEnabled) external onlyOwner {
120:         openLiquidationEnabled = _openLiquidationEnabled;
121:     }

['123']

123:     function setPositionImplementation(address _positionImplementation) external onlyOwner {
124:         positionImplementation = _positionImplementation;
125:     }

['127']

127:     function setLiquidationDebtRatio(uint256 _liquidationDebtRatio) external onlyOwner {
128:         liquidationDebtRatio = _liquidationDebtRatio;
129:     }

['131']

131:     function setReserveIds(uint256 _token0ReserveId, uint256 _token1ReserveId) external onlyOwner {
132:         address lendingPool = getLendingPool();
133: 
134:         IERC20(token0).approve(lendingPool, type(uint256).max);
135:         IERC20(token1).approve(lendingPool, type(uint256).max);
136:         token0ReserveId = _token0ReserveId;
137:         token1ReserveId = _token1ReserveId;
138:     }

['140']

140:     function setMinPositionSize(uint256 _minPositionSize) external onlyOwner {
141:         minPositionSize = _minPositionSize;
142:     }

['144']

144:     function setPerformanceFee(uint256 _performanceFee) external onlyOwner {
145:         performanceFee = _performanceFee;
146:     }

['148']

148:     function setShadowGauge(address _shadowGauge) external onlyOwner {
149:         shadowGauge = _shadowGauge;
150:     }

['29']

29:     function newVault(address _newVault) external onlyOwner returns (uint256 vaultId) {
30:         require(!isRegistered[_newVault], "Vault already registered");
31:         vaultId = nextVaultID;
32:         nextVaultID = nextVaultID + 1;
33: 
34:         IVault(_newVault).onReceiveRegisterCallback(vaultId);
35: 
36:         vaults[vaultId] = _newVault;
37:         isRegistered[_newVault] = true;
38:         emit NewVault(_newVault, vaultId);
39:     }

['39']

39:     function setOracleManager(address _oracleManager) external onlyOracleManager {
40:         require(_oracleManager != address(0), "Oracle manager cannot be zero address");
41:         oracleManager = _oracleManager;
42:     }

['44']

44:     function setPythPriceId(address token, bytes32 priceId) external onlyOracleManager {
45:         pythPriceIds[token] = priceId;
46:     }

['48']

48:     function setMaxPriceAge(uint256 _maxPriceAge) external onlyOracleManager {
49:         maxPriceAge = _maxPriceAge;
50:     }

[Gas-29] Lack of unchecked in loops

Resolution

In Solidity, the unchecked block allows arithmetic operations to not revert on overflow. Without using unchecked in loops, extra gas is consumed due to overflow checks. If it's certain that overflows won't occur within the loop, using unchecked can make the loop more gas-efficient by skipping unnecessary checks.

Num of instances: 16

Findings

Click to show findings

['452']

452:        for (uint256 i = 0; i < reserveIdArr.length; i++) {
453:             statusArr[i].reserveId = reserveIdArr[i];
454:             statusArr[i].underlyingTokenAddress = reserves[reserveIdArr[i]].underlyingTokenAddress;
455:             statusArr[i].eTokenAddress = reserves[reserveIdArr[i]].eTokenAddress;
456:             statusArr[i].stakingAddress = reserves[reserveIdArr[i]].stakingAddress;
457:             (statusArr[i].totalLiquidity, statusArr[i].totalBorrows) =
458:                 reserves[reserveIdArr[i]].totalLiquidityAndBorrows();
459:             statusArr[i].exchangeRate = reserves[reserveIdArr[i]].eTokenToReserveExchangeRate();
460:             statusArr[i].borrowingRate = reserves[reserveIdArr[i]].borrowingRate();
461:         }

['471']

471:        for (uint256 i = 0; i < reserveIdArr.length; i++) {
472:             statusArr[i].reserveId = reserveIdArr[i];
473:             statusArr[i].user = user;
474:             statusArr[i].eTokenStaked = IStakingRewards(reserves[reserveIdArr[i]].stakingAddress).balanceOf(user);
475:             statusArr[i].eTokenUnStaked = IERC20(reserves[reserveIdArr[i]].eTokenAddress).balanceOf(user);
476:             statusArr[i].liquidity = statusArr[i].eTokenStaked.add(statusArr[i].eTokenUnStaked).mul(
477:                 reserves[reserveIdArr[i]].eTokenToReserveExchangeRate()
478:             ).div(Precision.FACTOR1E18);
479:         }

['47']

47:        for (uint256 i; i < rewardTokens.length; i++) {
48:             address rewardToken = rewardTokens[i];
49:             rewardData[rewardToken].rewardPerTokenStored = rewardPerToken(rewardToken);
50:             rewardData[rewardToken].lastUpdateTime = Math.min(rewardData[rewardToken].endTime, block.timestamp);
51: 
52:             if (user != address(0)) {
53:                 userRewardsClaimable[user][rewardToken] =
54:                     earned(user, rewardToken, rewardData[rewardToken].rewardPerTokenStored);
55:                 userRewardPerTokenPaid[user][rewardToken] = rewardData[rewardToken].rewardPerTokenStored;
56:             }
57:         }

['196']

196:        for (uint256 i = 0; i < rewardTokens.length; i++) {
197:             address rewardToken = rewardTokens[i];
198:             uint256 claimable = userRewardsClaimable[msg.sender][rewardToken];
199:             if (claimable > 0) {
200:                 userRewardsClaimable[msg.sender][rewardToken] = 0;
201:                 require(IERC20(rewardToken).transfer(msg.sender, claimable), "transfer failed");
202:                 emit RewardPaid(msg.sender, rewardToken, claimable);
203:             }
204:         }

['27']

27:        for (uint256 i = 0; i < initialTokens.length; i++) {
28:             pythPriceIds[initialTokens[i]] = priceIds[i];
29:         }

['71']

71:        for (uint256 i = 0; i < ids.length; i++) {
72:             positions[i].vault = vault;
73:             positions[i].id = ids[i];
74:             positions[i].token0 = IVault(vault).token0();
75:             positions[i].token1 = IVault(vault).token1();
76:             (positions[i].amount0, positions[i].amount1) = IVault(vault).getPositionAmounts(ids[i]);
77:             if (positions[i].amount0 > 0 || positions[i].amount1 > 0) {
78:                 (positions[i].amount0Debt, positions[i].amount1Debt) = IVault(vault).getPositionDebt(ids[i]);
79:                 positions[i].debtRatio = IVault(vault).getDebtRatio(ids[i]);
80:             }
81:             positions[i].price0 = IVault(vault).getTokenPrice(positions[i].token0);
82:             positions[i].price1 = IVault(vault).getTokenPrice(positions[i].token1);
83: 
84:             IVault.PositionInfo memory positionInfo = IVault(vault).getPositionInfos(ids[i]);
85: 
86:             positions[i].positionAddress = positionInfo.positionAddress;
87:             positions[i].positionNftId = positionInfo.shadowPositionId;
88:             positions[i].tickUpper = positionInfo.tickUpper;
89:             positions[i].tickLower = positionInfo.tickLower;
90:             positions[i].ul = positionInfo.ul;
91:             positions[i].ll = positionInfo.ll;
92: 
93:             positions[i].inRange = positions[i].amount0 > 0 && positions[i].amount1 > 0;
94: 
95:             (positions[i].rewards, positions[i].rewardsAmounts) =
96:                 IPositionImpl(positionInfo.positionAddress).claimableRewards();
97: 
98:             positions[i].currentTick = IVault(vault).getCurrentTick();
99: 
100:             (,,,,, positions[i].liquidity,,,,) = INFTPositionManager(
101:                 IAddressRegistry(addressProvider).getAddress(AddressId.ADDRESS_ID_SHADOW_NONFUNGIBLE_POSITION_MANAGER)
102:             ).positions(positionInfo.shadowPositionId);
103:         }

['109']

109:        for (uint256 i = 0; i < vaults.length; i++) {
110:             positions[i] = getUserPosition(vaults[i], owner);
111:         }

['452']

452:         for (uint256 i = 0; i < reserveIdArr.length; i++) {
453:             statusArr[i].reserveId = reserveIdArr[i];
454:             statusArr[i].underlyingTokenAddress = reserves[reserveIdArr[i]].underlyingTokenAddress;
455:             statusArr[i].eTokenAddress = reserves[reserveIdArr[i]].eTokenAddress;
456:             statusArr[i].stakingAddress = reserves[reserveIdArr[i]].stakingAddress;
457:             (statusArr[i].totalLiquidity, statusArr[i].totalBorrows) =
458:                 reserves[reserveIdArr[i]].totalLiquidityAndBorrows();
459:             statusArr[i].exchangeRate = reserves[reserveIdArr[i]].eTokenToReserveExchangeRate();
460:             statusArr[i].borrowingRate = reserves[reserveIdArr[i]].borrowingRate();
461:         }

['471']

471:         for (uint256 i = 0; i < reserveIdArr.length; i++) {
472:             statusArr[i].reserveId = reserveIdArr[i];
473:             statusArr[i].user = user;
474:             statusArr[i].eTokenStaked = IStakingRewards(reserves[reserveIdArr[i]].stakingAddress).balanceOf(user);
475:             statusArr[i].eTokenUnStaked = IERC20(reserves[reserveIdArr[i]].eTokenAddress).balanceOf(user);
476:             statusArr[i].liquidity = statusArr[i].eTokenStaked.add(statusArr[i].eTokenUnStaked).mul(
477:                 reserves[reserveIdArr[i]].eTokenToReserveExchangeRate()
478:             ).div(Precision.FACTOR1E18);
479:         }

['47']

47:         for (uint256 i; i < rewardTokens.length; i++) {
48:             address rewardToken = rewardTokens[i];
49:             rewardData[rewardToken].rewardPerTokenStored = rewardPerToken(rewardToken);
50:             rewardData[rewardToken].lastUpdateTime = Math.min(rewardData[rewardToken].endTime, block.timestamp);
51: 
52:             if (user != address(0)) {
53:                 userRewardsClaimable[user][rewardToken] =
54:                     earned(user, rewardToken, rewardData[rewardToken].rewardPerTokenStored);
55:                 userRewardPerTokenPaid[user][rewardToken] = rewardData[rewardToken].rewardPerTokenStored;
56:             }
57:         }

['196']

196:         for (uint256 i = 0; i < rewardTokens.length; i++) {
197:             address rewardToken = rewardTokens[i];
198:             uint256 claimable = userRewardsClaimable[msg.sender][rewardToken];
199:             if (claimable > 0) {
200:                 userRewardsClaimable[msg.sender][rewardToken] = 0;
201:                 require(IERC20(rewardToken).transfer(msg.sender, claimable), "transfer failed");
202:                 emit RewardPaid(msg.sender, rewardToken, claimable);
203:             }
204:         }

['27']

27:         for (uint256 i = 0; i < initialTokens.length; i++) {
28:             pythPriceIds[initialTokens[i]] = priceIds[i];
29:         }

['341']

341:             for (uint256 i = 0; i < tokens.length; i++) {
342:                 amounts[i] = IShadowGaugeV3(shadowGauge).earned(tokens[i], shadowPositionId);
343:             }

['382']

382:             for (uint256 i = 0; i < tokens.length; i++) {
383:                 uint256 balance = IERC20(tokens[i]).balanceOf(address(this));
384:                 if (balance > 0) {
385:                     if (tokens[i] == xShadow) {
386:                         
387:                         
388:                         
389:                         
390:                         
391:                         if (IShadowX33(x33).isUnlocked()) {
392:                             address x33Adapter = getX33Adapter();
393:                             IERC20(tokens[i]).approve(x33Adapter, balance);
394:                             IERC4626(x33Adapter).deposit(balance, address(this));
395:                             balance = IERC20(x33).balanceOf(address(this));
396:                             IERC20(x33).approve(vault, balance);
397:                             IVault(vault).claimCallback(x33, balance);
398:                         }
399:                     } else {
400:                         IERC20(tokens[i]).approve(vault, balance);
401:                         IVault(vault).claimCallback(tokens[i], balance);
402:                     }
403:                 }
404:             }

['71']

71:         for (uint256 i = 0; i < ids.length; i++) {
72:             positions[i].vault = vault;
73:             positions[i].id = ids[i];
74:             positions[i].token0 = IVault(vault).token0();
75:             positions[i].token1 = IVault(vault).token1();
76:             (positions[i].amount0, positions[i].amount1) = IVault(vault).getPositionAmounts(ids[i]);
77:             if (positions[i].amount0 > 0 || positions[i].amount1 > 0) {
78:                 (positions[i].amount0Debt, positions[i].amount1Debt) = IVault(vault).getPositionDebt(ids[i]);
79:                 positions[i].debtRatio = IVault(vault).getDebtRatio(ids[i]);
80:             }
81:             positions[i].price0 = IVault(vault).getTokenPrice(positions[i].token0);
82:             positions[i].price1 = IVault(vault).getTokenPrice(positions[i].token1);
83: 
84:             IVault.PositionInfo memory positionInfo = IVault(vault).getPositionInfos(ids[i]);
85: 
86:             positions[i].positionAddress = positionInfo.positionAddress;
87:             positions[i].positionNftId = positionInfo.shadowPositionId;
88:             positions[i].tickUpper = positionInfo.tickUpper;
89:             positions[i].tickLower = positionInfo.tickLower;
90:             positions[i].ul = positionInfo.ul;
91:             positions[i].ll = positionInfo.ll;
92: 
93:             positions[i].inRange = positions[i].amount0 > 0 && positions[i].amount1 > 0;
94: 
95:             (positions[i].rewards, positions[i].rewardsAmounts) =
96:                 IPositionImpl(positionInfo.positionAddress).claimableRewards();
97: 
98:             positions[i].currentTick = IVault(vault).getCurrentTick();
99: 
100:             (,,,,, positions[i].liquidity,,,,) = INFTPositionManager(
101:                 IAddressRegistry(addressProvider).getAddress(AddressId.ADDRESS_ID_SHADOW_NONFUNGIBLE_POSITION_MANAGER)
102:             ).positions(positionInfo.shadowPositionId);
103:         }

['109']

109:         for (uint256 i = 0; i < vaults.length; i++) {
110:             positions[i] = getUserPosition(vaults[i], owner);
111:         }

[Gas-30] Assembly let var only used on once

Resolution

If a variable is only once, it makes more sense to use the value the variable holds directly

Num of instances: 1

Findings

Click to show findings

['58']

58:         assembly {
59:             
60:             
61:             
62:             calldatacopy(0, 0, calldatasize())
63: 
64:             
65:             
66:             let result := delegatecall(gas(), impl, 0, calldatasize(), 0, 0) // <= FOUND
67: 
68:             
69:             returndatacopy(0, 0, returndatasize())
70: 
71:             switch result // <= FOUND
72:             
73:             case 0 { revert(0, returndatasize()) }
74:             default { return(0, returndatasize()) }
75:         }

[Gas-31] Use assembly to validate msg.sender

Resolution

Utilizing assembly for validating msg.sender can potentially save gas as it allows for more direct and efficient access to Ethereum’s EVM opcodes, bypassing some of the overhead introduced by Solidity’s higher-level abstractions. However, this practice requires deep expertise in EVM, as incorrect implementation can introduce critical vulnerabilities. It is a trade-off between gas efficiency and code safety.

Num of instances: 9

Findings

Click to show findings

['24']

24:         require(msg.sender == _owner()); // <= FOUND

['28']

28:         require(msg.sender == lendingPool, Errors.LP_CALLER_MUST_BE_LENDING_POOL); // <= FOUND

['23']

23:         require(msg.sender == WETH9, Errors.VL_NOT_WETH9); // <= FOUND

['35']

35:         require(msg.sender == oracleManager, "Only oracle manager can call this function"); // <= FOUND

['63']

63:         require(msg.sender == vault, "Only vault can call this function"); // <= FOUND

['78']

78:         require(msg.sender == vaultRegistry, "IS"); // <= FOUND

['42']

42:         require(lendingPool == msg.sender); // <= FOUND

['248']

248:         require(positionInfo.owner == msg.sender, "NO"); // <= FOUND

['296']

296:         require(positionInfo.owner == msg.sender, "Not the owner"); // <= FOUND

[Gas-32] Simple checks for zero uint can be done using assembly to save gas

Resolution

Using assembly for simple zero checks on unsigned integers can save gas due to lower-level, optimized operations.

Resolution: Implement inline assembly with Solidity's assembly block to perform zero checks. Ensure thorough testing and verification, as assembly lacks the safety checks of high-level Solidity, potentially introducing vulnerabilities if not used carefully.

Num of instances: 3

Findings

Click to show findings

['79']

79:         require(vaultId == 0, "VAR"); // <= FOUND

['383']

383:         require(positionId != 0, "Not a position"); // <= FOUND

['142']

142:         require(amount > 0, "amount = 0");

[Gas-33] Using nested if to save gas

Resolution

Using nested if statements instead of logical AND (&&) operators can potentially save gas in Solidity contracts. When a series of conditions are connected with &&, all conditions must be evaluated even if the first one fails. In contrast, nested if statements allow for short-circuiting; if the first condition fails, the rest are skipped, saving gas. This approach is more gas-efficient, especially when dealing with complex or gas-intensive conditions. However, it's crucial to balance gas savings with code readability and maintainability, ensuring that the code remains clear and easy to understand.

Num of instances: 17

Findings

Click to show findings

['242']

242:         if (reserve.underlyingTokenAddress == WETH9 && receiveNativeETH) { // <= FOUND
243:             IExtraInterestBearingToken(reserve.eTokenAddress).burn(address(this), eTokenAmount, underlyingTokenAmount);
244:             unwrapWETH9(underlyingTokenAmount, to);
245:         }

['47']

47:        if (token == WETH9 && address(this).balance >= value) { // <= FOUND
48:             
49:             IWETH9(WETH9).deposit{value: value}(); 
50:             require(IWETH9(WETH9).transfer(recipient, value), "transfer failed");
51:         }

['115']

115:         if (block.timestamp > startTime && totalStaked > 0) { // <= FOUND
116:             uint256 dt = block.timestamp - startTime;
117: 
118:             rewardData[rewardToken].rewardPerTokenStored +=
119:                 (rewardData[rewardToken].rewardRate * dt * 1e18) / totalStaked;
120:         }

['122']

122:         if (block.timestamp > startTime && totalStaked == 0) { // <= FOUND
123:             
124:             
125:             uint256 dt = block.timestamp - startTime;
126:             totalRewards -= rewardData[rewardToken].rewardRate * dt;
127:         }

['259']

259:         if (
260:             (positionInfo.ll != 0 && currentTick < positionInfo.ll) // <= FOUND
261:                 || (positionInfo.ul != 0 && currentTick > positionInfo.ul) // <= FOUND
262:         ) {
263:             _closePosition(positionId);
264:         }

['314']

314:         if (amount0 > 0 && positionInfo.token0DebtId != 0) { // <= FOUND
315:             (uint256 currentDebt0,) = ILendingPool(lendingPool).getCurrentDebt(positionInfo.token0DebtId);
316:             if (amount0 > currentDebt0) {
317:                 amount0 = currentDebt0;
318:             }
319: 
320:             pay(token0, msg.sender, address(this), amount0);
321: 
322:             ILendingPool(lendingPool).repay(address(this), positionInfo.token0DebtId, amount0);
323:         }

['324']

324:         if (amount1 > 0 && positionInfo.token1DebtId != 0) { // <= FOUND
325:             (uint256 currentDebt1,) = ILendingPool(lendingPool).getCurrentDebt(positionInfo.token1DebtId);
326:             if (amount1 > currentDebt1) {
327:                 amount1 = currentDebt1;
328:             }

['242']

242:         if (reserve.underlyingTokenAddress == WETH9 && receiveNativeETH) { // <= FOUND

['47']

47:         if (token == WETH9 && address(this).balance >= value) { // <= FOUND

['115']

115:         if (block.timestamp > startTime && totalStaked > 0) { // <= FOUND

['122']

122:         if (block.timestamp > startTime && totalStaked == 0) { // <= FOUND

['259']

259:         if (
260:             (positionInfo.ll != 0 && currentTick < positionInfo.ll) // <= FOUND
261:                 || (positionInfo.ul != 0 && currentTick > positionInfo.ul) // <= FOUND
262:         ) {

['278']

278:         require(currentDebt0 == 0 && currentDebt1 == 0, "Still debt"); // <= FOUND

['297']

297:         require(params.reducePercentage > 0 && params.reducePercentage < 10000, "Invalid reduce percentage"); // <= FOUND

['314']

314:         if (amount0 > 0 && positionInfo.token0DebtId != 0) { // <= FOUND

['324']

324:         if (amount1 > 0 && positionInfo.token1DebtId != 0) { // <= FOUND

['93']

93:             positions[i].inRange = positions[i].amount0 > 0 && positions[i].amount1 > 0; // <= FOUND

[Gas-34] Using delete instead of setting mapping to 0 saves gas

Num of instances: 1

Findings

Click to show findings

['200']

200:                 userRewardsClaimable[msg.sender][rewardToken] = 0; // <= FOUND

[Gas-35] Stack variable cost less than state variables while used in emiting event

Resolution

When emitting events in Solidity, using stack variables (local variables within a function) instead of state variables can lead to significant gas savings. Stack variables reside in memory only for the duration of the function execution and are less costly to access compared to state variables, which are stored on the blockchain. When an event is emitted, accessing these stack variables requires less gas than fetching data from state variables, which involves reading from the contract's storage - a more expensive operation. Thus, for efficiency, prefer using local variables within functions for event emission, especially in functions that are called frequently.

Num of instances: 9

Findings

Click to show findings

['280']

280:         emit PositionClosed( // <= FOUND
281:             positionInfo.owner,
282:             vaultId, // <= FOUND
283:             positionId, // <= FOUND
284:             positionInfo.positionAddress,
285:             token0Reduced,
286:             token1Reduced,
287:             liquidity,
288:             block.timestamp
289:         );

['359']

359:         emit PositionLiquidated( // <= FOUND
360:             positionInfo.owner,
361:             vaultId, // <= FOUND
362:             positionId, // <= FOUND
363:             token0Reduced,
364:             token1Reduced,
365:             token0Fees,
366:             token1Fees,
367:             token0Left,
368:             token1Left,
369:             block.timestamp
370:         );

['388']

388:         emit RewardClaimed(positionInfos[positionId].owner, vaultId, positionId, reward, amount - feeAmount, feeAmount); // <= FOUND

['223']

223:         emit PositionOpened( // <= FOUND
224:             msg.sender,
225:             vaultId, // <= FOUND
226:             _positionId,
227:             _positionAddress,
228:             params.amount0Principal,
229:             params.amount1Principal,
230:             amount0,
231:             amount1,
232:             liquidity,
233:             block.timestamp
234:         );

['306']

306:         emit PositionReduced(positionInfo.owner, vaultId, params.positionId, reduceLiquidity, block.timestamp); // <= FOUND

['38']

38:         emit NewVault(_newVault, vaultId); // <= FOUND

['549']

549:         emit EnableVaultToBorrow(vaultId, vaultAddr); // <= FOUND

['556']

556:         emit DisableVaultToBorrow(vaultId, vaultAddr); // <= FOUND

['562']

562:         emit SetCreditsOfVault(vaultId, vaultAddr, reserveId, credit); // <= FOUND

[Gas-36] Stack variable cost less than mappings while used in emiting event

Resolution

When emitting events in Solidity, using stack variables (local variables within a function) instead of mappings can lead to significant gas savings. Stack variables reside in memory only for the duration of the function execution and are less costly to access compared to mappings, which are stored on the blockchain. When an event is emitted, accessing these stack variables requires less gas than fetching data from mappings, which involves reading from the contract's storage - a more expensive operation. Thus, for efficiency, prefer using local variables within functions for event emission, especially in functions that are called frequently.

Num of instances: 1

Findings

Click to show findings

['388']

388:         emit RewardClaimed(positionInfos[positionId].owner, vaultId, positionId, reward, amount - feeAmount, feeAmount); // <= FOUND

[Gas-37] Using constants directly, rather than caching the value, saves gas

Num of instances: 1

Findings

Click to show findings

['33']

33:         bytes32 slot = _ADMIN_SLOT; // <= FOUND

[Gas-38] Inline modifiers used only once

Num of instances: 1

Findings

Click to show findings

['72']

72:     modifier checkDeadline(uint256 deadline) { // <= FOUND
73:         require(block.timestamp < deadline, "D");
74:         _;
75:     }

[Gas-39] Solidity versions 0.8.19 and above are more gas efficient

Resolution

Solidity version 0.8.19 introduced a array of gas optimizations which make contracts which use it more efficient. Provided compatability it may be beneficial to upgrade to this version

Num of instances: 1

Findings

Click to show findings

['3']

3: pragma solidity ^0.8.0;

[Gas-40] Calling .length in a for loop wastes gas

Resolution

Rather than calling .length for an array in a for loop declaration, it is far more gas efficient to cache this length before and use that instead. This will prevent the array length from being called every loop iteration

Num of instances: 18

Findings

Click to show findings

['452']

452: for (uint256 i = 0; i < reserveIdArr.length; i++)  // <= FOUND

['452']

452: for (uint256 i = 0; i < reserveIdArr.length; i++)  // <= FOUND

['47']

47: for (uint256 i; i < rewardTokens.length; i++)  // <= FOUND

['196']

196: for (uint256 i = 0; i < rewardTokens.length; i++)  // <= FOUND

['27']

27: for (uint256 i = 0; i < initialTokens.length; i++)  // <= FOUND

['341']

341: for (uint256 i = 0; i < tokens.length; i++)  // <= FOUND

['341']

341: for (uint256 i = 0; i < tokens.length; i++)  // <= FOUND

['71']

71: for (uint256 i = 0; i < ids.length; i++)  // <= FOUND

['109']

109: for (uint256 i = 0; i < vaults.length; i++)  // <= FOUND

['452']

452: for (uint256 i = 0; i < reserveIdArr.length; i++)  // <= FOUND

['452']

452: for (uint256 i = 0; i < reserveIdArr.length; i++)  // <= FOUND

['47']

47: for (uint256 i; i < rewardTokens.length; i++)  // <= FOUND

['196']

196: for (uint256 i = 0; i < rewardTokens.length; i++)  // <= FOUND

['27']

27: for (uint256 i = 0; i < initialTokens.length; i++)  // <= FOUND

['341']

341: for (uint256 i = 0; i < tokens.length; i++)  // <= FOUND

['341']

341: for (uint256 i = 0; i < tokens.length; i++)  // <= FOUND

['71']

71: for (uint256 i = 0; i < ids.length; i++)  // <= FOUND

['109']

109: for (uint256 i = 0; i < vaults.length; i++)  // <= FOUND

[Gas-41] Internal functions only used once can be inlined to save gas

Resolution

If a internal function is only used once it doesn't make sense to modularise it unless the function which does call the function would be overly long and complex otherwise

Num of instances: 4

Findings

Click to show findings

['69']

69:     function initReserve(address asset) external onlyOwner notPaused  // <= FOUND

['362']

362:     function initReserve( // <= FOUND
363:         DataTypes.ReserveData storage reserveData,
364:         address underlyingTokenAddress,
365:         address eTokenAddress,
366:         uint256 reserveCapacity,
367:         uint256 id
368:     ) internal 

['384']

384:     function createStakingPoolForReserve(uint256 reserveId) internal  // <= FOUND

['391']

391:     function _createNewPosition() internal returns (uint256 newPositionId, address positionAddress)  // <= FOUND

[Gas-42] Constructors can be marked as payable to save deployment gas

Num of instances: 8

Findings

Click to show findings

['10']

10:     constructor(address _weth9) {
11:         setAddress(AddressId.ADDRESS_ID_WETH9, _weth9);
12:     }

['12']

12:     constructor(uint256 _positionId) {
13:         assert(_ADMIN_SLOT == bytes32(uint256(keccak256("eip1967.proxy.admin")) - 1));
14:         _setOwner(msg.sender);
15: 
16:         address impl = IPositionImplGetter(_owner()).positionImplementation();
17:         (bool success,) = impl.delegatecall(abi.encodeWithSignature("initialize(uint256)", _positionId));
18:         require(success);
19:     }

['32']

32:     constructor(string memory name_, string memory symbol_, uint8 decimals_, address underlyingAsset_)
33:         ERC20(name_, symbol_)
34:     {
35:         _decimals = decimals_;
36: 
37:         require(underlyingAsset_ != address(0), Errors.VL_ADDRESS_CANNOT_ZERO);
38:         underlyingAsset = underlyingAsset_;
39:         lendingPool = msg.sender;
40:     }

['18']

18:     constructor(address _WETH9) {
19:         WETH9 = _WETH9;
20:     }

['62']

62:     constructor(address _stakingToken) {
63:         stakedToken = IERC20(_stakingToken);
64:         lendingPool = msg.sender;
65:     }

['8']

8:     constructor(uint256 minDelay, address[] memory proposers, address[] memory executors, address admin)
9:         TimelockController(minDelay, proposers, executors, admin)
10:     {
11:         
12:     }

['19']

19:     constructor(address _addressRegistry) {
20:         require(_addressRegistry != address(0), Errors.VL_ADDRESS_CANNOT_ZERO);
21:         addressRegistry = _addressRegistry;
22:         nextVaultID = 1;
23:     }

['34']

34:     constructor(address _addressProvider) {
35:         addressProvider = _addressProvider;
36:     }

[Gas-43] Assigning to structs can be more efficient

Resolution

Rather defining the struct in a single line, it is more efficient to declare an empty struct and then assign each struct element individually. This can net quite a large gas saving of 130 per instance.

Num of instances: 5

Findings

Click to show findings

['83']

83:     function openPosition(OpenPositionParams memory params) // <= FOUND
84:         external
85:         onlyVault
86:         returns (uint256 tokenId, uint128 liquidity, uint256 amount0, uint256 amount1)
87:     {
88:         uint256 token0Balance = IERC20(token0).balanceOf(address(this));
89:         if (params.amount0Desired > token0Balance) {
90:             _swapTokenExactInput(
91:                 token1,
92:                 token0,
93:                 IERC20(token1).balanceOf(address(this)) - params.amount1Desired,
94:                 params.amount0Desired - token0Balance
95:             );
96:         }
97: 
98:         uint256 token1Balance = IERC20(token1).balanceOf(address(this));
99:         if (params.amount1Desired > token1Balance) {
100:             _swapTokenExactInput(
101:                 token0,
102:                 token1,
103:                 IERC20(token0).balanceOf(address(this)) - params.amount0Desired,
104:                 params.amount1Desired - token1Balance
105:             );
106:         }
107:         address shadowNonfungiblePositionManager = getShadowNonfungiblePositionManager();
108: 
109:         IERC20(token0).approve(shadowNonfungiblePositionManager, params.amount0Desired);
110:         IERC20(token1).approve(shadowNonfungiblePositionManager, params.amount1Desired);
111: 
112:         (tokenId, liquidity, amount0, amount1) = IShadowNonfungiblePositionManager(shadowNonfungiblePositionManager) // <= FOUND
113:             .mint(
114:             IShadowNonfungiblePositionManager.MintParams({
115:                 token0: token0,
116:                 token1: token1,
117:                 tickSpacing: tickSpacing,
118:                 tickLower: params.tickLower,
119:                 tickUpper: params.tickUpper,
120:                 amount0Desired: params.amount0Desired,
121:                 amount1Desired: params.amount1Desired,
122:                 amount0Min: 0,
123:                 amount1Min: 0,
124:                 recipient: address(this),
125:                 deadline: block.timestamp
126:             })
127:         );
128: 
129:         shadowPositionId = tokenId;
130:         positionOwner = params.positionOwner;
131: 
132:         unwrapWETH9(0, positionOwner);
133: 
134:         token0Balance = IERC20(token0).balanceOf(address(this));
135:         token1Balance = IERC20(token1).balanceOf(address(this));
136: 
137:         if (token0Balance > 0) {
138:             pay(token0, address(this), positionOwner, token0Balance);
139:         }
140:         if (token1Balance > 0) {
141:             pay(token1, address(this), positionOwner, token1Balance);
142:         }
143:     }

['254']

254:     function liquidatePosition(address caller) // <= FOUND
255:         external
256:         onlyVault
257:         returns (
258:             uint128 liquidity,
259:             uint256 token0Reduced,
260:             uint256 token1Reduced,
261:             uint256 token0Fees,
262:             uint256 token1Fees,
263:             uint256 token0Left,
264:             uint256 token1Left
265:         )
266:     {
267:         _claimFees();
268:         _claimRewards();
269:         (,,,,, liquidity,,,,) =
270:             IShadowNonfungiblePositionManager(getShadowNonfungiblePositionManager()).positions(shadowPositionId);
271: 
272:         (token0Reduced, token1Reduced) = _decreasePosition(liquidity);
273:         (uint256 currentDebt0, uint256 currentDebt1) = IVault(vault).getPositionDebt(positionId);
274: 
275:         LiquidationFeeVars memory vars = LiquidationFeeVars({ // <= FOUND
276:             liquidationFee: IVault(vault).liquidationFee(),
277:             liquidationCallerFee: IVault(vault).liquidationCallerFee(),
278:             liquidationFeeRecipient: IVault(vault).liquidationFeeRecipient()
279:         });
280:         
281:         if (token0Reduced > 0) {
282:             token0Fees = token0Reduced * vars.liquidationFee / 10000;
283:             pay(token0, address(this), vars.liquidationFeeRecipient, token0Fees);
284: 
285:             uint256 token0CallerFees = token0Fees * vars.liquidationCallerFee / 10000;
286:             if (token0CallerFees > 0) {
287:                 pay(token0, address(this), caller, token0CallerFees);
288:             }
289: 
290:             token0Reduced = token0Reduced - token0Fees - token0CallerFees;
291:         }
292: 
293:         if (token1Reduced > 0) {
294:             token1Fees = token1Reduced * vars.liquidationFee / 10000;
295: 
296:             pay(token1, address(this), vars.liquidationFeeRecipient, token1Fees);
297: 
298:             uint256 token1CallerFees = token1Fees * vars.liquidationCallerFee / 10000;
299:             if (token1CallerFees > 0) {
300:                 pay(token1, address(this), caller, token1CallerFees);
301:             }
302: 
303:             token1Reduced = token1Reduced - token1Fees - token1CallerFees;
304:         }
305: 
306:         if (currentDebt0 > 0) {
307:             
308:             if (currentDebt0 > token0Reduced) {
309:                 uint256 amount1Excess = token1Reduced - currentDebt1;
310:                 _swapTokenExactInput(token1, token0, amount1Excess, currentDebt0 - token0Reduced);
311:             }
312:             IERC20(token0).approve(vault, currentDebt0);
313:         }
314: 
315:         if (currentDebt1 > 0) {
316:             
317:             if (currentDebt1 > token1Reduced) {
318:                 uint256 amount0Excess = token0Reduced - currentDebt0;
319:                 _swapTokenExactInput(token0, token1, amount0Excess, currentDebt1 - token1Reduced);
320:             }
321:             IERC20(token1).approve(vault, currentDebt1);
322:         }
323: 
324:         IVault(vault).repayExact(positionId, currentDebt0, currentDebt1);
325: 
326:         token0Left = IERC20(token0).balanceOf(address(this));
327:         token1Left = IERC20(token1).balanceOf(address(this));
328:         if (token0Left > 0) {
329:             pay(token0, address(this), positionOwner, token0Left);
330:         }
331:         if (token1Left > 0) {
332:             pay(token1, address(this), positionOwner, token1Left);
333:         }
334:     }

['432']

432:     function _swapTokenExactInput(address tokenIn, address tokenOut, uint256 amountIn, uint256 amountOutMinimum) // <= FOUND
433:         internal
434:         returns (uint256 amountOut)
435:     {
436:         address router =
437:             IAddressRegistry(IVault(vault).addressProvider()).getAddress(AddressId.ADDRESS_ID_SHADOW_ROUTER);
438:         IERC20(tokenIn).approve(router, amountIn);
439: 
440:         amountOut = IShadowSwapRouter(router).exactInputSingle( // <= FOUND
441:             IShadowSwapRouter.ExactInputSingleParams({
442:                 tokenIn: tokenIn,
443:                 tokenOut: tokenOut,
444:                 tickSpacing: tickSpacing,
445:                 recipient: address(this),
446:                 deadline: block.timestamp,
447:                 amountIn: amountIn,
448:                 amountOutMinimum: amountOutMinimum,
449:                 sqrtPriceLimitX96: 0
450:             })
451:         );
452:     }

['454']

454:     function _swapTokenExactOutput(address tokenIn, address tokenOut, uint256 amountOut, uint256 amountInMaximum) // <= FOUND
455:         internal
456:         returns (uint256 amountIn)
457:     {
458:         address router =
459:             IAddressRegistry(IVault(vault).addressProvider()).getAddress(AddressId.ADDRESS_ID_SHADOW_ROUTER);
460:         IERC20(tokenIn).approve(router, amountInMaximum);
461: 
462:         amountIn = IShadowSwapRouter(router).exactOutputSingle( // <= FOUND
463:             IShadowSwapRouter.ExactOutputSingleParams({
464:                 tokenIn: tokenIn,
465:                 tokenOut: tokenOut,
466:                 tickSpacing: tickSpacing,
467:                 recipient: address(this),
468:                 deadline: block.timestamp,
469:                 amountOut: amountOut,
470:                 amountInMaximum: amountInMaximum,
471:                 sqrtPriceLimitX96: 0
472:             })
473:         );
474:     }

['152']

152:     function openPosition(OpenPositionParams calldata params) // <= FOUND
153:         external
154:         payable
155:         nonReentrant
156:         checkDeadline(params.deadline)
157:     {
158:         (uint256 _positionId, address _positionAddress) = _createNewPosition();
159: 
160:         PositionInfo storage positionInfo = positionInfos[_positionId];
161:         positionInfo.owner = msg.sender;
162:         positionInfo.vaultId = vaultId;
163:         positionInfo.positionAddress = _positionAddress;
164:         positionInfo.positionId = _positionId;
165:         positionIds[msg.sender].push(_positionId);
166:         positionAddressToId[_positionAddress] = _positionId;
167: 
168:         
169:         if (params.amount0Principal > 0) {
170:             pay(token0, msg.sender, address(this), params.amount0Principal);
171:         }
172:         if (params.amount1Principal > 0) {
173:             pay(token1, msg.sender, address(this), params.amount1Principal);
174:         }
175: 
176:         
177:         address lendingPool = getLendingPool();
178:         if (params.amount0Borrow > 0) {
179:             positionInfo.token0DebtId = ILendingPool(lendingPool).newDebtPosition(token0ReserveId);
180: 
181:             ILendingPool(lendingPool).borrow(address(this), positionInfo.token0DebtId, params.amount0Borrow);
182:         }
183:         if (params.amount1Borrow > 0) {
184:             positionInfo.token1DebtId = ILendingPool(lendingPool).newDebtPosition(token1ReserveId);
185: 
186:             ILendingPool(lendingPool).borrow(address(this), positionInfo.token1DebtId, params.amount1Borrow);
187:         }
188: 
189:         require(
190:             getDebtRatioFromAmounts(
191:                 params.amount0Principal, params.amount1Principal, params.amount0Borrow, params.amount1Borrow
192:             ) < liquidationDebtRatio,
193:             "Borrow value is too high"
194:         );
195: 
196:         if (params.amount0Principal > 0 || params.amount0Borrow > 0) {
197:             pay(token0, address(this), positionInfo.positionAddress, params.amount0Principal + params.amount0Borrow);
198:         }
199:         if (params.amount1Principal > 0 || params.amount1Borrow > 0) {
200:             pay(token1, address(this), positionInfo.positionAddress, params.amount1Principal + params.amount1Borrow);
201:         }
202:         
203:         (uint256 tokenId, uint128 liquidity, uint256 amount0, uint256 amount1) = IShadowRangePositionImpl( // <= FOUND
204:             positionInfo.positionAddress
205:         ).openPosition(
206:             IShadowRangePositionImpl.OpenPositionParams({
207:                 amount0Desired: params.amount0Desired,
208:                 amount1Desired: params.amount1Desired,
209:                 tickLower: params.tickLower,
210:                 tickUpper: params.tickUpper,
211:                 positionOwner: msg.sender
212:             })
213:         );
214: 
215:         require(getPositionValueByNftId(tokenId) > minPositionSize, "PVL");
216: 
217:         positionInfo.shadowPositionId = tokenId;
218:         positionInfo.tickLower = params.tickLower;
219:         positionInfo.tickUpper = params.tickUpper;
220:         positionInfo.ul = params.ul;
221:         positionInfo.ll = params.ll;
222: 
223:         emit PositionOpened(
224:             msg.sender,
225:             vaultId,
226:             _positionId,
227:             _positionAddress,
228:             params.amount0Principal,
229:             params.amount1Principal,
230:             amount0,
231:             amount1,
232:             liquidity,
233:             block.timestamp
234:         );
235: 
236:         if (msg.value > 0) {
237:             refundETH();
238:         }
239:         require(getDebtRatio(_positionId) < liquidationDebtRatio, "DRH");
240:     }

[Gas-44] Internal functions never used once can be removed

Resolution

Internal functions which are never used use unnecessary gas and should be safely removed.

Num of instances: 2

Findings

Click to show findings

['55']

55:     function _delegate() internal  // <= FOUND

['454']

454:     function _swapTokenExactOutput(address tokenIn, address tokenOut, uint256 amountOut, uint256 amountInMaximum) // <= FOUND
455:         internal
456:         returns (uint256 amountIn)
457:     

[Gas-45] Empty functions should be removed to save gas

Num of instances: 1

Findings

Click to show findings

['207']

207:     function update() external updateReward(address(0)) onlyOwner {}

[Gas-46] Only emit event in setter function if the state variable was changed

Resolution

Emitting events in setter functions of smart contracts only when state variables change saves gas. This is because emitting events consumes gas, and unnecessary events, where no actual state change occurs, lead to wasteful consumption.

Num of instances: 5

Findings

Click to show findings

['14']

14:     function setAddress(uint256 id, address _addr) public onlyOwner { // <= FOUND
15:         libraryAndContractAddresses[id] = _addr;
16:         emit SetAddress(_msgSender(), id, _addr); // <= FOUND
17:     }

['559']

559:     function setCreditsOfVault(uint256 vaultId, uint256 reserveId, uint256 credit) external onlyOwner notPaused { // <= FOUND
560:         address vaultAddr = getVault(vaultId);
561:         credits[reserveId][vaultAddr] = credit;
562:         emit SetCreditsOfVault(vaultId, vaultAddr, reserveId, credit); // <= FOUND
563:     }

['602']

602:     function setReserveFeeRate(uint256 reserveId, uint16 _rate) public onlyOwner notPaused { // <= FOUND
603:         require(_rate <= Constants.PERCENT_100, "invalid percent");
604:         DataTypes.ReserveData storage reserve = reserves[reserveId];
605:         reserve.reserveFeeRate = _rate;
606: 
607:         emit SetReserveFeeRate(reserveId, _rate); // <= FOUND
608:     }

['610']

610:     function setBorrowingRateConfig( // <= FOUND
611:         uint256 reserveId,
612:         uint16 utilizationA,
613:         uint16 borrowingRateA,
614:         uint16 utilizationB,
615:         uint16 borrowingRateB,
616:         uint16 maxBorrowingRate
617:     ) public onlyOwner notPaused {
618:         DataTypes.ReserveData storage reserve = reserves[reserveId];
619:         setBorrowingRateConfig(reserve, utilizationA, borrowingRateA, utilizationB, borrowingRateB, maxBorrowingRate);
620: 
621:         emit SetInterestRateConfig( // <= FOUND
622:             reserveId, utilizationA, borrowingRateA, utilizationB, borrowingRateB, maxBorrowingRate
623:         );
624:     }

['626']

626:     function setReserveCapacity(uint256 reserveId, uint256 cap) public onlyOwner notPaused { // <= FOUND
627:         DataTypes.ReserveData storage reserve = reserves[reserveId];
628: 
629:         reserve.reserveCapacity = cap;
630:         emit SetReserveCapacity(reserveId, cap); // <= FOUND
631:     }

[Gas-47] Use OZ Array.unsafeAccess() to avoid repeated array length checks

Resolution

The OpenZeppelin Array.unsafeAccess() method is a optimization strategy for Solidity, aimed at reducing gas costs by bypassing automatic length checks on storage array accesses. In Solidity, every access to an array element involves a hidden gas cost due to a length check, ensuring that accesses do not exceed the array bounds. However, if a developer has already verified the array's bounds earlier in the function or knows through logic that the access is safe, directly accessing the array elements without redundant length checks can save gas. This approach requires careful consideration to avoid out-of-bounds errors, as it trades off safety checks for efficiency.

Num of instances: 41

Findings

Click to show findings

['453']

453:             statusArr[i].reserveId = reserveIdArr[i]; // <= FOUND

['454']

454:             statusArr[i].underlyingTokenAddress = reserves[reserveIdArr[i]].underlyingTokenAddress; // <= FOUND

['455']

455:             statusArr[i].eTokenAddress = reserves[reserveIdArr[i]].eTokenAddress; // <= FOUND

['456']

456:             statusArr[i].stakingAddress = reserves[reserveIdArr[i]].stakingAddress; // <= FOUND

['457']

457:             (statusArr[i].totalLiquidity, statusArr[i].totalBorrows) = // <= FOUND
458:                 reserves[reserveIdArr[i]].totalLiquidityAndBorrows(); // <= FOUND

['459']

459:             statusArr[i].exchangeRate = reserves[reserveIdArr[i]].eTokenToReserveExchangeRate(); // <= FOUND

['460']

460:             statusArr[i].borrowingRate = reserves[reserveIdArr[i]].borrowingRate(); // <= FOUND

['473']

473:             statusArr[i].user = user; // <= FOUND

['474']

474:             statusArr[i].eTokenStaked = IStakingRewards(reserves[reserveIdArr[i]].stakingAddress).balanceOf(user); // <= FOUND

['475']

475:             statusArr[i].eTokenUnStaked = IERC20(reserves[reserveIdArr[i]].eTokenAddress).balanceOf(user); // <= FOUND

['476']

476:             statusArr[i].liquidity = statusArr[i].eTokenStaked.add(statusArr[i].eTokenUnStaked).mul( // <= FOUND
477:                 reserves[reserveIdArr[i]].eTokenToReserveExchangeRate() // <= FOUND
478:             ).div(Precision.FACTOR1E18);

['48']

48:             address rewardToken = rewardTokens[i]; // <= FOUND

['28']

28:             pythPriceIds[initialTokens[i]] = priceIds[i]; // <= FOUND

['342']

342:                 amounts[i] = IShadowGaugeV3(shadowGauge).earned(tokens[i], shadowPositionId); // <= FOUND

['383']

383:                 uint256 balance = IERC20(tokens[i]).balanceOf(address(this)); // <= FOUND

['385']

385:                     if (tokens[i] == xShadow) { // <= FOUND

['393']

393:                             IERC20(tokens[i]).approve(x33Adapter, balance); // <= FOUND

['400']

400:                         IERC20(tokens[i]).approve(vault, balance); // <= FOUND

['401']

401:                         IVault(vault).claimCallback(tokens[i], balance); // <= FOUND

['72']

72:             positions[i].vault = vault; // <= FOUND

['73']

73:             positions[i].id = ids[i]; // <= FOUND

['74']

74:             positions[i].token0 = IVault(vault).token0(); // <= FOUND

['75']

75:             positions[i].token1 = IVault(vault).token1(); // <= FOUND

['76']

76:             (positions[i].amount0, positions[i].amount1) = IVault(vault).getPositionAmounts(ids[i]); // <= FOUND

['77']

77:             if (positions[i].amount0 > 0 || positions[i].amount1 > 0) { // <= FOUND

['78']

78:                 (positions[i].amount0Debt, positions[i].amount1Debt) = IVault(vault).getPositionDebt(ids[i]); // <= FOUND

['79']

79:                 positions[i].debtRatio = IVault(vault).getDebtRatio(ids[i]); // <= FOUND

['81']

81:             positions[i].price0 = IVault(vault).getTokenPrice(positions[i].token0); // <= FOUND

['82']

82:             positions[i].price1 = IVault(vault).getTokenPrice(positions[i].token1); // <= FOUND

['84']

84:             IVault.PositionInfo memory positionInfo = IVault(vault).getPositionInfos(ids[i]); // <= FOUND

['86']

86:             positions[i].positionAddress = positionInfo.positionAddress; // <= FOUND

['87']

87:             positions[i].positionNftId = positionInfo.shadowPositionId; // <= FOUND

['88']

88:             positions[i].tickUpper = positionInfo.tickUpper; // <= FOUND

['89']

89:             positions[i].tickLower = positionInfo.tickLower; // <= FOUND

['90']

90:             positions[i].ul = positionInfo.ul; // <= FOUND

['91']

91:             positions[i].ll = positionInfo.ll; // <= FOUND

['93']

93:             positions[i].inRange = positions[i].amount0 > 0 && positions[i].amount1 > 0; // <= FOUND

['95']

95:             (positions[i].rewards, positions[i].rewardsAmounts) = // <= FOUND
96:                 IPositionImpl(positionInfo.positionAddress).claimableRewards();

['98']

98:             positions[i].currentTick = IVault(vault).getCurrentTick(); // <= FOUND

['100']

100:             (,,,,, positions[i].liquidity,,,,) = INFTPositionManager( // <= FOUND
101:                 IAddressRegistry(addressProvider).getAddress(AddressId.ADDRESS_ID_SHADOW_NONFUNGIBLE_POSITION_MANAGER)
102:             ).positions(positionInfo.shadowPositionId);

['110']

110:             positions[i] = getUserPosition(vaults[i], owner); // <= FOUND

[Gas-48] State variable written in a loop

Resolution

To optimize gas consumption in smart contracts, especially when dealing with loops that write to state variables, a recommended approach is to use a local (memory) variable within the loop for calculations and then update the state variable only once after the loop completes. This method reduces the number of state changes, each of which costs gas, thereby making the operation more gas-efficient.

Num of instances: 2

Findings

Click to show findings

['71']

71:        for (uint256 i = 0; i < ids.length; i++) {
72:             positions[i].vault = vault; // <= FOUND
73:             positions[i].id = ids[i];
74:             positions[i].token0 = IVault(vault).token0();
75:             positions[i].token1 = IVault(vault).token1();
76:             (positions[i].amount0, positions[i].amount1) = IVault(vault).getPositionAmounts(ids[i]);
77:             if (positions[i].amount0 > 0 || positions[i].amount1 > 0) {
78:                 (positions[i].amount0Debt, positions[i].amount1Debt) = IVault(vault).getPositionDebt(ids[i]);
79:                 positions[i].debtRatio = IVault(vault).getDebtRatio(ids[i]);
80:             }
81:             positions[i].price0 = IVault(vault).getTokenPrice(positions[i].token0);
82:             positions[i].price1 = IVault(vault).getTokenPrice(positions[i].token1);
83: 
84:             IVault.PositionInfo memory positionInfo = IVault(vault).getPositionInfos(ids[i]);
85: 
86:             positions[i].positionAddress = positionInfo.positionAddress;
87:             positions[i].positionNftId = positionInfo.shadowPositionId;
88:             positions[i].tickUpper = positionInfo.tickUpper;
89:             positions[i].tickLower = positionInfo.tickLower;
90:             positions[i].ul = positionInfo.ul;
91:             positions[i].ll = positionInfo.ll;
92: 
93:             positions[i].inRange = positions[i].amount0 > 0 && positions[i].amount1 > 0;
94: 
95:             (positions[i].rewards, positions[i].rewardsAmounts) =
96:                 IPositionImpl(positionInfo.positionAddress).claimableRewards();
97: 
98:             positions[i].currentTick = IVault(vault).getCurrentTick();
99: 
100:             (,,,,, positions[i].liquidity,,,,) = INFTPositionManager(
101:                 IAddressRegistry(addressProvider).getAddress(AddressId.ADDRESS_ID_SHADOW_NONFUNGIBLE_POSITION_MANAGER)
102:             ).positions(positionInfo.shadowPositionId);
103:         }

['71']

71:         for (uint256 i = 0; i < ids.length; i++) {
72:             positions[i].vault = vault; // <= FOUND
73:             positions[i].id = ids[i];
74:             positions[i].token0 = IVault(vault).token0();
75:             positions[i].token1 = IVault(vault).token1();
76:             (positions[i].amount0, positions[i].amount1) = IVault(vault).getPositionAmounts(ids[i]);
77:             if (positions[i].amount0 > 0 || positions[i].amount1 > 0) {
78:                 (positions[i].amount0Debt, positions[i].amount1Debt) = IVault(vault).getPositionDebt(ids[i]);
79:                 positions[i].debtRatio = IVault(vault).getDebtRatio(ids[i]);
80:             }
81:             positions[i].price0 = IVault(vault).getTokenPrice(positions[i].token0);
82:             positions[i].price1 = IVault(vault).getTokenPrice(positions[i].token1);
83: 
84:             IVault.PositionInfo memory positionInfo = IVault(vault).getPositionInfos(ids[i]);
85: 
86:             positions[i].positionAddress = positionInfo.positionAddress;
87:             positions[i].positionNftId = positionInfo.shadowPositionId;
88:             positions[i].tickUpper = positionInfo.tickUpper;
89:             positions[i].tickLower = positionInfo.tickLower;
90:             positions[i].ul = positionInfo.ul;
91:             positions[i].ll = positionInfo.ll;
92: 
93:             positions[i].inRange = positions[i].amount0 > 0 && positions[i].amount1 > 0;
94: 
95:             (positions[i].rewards, positions[i].rewardsAmounts) =
96:                 IPositionImpl(positionInfo.positionAddress).claimableRewards();
97: 
98:             positions[i].currentTick = IVault(vault).getCurrentTick();
99: 
100:             (,,,,, positions[i].liquidity,,,,) = INFTPositionManager(
101:                 IAddressRegistry(addressProvider).getAddress(AddressId.ADDRESS_ID_SHADOW_NONFUNGIBLE_POSITION_MANAGER)
102:             ).positions(positionInfo.shadowPositionId);
103:         }

[Gas-49] State variable read in a loop

Resolution

Reading a state variable inside a loop in a smart contract can unnecessarily increase gas consumption, as each read operation from the blockchain state is costly. This inefficiency becomes pronounced in loops with many iterations. To optimize gas usage, it's advisable to read the state variable once before the loop starts, store its value in a local (memory) variable, and then use this local variable within the loop. This approach minimizes the number of state read operations, thereby reducing the gas cost associated with executing the contract function, making the smart contract more efficient and cost-effective to run.

Num of instances: 4

Findings

Click to show findings

['341']

341:             for (uint256 i = 0; i < tokens.length; i++) {
342:                 amounts[i] = IShadowGaugeV3(shadowGauge).earned(tokens[i], shadowPositionId); // <= FOUND
343:             }

['71']

71:        for (uint256 i = 0; i < ids.length; i++) {
72:             positions[i].vault = vault; // <= FOUND
73:             positions[i].id = ids[i];
74:             positions[i].token0 = IVault(vault).token0();
75:             positions[i].token1 = IVault(vault).token1();
76:             (positions[i].amount0, positions[i].amount1) = IVault(vault).getPositionAmounts(ids[i]);
77:             if (positions[i].amount0 > 0 || positions[i].amount1 > 0) {
78:                 (positions[i].amount0Debt, positions[i].amount1Debt) = IVault(vault).getPositionDebt(ids[i]);
79:                 positions[i].debtRatio = IVault(vault).getDebtRatio(ids[i]);
80:             }
81:             positions[i].price0 = IVault(vault).getTokenPrice(positions[i].token0);
82:             positions[i].price1 = IVault(vault).getTokenPrice(positions[i].token1);
83: 
84:             IVault.PositionInfo memory positionInfo = IVault(vault).getPositionInfos(ids[i]);
85: 
86:             positions[i].positionAddress = positionInfo.positionAddress;
87:             positions[i].positionNftId = positionInfo.shadowPositionId;
88:             positions[i].tickUpper = positionInfo.tickUpper;
89:             positions[i].tickLower = positionInfo.tickLower;
90:             positions[i].ul = positionInfo.ul;
91:             positions[i].ll = positionInfo.ll;
92: 
93:             positions[i].inRange = positions[i].amount0 > 0 && positions[i].amount1 > 0;
94: 
95:             (positions[i].rewards, positions[i].rewardsAmounts) =
96:                 IPositionImpl(positionInfo.positionAddress).claimableRewards();
97: 
98:             positions[i].currentTick = IVault(vault).getCurrentTick();
99: 
100:             (,,,,, positions[i].liquidity,,,,) = INFTPositionManager(
101:                 IAddressRegistry(addressProvider).getAddress(AddressId.ADDRESS_ID_SHADOW_NONFUNGIBLE_POSITION_MANAGER)
102:             ).positions(positionInfo.shadowPositionId);
103:         }

['71']

71:         for (uint256 i = 0; i < ids.length; i++) {
72:             positions[i].vault = vault; // <= FOUND
73:             positions[i].id = ids[i];
74:             positions[i].token0 = IVault(vault).token0();
75:             positions[i].token1 = IVault(vault).token1();
76:             (positions[i].amount0, positions[i].amount1) = IVault(vault).getPositionAmounts(ids[i]);
77:             if (positions[i].amount0 > 0 || positions[i].amount1 > 0) {
78:                 (positions[i].amount0Debt, positions[i].amount1Debt) = IVault(vault).getPositionDebt(ids[i]);
79:                 positions[i].debtRatio = IVault(vault).getDebtRatio(ids[i]);
80:             }
81:             positions[i].price0 = IVault(vault).getTokenPrice(positions[i].token0);
82:             positions[i].price1 = IVault(vault).getTokenPrice(positions[i].token1);
83: 
84:             IVault.PositionInfo memory positionInfo = IVault(vault).getPositionInfos(ids[i]);
85: 
86:             positions[i].positionAddress = positionInfo.positionAddress;
87:             positions[i].positionNftId = positionInfo.shadowPositionId;
88:             positions[i].tickUpper = positionInfo.tickUpper;
89:             positions[i].tickLower = positionInfo.tickLower;
90:             positions[i].ul = positionInfo.ul;
91:             positions[i].ll = positionInfo.ll;
92: 
93:             positions[i].inRange = positions[i].amount0 > 0 && positions[i].amount1 > 0;
94: 
95:             (positions[i].rewards, positions[i].rewardsAmounts) =
96:                 IPositionImpl(positionInfo.positionAddress).claimableRewards();
97: 
98:             positions[i].currentTick = IVault(vault).getCurrentTick();
99: 
100:             (,,,,, positions[i].liquidity,,,,) = INFTPositionManager(
101:                 IAddressRegistry(addressProvider).getAddress(AddressId.ADDRESS_ID_SHADOW_NONFUNGIBLE_POSITION_MANAGER)
102:             ).positions(positionInfo.shadowPositionId);
103:         }

['382']

382:             for (uint256 i = 0; i < tokens.length; i++) {
383:                 uint256 balance = IERC20(tokens[i]).balanceOf(address(this));
384:                 if (balance > 0) {
385:                     if (tokens[i] == xShadow) { // <= FOUND
386:                         
387:                         
388:                         
389:                         
390:                         
391:                         if (IShadowX33(x33).isUnlocked()) {
392:                             address x33Adapter = getX33Adapter();
393:                             IERC20(tokens[i]).approve(x33Adapter, balance);
394:                             IERC4626(x33Adapter).deposit(balance, address(this));
395:                             balance = IERC20(x33).balanceOf(address(this));
396:                             IERC20(x33).approve(vault, balance);
397:                             IVault(vault).claimCallback(x33, balance);
398:                         }
399:                     } else {
400:                         IERC20(tokens[i]).approve(vault, balance);
401:                         IVault(vault).claimCallback(tokens[i], balance);
402:                     }
403:                 }
404:             }

[Gas-50] Use uint256(1)/uint256(2) instead of true/false to save gas for changes

Resolution

In Solidity, the use of uint256 values instead of boolean for certain state variables can result in gas savings. This is due to how Ethereum's storage optimization works: changing a variable from 0 to a non-zero value (like flipping false to true) incurs a higher gas cost compared to modifying an already non-zero value. By using uint256 with values 1 and 2 instead of true and false, you avoid the higher cost associated with the 0 to non-zero change, since 1 and 2 are both non-zero. This approach is notably used in OpenZeppelin's ReentrancyGuard as a gas optimization technique. However, this should be applied where it makes sense and where gas optimization is critical, as it can decrease code readability.

Num of instances: 7

Findings

Click to show findings

['536']

536:         paused = true; // <= FOUND

['548']

548:         borrowingWhiteList[vaultAddr] = true; // <= FOUND

['107']

107:             inRewardsTokenList[rewardToken] = true; // <= FOUND

['37']

37:         isRegistered[_newVault] = true; // <= FOUND

['50']

50:     bool public paused = false; // <= FOUND

['541']

541:         paused = false; // <= FOUND

['555']

555:         borrowingWhiteList[vaultAddr] = false; // <= FOUND

[Gas-51] Avoid emitting events in loops

Resolution

Emitting events inside loops can significantly increase gas costs in Ethereum smart contracts, as each event emission consumes gas. This practice can quickly escalate transaction fees, especially with a high number of iterations. To optimize for efficiency and cost, it's advisable to minimize event emissions within loops, possibly aggregating data to emit a single event post-loop or reconsidering the design to reduce looped emissions. This approach helps maintain manageable transaction costs and enhances contract performance.

Num of instances: 4

Findings

Click to show findings

['196']

196:        for (uint256 i = 0; i < rewardTokens.length; i++) {
197:             address rewardToken = rewardTokens[i];
198:             uint256 claimable = userRewardsClaimable[msg.sender][rewardToken];
199:             if (claimable > 0) {
200:                 userRewardsClaimable[msg.sender][rewardToken] = 0;
201:                 require(IERC20(rewardToken).transfer(msg.sender, claimable), "transfer failed");
202:                 emit RewardPaid(msg.sender, rewardToken, claimable); // <= FOUND
203:             }
204:         }

['196']

196:         for (uint256 i = 0; i < rewardTokens.length; i++) {
197:             address rewardToken = rewardTokens[i];
198:             uint256 claimable = userRewardsClaimable[msg.sender][rewardToken];
199:             if (claimable > 0) {
200:                 userRewardsClaimable[msg.sender][rewardToken] = 0;
201:                 require(IERC20(rewardToken).transfer(msg.sender, claimable), "transfer failed");
202:                 emit RewardPaid(msg.sender, rewardToken, claimable); // <= FOUND
203:             }
204:         }

['196']

196:        for (uint256 i = 0; i < rewardTokens.length; i++) {
197:             address rewardToken = rewardTokens[i];
198:             uint256 claimable = userRewardsClaimable[msg.sender][rewardToken];
199:             if (claimable > 0) {
200:                 userRewardsClaimable[msg.sender][rewardToken] = 0;
201:                 require(IERC20(rewardToken).transfer(msg.sender, claimable), "transfer failed");
202:                 emit RewardPaid(msg.sender, rewardToken, claimable); // <= FOUND
203:             }
204:         }

['196']

196:         for (uint256 i = 0; i < rewardTokens.length; i++) {
197:             address rewardToken = rewardTokens[i];
198:             uint256 claimable = userRewardsClaimable[msg.sender][rewardToken];
199:             if (claimable > 0) {
200:                 userRewardsClaimable[msg.sender][rewardToken] = 0;
201:                 require(IERC20(rewardToken).transfer(msg.sender, claimable), "transfer failed");
202:                 emit RewardPaid(msg.sender, rewardToken, claimable); // <= FOUND
203:             }
204:         }

[Gas-52] Consider pre-calculating the address of address(this) to save gas

Resolution

Consider saving the address(this) value within a constant using foundry's script.sol or solady's LibRlp.sol to save gas

Num of instances: 50

Findings

Click to show findings

['130']

130:         eTokenAmount = _deposit(reserveId, amount, address(this)); // <= FOUND

['174']

174:         
175:         IERC20(reserve.eTokenAddress).safeTransferFrom(_msgSender(), address(this), eTokenAmount); // <= FOUND

['196']

196:         IStakingRewards(stakingPool).withdrawByLendingPool(eTokenAmount, _msgSender(), address(this)); // <= FOUND

['243']

243:             IExtraInterestBearingToken(reserve.eTokenAddress).burn(address(this), eTokenAmount, underlyingTokenAmount); // <= FOUND

['27']

27:         uint256 balanceWETH9 = IWETH9(WETH9).balanceOf(address(this)); // <= FOUND

['37']

37:         if (address(this).balance > 0) { // <= FOUND

['38']

38:             TransferHelper.safeTransferETH(msg.sender, address(this).balance); // <= FOUND

['47']

47:         if (token == WETH9 && address(this).balance >= value) { // <= FOUND

['51']

51:         } else if (payer == address(this)) { // <= FOUND

['129']

129:         IERC20(rewardToken).safeTransferFrom(msg.sender, address(this), totalRewards); // <= FOUND

['144']

144:         stakedToken.safeTransferFrom(msg.sender, address(this), amount); // <= FOUND

['88']

88:         uint256 token0Balance = IERC20(token0).balanceOf(address(this)); // <= FOUND

['90']

90:             _swapTokenExactInput(
91:                 token1,
92:                 token0,
93:                 IERC20(token1).balanceOf(address(this)) - params.amount1Desired, // <= FOUND
94:                 params.amount0Desired - token0Balance
95:             );

['98']

98:         uint256 token1Balance = IERC20(token1).balanceOf(address(this)); // <= FOUND

['100']

100:             _swapTokenExactInput(
101:                 token0,
102:                 token1,
103:                 IERC20(token0).balanceOf(address(this)) - params.amount0Desired, // <= FOUND
104:                 params.amount1Desired - token1Balance
105:             );

['112']

112:         (tokenId, liquidity, amount0, amount1) = IShadowNonfungiblePositionManager(shadowNonfungiblePositionManager)
113:             .mint(
114:             IShadowNonfungiblePositionManager.MintParams({
115:                 token0: token0,
116:                 token1: token1,
117:                 tickSpacing: tickSpacing,
118:                 tickLower: params.tickLower,
119:                 tickUpper: params.tickUpper,
120:                 amount0Desired: params.amount0Desired,
121:                 amount1Desired: params.amount1Desired,
122:                 amount0Min: 0,
123:                 amount1Min: 0,
124:                 recipient: address(this), // <= FOUND
125:                 deadline: block.timestamp
126:             })

['134']

134:         token0Balance = IERC20(token0).balanceOf(address(this)); // <= FOUND

['135']

135:         token1Balance = IERC20(token1).balanceOf(address(this)); // <= FOUND

['138']

138:             pay(token0, address(this), positionOwner, token0Balance); // <= FOUND

['141']

141:             pay(token1, address(this), positionOwner, token1Balance); // <= FOUND

['224']

224:         uint256 token0Left = IERC20(token0).balanceOf(address(this)); // <= FOUND

['225']

225:         uint256 token1Left = IERC20(token1).balanceOf(address(this)); // <= FOUND

['237']

237:         token0Left = IERC20(token0).balanceOf(address(this)); // <= FOUND

['238']

238:         token1Left = IERC20(token1).balanceOf(address(this)); // <= FOUND

['240']

240:             pay(token0, address(this), positionOwner, token0Left); // <= FOUND

['243']

243:             pay(token1, address(this), positionOwner, token1Left); // <= FOUND

['283']

283:             pay(token0, address(this), vars.liquidationFeeRecipient, token0Fees); // <= FOUND

['287']

287:                 pay(token0, address(this), caller, token0CallerFees); // <= FOUND

['296']

296:             pay(token1, address(this), vars.liquidationFeeRecipient, token1Fees); // <= FOUND

['300']

300:                 pay(token1, address(this), caller, token1CallerFees); // <= FOUND

['355']

355:         IShadowNonfungiblePositionManager(getShadowNonfungiblePositionManager()).collect(
356:             IShadowNonfungiblePositionManager.CollectParams({
357:                 tokenId: shadowPositionId,
358:                 recipient: address(this), // <= FOUND
359:                 amount0Max: type(uint128).max,
360:                 amount1Max: type(uint128).max
361:             })

['364']

364:         uint256 token0Fees = IERC20(token0).balanceOf(address(this)); // <= FOUND

['365']

365:         uint256 token1Fees = IERC20(token1).balanceOf(address(this)); // <= FOUND

['383']

383:                 uint256 balance = IERC20(tokens[i]).balanceOf(address(this)); // <= FOUND

['394']

394:                             IERC4626(x33Adapter).deposit(balance, address(this)); // <= FOUND

['395']

395:                             balance = IERC20(x33).balanceOf(address(this)); // <= FOUND

['428']

428:         amount0 = IERC20(token0).balanceOf(address(this)); // <= FOUND

['429']

429:         amount1 = IERC20(token1).balanceOf(address(this)); // <= FOUND

['440']

440:         amountOut = IShadowSwapRouter(router).exactInputSingle(
441:             IShadowSwapRouter.ExactInputSingleParams({
442:                 tokenIn: tokenIn,
443:                 tokenOut: tokenOut,
444:                 tickSpacing: tickSpacing,
445:                 recipient: address(this), // <= FOUND
446:                 deadline: block.timestamp,
447:                 amountIn: amountIn,
448:                 amountOutMinimum: amountOutMinimum,
449:                 sqrtPriceLimitX96: 0
450:             })

['462']

462:         amountIn = IShadowSwapRouter(router).exactOutputSingle(
463:             IShadowSwapRouter.ExactOutputSingleParams({
464:                 tokenIn: tokenIn,
465:                 tokenOut: tokenOut,
466:                 tickSpacing: tickSpacing,
467:                 recipient: address(this), // <= FOUND
468:                 deadline: block.timestamp,
469:                 amountOut: amountOut,
470:                 amountInMaximum: amountInMaximum,
471:                 sqrtPriceLimitX96: 0
472:             })

['170']

170:             pay(token0, msg.sender, address(this), params.amount0Principal); // <= FOUND

['173']

173:             pay(token1, msg.sender, address(this), params.amount1Principal); // <= FOUND

['181']

181:             ILendingPool(lendingPool).borrow(address(this), positionInfo.token0DebtId, params.amount0Borrow); // <= FOUND

['186']

186:             ILendingPool(lendingPool).borrow(address(this), positionInfo.token1DebtId, params.amount1Borrow); // <= FOUND

['197']

197:             pay(token0, address(this), positionInfo.positionAddress, params.amount0Principal + params.amount0Borrow); // <= FOUND

['200']

200:             pay(token1, address(this), positionInfo.positionAddress, params.amount1Principal + params.amount1Borrow); // <= FOUND

['320']

320:             pay(token0, msg.sender, address(this), amount0); // <= FOUND

['322']

322:             ILendingPool(lendingPool).repay(address(this), positionInfo.token0DebtId, amount0); // <= FOUND

['330']

330:             pay(token1, msg.sender, address(this), amount1); // <= FOUND

['332']

332:             ILendingPool(lendingPool).repay(address(this), positionInfo.token1DebtId, amount1); // <= FOUND

[Gas-53] Use 'storage' instead of 'memory' for struct/array state variables

Resolution

In Solidity, choosing between memory and storage for variables, especially when dealing with structs or arrays, is crucial for optimizing gas costs. Variables declared as storage are pointers to the blockchain data, leading to lower gas consumption when fields are accessed or modified, as they don't require reading the entire structure. In contrast, memory variables copy the entire struct or array from storage, incurring significant gas costs, especially for large or complex structures. Therefore, use storage for state variables or when working within functions to manipulate existing contract data. Reserve memory for temporary data or when data needs to be passed to external functions as copies, ensuring efficient use of gas and avoiding unnecessary costs.

Num of instances: 2

Findings

Click to show findings

['247']

247:         PositionInfo memory positionInfo = positionInfos[positionId]; // <= FOUND

['295']

295:         PositionInfo memory positionInfo = positionInfos[params.positionId]; // <= FOUND

[Gas-54] Empty blocks should be removed or emit something

Resolution

Empty code blocks (i.e., {}) in a Solidity contract can be harmful as they can lead to ambiguity, misinterpretation, and unintended behavior. When developers encounter empty code blocks, it may be unclear whether the absence of code is intentional or the result of an oversight. This uncertainty can cause confusion during development, testing, and debugging, increasing the likelihood of introducing errors or vulnerabilities. Moreover, empty code blocks may give a false impression of implemented functionality or security measures, creating a misleading sense of assurance. To ensure clarity and maintainability, it is essential to avoid empty code blocks and explicitly document the intended behavior or any intentional omissions.

Num of instances: 1

Findings

Click to show findings

['207']

207:     function update() external updateReward(address(0)) onlyOwner {}

[Gas-55] Use constants instead of type(uint).max

Resolution

In smart contract development, utilizing constants for known maximum or minimum values, rather than computing type(uint<n>).max or assuming 0 for .min, can significantly reduce gas costs. Constants require less runtime computation and storage, optimizing contract efficiency—a crucial strategy for developers aiming for cost-effective and performant code.

Num of instances: 8

Findings

Click to show findings

['84']

84:         initReserve(reserveData, asset, eTokenAddress, type(uint256).max, id); // <= FOUND

['170']

170:         if (eTokenAmount == type(uint256).max) { // <= FOUND

['51']

51:         IERC20(token0).approve(vault, type(uint256).max); // <= FOUND

['52']

52:         IERC20(token1).approve(vault, type(uint256).max); // <= FOUND

['359']

359:                 amount0Max: type(uint128).max, // <= FOUND

['360']

360:                 amount1Max: type(uint128).max // <= FOUND

['134']

134:         IERC20(token0).approve(lendingPool, type(uint256).max); // <= FOUND

['135']

135:         IERC20(token1).approve(lendingPool, type(uint256).max); // <= FOUND

[Gas-56] Using named returns for pure and view functions is cheaper than using regular returns

Num of instances: 42

Findings

Click to show findings

['19']

19:     function getAddress(uint256 id) external view returns (address) {
20:         return libraryAndContractAddresses[id]; // <= FOUND
21:     }

['28']

28:     function Owner() external view returns (address) {
29:         return _owner(); // <= FOUND
30:     }

['102']

102:     function decimals() public view override returns (uint8) {
103:         return _decimals; // <= FOUND
104:     }

['490']

490:     function getReserveIdOfDebt(uint256 debtId) public view returns (uint256) {
491:         return debtPositions[debtId].reserveId; // <= FOUND
492:     }

['494']

494:     function getUnderlyingTokenAddress(uint256 reserveId) public view returns (address) {
495:         DataTypes.ReserveData storage reserve = reserves[reserveId];
496:         return reserve.underlyingTokenAddress; // <= FOUND
497:     }

['499']

499:     function getETokenAddress(uint256 reserveId) public view returns (address) {
500:         DataTypes.ReserveData storage reserve = reserves[reserveId];
501:         return reserve.eTokenAddress; // <= FOUND
502:     }

['504']

504:     function getStakingAddress(uint256 reserveId) public view returns (address) {
505:         DataTypes.ReserveData storage reserve = reserves[reserveId];
506:         return reserve.stakingAddress; // <= FOUND
507:     }

['509']

509:     function exchangeRateOfReserve(uint256 reserveId) public view returns (uint256) {
510:         DataTypes.ReserveData storage reserve = reserves[reserveId];
511:         return reserve.eTokenToReserveExchangeRate(); // <= FOUND
512:     }

['514']

514:     function utilizationRateOfReserve(uint256 reserveId) public view returns (uint256) {
515:         DataTypes.ReserveData storage reserve = reserves[reserveId];
516:         return reserve.utilizationRate(); // <= FOUND
517:     }

['519']

519:     function borrowingRateOfReserve(uint256 reserveId) public view returns (uint256) {
520:         DataTypes.ReserveData storage reserve = reserves[reserveId];
521:         return uint256(reserve.borrowingRate()); // <= FOUND
522:     }

['67']

67:     function rewardPerToken(address rewardToken) public view returns (uint256) {
68:         if (block.timestamp <= rewardData[rewardToken].startTime) {
69:             
70:             return rewardData[rewardToken].rewardPerTokenStored; // <= FOUND
71:         }
72: 
73:         uint256 dt =
74:             Math.min(rewardData[rewardToken].endTime, block.timestamp) - (rewardData[rewardToken].lastUpdateTime);
75: 
76:         if (dt == 0 || totalStaked == 0) {
77:             return rewardData[rewardToken].rewardPerTokenStored; // <= FOUND
78:         }
79: 
80:         return rewardData[rewardToken].rewardPerTokenStored // <= FOUND
81:             + (rewardData[rewardToken].rewardRate * dt * 1e18) / totalStaked;
82:     }

['84']

84:     function earned(address user, address rewardToken) public view returns (uint256) {
85:         uint256 curRewardPerToken = rewardPerToken(rewardToken);
86: 
87:         return earned(user, rewardToken, curRewardPerToken); // <= FOUND
88:     }

['90']

90:     function earned(address user, address rewardToken, uint256 curRewardPerToken) internal view returns (uint256) {
91:         uint256 d = curRewardPerToken - userRewardPerTokenPaid[user][rewardToken];
92: 
93:         return (balanceOf[user] * d) / 1e18 + userRewardsClaimable[user][rewardToken]; // <= FOUND
94:     }

['209']

209:     function rewardsTokenListLength() external view returns (uint256) {
210:         return rewardTokens.length; // <= FOUND
211:     }

['52']

52:     function getTokenPrice(address token) public view returns (uint256) {
53:         return getPythPrice(token); // <= FOUND
54:     }

['56']

56:     function getPythPrice(address token) public view returns (uint256) {
57:         bytes32 priceId = pythPriceIds[token];
58:         if (priceId == bytes32(0)) {
59:             revert("PriceId not set");
60:         }
61:         PythStructs.Price memory priceStruct = pyth.getPriceUnsafe(priceId);
62: 
63:         require(priceStruct.publishTime + maxPriceAge > block.timestamp, "Price is too old");
64: 
65:         uint256 price = uint256(uint64(priceStruct.price));
66: 
67:         return price; // <= FOUND
68:     }

['18']

18:     function principal(IShadowNonfungiblePositionManager positionManager, uint256 tokenId, address pool)
19:         public
20:         view
21:         returns (uint256 amount0, uint256 amount1)
22:     {
23:         IShadowV3Pool v3Pool = IShadowV3Pool(pool);
24:         (uint160 sqrtPriceX96,,,,,,) = v3Pool.slot0();
25: 
26:         (,,, int24 tickLower, int24 tickUpper, uint128 liquidity,,,,) = positionManager.positions(tokenId);
27: 
28:         return LiquidityAmounts.getAmountsForLiquidity( // <= FOUND
29:             sqrtPriceX96, TickMath.getSqrtRatioAtTick(tickLower), TickMath.getSqrtRatioAtTick(tickUpper), liquidity
30:         );
31:     }

['33']

33:     function getCurrentTick(address v3Pool) public view returns (int24) {
34:         (uint160 sqrtPriceX96,,,,,,) = IShadowV3Pool(v3Pool).slot0();
35:         return TickMath.getTickAtSqrtRatio(sqrtPriceX96); // <= FOUND
36:     }

['71']

71:     function getX33Adapter() public view returns (address) {
72:         return 0x9710E10A8f6FbA8C391606fee18614885684548d; // <= FOUND
73:     }

['75']

75:     function getShadowNonfungiblePositionManager() public view returns (address) {
76:         return IAddressRegistry(addressProvider).getAddress(AddressId.ADDRESS_ID_SHADOW_NONFUNGIBLE_POSITION_MANAGER); // <= FOUND
77:     }

['79']

79:     function getSwapRouter() public view returns (address) {
80:         return IAddressRegistry(addressProvider).getAddress(AddressId.ADDRESS_ID_SHADOW_ROUTER); // <= FOUND
81:     }

['336']

336:     function claimableRewards() external view returns (address[] memory, uint256[] memory) {
337:         address shadowGauge = IShadowRangeVault(vault).shadowGauge();
338:         if (shadowGauge != address(0)) {
339:             address[] memory tokens = IShadowGaugeV3(shadowGauge).getRewardTokens();
340:             uint256[] memory amounts = new uint256[](tokens.length);
341:             for (uint256 i = 0; i < tokens.length; i++) {
342:                 amounts[i] = IShadowGaugeV3(shadowGauge).earned(tokens[i], shadowPositionId);
343:             }
344:             return (tokens, amounts); // <= FOUND
345:         }
346:         return (new address[](0), new uint256[](0)); // <= FOUND
347:     }

['398']

398:     function getTokenPrice(address token) public view returns (uint256) {
399:         return IPriceOracle(IAddressRegistry(addressProvider).getAddress(AddressId.ADDRESS_ID_PRICE_ORACLE)) // <= FOUND
400:             .getTokenPrice(token);
401:     }

['403']

403:     function getPositionAmounts(uint256 positionId) public view returns (uint256 amount0, uint256 amount1) {
404:         return getPositionAmountsByNftId(positionInfos[positionId].shadowPositionId); // <= FOUND
405:     }

['407']

407:     function getPositionAmountsByNftId(uint256 nftId) public view returns (uint256 amount0, uint256 amount1) {
408:         return IPositionValueCalculator( // <= FOUND
409:             IAddressRegistry(addressProvider).getAddress(AddressId.ADDRESS_ID_SHADOW_POSITION_VALUE_CALCULATOR)
410:         ).principal(
411:             IShadowNonfungiblePositionManager(
412:                 IAddressRegistry(addressProvider).getAddress(AddressId.ADDRESS_ID_SHADOW_NONFUNGIBLE_POSITION_MANAGER)
413:             ),
414:             nftId,
415:             shadowV3Pool
416:         );
417:     }

['419']

419:     function getPositionValue(uint256 positionId) public view returns (uint256 value) {
420:         (uint256 amount0, uint256 amount1) = getPositionAmounts(positionId);
421:         return amount0 * getTokenPrice(token0) / 10 ** token0Decimals // <= FOUND
422:             + amount1 * getTokenPrice(token1) / 10 ** token1Decimals;
423:     }

['425']

425:     function getPositionValueByNftId(uint256 nftId) public view returns (uint256 value) {
426:         (uint256 amount0, uint256 amount1) = getPositionAmountsByNftId(nftId);
427:         return amount0 * getTokenPrice(token0) / 10 ** token0Decimals // <= FOUND
428:             + amount1 * getTokenPrice(token1) / 10 ** token1Decimals;
429:     }

['431']

431:     function getDebtValue(uint256 positionId) public view returns (uint256 value) {
432:         (uint256 amount0Debt, uint256 amount1Debt) = getPositionDebt(positionId);
433:         return amount0Debt * getTokenPrice(token0) / 10 ** token0Decimals // <= FOUND
434:             + amount1Debt * getTokenPrice(token1) / 10 ** token1Decimals;
435:     }

['447']

447:     function getDebtRatioFromAmounts(
448:         uint256 amount0Principal,
449:         uint256 amount1Principal,
450:         uint256 amount0Borrow,
451:         uint256 amount1Borrow
452:     ) public view returns (uint256 debtRatio) {
453:         uint256 principalValue = amount0Principal * getTokenPrice(token0) / 10 ** token0Decimals
454:             + amount1Principal * getTokenPrice(token1) / 10 ** token1Decimals;
455:         uint256 borrowValue = amount0Borrow * getTokenPrice(token0) / 10 ** token0Decimals
456:             + amount1Borrow * getTokenPrice(token1) / 10 ** token1Decimals;
457:         return borrowValue * 10000 / (principalValue + borrowValue); // <= FOUND
458:     }

['460']

460:     function getDebtRatio(uint256 positionId) public view returns (uint256 debtRatio) {
461:         return getDebtValue(positionId) * 10000 / getPositionValue(positionId); // <= FOUND
462:     }

['464']

464:     function getLendingPool() public view returns (address) {
465:         return IAddressRegistry(addressProvider).getAddress(AddressId.ADDRESS_ID_LENDING_POOL); // <= FOUND
466:     }

['468']

468:     function getPositionIds(address owner) public view returns (uint256[] memory) {
469:         return positionIds[owner]; // <= FOUND
470:     }

['472']

472:     function getPositionInfos(uint256 positionId) public view returns (PositionInfo memory) {
473:         return positionInfos[positionId]; // <= FOUND
474:     }

['476']

476:     function liquidationFeeRecipient() public view returns (address) {
477:         return IAddressRegistry(addressProvider).getAddress(AddressId.ADDRESS_ID_LIQUIDATION_FEE_RECIPIENT); // <= FOUND
478:     }

['480']

480:     function performanceFeeRecipient() public view returns (address) {
481:         return IAddressRegistry(addressProvider).getAddress(AddressId.ADDRESS_ID_PERFORMANCE_FEE_RECIPIENT); // <= FOUND
482:     }

['484']

484:     function getCurrentTick() public view returns (int24) {
485:         return IPositionValueCalculator( // <= FOUND
486:             IAddressRegistry(addressProvider).getAddress(AddressId.ADDRESS_ID_SHADOW_POSITION_VALUE_CALCULATOR)
487:         ).getCurrentTick(shadowV3Pool);
488:     }

['18']

18:     function principal(ISwapXNonfungiblePositionManager positionManager, uint256 tokenId, address pool)
19:         public
20:         view
21:         returns (uint256 amount0, uint256 amount1)
22:     {
23:         (uint160 sqrtPriceX96,,,,,) = ISwapXV3Pool(pool).globalState();
24: 
25:         (,,,, int24 tickLower, int24 tickUpper, uint128 liquidity,,,,) = positionManager.positions(tokenId);
26: 
27:         return LiquidityAmounts.getAmountsForLiquidity( // <= FOUND
28:             sqrtPriceX96, TickMath.getSqrtRatioAtTick(tickLower), TickMath.getSqrtRatioAtTick(tickUpper), liquidity
29:         );
30:     }

['32']

32:     function getCurrentTick(address v3Pool) public view returns (int24) {
33:         (, int24 tick,,,,) = ISwapXV3Pool(v3Pool).globalState();
34:         return tick; // <= FOUND
35:     }

['38']

38:     function getPositionIds(address vault, address owner) public view returns (uint256[] memory) {
39:         return IVault(vault).getPositionIds(owner); // <= FOUND
40:     }

['68']

68:     function getUserPosition(address vault, address owner) public view returns (PositionInfo[] memory) {
69:         uint256[] memory ids = getPositionIds(vault, owner);
70:         PositionInfo[] memory positions = new PositionInfo[](ids.length);
71:         for (uint256 i = 0; i < ids.length; i++) {
72:             positions[i].vault = vault;
73:             positions[i].id = ids[i];
74:             positions[i].token0 = IVault(vault).token0();
75:             positions[i].token1 = IVault(vault).token1();
76:             (positions[i].amount0, positions[i].amount1) = IVault(vault).getPositionAmounts(ids[i]);
77:             if (positions[i].amount0 > 0 || positions[i].amount1 > 0) {
78:                 (positions[i].amount0Debt, positions[i].amount1Debt) = IVault(vault).getPositionDebt(ids[i]);
79:                 positions[i].debtRatio = IVault(vault).getDebtRatio(ids[i]);
80:             }
81:             positions[i].price0 = IVault(vault).getTokenPrice(positions[i].token0);
82:             positions[i].price1 = IVault(vault).getTokenPrice(positions[i].token1);
83: 
84:             IVault.PositionInfo memory positionInfo = IVault(vault).getPositionInfos(ids[i]);
85: 
86:             positions[i].positionAddress = positionInfo.positionAddress;
87:             positions[i].positionNftId = positionInfo.shadowPositionId;
88:             positions[i].tickUpper = positionInfo.tickUpper;
89:             positions[i].tickLower = positionInfo.tickLower;
90:             positions[i].ul = positionInfo.ul;
91:             positions[i].ll = positionInfo.ll;
92: 
93:             positions[i].inRange = positions[i].amount0 > 0 && positions[i].amount1 > 0;
94: 
95:             (positions[i].rewards, positions[i].rewardsAmounts) =
96:                 IPositionImpl(positionInfo.positionAddress).claimableRewards();
97: 
98:             positions[i].currentTick = IVault(vault).getCurrentTick();
99: 
100:             (,,,,, positions[i].liquidity,,,,) = INFTPositionManager(
101:                 IAddressRegistry(addressProvider).getAddress(AddressId.ADDRESS_ID_SHADOW_NONFUNGIBLE_POSITION_MANAGER)
102:             ).positions(positionInfo.shadowPositionId);
103:         }
104:         return positions; // <= FOUND
105:     }

['107']

107:     function getUserPositions(address[] memory vaults, address owner) public view returns (PositionInfo[][] memory) {
108:         PositionInfo[][] memory positions = new PositionInfo[][](vaults.length);
109:         for (uint256 i = 0; i < vaults.length; i++) {
110:             positions[i] = getUserPosition(vaults[i], owner);
111:         }
112:         return positions; // <= FOUND
113:     }

['67']

67:     function version() external pure returns (uint256) {
68:         return 1; // <= FOUND
69:     }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment