├── .gitignore ├── README.md ├── Report-template.md ├── Security-review-process.md └── solo ├── Arcana-security-review.md ├── NinjaYielder-security-review.md ├── Mugen-security-review.md ├── CadmosFinance-security-review.md ├── Azuro-security-review.md ├── GMD-security-review.md └── Zerem-security-review.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pashov's security audits, reviews, contributions 2 | 3 | Some of my personal security audits, reviews and contributions will be shared here 4 | 5 | ## Solo 6 | 7 | - [Ninja Yield - yield aggregator](solo/NinjaYielder-security-review.md) 8 | - [Zerem - DeFi Circuit Breaker](solo/Zerem-security-review.md) 9 | - [Arcana - Extension of ERC721A](solo/Arcana-security-review.md) 10 | - [Cadmos Finance - assets management](solo/CadmosFinance-security-review.md) 11 | - [GMD - yield aggregator](solo/GMD-security-review.md) 12 | - [Azuro - decentralized betting](solo/Azuro-security-review.md) 13 | - [Mugen - cross-chain DEX adapter](solo/Mugen-security-review.md) 14 | 15 | ## Other 16 | 17 | - 1st place out of ~200 people in a Code4rena contest - [VTVL](https://code4rena.com/contests/2022-09-vtvl-contest) 18 | 19 | I am available for security consulting. Reach out to me on Twitter [@pashovkrum](https://twitter.com/pashovkrum) 20 | -------------------------------------------------------------------------------- /Report-template.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | A time-boxed security review of the **protocol name** protocol was done by **pashov**, with a focus on the security aspects of the application's implementation. 4 | 5 | # Disclaimer 6 | 7 | A smart contract security review can never verify the complete absence of vulnerabilities. This is a time, resource and expertise bound effort where I try to find as many vulnerabilities as possible. I can not guarantee 100% security after the review or even if the review will find any problems with your smart contracts. 8 | 9 | # Protocol Overview 10 | 11 | _explanation what the protocol does, some architectural comments_ 12 | 13 | # Severity classification 14 | 15 | | Severity | Impact: High | Impact: Medium | Impact: Low | 16 | | ---------------------- | ------------ | -------------- | ----------- | 17 | | **Likelihood: High** | Critical | High | Medium | 18 | | **Likelihood: Medium** | High | Medium | Low | 19 | | **Likelihood: Low** | Medium | Low | Low | 20 | 21 | # Security Assessment Summary 22 | 23 | **_review commit hash_ - [fffffffff](url)** 24 | 25 | ### Scope 26 | 27 | The following smart contracts were in scope of the audit: 28 | 29 | - `smart-contract-name` 30 | - `smart-contract-name` 31 | 32 | The following number of issues were found, categorized by their severity: 33 | 34 | - High: x issues 35 | - Medium: x issues 36 | - Low: x issues 37 | - Informational: x issues 38 | 39 | --- 40 | 41 | # Findings Summary 42 | 43 | | ID | Title | Severity | 44 | | ------ | ---------------------------- | ------------- | 45 | | [H-01] | Any High Title Here | High | 46 | | [M-01] | Any Medium Title Here | Medium | 47 | | [L-01] | Any Low Title Here | Low | 48 | | [I-01] | Any Informational Title Here | Informational | 49 | 50 | # Detailed Findings 51 | 52 | # [S-01] {name} 53 | 54 | ## Severity 55 | 56 | **Likelihood:** 57 | 58 | **Impact:** 59 | 60 | ## Description 61 | 62 | ## Recommendations 63 | -------------------------------------------------------------------------------- /Security-review-process.md: -------------------------------------------------------------------------------- 1 | # Security review process guide 2 | 3 | ## Questions to project 4 | 1. What is the clear scope (`.sol` files) of the security review? 5 | 2. Does the project have well written specifications & code documentation? 6 | 3. What is the code coverage percentage? 7 | 4. Are there any protocols that are similar to yours, which are they? 8 | 5. What is your intended budget for this review? 9 | 10 | Based on the answers we can discuss the effort needed, the payment amount and timing. 11 | 12 | ### Other questions to project: 13 | 1. What is your preferred channel of communication (Discord, Twitter, Telegram)? 14 | 2. Are you okay with me publishing the security review report after you apply fixes? 15 | 3. Are you okay with me being transparent with my work, findings and pay? 16 | 17 | ## Security review result & fixes review 18 | After the time agreed upon has passed, the project will receive the security review report. The project has 7 days to apply fixes on issues found. Each issues should be fixed in a separate commit that has a message pointing to the issue being fixed. Then, a single iteration of a "fixes review" will be executed by me, free of additional charges, to verify your fixes are correct and secure. 19 | 20 | ### Important notes for the fixes review 21 | - for any questions or clarifications on the vulnerabilities/recommendations in the report, you can reach out to me on the intended channel of communication 22 | - changes to be reviewed should not include anything else other than fixes for the reported issues, so no big refactorings, new features or architectural changes 23 | - in the case that fixes are too difficult to implement or more than one iteration of reviews is needed then this is a special case that can be discussed independently of this review 24 | 25 | ## Disclaimer 26 | A smart contract security review can never verify the complete absence of vulnerabilities. This is a time, resource and expertise bound effort where I try to find as many vulnerabilities as possible. I can not guarantee 100% security after the review or if even the review will find any problems with your smart contracts. -------------------------------------------------------------------------------- /solo/Arcana-security-review.md: -------------------------------------------------------------------------------- 1 | # Arcana protocol security review by pashov 2 | 3 | ***review commit hash* - [51fc65fdd6474c9632975294c560ddee24135f2f](https://github.com/Prominence-Games/arcana-foundry-erc721a/tree/51fc65fdd6474c9632975294c560ddee24135f2f)** 4 | 5 | **Scope: `ArcanaPrime.sol`** 6 | 7 | --- 8 | 9 | # [H-01] There is no way to withdraw the ETH paid by minters 10 | 11 | ## Proof of Concept 12 | 13 | There is currently no possible way for the contract deployer/owner to withdraw the ETH that was paid by miners. This means that value will be stuck & lost forever. 14 | This is also the case for the `ERC721A` standard, which this project actually extends as well, but it was verified in a conversation with the developer that the `ArcanaPrime` contract is expected to be used as-is, without a need for inheritance/extension. 15 | 16 | ## Impact 17 | 18 | This will mean hundreds of thousands of dollars (since `MAX_SUPPLY = 10_000` and `MINT_PRICE = 0.08 ether`) will be irretrievable, essentially drying the runway of the NFT project, so it is High severity. 19 | 20 | ## Recommendation 21 | 22 | Add a method to withdraw the value in the contract, for example 23 | ```solidity 24 | function withdrawBalance() external onlyOwner { 25 | (bool success, ) = msg.sender.call{value: address(this).balance}(""); 26 | require(success); 27 | } 28 | ``` 29 | 30 | # [M-01] If address is a smart contract that can't handle ERC721 tokens they will be stuck after a whitelisted mint 31 | 32 | ## Proof of Concept 33 | 34 | The `mintPublic` method has a check that allows only EOAs to call it 35 | ```solidity 36 | if (tx.origin != msg.sender) revert ContractsNotAllowed(); 37 | ``` 38 | but it is missing in the whitelisted mint methods (`mintArcanaList`, `mintAspirantList`, `mintAllianceList`). This means that if the address that is whitelisted is a contract and it calls those functions but it can't handle ERC721 tokens correctly, they will be stuck. This problem is usually handled by using `_safeMint` instead of `_mint` but all `mint` functionality in `ArcanaPrime` uses `_mint`. 39 | 40 | ## Impact 41 | 42 | This can result in a user losing his newly minted tokens forever, which is a potential values loss. It requires the user to be using a smart contract that does not handle ERC721 properly, so it is Medium severity. 43 | 44 | ## Recommendation 45 | 46 | In `mintArcanaList`, `mintAspirantList` and `mintAllianceList` change the `_mint` call to `_safeMint`. Keep in mind this adds a reentrancy possibility, so it is best to add a `nonReentrant` modifier as well. 47 | 48 | 49 | # [L-01] Usage of `ecrecover` should be replaced with usage of OpenZeppelin's `ECDSA` library 50 | 51 | [Signature malleability](https://swcregistry.io/docs/SWC-117) is one of the potential issues with ecrecover. Even though it is not a threat to the current implementation using the highest security standards is always good. `ECDSA` is already imported, but not actually used. Replace the usage of `ecrecover` with the `ECDSA.recover` functionality. 52 | 53 | # Gas optimisation report 54 | 55 | ## [G-01] Remove pausability as it is not useful 56 | 57 | The pausability functionality (the `isNotPaused` modifier and the `togglePause` method) behaves the same way as if you just use the `setCurrentPhase` method with `Phases.Closed`. Using only the latter will save a lot of storage reads. 58 | 59 | ## [G-02] Remove `public` visibility from `constant` variables 60 | 61 | `constant` variables are custom to the contract and won't need to be read on-chain - anyone can just see their values from the source code and, if needed, hardcode them into other contracts. Removing the `public` visibility will optimise deployment cost since no automatically generated getters will exist in the bytecode of the contract. 62 | 63 | ## [G-03] Use `external` instead of `public` for functions not called internally 64 | 65 | Functions that are `external` always read their arguments directly from `calldata` without a need to copy them to `memory`, which results in gas savings. Do this for the following methods: 66 | - `transferFrom` 67 | - `safeTransferFrom` (both overrides) 68 | - `setOperatorFilteringEnabled` 69 | - `approve` 70 | - `setApprovalForAll` 71 | - `repeatRegistration` 72 | - `registerCustomBlacklist` 73 | 74 | ## [G-04] Remove `nextStartTime` storage variable and setter as it is not mandatory 75 | 76 | Remove both the `nextStartTime` storage variable and the `setNextStartTime` function. If this functionality is still needed, move it off-chain. -------------------------------------------------------------------------------- /solo/NinjaYielder-security-review.md: -------------------------------------------------------------------------------- 1 | # Ninja Yield protocol security review by pashov 2 | 3 | *********************review commit hash -********************* **[9e9367120d45fdd6144328964fabb8f57610661c](https://github.com/0xTK421/ninja-core-contracts/tree/9e9367120d45fdd6144328964fabb8f57610661c)** 4 | 5 | ### The code was reviewed for a total of 6 hours. 6 | --- 7 | 8 | 9 | # [H-01] Incorrect user accounting in `withdraw` method 10 | 11 | ## Proof of Concept 12 | 13 | The `withdraw` method in `NYProfitTakingVaultBaseV1` does incorrect user accounting in the following line: 14 | 15 | ```solidity 16 | user.amount = user.amount - _shares; 17 | ``` 18 | 19 | In the `deposit` method we use the `user.amount` to store the amount of `underlying` tokens deposited, but in `withdraw` instead of subtracting the `underlying` tokens the code subtracts the shares burned. 20 | 21 | ## Impact 22 | 23 | Since `shares` are not 1:1 with `underlying` this will completely mess up the user accounting on each withdraw. It is possible to be in two directions - if `_shares` was less than the amount withdrawn, then the user will be able to withdraw more than he deposited, essentially a possibility to deplete the vault to zero. If `_shares` was more than the amount withdrawn, then the user will be able to withdraw less than he deposited, essentially a loss of value for users. 24 | 25 | ## Recommendation 26 | 27 | Make the following change: 28 | 29 | ```diff 30 | - user.amount = user.amount - _shares; 31 | + user.amount = user.amount - r; 32 | ``` 33 | 34 | # [H-02] First vault depositor can steal subsequent depositors’ tokens 35 | 36 | ## Proof of Concept 37 | 38 | Imagine the following scenario: 39 | 40 | 1. A new vault has been deployed and configured, no depositors yet 41 | 2. Alice wants to deposit 10 ether(10e18) worth of `underlying` and sends a transaction to the public mempool 42 | 3. A MEV bot sees Alice’s transaction and front runs it by depositing 1 wei(1e-18) of `underlying`, resulting in him receiving 1 wei(1e-18) of vault tokens (shares) 43 | 4. The MEV bot also front runs Alice’s transaction with a transfer of 10 ether(10e18) of `underlying` to the vault via `ERC20::transfer` 44 | 5. Now the code calculates Alice’s shares as `shares = (_amount * totalSupply()) / _pool;` which is 10e18 * 1 / (10e18 + 1) which is 0 45 | 6. Alice gets minted 0 shares, but she deposited 10e18 of `underlying` 46 | 7. Now the MEV bot backruns Alice’s transaction calling `withdraw` with his 1e-18 (1 wei) of share, which is the total supply, so he withdraws his deposit + Alice’s whole deposit 47 | 48 | This can be replayed multiple times until the depositors notice the problem. 49 | 50 | ## Impact 51 | 52 | The result of this is 100% value loss for all subsequent depositors. 53 | 54 | ## Recommendation 55 | 56 | UniswapV2 fixed this with two types of protection: 57 | 58 | [First](https://github.com/Uniswap/v2-core/blob/master/contracts/UniswapV2Pair.sol#L119-L121), on the first `mint` it actually mints the first 1000 shares to the zero-address 59 | 60 | [Second](https://github.com/Uniswap/v2-core/blob/ee547b17853e71ed4e0101ccfd52e70d5acded58/contracts/UniswapV2Pair.sol#L125), it requires that the minted shares are not 0 61 | 62 | Implementing them both will resolve this vulnerability. 63 | 64 | # [M-01] MEV can sandwich every harvest due to missing slippage tolerance value 65 | 66 | ## Proof of Concept 67 | 68 | In `NyPtvFantomWftmBooSpookyV2StrategyToUsdc` each time the `_harvestCore` method is called (on each harvest) it will call the `_swapFarmEmissionTokens` method which itself has the following code: 69 | 70 | ```solidity 71 | IUniswapV2Router02(SPOOKY_ROUTER).swapExactTokensForTokensSupportingFeeOnTransferTokens( 72 | booBalance, 73 | 0, 74 | booToUsdcPath, 75 | address(this), 76 | block.timestamp 77 | ); 78 | ``` 79 | 80 | The “0” here is the value of the `amountOutMin` argument which is used for slippage tolerance. 0 value here essentially means 100% slippage tolerance. This is a very easy target for MEV and bots to do a flash loan sandwich attack on each of the strategy’s swaps, resulting in very big slippage on each trade. 81 | 82 | ## Impact 83 | 84 | 100% slippage tolerance can be exploited in a way that the strategy (so the vault and the users) receive much less value than it should had. This can be done on every trade if the trade transaction goes through a public mempool. 85 | 86 | ## Recommendation 87 | 88 | The best solution here is to make the `harvest` method of the vault be callable only by a list of trusted addresses which will send the transaction through a private mempool. This, combined with an on-chain calculation for an `amountOutMin` that is off from the expected `amountOut` by a slippage tolerance percentage (that might be configurable through a setter in the strategy) should be good enough to protect you from MEV sandwich attacks. 89 | 90 | # [M-02] Hardcoded swap path might not be the most optimal/liquid one 91 | 92 | ## Proof of Concept 93 | 94 | Currently in `NyPtvFantomWftmBooSpookyV2StrategyToUsdc` the value of the `booToUsdcPath` trade path is not configurable and is basically hardcoded to be `[BOO, USDC]`. It is the same for the swap router, as it is currently hardcoded to point to the SpookySwap router. The problem is that the `BOO/USDC` pool on SpookySwap might not be the most optimal and liquid one, and maybe instead it would be better to go `BOO/USDT` and then `USDT/USDC`. If the `BOO/USDC` pair for example loses most of its liquidity (maybe LPs are not incentivised as much or they decided to move elsewhere) then the strategy will still be forced to do its swaps on `harvest` through the illiquid/non-optimal `BOO/USDC` pair on SpookySwap. 95 | 96 | ## Impact 97 | 98 | This can result in a loss of value for vault users, as if a more liquid pool was used for swaps it could have resulted in less slippage so a bigger reward. 99 | 100 | ## Recommendation 101 | 102 | Add setter functions for both the trade router and the trade path - make them configurable. One possible option is to hardcode the 3 most liquid Fantom exchanges and 3 possible trade paths and switch through them via the setter. 103 | 104 | # [L-01] Missing `nonReentrant` modifier in functions with external calls 105 | 106 | ## Proof of Concept 107 | 108 | The `NYProfitTakingVaultBaseV1` contract inherits from OpenZeppelin’s `ReentrancyGuard` contract and has marked most of its state-changing methods that do ERC20 external calls with the `nonReentrant` modifier. The problem is the modifier is missing on some of the functions that also do ERC20 external calls - the `depositOutputTokenForUsers` and `earn` methods. Currently the methods are not exploitable, but ERC777 tokens (which are ERC20 compatible) can reenter a method call because of their pre and post hooks, so the `nonReentrant` modifier is an important security measure when doing unsafe ERC20 external calls. 109 | 110 | ## Impact 111 | 112 | If ERC777 tokens were used as `rewardToken` or `underlying` they can reenter the `depositOutputTokenForUsers` and `earn` methods, which currently is not exploitable, but might become a big problem when new code is added. 113 | 114 | ## Recommendation 115 | 116 | Add the `nonReentrant` modifier to the `depositOutputTokenForUsers` and `earn` methods 117 | 118 | # [L-02] If `underlying` or `rewardToken` is a two-address token then `inCaseTokensGetStuck` method can be used to rug users 119 | 120 | ## Proof of Concept 121 | 122 | Some ERC20 tokens on the blockchain are deployed behind a proxy, so they have at least 2 entrypoints (the proxy and the implementation) for their functionality. Example is Synthetix’s `ProxyERC20` contract from where you can interact with `sUSD, sBTC etc). If such a token was used as the `underlying` token in a vault, then the owner will be able to rug all depositors with the `inCaseTokensGetStuck` method, even though it has the following checks 123 | 124 | ```solidity 125 | if (_token == address(underlying)) { 126 | revert NYProfitTakingVault__CannotWithdrawUnderlying(); 127 | } 128 | if (_token == address(rewardToken)) { 129 | revert NYProfitTakingVault__CannotWithdrawRewardToken(); 130 | } 131 | ``` 132 | 133 | Since the tokens have multiple addresses the admin can give another address and pass those checks. 134 | 135 | ## Impact 136 | 137 | The potential impact is 100% loss of deposited tokens for users, but it requires a malicious/compromised owner and a special type of ERC20 token used in the vault. 138 | 139 | ## Recommendation 140 | 141 | Instead of checking the address of the withdrawn token, it is a better approach to check the balance of `underlying` and `rewardToken` before and after the transfer and to verify it is the same. 142 | 143 | # [L-03] The `getPricePerFullShare` method returns a wrong value when `totalSupply` is 0 144 | 145 | ## Proof of Concept 146 | 147 | The `getPricePerFullShare` method currently computes the result by the following formula: 148 | 149 | ```solidity 150 | return totalSupply() == 0 ? 1e18 : (balance() * 1e18) / totalSupply(); 151 | ``` 152 | 153 | The problem is that when the underlying token used is with less or more than 18 decimals and the `totalSupply` is still 0, then the price returned won’t be correct, as it will be 1e18. The intention to return 1e18 when `totalSupply` is 0 looks like it comes from the amount of shares minted on the first deposit 154 | 155 | ```solidity 156 | if (totalSupply() == 0) { 157 | shares = _amount; 158 | } 159 | ``` 160 | 161 | so it looks like 1 share will equal 1 token, but if the token’s decimals are not 18 then 1 token is not 1e18 wei of this token. 162 | 163 | ## Impact 164 | 165 | Since this function is market with `public view` and is not used anywhere in the protocol, it will probably be used in front ends only, impacting the initial pricing of a vault share. 166 | 167 | ## Recommendation 168 | 169 | Make the following change: 170 | 171 | ```diff 172 | - return totalSupply() == 0 ? 1e18 : (balance() * 1e18) / totalSupply(); 173 | 174 | + return totalSupply() == 0 ? 10**underlying.decimals() : (balance() * 1e18) / totalSupply(); 175 | ``` -------------------------------------------------------------------------------- /solo/Mugen-security-review.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | A time-boxed security review of the **Mugen** protocol was done by pashov, with a focus on the security aspects of the application's implementation. 4 | 5 | # Disclaimer 6 | 7 | A smart contract security review can never verify the complete absence of vulnerabilities. This is a time, resource and expertise bound effort where I try to find as many vulnerabilities as possible. I can not guarantee 100% security after the review or even if the review will find any problems with your smart contracts. 8 | 9 | # Protocol Overview 10 | 11 | The protocol is a DEX/swap adapter that allows complex transactions, for example multiple tokens in to multiple/single tokens out and vice versa on several DEXes (Uniswap, Sushiswap, 3xcalibur). The protocol integrates the native adapters provided by the DEXes to ensure flawless transfers of tokens. It also allows cross-chain swaps as it is integrated with LayerZero's Stargate protocol. 12 | 13 | # Severity classification 14 | 15 | | Severity | Impact: High | Impact: Medium | Impact: Low | 16 | | ---------------------- | ------------ | -------------- | ----------- | 17 | | **Likelihood: High** | Critical | High | Medium | 18 | | **Likelihood: Medium** | High | Medium | Low | 19 | | **Likelihood: Low** | Medium | Low | Low | 20 | 21 | # Security Assessment Summary 22 | 23 | **_review commit hash_ - [61564a3e1eac743cb9b89976cbefbcb0fd15f38f](https://github.com/Mugen-Finance/Products/tree/61564a3e1eac743cb9b89976cbefbcb0fd15f38f)** 24 | 25 | ### Scope 26 | 27 | The following smart contracts were in scope of the audit: 28 | 29 | - `ArbitrumSwaps` 30 | - `StargateArbitrum` 31 | 32 | The following number of issues were found, categorized by their severity: 33 | 34 | - High: 2 issues 35 | - Medium: 1 issue 36 | - Low: 3 issues 37 | - Informational: 10 issues 38 | 39 | --- 40 | 41 | # Findings Summary 42 | 43 | | ID | Title | Severity | 44 | | ------ | ----------------------------------------------------------------------------------------------- | ------------- | 45 | | [H-01] | Anyone can use or steal `ArbitrumSwaps` native asset balance | High | 46 | | [H-02] | Malicious user can easily make the protocol revert on every `USDT` swap on Uniswap | High | 47 | | [M-01] | Use `quoteLayerZeroFee` instead of sending all native asset balance as gas fee for `swap` call | Medium | 48 | | [L-01] | Check array arguments have the same length | Low | 49 | | [L-02] | A `require` check can easily be bypassed | Low | 50 | | [L-03] | The `gasLeft()` after gas-limited external call might not be enough to complete the transaction | Low | 51 | | [I-01] | Prefer battle-tested code over reimplementing common patterns | Informational | 52 | | [I-02] | Use an enum for the "step" types in `ArbitrumSwaps` | Informational | 53 | | [I-03] | Move code to bring cohesion up | Informational | 54 | | [I-04] | Use `x != 0` to get positive-only uint values | Informational | 55 | | [I-05] | Remove not needed custom error | Informational | 56 | | [I-06] | Solidity safe pragma best practices are not used | Informational | 57 | | [I-07] | External method missing a NatSpec | Informational | 58 | | [I-08] | Mismatch between contract and file names | Informational | 59 | | [I-09] | Missing `override` keyword | Informational | 60 | | [I-10] | Typos in comments | Informational | 61 | 62 | # Detailed Findings 63 | 64 | # [H-01] Anyone can use or steal `ArbitrumSwaps` native asset balance 65 | 66 | ## Severity 67 | 68 | **Likelihood:** 69 | High, because this can easily be noticed and exploited 70 | 71 | **Impact:** 72 | Medium, because value can be stolen, but it should be limited to gas refunds 73 | 74 | ## Description 75 | 76 | An attacker can steal the `ArbitrumSwaps` native asset balance by doing a call to the `arbitrumSwaps` method with steps `WETH_DEPOSIT` and `WETH_WITHDRAW` - this will send over the whole contract balance to a caller-supplied address. This shouldn't be a problem, because the contract is a "swap router" and is not expected to hold any native asset balance at any time. Well this assumption does not hold, because in the `stargateSwap` method the `_refundAddress` argument of the `swap` method call to the `stargateRouter` is `address(this)`. This means that all of the native asset that is refunded will be held by the `ArbitrumSwaps` contract and an attacker can back-run this refund and steal the balance. 77 | 78 | ## Recommendations 79 | 80 | The refund address should be `msg.sender` and not `address(this)`. This way the protocol won't be expected to receive native assets, so they can be stolen only if someone mistakenly sends them to the `ArbitrumSwaps` contract which is an expected risk. 81 | 82 | # [H-02] Malicious user can easily make the protocol revert on every `USDT` swap on Uniswap 83 | 84 | ## Severity 85 | 86 | **Likelihood:** 87 | High, attack can easily be done and it exploits a well-known attack vector of `USDT` 88 | 89 | **Impact:** 90 | Medium, because the protocol will not work with only one ERC20 token, but it is a widely used one 91 | 92 | ## Description 93 | 94 | A malicious user can get the `ArbitrumSwaps` contract to revert on each `USDT` swap on Uniswap, because of a well-known attack vector of the token implementation. The problem is in the following code from `UniswapAdapter.sol` and is present in both `swapExactInputSingle` and `swapExactInputMultihop` 95 | 96 | ```solidity 97 | TransferHelper.safeApprove( 98 | swapParams.token1, address(swapRouter), IERC20(swapParams.token1).balanceOf(address(this)) 99 | ); 100 | ``` 101 | 102 | Here is how the attack can be done: 103 | 104 | 1. Malicious user transfers manually 1 wei of USDT to `ArbitrumSwaps` 105 | 2. Now he calls `ArbitrumSwaps::arbitrumSwaps` with `step == UNI_SINGLE` to swap 1 USDT 106 | 3. Now `swapExactInputSingle` approves 1 + 1e-18 USDT, because of the `balanceOf` call 107 | 4. Transaction will complete successfully, but next time anyone wants to swap USDT on Uniswap the transaction will revert, because of USDT approval race condition - to do an `approve` call the allowance should be either 0 or `type(uint256).max`, which is not the case because allowance is 1 wei 108 | 109 | ## Recommendation 110 | 111 | Instead of using the `IERC20(multiParams.token1).balanceOf(address(this))` as the approved allowance, use the `amountIn` parameter. 112 | 113 | # [M-01] Use `quoteLayerZeroFee` instead of sending all native asset balance as gas fee for `swap` call 114 | 115 | ## Severity 116 | 117 | **Likelihood:** 118 | High, because the wrong value will be sent always 119 | 120 | **Impact:** 121 | Low, because the `swap` function has a gas refund mechanism 122 | 123 | ## Description 124 | 125 | Currently in `StargateArbitrum::stargateSwap` when doing a call to the `swap` method of `stargateRouter`, all of the contract's native asset balance is sent to it so it can be used to pay the gas fee. The [Stargate docs](https://stargateprotocol.gitbook.io/stargate/developers/cross-chain-swap-fee) show that there is a proper way to calculate the fee and it is by utilizing the `quoteLayerZeroFee` method of `stargateRouter`. 126 | 127 | ## Recommendations 128 | 129 | Follow the documentation to calculate the fee correctly instead of always sending the whole contract's balance as a fee, even though there is a refund mechanism. 130 | 131 | # [L-01] Check array arguments have the same length 132 | 133 | In both `ArbitrumSwaps::arbitrumSwaps` and in `StargateArbitrum::stargateSwap` you have multiple array-type arguments. Validate in both places that the arguments have the same length so you do not get unexpected errors if they don't. 134 | 135 | # [L-02] A `require` check can easily be bypassed 136 | 137 | In `StargateArbitrum::stargateSwap` we have the following code 138 | 139 | ```solidity 140 | if (msg.value <= 0) revert MustBeGt0(); 141 | ``` 142 | 143 | This check can easily be bypassed by just sending 1 wei. Remove the check completely, since `msg.value` is not used in the method anyway. 144 | 145 | # [L-03] The `gasLeft()` after gas-limited external call might not be enough to complete the transaction 146 | 147 | In `StargateArbitrum::sgReceive` we have the following piece of code 148 | 149 | ```solidity 150 | try IArbitrumSwaps(payable(address(this))).arbitrumSwaps{gas: 200000}(steps, data) {} 151 | catch (bytes memory) { 152 | IERC20(_token).safeTransfer(to, amountLD); 153 | failed = true; 154 | } 155 | ``` 156 | 157 | Now if the `arbitrumSwaps` call took up all of the gas it is possible that there is not enough gas left for the `safeTransfer` call, as well for the code below it. Consider a different approach, that will check `gasleft()` and make sure that there will be enough, something like in [this method](https://github.com/sushiswap/sushiswap/blob/9a85946574135d57194c44bf27376732091974cc/protocols/sushixswap/contracts/adapters/StargateAdapter.sol#L114-L171) 158 | 159 | # [I-01] Prefer battle-tested code over reimplementing common patterns 160 | 161 | Replace the `locked` modifier in `ArbitrumSwaps` with the `nonReentrant` from OpenZeppelin, since it is well tested and optimized. 162 | 163 | # [I-02] Use an enum for the "step" types in `ArbitrumSwaps` 164 | 165 | Currently the step types are handled by constants that have an integer value and are not sequential (numbers 7 to 11 are missing). This is a great use case for an enum, where you will have sequential numbering and proper naming. Remove the constants and use an enum. 166 | 167 | # [I-03] Move code to bring cohesion up 168 | 169 | The event `FeePaid` and the `calculateFee` method should both be in `ArbitrumSwaps` instead of in `StargateArbitrum` since they have nothing to do with the Stargate logic, but are used in some swap/transfer scenarios. 170 | 171 | # [I-04] Use `x != 0` to get positive-only uint values 172 | 173 | The "positive uint" checks in the code are not done in the best possible way, one example is `_amount <= 0` - if a number is expected to be of a `uint` type, then you can check that it is positive by doing `x != 0` since `uint` can never be a negative number. Replace all `x <= 0` occurrences with `x != 0` when `x` is a `uint`. 174 | 175 | # [I-05] Remove not needed custom error 176 | 177 | The `MoreThanZero` custom error in `ArbitrumSwaps` is badly named and also duplicates the inherited from `StargateArbitrum` custom error `MustBeGt0` - prefer using the latter and remove the former. 178 | 179 | # [I-06] Solidity safe pragma best practices are not used 180 | 181 | Always use a stable pragma to be certain that you deterministically compile the Solidity code to the same bytecode every time. Also `IStargateReceiver` and `IStargateRouter` interfaces are using an old compiler version - upgrade it to a newer version, use the same pragma statement throughout the whole codebase. 182 | 183 | # [I-07] External method missing a NatSpec 184 | 185 | The `arbitrumSwaps` method in `ArbitrumSwaps` is missing a NatSpec doc, add one to improve the code technical documentation. 186 | 187 | # [I-08] Mismatch between contract and file names 188 | 189 | The `ArbitrumSwaps` contract inherits from `SushiLegacyAdapter` which is imported from `SushiAdapter.sol`. Use the same name for the smart contract and the file. 190 | 191 | # [I-09] Missing `override` keyword 192 | 193 | The method `arbitrumSwaps` in `ArbitrumSwaps` is inheriting the method from `IArbitrumSwaps` but is missing the `override` keyword which should be there. 194 | 195 | # [I-10] Typos in comments 196 | 197 | In `StargateArbitrum` you wrote `arrat` -> `array` 198 | -------------------------------------------------------------------------------- /solo/CadmosFinance-security-review.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | A time-boxed security review of the **Cadmos Finance** protocol was done by pashov, with a focus on the security aspects of the application's implementation. 4 | 5 | # Disclaimer 6 | 7 | A smart contract security review can never verify the complete absence of vulnerabilities. This is a time, resource and expertise bound effort where I try to find as many vulnerabilities as possible. I can not guarantee 100% security after the review or if even the review will find any problems with your smart contracts. 8 | 9 | # Protocol Overview 10 | 11 | The protocol has three main types of actors: 12 | 13 | 1. Investors - the users of the protocol, they deposit their capital and expect to earn some yield on it 14 | 2. Strategists - the actors who manage the Protocol Treasury with the goal of a high yield on the Treasury funds 15 | 3. Administrators - they manage Investment Pools, for example by computing the performance of the Strategists and writing it on-chain, acting as an off-chain oracle 16 | 17 | A typical usage flow would be the following: 18 | 19 | 1. Investors deposit capital (for example `DAI` tokens) to a Settlement Pool and receive Settlement Pool Tokens (ERC20) back 20 | 2. At some point Administrator triggers a transaction to pull the deposited funds from the Settlement Pool to an Investment Pool 21 | 3. Now Administrator moves the deposited funds again, this time from Investment Pool to Treasury 22 | 4. A Strategist fine-tunes the investment approach for the deposited funds so that he can increase the Pool Net Asset Value 23 | 5. After a while, the Investor can redeem his initial capital plus the yield accrued by burning his Investment Pool Tokens. 24 | 25 | **Liquidation state of an Investment Pool** 26 | 27 | An Investment Pool's Administrator can place it in a liquidation state at any time. This applies some constraints to the Pool, most important ones are: 28 | 29 | 1. Deposits are not allowed 30 | 2. Transfers to Pool Treasury are not allowed 31 | 3. All rewards are set to 0 32 | 4. The Pool state can't be changed 33 | 34 | When an Investment Pool is in a liquidation state, an Investor can call `InvestmentPoolCore::liquidate` to directly withdraw his deposit. 35 | 36 | Both the Strategists and the Administrators should be 100% trusted, since they have great power in the system, mainly for moving user funds. 37 | 38 | # Severity classification 39 | 40 | | Severity | Impact: High | Impact: Medium | Impact: Low | 41 | | ---------------------- | ------------ | -------------- | ----------- | 42 | | **Likelihood: High** | Critical | High | Medium | 43 | | **Likelihood: Medium** | High | Medium | Low | 44 | | **Likelihood: Low** | Medium | Low | Low | 45 | 46 | # Security Assessment Summary 47 | 48 | **_review commit hash_ - [a3754f182851ce90f33f514e6f0bd1dd2d539cdb](https://github.com/Cadmos-finance/InvestmentPool/tree/a3754f182851ce90f33f514e6f0bd1dd2d539cdb)** 49 | 50 | ### Scope 51 | 52 | The following smart contracts were in scope of the audit: 53 | 54 | - `InvestmentPoolCore` 55 | - `InvestmentPoolFactory` 56 | - `ProtocolRegistry` 57 | - `SettlementPool` 58 | - `SimpleAdministrator` 59 | - `Whitelist` 60 | 61 | The following number of issues were found, categorized by their severity: 62 | 63 | - Medium: 5 issues 64 | - Low: 6 issues 65 | - Informational: 6 issues 66 | 67 | --- 68 | 69 | # Findings Summary 70 | 71 | | ID | Title | Severity | 72 | | ------ | -------------------------------------------------------------------------------------------- | ------------- | 73 | | [M-01] | Hardcoding gas costs should be avoided | Medium | 74 | | [M-02] | `transferERC20ToTreasury` won't work as intended if `assetToken` is a multiple-address token | Medium | 75 | | [M-03] | Front-running risk in key admin actions | Medium | 76 | | [M-04] | An important flow of admin actions is not enforced, just documented | Medium | 77 | | [M-05] | Single-step ownership transfer can be dangerous | Medium | 78 | | [L-01] | Contracts are not directly implementing their interface contracts | Low | 79 | | [L-02] | Using OpenZeppelin's `ECDSA` with a vulnerable library version | Low | 80 | | [L-03] | Missing input validation in InvestmentPoolCore::setWhitelistOnly | Low | 81 | | [L-04] | Missing event emission | Low | 82 | | [L-05] | Flag has too many purposes | Low | 83 | | [L-06] | Wrong NatSpec/implementation | Low | 84 | | [I-01] | Protocol is using an older Solidity version | Informational | 85 | | [I-02] | All methods have `nonReentrant` modifier | Informational | 86 | | [I-03] | Check for zero balance in `cancelDeposit` | Informational | 87 | | [I-04] | Not used event can be removed | Informational | 88 | | [I-05] | Not used import can be removed | Informational | 89 | | [I-06] | Whitelisting modes should be handled by an enum | Informational | 90 | 91 | # Detailed Findings 92 | 93 | # [M-01] Hardcoding gas costs should be avoided 94 | 95 | ## Severity 96 | 97 | **Likelihood:** 98 | Medium, because changes to gas costs have happened before, but it is not certain that there will be changes that affect the protocol. 99 | 100 | **Impact:** 101 | Low, because even though calculations will be wrong they can still be done off-chain 102 | 103 | ## Description 104 | 105 | The modifier `markCost` in `SimpleAdministrator` has some hard coded gas cost values like for example 21000 (the base cost of an EVM transaction). We have seen previous EVM forks changing the gas cost of some key things, for example the SSTORE opcode. This can happen again and in this case the hardcoded values in `markCost` might not be correct anymore which will lead to wrong accounting for incurred gas costs. Also if the project is deployed on a different EVM-compatible chain, the gas costs there might be different. 106 | 107 | ## Recommendations 108 | 109 | Initialize the expected gas costs in the `initialize` method and add setter functions to be able to update them in case of an EVM fork 110 | 111 | ## Discussion 112 | 113 | ### CADMOS 114 | 115 | Acknowledged - corrected. 116 | 117 | # [M-02] `transferERC20ToTreasury` won't work as intended if `assetToken` is a multiple-address token 118 | 119 | ## Severity 120 | 121 | **Likelihood:** 122 | Low, because it requires using a multiple-address token and a malicious/compromised admin 123 | 124 | **Impact:** 125 | High, because users can use 100% of their deposits 126 | 127 | ## Description 128 | 129 | Some ERC20 tokens on the blockchain are deployed behind a proxy, so they have at least 2 entry points (the proxy and the implementation) for their functionality. Example is Synthetix’s `ProxyERC20` contract from where you can interact with `sUSD, sBTC etc). If such a token was used as the `assetToken`token in an InvestmentPool, then the admin will be able to rug all depositors with the`transferERC20ToTreasury` method, even though it has the following check 130 | 131 | ```solidity 132 | require(tokenAddress != _assetTokenAddress, "IP: Asset transfer"); 133 | ``` 134 | 135 | Since the tokens have multiple addresses the admin can give another address and pass those checks. 136 | 137 | ## Recommendations 138 | 139 | Instead of checking the address of the transferred token, it is a better approach to check the balance of it before and after the transfer and to verify it is the same. 140 | 141 | ## Discussion 142 | 143 | ### CADMOS 144 | 145 | Acknowledged - corrected. 146 | 147 | # [M-03] Front-running risk in key admin actions 148 | 149 | ## Severity 150 | 151 | **Likelihood:** 152 | Medium, because it requires the malicious user to have a script that monitors the public mempool 153 | 154 | **Impact:** 155 | Medium, because key admin functionality will revert 156 | 157 | ## Description 158 | 159 | The methods `forceTransfer`, `whitelistAccount` and `freezeAccount` from `InvestmentPoolCore` and `Whitelist` can be monitored for transactions and front-ran. Imagine the following scenario: 160 | 161 | 1. Bob holds some `InvestmentPool` ERC20 tokens 162 | 2. For some reason, a holder of the `TOKEN_FREEZE_ROLE` decides Bob is malicious and his balance should be frozen, so he calls `Whitelist::freezeAccount` 163 | 3. Bob was expecting that and was already monitoring the mempool, so he front-runs the transaction with a transfer transaction to another address he controls 164 | 4. Now his address is frozen, but he can still move/redeem/swap his tokens since the new address is not frozen 165 | 166 | The same logic applies for the `whitelistAccount` and `forceTransfer` functionalities. 167 | 168 | ## Recommendations 169 | 170 | Always execute transactions to the mentioned functions through a private mempool or redesign them so they are not front-runnable. 171 | 172 | ## Discussion 173 | 174 | ### CADMOS 175 | 176 | Acknowledged. 177 | 178 | # [M-04] An important flow of admin actions is not enforced, just documented 179 | 180 | ## Severity 181 | 182 | **Likelihood:** 183 | Low, because it requires either a malicious/compromised admin or the admin to forget it has to do the correct flow of operations 184 | 185 | **Impact:** 186 | High, because users will lose their funds 187 | 188 | ## Description 189 | 190 | The NatSpec of `InvestmentPoolCore::setInflowOutflowPool` contains the following comment: 191 | 192 | ```solidity 193 | /// @notice call batchSettlement(id) beforehand, otherwise it will rug the old pool tokenholders 194 | ``` 195 | 196 | This can easily be forgotten or missed when executing a call to the method. This way of ensuring proper flow of operations is used is error-prone. 197 | 198 | ## Recommendations 199 | 200 | Ensure that `batchSettlement(id)` was called beforehand by using a flag or some storage variable to be certain that users won't be rugged. 201 | 202 | ## Discussion 203 | 204 | ### CADMOS 205 | 206 | Acknowledged - corrected. 207 | 208 | # [M-05] Single-step ownership transfer can be dangerous 209 | 210 | ## Severity 211 | 212 | **Likelihood:** 213 | Low, because it requires an error on the admin side 214 | 215 | **Impact:** 216 | High, because protocol will be bricked 217 | 218 | ## Description 219 | 220 | Single-step ownership transfer means that if a wrong address was passed when transferring ownership or admin rights it can mean that role is lost forever. This can be detrimental in the context of `InvestmentPoolCore`, where if `transferAdminRole` method was called with a wrong `newAdmin` address, then the `InvestmentPoolCore` contract will be bricked, since it relies heavily on admin-only methods. 221 | 222 | ## Recommendations 223 | 224 | It is a best practice to use two-step ownership transfer pattern, meaning ownership transfer gets to a "pending" state and the new owner should claim his new rights, otherwise the old owner still has control of the contract. 225 | 226 | ## Discussion 227 | 228 | ### CADMOS 229 | 230 | Acknowledged - corrected: 231 | 232 | - ProtocolRegistry now is Ownable2Step instead of Ownable. 233 | - 2-Step transfer for InvestmentPool Admin. Role change 234 | - 2-Step Admin Right transfer in simpleAdmin. 235 | 236 | 237 | # [L-01] Contracts are not directly implementing their interface contracts 238 | 239 | There are interface contracts in `interfaces/` for all contracts in `contracts/` but they are not used directly. This means some method might actually not be overriden since the code is not making use of compiler checks. Make sure implementation contracts inherit directly from interface contracts. 240 | 241 | ## Discussion 242 | 243 | ### CADMOS 244 | 245 | Acknowledged - corrected. 246 | 247 | # [L-02] Using OpenZeppelin's `ECDSA` with a vulnerable library version 248 | 249 | The codebase uses version `4.4.0` for its OpenZeppelin's dependencies, but this version has a High severity vulnerability related to ECDSA - [Reference](https://github.com/OpenZeppelin/openzeppelin-contracts/security/advisories/GHSA-4h98-2769-gh6h) 250 | Even though the code is not exploitable in its current state, it is best to upgrade the OpenZeppelin library dependency to the latest safe version (4.7.3) 251 | 252 | ## Discussion 253 | 254 | ### CADMOS 255 | 256 | Acknowledged - bumped to 4.8.0. 257 | 258 | 259 | # [L-03] Missing input validation in InvestmentPoolCore::setWhitelistOnly 260 | 261 | The only correct values of the `flag` argument are either 0, 1 or 2. This should be validated with a `require` statement. 262 | 263 | ## Discussion 264 | 265 | ### CADMOS 266 | 267 | Acknowledged - corrected. 268 | 269 | # [L-04] Missing event emission 270 | 271 | The `_newAdmin` method in `SimpleAdministrator` does not emit an event, but it should, because it is important that admin additions can be tracked easily off-chain. Emit a proper event in `_newAdmin`. 272 | Same thing for the `whitelistOffChain` method in `Whitelist` - it should emit `Whitelisted` event. 273 | 274 | ## Discussion 275 | 276 | ### CADMOS 277 | 278 | - \_newAdmin emits `AdminRightsChanged(newAdmin, 0, flag)`. 279 | - Acknowledged for `whitelistOffChain` - corrected. 280 | 281 | # [L-05] Flag has too many purposes 282 | 283 | The `setTreasury` method in `SimpleAdministrator` asks for the `FLAG_STRAT_CHANGE` flag, but it is better for that action to have its own flag, for example `FLAG_TREASURY_CHANGE`. Add a separate flag for this functionality. 284 | 285 | ## Discussion 286 | 287 | ### CADMOS [24/12/2022] 288 | 289 | Acknowledged - corrected. 290 | 291 | # [L-06] Wrong NatSpec/implementation 292 | 293 | The `setNewSoftHurdleRate` method in `SimpleAdministrator` says "activate/deactivate via FLAG_HURDLE_RATE_CHANGE" but it actually uses `FLAG_REWARD_CHANGE`. Update the flag validation or the NatSpec appropriately. 294 | 295 | ## Discussion 296 | 297 | ### CADMOS 298 | 299 | Acknowledged - corrected. 300 | 301 | # [I-01] Protocol is using an older Solidity version 302 | 303 | The protocol is using Solidity compiler version 0.8.3, while the latest is 0.8.17 - you can get a lot of features and optimisations, for example Custom Errors by upgrading versions 304 | 305 | ## Discussion 306 | 307 | ### CADMOS 308 | 309 | Acknowledged - bumped to 0.8.7 310 | 311 | # [I-02] All methods have `nonReentrant` modifier 312 | 313 | If a method does not have an external call then it is impossible to reenter, so you can skip this modifier in such methods 314 | 315 | ## Discussion 316 | 317 | ### CADMOS 318 | 319 | Acknowledged - this must carefully be done as though the function cannot reenter another function it can itself be reentered. 320 | 321 | # [I-03] Check for zero balance in `cancelDeposit` 322 | 323 | The `cancelDeposit` method in `SettlementPool` is missing a check if the caller has more than zero balance. 324 | 325 | ## Discussion 326 | 327 | ### CADMOS 328 | 329 | Acknowledged - corrected. 330 | 331 | # [I-04] Not used event can be removed 332 | 333 | The `ForcedTransfer` event in `SettlementPool` is not used and can be removed. 334 | 335 | ## Discussion 336 | 337 | ### CADMOS 338 | 339 | Acknowledged - corrected. 340 | 341 | # [I-05] Not used import can be removed 342 | 343 | The `ReentrancyGuard` smart contract is imported in `ProtocolRegistry` but is not used and can be removed. 344 | 345 | ## Discussion 346 | 347 | ### CADMOS 348 | 349 | Acknowledged - corrected. 350 | 351 | # [I-06] Whitelisting modes should be handled by an enum 352 | 353 | The `_BLACKLISTMODE`, `_WHITELISTPRIMARY` and `_WHITELISTALL` modes should be turned to a `WhitelistMode` enum . 354 | 355 | ## Discussion 356 | 357 | ### CADMOS 358 | 359 | Acknowledged - corrected 360 | -------------------------------------------------------------------------------- /solo/Azuro-security-review.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | A time-boxed security review of the **Azuro** protocol was done by pashov, with a focus on the security aspects of the application's implementation. 4 | 5 | # Disclaimer 6 | 7 | A smart contract security review can never verify the complete absence of vulnerabilities. This is a time, resource and expertise bound effort where I try to find as many vulnerabilities as possible. I can not guarantee 100% security after the review or if even the review will find any problems with your smart contracts. 8 | 9 | # Protocol Overview 10 | 11 | Azuro is a decentralized betting protocol. Anyone can launch a frontend service that connects to the smart contracts and to receive an affiliate bonus for each bet made through the given frontend. Different betting events can be hosted, for example a football game. Odds are provided once by a Data Feed provider (Oracle) for initialization and then odds change based on the betting on the platform. A user bet gets automatically converted to an NFT in the user's wallet. 12 | 13 | # Severity classification 14 | 15 | | Severity | Impact: High | Impact: Medium | Impact: Low | 16 | | ---------------------- | ------------ | -------------- | ----------- | 17 | | **Likelihood: High** | Critical | High | Medium | 18 | | **Likelihood: Medium** | High | Medium | Low | 19 | | **Likelihood: Low** | Medium | Low | Low | 20 | 21 | # Security Assessment Summary 22 | 23 | **_review commit hash_ - [7c6f477ca345ef8ca7a1c1f697daf479174b7060](https://github.com/Azuro-protocol/Azuro-v2/tree/7c6f477ca345ef8ca7a1c1f697daf479174b7060)** 24 | 25 | ### Scope 26 | 27 | The following smart contracts were in scope of the audit: 28 | 29 | - `Access` 30 | - `AzuroBet` 31 | - `Core` 32 | - `CoreBase` 33 | - `Factory` 34 | - `LP` 35 | - `interface/**` 36 | - `libraries/**` 37 | - `utils/**` 38 | 39 | Contracts `SafeOracle`, `FreeBet`, `BetExpress` and `LiveCore` were out of scope for this audit. 40 | 41 | The following number of issues were found, categorized by their severity: 42 | 43 | - High: 0 issues 44 | - Medium: 5 issues 45 | - Low: 7 issues 46 | - Informational: 12 issues 47 | 48 | --- 49 | 50 | # Findings Summary 51 | 52 | | ID | Title | Severity | 53 | | ------ | ------------------------------------------------------------------------------------------------------------- | ------------- | 54 | | [M-01] | `claimTimeout` is not checked for first claim by an account | Medium | 55 | | [M-02] | Protocol can't use smaller decimals tokens as bet tokens | Medium | 56 | | [M-03] | Missing admin input sanitization | Medium | 57 | | [M-04] | `OwnableUpgradeable` uses single-step ownership transfer | Medium | 58 | | [M-05] | Admin privileges are dangerous | Medium | 59 | | [L-01] | Prefer using `_safeMint` over `_mint` | Low | 60 | | [L-02] | Missing event emission` | Low | 61 | | [L-03] | Protocol won't work with tokens with a fee-on-transfer or a rebasing mechanism | Low | 62 | | [L-04] | The `coreAffRewards` mapping is not checked in `claimAffiliateReward` | Low | 63 | | [L-05] | Call to `azuroBet.mint()` can reenter | Low | 64 | | [L-06] | Code is lacking technical documentation | Low | 65 | | [L-07] | Unused method is not working as intended | Low | 66 | | [I-01] | Use braces around operators with uncertain precedence | Informational | 67 | | [I-02] | The `stopCondition` method can start a condition as well as stopping | Informational | 68 | | [I-03] | Move not essential logic to off-chain computations | Informational | 69 | | [I-04] | Redundant code | Informational | 70 | | [I-05] | Open `TODO` in code | Informational | 71 | | [I-06] | Unused imports | Informational | 72 | | [I-07] | Method inherited from interface is missing the `override` keyword | Informational | 73 | | [I-08] | Use a safe pragma statement | Informational | 74 | | [I-09] | Small issues in initializer methods | Informational | 75 | | [I-10] | Typos in NatSpec | Informational | 76 | | [I-11] | Wrong import | Informational | 77 | | [I-12] | Consider using custom errors instead of require statements with string error | Informational | 78 | 79 | # Detailed Findings 80 | 81 | # [M-01] `claimTimeout` is not checked for first claim by an account 82 | 83 | ## Severity 84 | 85 | **Likelihood:** 86 | High, because it will happen for each account's first claim 87 | 88 | **Impact:** 89 | Low, because there is no loss of funds, but code is not working as intended 90 | 91 | ## Description 92 | 93 | In `claimRewards` in `LP.sol` there is the following check 94 | 95 | ```solidity 96 | if ((block.timestamp - reward.claimedAt) < claimTimeout) 97 | revert ClaimTimeout(reward.claimedAt + claimTimeout); 98 | ``` 99 | 100 | which basically forces an account that claims his rewards to wait for at least `claimTimeout` amount of time. The problem is, in `addReserve` the reward amount is set, but `reward.claimedAt` is not set to `block.timestamp`. This means that `reward.claimedAt` will be 0 the first time `claimRewards` is called for an address, so the `claimTimeout` check will pass even though the time might have not passed yet. 101 | 102 | ## Recommendations 103 | 104 | When setting the reward amount in `addReserve` also set `reward.claimedAt = block.timestamp` 105 | 106 | # [M-02] Protocol can't use smaller decimals tokens as bet tokens 107 | 108 | ## Severity 109 | 110 | **Likelihood:** 111 | Medium, because such tokens are widely used and accepted 112 | 113 | **Impact:** 114 | Medium, because it limits the functionality of the protocol 115 | 116 | ## Description 117 | 118 | The current implementation of the protocol allows it to only use higher (for example 18) decimals tokens like `DAI` for betting and liquidity provision. This is enforced by the `minDepo` property in `LP.sol` which can't be less than 1e12 for adding liquidity, as well as the check for `amount` in `putBet` in `Core.sol` where `amount` should be >1e12. If a smaller decimals tokens is to be used (for example `USDT`, `USDC`, `wBTC`) then the users and LPs will need to have a very high amount of capital to interact with the platform. 119 | 120 | ## Recommendations 121 | 122 | Revisit the validations for `minDepo` and `amount`, one possible approach is to calculate those based on the token's decimals 123 | 124 | # [M-03] Missing admin input sanitization 125 | 126 | ## Severity 127 | 128 | **Likelihood:** 129 | Low, because it requires a malicious/compromised admin or an error on admin side 130 | 131 | **Impact:** 132 | High, because important protocol functionality can be bricked 133 | 134 | ## Description 135 | 136 | It is not checked that the `claimTimeout` property in `LP.sol` both in its setter function and in `initialize` does not have a very big value. Same thing for the setter function of `withdrawTimeout`. Also, the `checkFee` method in `LP.sol` has a loose validation - the max sum of all fees should be much lower than 100%. Finally the `startsAt` argument of `shiftGame` in `LP.sol` is not validated that it is not after the current timestamp. 137 | 138 | ## Recommendations 139 | 140 | Add an upper cap for `claimTimeout` & `withdrawTimeout`. Make the max sum of all fees to be lower - for example 20%. In `shiftGame` check that `startsAt >= blockTimestamp`. 141 | 142 | # [M-04] `OwnableUpgradeable` uses single-step ownership transfer 143 | 144 | ## Severity 145 | 146 | **Likelihood:** 147 | Low, because it requires an error on the admin side 148 | 149 | **Impact:** 150 | High, because important protocol functionality will be bricked 151 | 152 | ## Description 153 | 154 | Single-step ownership transfer means that if a wrong address was passed when transferring ownership or admin rights it can mean that role is lost forever. The ownership pattern implementation for the protocol is in `OwnableUpgradeable.sol` where a single-step transfer is implemented.This can be a problem for all methods marked in `onlyOwner` throughout the protocol, some of which are core protocol functionality. 155 | 156 | ## Recommendations 157 | 158 | It is a best practice to use two-step ownership transfer pattern, meaning ownership transfer gets to a "pending" state and the new owner should claim his new rights, otherwise the old owner still has control of the contract. Consider using OpenZeppelin's `Ownable2Step` contract 159 | 160 | # [M-05] Admin privileges are dangerous 161 | 162 | ## Severity 163 | 164 | **Likelihood:** 165 | Low, because it requires a malicious/compromised admin 166 | 167 | **Impact:** 168 | High, because a rug pull can be executed 169 | 170 | ## Description 171 | 172 | A malicious or a compromised admin can execute a 100% rug pull in the following way: 173 | 174 | 1. The `LP` admin calls the `Factory` contract to add a malicious `core` to the `LP` 175 | 2. The malicious `core` returns the `LP` contract balance when its `resolveAffiliateReward` method is called 176 | 3. Now calling `claimAffiliateReward` with the fake `core` as an argument will result in a 100% of the `LP` balance stolen 177 | 178 | Same thing applies to `withdrawPayout`. 179 | 180 | ## Recommendations 181 | 182 | Make the process of adding a new `coreType` or calling `plugCore` to be safer. One possible approach is by adding a time delay before a core is added to the `LP`, up until which the request will be pending. 183 | 184 | # [L-01] Prefer using `_safeMint` over `_mint` 185 | 186 | Both `Access::grantRole` and `LP::_addLiquidity` use ERC721's `_mint` method, which is missing the check if the account to mint the NFT to is a smart contract that can handle ERC721 tokens. The `_safeMint` method does exactly this, so prefer using it over `_mint` but always add a `nonReentrant` modifier, since calls to `_safeMint` can reenter. 187 | 188 | # [L-02] Missing event emission 189 | 190 | The `changeLockedLiquidity` method in `LP.sol` does not emit an event which might not be good for off-chain monitoring. Emit a proper event in both paths, adding liquidity and reducing it. Same problem exists in `AzuroBet::setURI` - emit an event on state change. 191 | 192 | # [L-03] Protocol won't work with tokens with a fee-on-transfer or a rebasing mechanism 193 | 194 | Some tokens on the blockchain make arbitrary changes to account balances. Examples are fee-on-transfer tokens and tokens with rebasing mechanisms. There is no specific handling for such tokens, as the amount held by the `LP` contract might actually be less than it has accounted for. Think about handling such tokens or document the list of ERC20 tokens you intend to support in the protocol. 195 | 196 | # [L-04] The `coreAffRewards` mapping is not checked in `claimAffiliateReward` 197 | 198 | When calling `claimAffiliateReward` for a core in `LP.sol` the `coreAffRewards` mapping is not checked to see if the core has actually earned enough rewards for the claim. Add a validation for the mapping. 199 | 200 | # [L-05] Call to `azuroBet.mint()` can reenter 201 | 202 | The `mint` function in `azuroBet` does an external call to check if the recipient is a smart contract that can handle such tokens. This call is unsafe, as the recipient can be malicious and do a reentrancy call. Consider adding a `nonReentrant` modifier to methods in `Core.sol` 203 | 204 | # [L-06] Code is lacking technical documentation 205 | 206 | In multiple places throughout the code there is a need for technical documentation as dev assumptions are not clear and some math formulas are used but it is not clear why. One example for this is `CoreBase::_applyOdds` - consider adding technical documentation to complex code for easier understandability by users & auditors. Also revisit existing NatSpec docs and add information for all parameters, as that is missing in multiple places. 207 | 208 | # [L-07] Unused method is not working as intended 209 | 210 | The method `getLeavesAmount` in `LiquidityTree` will return just the `node` amount and won't consider the amounts in its leaves. Method is not used anywhere, consider removing it. 211 | 212 | # [I-01] Use braces around operators with uncertain precedence 213 | 214 | In `Access::roleGranted` we see the following code 215 | 216 | ```solidity 217 | return userRoles[account] & roleBit == roleBit; 218 | ``` 219 | 220 | In Solidity the `&` operator will be executed before `==` but this might not always be clear and might be different in other languages. I suggest adding braces around `userRoles[account] & roleBit` to clarify operator precedence. 221 | 222 | Both `Access::grantRole` and `LP::_addLiquidity` use ERC721's `_mint` method, which is missing the check if the account to mint the NFT to is a smart contract that can handle ERC721 tokens. The `_safeMint` method does exactly this, so prefer using it over `_mint` but always add a `nonReentrant` modifier, since calls to `_safeMint` can reenter. 223 | 224 | # [I-02] The `stopCondition` method can start a condition as well as stopping 225 | 226 | The `stopCondition` method and its event show intention that they only have functionality for stopping a condition, but they can start it again. Use different wording, for example `updateConditionStatus`. 227 | 228 | # [I-03] Move not essential logic to off-chain computations 229 | 230 | The `game.conditions` array is written to in `LP::addCondition` but is never read in the system. Consider moving this logic to the front end services. 231 | 232 | # [I-04] Redundant code 233 | 234 | The `assert(affiliateProfits[i] >= oldProfit - newProfit);` check is redundant, since the next line of code `affiliateProfits[i] -= (oldProfit - newProfit).toUint128();` will fail if condition checked is false. Consider removing redundant code. 235 | 236 | # [I-05] Open `TODO` in code 237 | 238 | The `_resolveCondition` method in `CoreBase.sol` has an open `TODO`, consider fixing or deleting it. 239 | 240 | # [I-06] Unused imports 241 | 242 | All imports in `IBet.sol` are unused, consider removing those. Same for `OwnableUpgradeable` import in `Core` 243 | 244 | # [I-07] Method inherited from interface is missing the `override` keyword 245 | 246 | The `changeOdds` method in `CoreBase` is missing the `override` keyword, consider adding it. 247 | 248 | # [I-08] Use a safe pragma statement 249 | 250 | Always use stable pragma statement to lock the compiler version. Also there are different versions of the compiler used throughout the codebase, use only one. Finally consider upgrading the version to a newer one to use bugfixes and optimizations in the compiler. 251 | 252 | # [I-09] Small issues in initializer methods 253 | 254 | In `AzuroBet::initialize` the call to `__ERC165_init` is missing and should be added. Also `__Ownable_init_unchained` is called in `Access::initialize` which does not initialize call the `Context` initializer, call `__Ownable_init` instead. 255 | 256 | # [I-10] Typos in NatSpec 257 | 258 | In `IWNative`, the NatSpec has two typos - `interrface` -> `interface` and `vbased` -> `based`. Also move the NatSpec to be just above the interface declaration, not before the `pragma` statement. 259 | 260 | # [I-11] Wrong import 261 | 262 | Change the `ICore` import in `Factory` to `ICoreBase` since that is the one that is used. 263 | 264 | # [I-12] Consider using custom errors instead of require statements with string error 265 | 266 | Custom errors reduce the contract size and can provide easier integration with a protocol. Consider using those instead of require statements with string error 267 | -------------------------------------------------------------------------------- /solo/GMD-security-review.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | A time-boxed security review of the **GMD** protocol was done by pashov, with a focus on the security aspects of the application's implementation. 4 | 5 | # Disclaimer 6 | 7 | A smart contract security review can never verify the complete absence of vulnerabilities. This is a time, resource and expertise bound effort where I try to find as many vulnerabilities as possible. I can not guarantee 100% security after the review or if even the review will find any problems with your smart contracts. 8 | 9 | # Protocol Overview 10 | 11 | The GMD protocol is built on top of GMX and allows you to stake a token (USDC, ETH, BTC) to earn some APY. It fits into the Yield Aggregator category. Users can enter the vault with either a native or an ERC20 asset. The admin takes care of claiming rewards and compounding them. Currently supported assets are `USDC`, `WETH` and `wBTC`. 12 | 13 | # Severity classification 14 | 15 | | Severity | Impact: High | Impact: Medium | Impact: Low | 16 | | ---------------------- | ------------ | -------------- | ----------- | 17 | | **Likelihood: High** | Critical | High | Medium | 18 | | **Likelihood: Medium** | High | Medium | Low | 19 | | **Likelihood: Low** | Medium | Low | Low | 20 | 21 | # Security Assessment Summary 22 | 23 | **_review commit hash_ - [c27c012fde95d9e2ac40fc0fe795489b76284eee](https://github.com/saulgoodmandev/gmd/tree/c27c012fde95d9e2ac40fc0fe795489b76284eee)** 24 | 25 | ### Scope 26 | 27 | The following smart contracts were in scope of the audit: 28 | 29 | - `final_vault` 30 | 31 | The following number of issues were found, categorized by their severity: 32 | 33 | - High: 0 issues 34 | - Medium: 7 issues 35 | - Low: 6 issues 36 | - Informational: 11 issues 37 | 38 | --- 39 | 40 | # Findings Summary 41 | 42 | | ID | Title | Severity | 43 | | ------ | ------------------------------------------------------------------------------------------------ | ------------- | 44 | | [M-01] | Hardcoded handling of non-18 decimal token pools limits the interoperability of the protocol | Medium | 45 | | [M-02] | As protocol relies heavily on admin actions, single-step ownership transfer pattern is dangerous | Medium | 46 | | [M-03] | If `addPool` is called too many times it can brick core functionality | Medium | 47 | | [M-04] | Call to `updatePoolRate` is missing | Medium | 48 | | [M-05] | Admin privilege actions can be risky for users | Medium | 49 | | [M-06] | Token approvals & allowances management is flawed | Medium | 50 | | [M-07] | Inverted slippage protection approach can lead to problems | Medium | 51 | | [L-01] | Inconsistent input validation | Low | 52 | | [L-02] | Storage variable is only written to but never read from | Low | 53 | | [L-03] | Missing parameter validation in `enter` | Low | 54 | | [L-04] | The `IWETH` interface has a method that `WETH` does not have | Low | 55 | | [L-05] | Code is calling a deprecated method | Low | 56 | | [L-06] | Value of slippage protection arguments is not set | Low | 57 | | [I-01] | Using `SafeMath` when compiler is ^0.8.0 | Informational | 58 | | [I-02] | `leaveETH` should not be `payable` | Informational | 59 | | [I-03] | Unused storage variable | Informational | 60 | | [I-04] | Misleading comments throughout the code | Informational | 61 | | [I-05] | Code is not properly formatted | Informational | 62 | | [I-06] | NatSpec missing from external functions | Informational | 63 | | [I-07] | Comment has no meaning | Informational | 64 | | [I-08] | Mismatch between the filename and the contract name | Informational | 65 | | [I-09] | Remove unused import | Informational | 66 | | [I-10] | Not all `require` statements have an error string | Informational | 67 | | [I-11] | External calls can be grouped together | Informational | 68 | 69 | # Detailed Findings 70 | 71 | # [M-01] Hardcoded handling of non-18 decimal token pools limits the interoperability of the protocol 72 | 73 | ## Severity 74 | 75 | **Likelihood:** 76 | Medium, because even though at this point no such pools are added, it is possible that they are in the future 77 | 78 | **Impact:** 79 | Medium, because it limits the functionality of the protocol 80 | 81 | ## Description 82 | 83 | The `enter` method implements specific handling for `USDC` and `wBTC` tokens, because they have decimals that are not equal to 18. This should be done for all such pools tokens, but since it is hardcoded it is not extensible - for example `USDT` pool can't be added. 84 | 85 | ## Recommendations 86 | 87 | Redesign the approach with the decimals that is hardcoded or implement it in an extensible-friendly way for new non-18 decimal token pools. 88 | 89 | ## Discussion 90 | 91 | **pashov**: Client has fixed the issue. 92 | 93 | # [M-02] As protocol relies heavily on admin actions, single-step ownership transfer pattern is dangerous 94 | 95 | ## Severity 96 | 97 | **Likelihood:** 98 | Low, because it requires admin error when transferring ownership 99 | 100 | **Impact:** 101 | High, because it bricks core protocol functionality 102 | 103 | ## Description 104 | 105 | Inheriting from OpenZeppelin's `Ownable` contract means you are using a single-step ownership transfer pattern. If an admin provides an incorrect address for the new owner this will result in none of the `onlyOwner` marked methods being callable again. The better way to do this is to use a two-step ownership transfer approach, where the new owner should first claim its new rights before they are transferred. 106 | 107 | ## Recommendations 108 | 109 | Use OpenZeppelin's `Ownable2Step` instead of `Ownable` 110 | 111 | ## Discussion 112 | 113 | **pashov**: Client has acknowledged the issue. 114 | 115 | # [M-03] If `addPool` is called too many times it can brick core functionality 116 | 117 | ## Severity 118 | 119 | **Likelihood:** 120 | Low, because it requires a malicious admin or a big admin error 121 | 122 | **Impact:** 123 | High, because it bricks core protocol functionality 124 | 125 | ## Description 126 | 127 | The `addPool` method pushes an entry to the `poolInfo` array. Methods like `swapGLPout` and `recoverTreasuryTokensFromGLP` have internal calls (`GLPbackingNeeded`) that iterate over the whole array. If too many pools are added then all calls to those methods will need too much gas to iterate over the array and if this cost is over the block gas limit it will lead to a DoS situation of core functionality. 128 | 129 | ## Recommendations 130 | 131 | Limit the number of pools that can be added, for example to 50. 132 | 133 | ## Discussion 134 | 135 | **pashov**: Client has fixed the issue. 136 | 137 | # [M-04] Call to `updatePoolRate` is missing 138 | 139 | ## Severity 140 | 141 | **Likelihood:** 142 | Medium, because it happens only for a paused and then resumed pool 143 | 144 | **Impact:** 145 | Medium, because it can hardly lead to big losses 146 | 147 | ## Description 148 | 149 | Every time the `totalStaked` amount of a pool is updated, the `updatePoolRate` method is called to update the `EarnRateSec`. This is not true for the `pauseReward` method, which calls `updatePool` that changes the `totalStaked` amount. Now if a pool is paused, when it gets resumed again and `updatePool` is called it will calculate less rewards than it should had, because `EarnRateSec` was not updated. 150 | 151 | ## Recommendations 152 | 153 | Call `updatePoolRate` after the `updatePool` call in `pauseReward` 154 | 155 | ## Discussion 156 | 157 | **pashov**: Client has fixed the issue. 158 | 159 | # [M-05] Admin privilege actions can be risky for users 160 | 161 | ## Severity 162 | 163 | **Likelihood:** 164 | Low, because it requires a malicious/compromised admin 165 | 166 | **Impact:** 167 | High, because it can brick the protocol 168 | 169 | ## Description 170 | 171 | The methods `updateOracle`, `updateRouter` and `updateRewardRouter` are admin controllable and callable anytime. Same for the `withdrawable` property of `PoolInfo`. A malicious/compromised admin can either provide non-existing addresses or set the `withdrawable` property to false for all pools, leading to a DoS for users of the protocol. 172 | 173 | ## Recommendations 174 | 175 | Consider using an role-based access control approach instead of a single admin role as well as a timelock for important admin actions. 176 | 177 | ## Discussion 178 | 179 | **pashov**: Client has acknowledged the issue. 180 | 181 | # [M-06] Token approvals & allowances management is flawed 182 | 183 | ## Severity 184 | 185 | **Likelihood:** 186 | Medium, because it will be problematic only with special type of tokens 187 | 188 | **Impact:** 189 | Medium, because it can lead to a limited loss of funds 190 | 191 | ## Description 192 | 193 | There are a few problems related to approvals & allowances in the contract. One is that the `swaptoGLP` method approves another contract to spend tokens, but some tokens (like `USDT`) have approval race condition protection, which requires the allowance before a call to `approve` to already be either 0 or `UINT_MAX`. If this is not the case, the call reverts, which can lead to a DoS situation with `swaptoGLP`. It looks like there was an idea to mitigate this, because at all places (apart from in `convertDust`) after calling the `swaptoGLP` method there is an `approve` call for 0 allowance, but it is done to the wrong address. `swaptoGLP` always approves `poolGLP` but the 0 allowance `approve` call is always to the `_GLPRouter` when the `_GLPRouter` should never have allowance. 194 | 195 | ## Recommendations 196 | 197 | Set allowance to zero after each `swaptoGLP` call for the `poolGLP` address 198 | 199 | ## Discussion 200 | 201 | **pashov**: Client has fixed the issue. 202 | 203 | # [M-07] Inverted slippage protection approach can lead to problems 204 | 205 | ## Severity 206 | 207 | **Likelihood:** 208 | Low, because it needs more than one special condition simultaneously 209 | 210 | **Impact:** 211 | Medium, because it can lead to limited amount of funds lost from the protocol 212 | 213 | ## Description 214 | 215 | Both the `leaveETH` and `leave` methods use the `slippage` storage variable to implement slippage protection for the users leaving the vault. The problem is that the slippage protection is done in an unusual approach which can result in problems. Both methods call the `swapGLPto` method which has the `min_receive` parameter that is passed to the `unstakeAndRedeemGlp` method in `GLPRouter`. The usual approach to slippage protection is to calculate how much tokens you expect to receive after a swap, let's say 100 $TKN, and then apply some slippage tolerance percentage to it - if the tolerance is 5% then the minimum expected tokens received is 95 $TKN. The protocol implemented a different approach, instead of providing a smaller expected received value it actually inflates the value to be sent for the swap. 216 | 217 | ```solidity 218 | uint256 percentage = 100000 - slippage; 219 | uint256 glpPrice = priceFeed.getGLPprice().mul(percentage).div(100000); 220 | uint256 glpOut = amountOut.mul(10**12).mul(tokenPrice).div(glpPrice).div(10**30); 221 | ``` 222 | 223 | As you see, the way it works is "expecting" a lower price of $GLP which means the protocol always sends more $GLP than needed to swap. Now if the slippage protection is bigger than the deposit fee this can be used as a griefing attack vector by depositing and then withdrawing from a vault multiple times to drain the pool's $GLP balance. 224 | 225 | ## Recommendations 226 | 227 | Think about redesigning the `leave` methods so that you make the user pay the slippage cost instead of the protocol. 228 | 229 | ## Discussion 230 | 231 | **pashov**: Client has acknowledged the issue. 232 | 233 | # [L-01] Inconsistent input validation 234 | 235 | `APR` has a maximum value of 1599 when calling `addPool`, but can be 3999 when calling `setAPR`. Also `glpFees` has a maximum value of 700 when adding a pool but when you call the setter method `setGLPFees` it can be 999. Make sure the input validation is consistent throughout the system. 236 | 237 | ## Discussion 238 | 239 | **pashov**: Client has fixed the issue. 240 | 241 | # [L-02] Storage variable is only written to but never read from 242 | 243 | The `GLPbacking` storage variable is only written to in `updateGLPbackingNeeded` but is never actually read from - this also means that the calls to `updateGLPbackingNeeded` are useless as they have no effect. If an external actor wants to get the “GLPbacking” he can just call the `GLPbackingNeeded` view function. Remove the `updateGLPbackingNeeded` method and the `GLPbacking` storage variable, or if some logic was not implemented - add it 244 | 245 | ## Discussion 246 | 247 | **pashov**: Client has fixed the issue. 248 | 249 | # [L-03] Missing parameter validation in `enter` 250 | 251 | `enterETH` has a check if `msg.value > 0` but `enter` does not check if `_amountin > 0`. Add that check. 252 | 253 | ## Discussion 254 | 255 | **pashov**: Client has fixed the issue. 256 | 257 | # [L-04] The `IWETH` interface has a method that `WETH` does not have 258 | 259 | The `safeTransfer` method is not part of the usual `IWETH` interface and is not actually used in the code, so it should be removed. 260 | 261 | ## Discussion 262 | 263 | **pashov**: Client has fixed the issue. 264 | 265 | # [L-05] Code is calling a deprecated method 266 | 267 | The `safeApprove` method from the `SafeERC20` library is deprecated so it should not be used. 268 | 269 | ## Discussion 270 | 271 | **pashov**: Client has acknowledged the issue. 272 | 273 | # [L-06] Value of slippage protection arguments is not set 274 | 275 | The `swaptoGLP` method does a `mintAndStakeGlp` call that has a 0 value for both `_minUsdg` and `_minGlp`. Also, in `recoverTreasuryTokensFromGLP` the `min_receive` parameter of the call to the `swapGLPto` method is 0 as well. This can hardly be exploited by the mechanics/tokenomics of `GLP` but it is still smart to add `minReceive` parameters to be provided by the user from external functions and pass them to the `swapToGLP` calls. 276 | 277 | ## Discussion 278 | 279 | **pashov**: Client has acknowledged the issue. 280 | 281 | # [I-01] Using `SafeMath` when compiler is ^0.8.0 282 | 283 | There is no need to use `SafeMath` when compiler is ^0.8.0 because it has built-in under/overflow checks. 284 | 285 | ## Discussion 286 | 287 | **pashov**: Client has acknowledged the issue. 288 | 289 | # [I-02] `leaveETH` should not be `payable` 290 | 291 | The `leaveETH` method only transfers ETH out, so `payable` keyword should be removed from its signature. 292 | 293 | ## Discussion 294 | 295 | **pashov**: Client has fixed the issue. 296 | 297 | # [I-03] Unused storage variable 298 | 299 | Storage variable `gdUSDC` is unused and should be removed. 300 | 301 | ## Discussion 302 | 303 | **pashov**: Client has fixed the issue. 304 | 305 | # [I-04] Misleading comments throughout the code 306 | 307 | Almost all comments that contain the words `usdc` or `gdUSDC` in the code are misleading and stale and should be removed or updated. 308 | 309 | ## Discussion 310 | 311 | **pashov**: Client has acknowledged the issue. 312 | 313 | # [I-05] Code is not properly formatted 314 | 315 | Run a formatter on the code, for example use the `prettier-solidity` plugin. 316 | 317 | ## Discussion 318 | 319 | **pashov**: Client has fixed the issue. 320 | 321 | # [I-06] NatSpec missing from external functions 322 | 323 | Add NatSpec docs for all external functions so their intentions and signatures are clear. 324 | 325 | ## Discussion 326 | 327 | **pashov**: Client has acknowledged the issue. 328 | 329 | # [I-07] Comment has no meaning 330 | 331 | This comment - `// Info of each user that stakes LP tokens.` has no meaning and it looks like it was related to a storage variable that is now gone. Remove it. 332 | 333 | ## Discussion 334 | 335 | **pashov**: Client has fixed the issue. 336 | 337 | # [I-08] Mismatch between the filename and the contract name 338 | 339 | While the file is named `final_vault.sol` the contract is named `vault` - rename file to `Vault.sol` and contract to `Vault` 340 | 341 | ## Discussion 342 | 343 | **pashov**: Client has fixed the issue. 344 | 345 | # [I-09] Remove unused import 346 | 347 | Remove the `import "@openzeppelin/contracts/token/ERC20/ERC20.sol";` import as it is unused. 348 | 349 | ## Discussion 350 | 351 | **pashov**: Client has fixed the issue. 352 | 353 | # [I-10] Not all `require` statements have an error string 354 | 355 | Since the project is using a compiler that is newer than version 0.8.4 it is best to use Solidity Custom Errors for error situations - replace all require statements with such custom errors. 356 | 357 | ## Discussion 358 | 359 | **pashov**: Client has acknowledged the issue. 360 | 361 | # [I-11] External calls can be grouped together 362 | 363 | The `RewardRouter` smart contract has the `handleRewards` function, which can be used in the place of `cycleRewardsETHandEsGMX` and `_cycleRewardsETH methods` - [code](https://arbiscan.io/address/0xA906F338CB21815cBc4Bc87ace9e68c87eF8d8F1#code) 364 | 365 | ## Discussion 366 | 367 | **pashov**: Client has fixed the issue. 368 | -------------------------------------------------------------------------------- /solo/Zerem-security-review.md: -------------------------------------------------------------------------------- 1 | # Zerem protocol security review by pashov 2 | 3 | ***review commit hash* - [667a41b577647f0d95591a5f9928a43b976b8e25](https://github.com/hananbeer/zerem/tree/667a41b577647f0d95591a5f9928a43b976b8e25)** 4 | 5 | **Scope: `Zerem.sol`** 6 | 7 | ### The code was reviewed for a total of 10 hours. 8 | --- 9 | 10 | # [H-01] The `unlockExponent` does not work as intended when it is ≠ 1 11 | 12 | ## Proof of Concept 13 | 14 | The documentation and the chart in `README.md` shows that it is expected that when `unlockExponent == 0` then immediately after funds `unlockDelaySec` the user can claim his whole locked amount. This is actually not working as intended, let’s look at the `_getWithdrawableAmount` function: 15 | 16 | 1. To calculate the amount to unlock, we have the following: `uint256 totalUnlockedAmount = (record.totalAmount * factor) / precision;` 17 | 2. `record.totalAmount` is the total locked amount, `precision` is a constant with a value of `1e8` and `factor` is calculated by this: 18 | 19 | ```solidity 20 | uint256 factor = deltaTimeNormalized ** unlockExponent; 21 | 22 | if (factor > precision) { 23 | factor = precision; 24 | } 25 | ``` 26 | 27 | 1. If we have `unlockExponent == 0` then `factor` is always equal to 1, which is less than `1e8` so `factor == 1` 28 | 2. Now if we go back to the total amount to unlock math, we will get `uint256 totalUnlockedAmount = (record.totalAmount * factor) / precision;` so `uint256 totalUnlockedAmount = record.totalAmount / precision` 29 | 30 | The expected unlocked amount was equal to `record.totalAmount` but instead we got `record.totalAmount / precision` which is incorrect. Now every subsequent time the `_getWithdrawableAmount` function is called, the math will be the same and the code will basically think there is no newly unlocked amount. This means that no user that has locked funds in Zerem will be able to withdraw more than `totalLockedAmount / 1e8` ever, all of the other tokens will be stuck. 31 | 32 | There is also a problem when `unlockExponent > 1` , because the computed `factor` can easily be `>= precision` which will result in 100% of funds being unlocked too early. 33 | 34 | Here is the important math: 35 | 36 | ```solidity 37 | uint256 deltaTimeNormalized = (deltaTimeDelayed * precision) / unlockPeriodSec; 38 | 39 | uint256 factor = deltaTimeNormalized ** unlockExponent; 40 | 41 | if (factor > precision) { 42 | factor = precision; 43 | } 44 | uint256 totalUnlockedAmount = (record.totalAmount * factor) / precision; 45 | ``` 46 | 47 | and let’s look at example scenario: 48 | 49 | 1. `unlockPeriodSec == 100 000`, `deltaTimeDelayed == 100` and `precision == 1e8` so `deltaTimeNormalized == 1e5` 50 | 2. if `unlockExponent > 1` for example when equal to 2, we will get `factor = 1e5 ** 2`, so `1e10` which is `> precision` that is `1e8` so now `factor = precision` 51 | 3. Now `totalUnlockedAmount = record.totalAmount * factor / factor` which is `record.totalAmount` 52 | 4. even though only 1/1000th of the unlock period has passed and the `unlockExponent` was just `2`, the user can already claim all of their locked tokens. 53 | 54 | ## Impact 55 | 56 | The protocol does not work as expected in its core functionality and can also result in stuck tokens (value loss) for users or tokens unlocked too early, so it is High severity. 57 | 58 | ## Recommendation 59 | 60 | Redesign the `unlockExponent` logic or just hardcode it to always be linear (a value of 1) 61 | 62 | ## Client response 63 | 64 | Fixed by removing the `unlockExponent` logic 65 | 66 | # [M-01] Unsafe call to `ERC20::transfer` can result in stuck funds 67 | 68 | ## Proof of Concept 69 | 70 | In the `_sendFunds` method we have the following code for transferring ERC20 tokens 71 | 72 | ```solidity 73 | IERC20(underlyingToken).transfer(receiver, amount); 74 | ``` 75 | 76 | The problem is that the `transfer` function from ERC20 returns a bool to indicate if the transfer was a success or not. As there are some tokens that do not revert on failure but instead return `false` (one such example is [ZRX](https://etherscan.io/address/0xe41d2489571d322189246dafa5ebde1f4699f498#code)) and also Zerem should work with all types of ERC20 tokens since it might be integrated with a protocol that does that, not checking the return value can result in tokens getting stuck. Let’s look at the following scenario: 77 | 78 | 1. Alice is trying to claim some tokens from a protocol that has integrated with Zerem, so her transaction makes a call to `Zerem::transferTo` 79 | 2. The amount to claim is less than the `lockThreshold` so the code goes to the`_sendFunds` functionality directly 80 | 3. The transfer fails and since the token does not revert but returns `false` this is not accounted for by the Zerem protocol and transaction completes successfully 81 | 4. The tokens are now stuck and are not claimable by Alice anymore 82 | 83 | ## Impact 84 | 85 | If an `ERC20::transfer` call fails it will lead to stuck funds for a user. This only happens with a special class of ERC20 tokens though, so it is Medium severity. 86 | 87 | ## Recommendation 88 | 89 | Use OpenZeppelin’s `SafeERC20` library and change `transfer` to `safeTransfer` 90 | 91 | ## Client response 92 | 93 | Fixed by adding `SafeERC20` 94 | 95 | # [M-02] Gas stipend for external call might be insufficient and lead to stuck ETH 96 | 97 | ## Proof of Concept 98 | 99 | The way the Zerem protocol transfers out ETH looks like this 100 | 101 | ```solidity 102 | payable(receiver).call{gas: 3000, value: amount}(hex"") 103 | ``` 104 | 105 | As you see, there is a gas stipend of 3000, but this might not be enough in some cases as some smart contract recipients need more than 3000 gas to receive ETH. 106 | 107 | Examples of problematic recipients: 108 | 109 | 1. Recipient is a smart contract that has a payable fallback method which uses more than 3000 gas 110 | 2. Recipient is a smart contract that has a payable fallback function that needs less than 3000 gas but is called through a proxy, raising the call's gas usage above 3000. 111 | 112 | Additionally, using higher than 3000 gas might be mandatory for some multi-sig wallets. 113 | 114 | ## Impact 115 | 116 | Some recipients will lose access to all of their claimable ETH from protocols that are integrated with Zerem. This requires a special type of recipient, so it is Medium severity. 117 | 118 | ## Recommendation 119 | 120 | At least doubling down the gas stipend should help in most scenarios, but maybe think about dynamic configuration options for it as well 121 | 122 | ## Client response 123 | 124 | Fixed by doubling the gas stipend 125 | 126 | # [M-03] Gas griefing/theft is possible on unsafe external call 127 | 128 | ## Proof of Concept 129 | 130 | This comment `// TODO: send relayer fees here` in the `unlockFor` method and its design show that it is possible that `unlockFor` is usually called by relayers. This opens up a new attack-vector in the contract and it is gas griefing on the ETH transfer 131 | 132 | ```solidity 133 | (bool success,) = payable(receiver).call{gas: 3000, value: amount}(hex""); 134 | ``` 135 | 136 | Now `(bool success, )` is actually the same as writing `(bool success, bytes memory data)` which basically means that even though the `data` is omitted it doesn’t mean that the contract does not handle it. Actually, the way it works is the `bytes data` that was returned from the `receiver` will be copied to memory. Memory allocation becomes very costly if the payload is big, so this means that if a `receiver` implements a fallback function that returns a huge payload, then the `msg.sender` of the transaction, in our case the relayer, will have to pay a huge amount of gas for copying this payload to memory. 137 | 138 | ## Impact 139 | 140 | Malicious actor can launch a gas griefing attack on a relayer. Since griefing attacks have no economic incentive for the attacker and it also requires relayers it should be Medium severity. 141 | 142 | ## Recommendation 143 | 144 | Use a low-level assembly `call` since it does not automatically copy return data to memory 145 | 146 | ```solidity 147 | bool success; 148 | assembly { 149 | success := call(3000, receiver, amount, 0, 0, 0) 150 | } 151 | ``` 152 | 153 | ## Client response 154 | 155 | Fixed by using a low-level assembly `call` 156 | 157 | # [M-04] Centralisation risk with `liquidationResolver` as it can steal 100% of locked funds 158 | 159 | ## Proof of Concept 160 | 161 | Currently the `liquidationResolver` has the power to steal 100% of locked funds in the following way: 162 | 163 | 1. Call `freezeFunds` for every user that has a locked balance 164 | 2. Wait some time until the liquidation delay has passed so the `require` statement in `liquidateFunds` succeeds 165 | 3. Call `liquidateFunds` and receive all of the users’ balances 166 | 167 | This can happen if the `liquidationResolver` becomes malicious or is compromised. 168 | 169 | ## Impact 170 | 171 | Centralisation vulnerabilities usually require a malicious or a compromised account and are of Medium severity 172 | 173 | ## Recommendation 174 | 175 | Reconsider if the freeze/liquidate funds is a mandatory mechanism for the protocol 176 | 177 | ## Client response 178 | 179 | Acknowledged 180 | 181 | # [M-05] Missing configuration validations & constraints can lead to stuck funds 182 | 183 | ## Proof of Concept 184 | 185 | If a protocol integrates with Zerem it needs to deploy different instances of `Zerem.sol` for each `underlyingToken`. In the constructor there are some configurations being set but the inputs are not validated at all. Now if the deployer did not configure them correctly, or fat-fingered the deployment or if the deployment scripts were incorrect, it is possible to misconfigure the protocol in such a way that it is not obvious but leads to all locked funds getting stuck forever. 186 | 187 | Let’s look at the following scenario: 188 | 189 | 1. Mistake in the deployment script sets the `unlockDelaySec` or the `unlockPeriodSec` to be huge, or to be a concrete timestamp 190 | 2. Now in `_getWithdrawableAmount` if `unlockDelaySec` is too big we will always get 0 withdrawable amount because of 191 | 192 | ``` 193 | if (deltaTime < unlockDelaySec) { 194 | return 0; 195 | } 196 | ``` 197 | 198 | 1. Also in `_getWithdrawableAmount` if `unlockPeriodSec` is too big we will always get 0 because this `uint256 deltaTimeNormalized = (deltaTimeDelayed * precision) / unlockPeriodSec;` will be zero 199 | 200 | This means the user will need to wait a huge amount (might be infinite) of time to be able to unlock his funds, and they won’t be unlockable even with the `liquidateFunds` functionality 201 | 202 | ## Impact 203 | 204 | This can possibly lead to user funds being stuck in Zerem, but this requires misconfiguration in deployment, so it is Medium severity 205 | 206 | ## Recommendation 207 | 208 | Add sensible constraints for the valid values of `unlockDelaySec` and `unlockPeriodSec` in the constructor of `Zerem.sol` 209 | 210 | ## Client response 211 | 212 | Fixed by adding constraints in the constructor 213 | 214 | # [M-06] ERC20 tokens that have a fee-on-transfer mechanism require special handling 215 | 216 | ## Proof of Concept 217 | 218 | Some tokens take a transfer fee (`STA`, `PAXG`) and there are some that currently do not but might do so in the future (`USDT`, `USDC`). Since Zerem might be integrated with a protocol that works with all types of ERC20 tokens, and Zerem should too, this can lead to problems. 219 | 220 | Let’s look at the following scenario: 221 | 222 | 1. Alice tries to claim tokens that have a fee-on-transfer mechanism from a protocol that is integrated with Zerem 223 | 2. The integrated protocol calls `Zerem::transferTo` method but the `amount` argument passed does not take the fee into consideration 224 | 3. The `require(transferredAmount >= amount, "not enough tokens");` check will always fail, since the `transferredAmount` will be less than `amount` due to the fee 225 | 226 | If this happens this means that all of users balances of such tokens won’t be claimable and stuck forever. 227 | 228 | ## Impact 229 | 230 | If a token with a fee-on-transfer mechanism is used and not properly handled on both the integration protocol and Zerem’s side, it can result to 100% stuck balances of this token of users. Since this happens only with a special type of ERC20 it is Medium severity. 231 | 232 | ## Recommendation 233 | 234 | Integration of such tokens will require special handling on the integrating protocol side (pre-calculating the fee, so the `amount` argument passed has the correct value) and possibly on Zerem’s side. Consider either better documentation for those or advise integrating protocols to not transfer such tokens through Zerem. 235 | 236 | ## Client response 237 | 238 | Added a warning comment in the code 239 | 240 | # [M-07] Protocol does not work with ERC20 tokens that have a mechanism for balance modifications outside of transfers 241 | 242 | ## Proof of Concept 243 | 244 | Some tokens may make arbitrary balance modifications outside of transfers. One example are Ampleforth-style rebasing tokens and there are other tokens with airdrop or mint/burn mechanisms. The Zerem system caches the locked balances for users and if such an arbitrary modification has happened this can mean that the protocol is operating with outdated information. Let’s look at the following scenario: 245 | 246 | 1. Alice claims some tokens with such a mechanism from a protocol that is integrated with Zerem 247 | 2. The protocol calls `Zerem::transferTo` method, but the amount sent is ≥ `lockThreshold` so the funds are locked 248 | 3. In `_lockFunds` the amount sent is cached in `record.totalAmount`, `record.remainingAmount` and `pendingTotalBalances[user]`. 249 | 4. Some time after this, let’s say a rebase of the token balances has happened and now actually Zerem holds less tokens 250 | 5. Now when the `unlockPeriodSec` passes and Alice wants to claim her tokens she is unable to because the cached amount is more than the actual amount that is held in the Zerem protocol, so the transaction always reverts leading to all of the locked funds getting stuck 251 | 252 | Also if the rebasing of the tokens actually increased the protocol balance, then those excess tokens will be stuck in it. 253 | 254 | ## Impact 255 | 256 | Funds can be stuck in Zerem, but it requires a special type of ERC20 token, so it is Medium severity. 257 | 258 | ## Recommendation 259 | 260 | Allow partial unlock of funds or document that the protocol does not support such tokens, so integrating protocols do not transfer them through Zerem. Also you can add functionality to rescue excess funds out of the Zerem protocol 261 | 262 | ## Client response 263 | 264 | Acknowledged 265 | 266 | # QA report - low severity & non-critical issues 267 | 268 | ## [QA-01] Use latest Solidity version with a stable pragma statement 269 | 270 | Using a floating pragma `^0.8.13` statement is discouraged as code can compile to different bytecodes with different compiler versions. Use a stable pragma statement to get a deterministic bytecode. Also use latest Solidity version to get all compiler features, bugfixes and optimizations 271 | 272 | ## [QA-02] Add NatSpec documentation 273 | 274 | NatSpec documentation to all public methods and variables is essential for better understanding of the code by developers and auditors and is strongly recommended. 275 | 276 | ## [QA-03] Code is not formatted properly 277 | 278 | Use a code formatter to keep code clean & tidy, I’d suggest adding the `forge fmt` command to your pre-commit hook 279 | 280 | ## [QA-04] Move event emissions to the methods where the action happens 281 | 282 | Move `TransferFulfilled` event emission to `_sendFunds()` and `TransferLocked` event emission to `_lockFunds()` 283 | 284 | ## [QA-05] Typos in comments 285 | 286 | Change `recive` to `receive` and `timeframe` to `time frame` 287 | 288 | ## [QA-06] Missing non-zero address checks in the constructor 289 | 290 | Add non-zero address checks for all `address` type arguments in `Zerem.sol`'s constructor 291 | 292 | ## [QA-07] No need to cast to `address payable` in `_sendFunds` 293 | 294 | Casting `address` to `address payable` is only needed when using `send` or `transfer`, so it is not needed here 295 | 296 | ## [QA-08] Some functions can be fused into one 297 | 298 | Merge `freezeFunds` and `unfreezeFunds` into `updateFundsFreezeStatus` - same for their events. There is no need for two separate methods and events 299 | 300 | ## [QA-09] Open `TODO` in the code 301 | 302 | There is an open `TODO` in `unlockFor` - this implies changes that might not be audited. Resolve it or remove it 303 | 304 | ## [QA-10] Remove duplicated code 305 | 306 | Reuse code, make `_getRecord` call `_getTransferId` because the `bytes32 transferId = keccak256(abi.encode(user, lockTimestamp));` logic is duplicated, same for `_unlockFor` 307 | 308 | ## [QA-11] Update external dependency to latest version 309 | 310 | The OpenZeppelin library dependency is using a stale version - upgrade to latest one to get all security patches, features and gas optimisations 311 | 312 | ## [QA-12] Consider using a differet key in the `pendingTransfers` mapping 313 | 314 | Currently the key in the `pendingTransfers` mapping is calculated by this 315 | 316 | `bytes32 transferId = keccak256(abi.encode(user, lockTimestamp));` 317 | 318 | I’d say that just using a simple uint256 nonce for each `TransferRecord` would work just fine. It will also be simpler and more gas efficient. 319 | 320 | # Gas optimisation report 321 | 322 | ## [G-01] Remove the `pendingTotalBalances` mapping as it is not used for anything 323 | 324 | Remove the mapping as `pendingTransfers[transferId].remainingAmount` has pretty much the same functionality 325 | 326 | ## [G-02] Use `constant` instead of `immutable` for predetermined values 327 | 328 | Both `precision` and `NATIVE` storage variables have predetermined values - change them into `constant`s not `immutable`s. Also make sure they are private, to save gas. 329 | 330 | ## [G-03] Use `uint256` instead of `uint8` for storage variables and function parameters 331 | 332 | Unless you are doing variable-packing in storage it is best to use always use `uint256` because all other `uint` types get automatically padded to `uint256` in memory anyway. Same applies for function parameters 333 | 334 | ## [G-04] Use custom errors instead of revert strings 335 | 336 | Solidity 0.8.4 added the custom errors functionality, which can be use instead of revert strings, resulting in big gas savings on errors. Replace all revert statements with custom error ones 337 | 338 | ## [G-05] All `public` functions can be changed to `external` 339 | 340 | Since a function is not called from within a contract, using `external` instead of `public` will save gas as all the function arguments will be in `calldata` instead of copied to memory 341 | 342 | ## [G-06] Use `x != 0` instead of `x > 0` for uint types 343 | 344 | The `!=` operator costs less gas than `>` and for uint types you can use it to check for non-zero values to save gas 345 | 346 | ## [G-07] `x = x - y` costs less gas than `x -= y`, same for addition 347 | 348 | You can replace all `-=` and `+=` occurrences to save gas 349 | 350 | ## [G-08] Change `totalUnlockedAmount < withdrawnAmount` to `totalUnlockedAmount <= withdrawnAmount` 351 | 352 | If in the `_getWithdrawableAmount` method you change `if (totalUnlockedAmount < withdrawnAmount)` to `if (totalUnlockedAmount <= withdrawnAmount)` you will save gas by skipping a few computations 353 | 354 | # Appendix - Architectural considerations 355 | 356 | 1. With the current design, a project will have to deploy a separate `Zerem` contract for each ERC20 token it supports. This might not be very efficient and to have much bigger gas costs for either the integrated protocol or its users 357 | 2. As pointed out in the `README` file, the Zerem integration has an invasive approach - additional code logic should be added to the outbound methods of the integrating protocol so that all transfers go through Zerem. This will not work with already existing and non-upgradeable protocols and also might not be very well accepted, since it more code means more maintenance and a bigger attack vector for protocols. 358 | 3. An attacker can always send `lockThreshold - 1 wei` amount, and it will always be directly transferred. He can just spray multiple transactions in a single block so in a few blocks he can steal very large amount of tokens --------------------------------------------------------------------------------