├── solo ├── DYAD-Security-Review.pdf ├── Cookie3-Security-Review.pdf ├── NOKS-AI-Security-Review.pdf ├── P2PSwap-Security-Review.pdf ├── HoneyFun-Security-Review.pdf ├── MetisForge-Security-Review.pdf ├── YieldMeme-Security-Review.pdf ├── Cookie3-Lock-Security-Review.pdf ├── GamblingDAO-Security-Review.pdf ├── AI-Agents-Layer-Security-Review.pdf ├── Cookie3-Farming-Security-Review.pdf ├── Lo-Fi-Pepe-NFT-Security-Review.pdf ├── Paytr-Protocol-Security-Review.pdf ├── Alaska-Gold-Rush-Security-Review.pdf ├── DayHub-FairLaunch-Security-Review.pdf ├── GameSwift-Staking-Security-Review.pdf ├── GameSwift-Vesting-Security-Review.pdf ├── HoneyFunStickers-Security-Review.pdf ├── GameSwift-Launchpool-Security-Review.pdf ├── HoneyFun-Tokenomics-Security-Review.pdf ├── Dayhub-Platfrom-Security-Review-Report.pdf ├── HoneyFun-Meme-Contract-Security-Review.pdf ├── HoneyFun-Tokenomics-V2-Security-Review.pdf ├── StarHeroes-Launchpool-Security-Review.pdf ├── Cookie3-Community-Gated-Leaderboards-Security-Review.pdf ├── RootedToken-Security-Review.md ├── GameSwift-Security-Review-3.md ├── GameSwift-Security-Review.md ├── SpartaDex-Security-Review-2.md ├── StarHeroes-Security-Review.md └── SpartaDex-Security-Review.md ├── README.md └── md └── HoneyFunStickers-Security-Review.md /solo/DYAD-Security-Review.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gkrastenov/audits/HEAD/solo/DYAD-Security-Review.pdf -------------------------------------------------------------------------------- /solo/Cookie3-Security-Review.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gkrastenov/audits/HEAD/solo/Cookie3-Security-Review.pdf -------------------------------------------------------------------------------- /solo/NOKS-AI-Security-Review.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gkrastenov/audits/HEAD/solo/NOKS-AI-Security-Review.pdf -------------------------------------------------------------------------------- /solo/P2PSwap-Security-Review.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gkrastenov/audits/HEAD/solo/P2PSwap-Security-Review.pdf -------------------------------------------------------------------------------- /solo/HoneyFun-Security-Review.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gkrastenov/audits/HEAD/solo/HoneyFun-Security-Review.pdf -------------------------------------------------------------------------------- /solo/MetisForge-Security-Review.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gkrastenov/audits/HEAD/solo/MetisForge-Security-Review.pdf -------------------------------------------------------------------------------- /solo/YieldMeme-Security-Review.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gkrastenov/audits/HEAD/solo/YieldMeme-Security-Review.pdf -------------------------------------------------------------------------------- /solo/Cookie3-Lock-Security-Review.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gkrastenov/audits/HEAD/solo/Cookie3-Lock-Security-Review.pdf -------------------------------------------------------------------------------- /solo/GamblingDAO-Security-Review.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gkrastenov/audits/HEAD/solo/GamblingDAO-Security-Review.pdf -------------------------------------------------------------------------------- /solo/AI-Agents-Layer-Security-Review.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gkrastenov/audits/HEAD/solo/AI-Agents-Layer-Security-Review.pdf -------------------------------------------------------------------------------- /solo/Cookie3-Farming-Security-Review.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gkrastenov/audits/HEAD/solo/Cookie3-Farming-Security-Review.pdf -------------------------------------------------------------------------------- /solo/Lo-Fi-Pepe-NFT-Security-Review.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gkrastenov/audits/HEAD/solo/Lo-Fi-Pepe-NFT-Security-Review.pdf -------------------------------------------------------------------------------- /solo/Paytr-Protocol-Security-Review.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gkrastenov/audits/HEAD/solo/Paytr-Protocol-Security-Review.pdf -------------------------------------------------------------------------------- /solo/Alaska-Gold-Rush-Security-Review.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gkrastenov/audits/HEAD/solo/Alaska-Gold-Rush-Security-Review.pdf -------------------------------------------------------------------------------- /solo/DayHub-FairLaunch-Security-Review.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gkrastenov/audits/HEAD/solo/DayHub-FairLaunch-Security-Review.pdf -------------------------------------------------------------------------------- /solo/GameSwift-Staking-Security-Review.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gkrastenov/audits/HEAD/solo/GameSwift-Staking-Security-Review.pdf -------------------------------------------------------------------------------- /solo/GameSwift-Vesting-Security-Review.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gkrastenov/audits/HEAD/solo/GameSwift-Vesting-Security-Review.pdf -------------------------------------------------------------------------------- /solo/HoneyFunStickers-Security-Review.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gkrastenov/audits/HEAD/solo/HoneyFunStickers-Security-Review.pdf -------------------------------------------------------------------------------- /solo/GameSwift-Launchpool-Security-Review.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gkrastenov/audits/HEAD/solo/GameSwift-Launchpool-Security-Review.pdf -------------------------------------------------------------------------------- /solo/HoneyFun-Tokenomics-Security-Review.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gkrastenov/audits/HEAD/solo/HoneyFun-Tokenomics-Security-Review.pdf -------------------------------------------------------------------------------- /solo/Dayhub-Platfrom-Security-Review-Report.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gkrastenov/audits/HEAD/solo/Dayhub-Platfrom-Security-Review-Report.pdf -------------------------------------------------------------------------------- /solo/HoneyFun-Meme-Contract-Security-Review.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gkrastenov/audits/HEAD/solo/HoneyFun-Meme-Contract-Security-Review.pdf -------------------------------------------------------------------------------- /solo/HoneyFun-Tokenomics-V2-Security-Review.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gkrastenov/audits/HEAD/solo/HoneyFun-Tokenomics-V2-Security-Review.pdf -------------------------------------------------------------------------------- /solo/StarHeroes-Launchpool-Security-Review.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gkrastenov/audits/HEAD/solo/StarHeroes-Launchpool-Security-Review.pdf -------------------------------------------------------------------------------- /solo/Cookie3-Community-Gated-Leaderboards-Security-Review.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gkrastenov/audits/HEAD/solo/Cookie3-Community-Gated-Leaderboards-Security-Review.pdf -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # KeySecurity 's smart contract security reviews 2 | 3 | KeySecurity is a new, innovative Web3 security company that hires top-talented security researchers for your project. We have conducted over 40+ security reviews for various projects, collectively holding over $300,000,000 in TVL. 4 | 5 | ## Contacts 6 | 7 | For security audit inquiries, you can reach out to us on: 8 | 9 | - X (Twitter) - [@gkrastenov](https://twitter.com/gkrastenov) 10 | - Telegram - [@gkrastenov](https://t.me/gkrastenov) 11 | - Discord - [@gkrastenov](discordapp.com/users/830181816433377310) 12 | 13 | ## KeySecurity Portfolio 14 | 15 | 1. [NOKS AI - OKX Fork](./solo/NOKS-AI-Security-Review.pdf) 16 | 2. StarHeroes - Staking 17 | 3. [DayHub - Funded Trading Platform, ERC-2535](./solo/Dayhub-Platfrom-Security-Review-Report.pdf) 18 | 4. Stargate Finance - Timelock system 19 | 5. [Cookie3 - Community gated Leaderboard](./solo/Cookie3-Community-Gated-Leaderboards-Security-Review.pdf) 20 | 6. StarHeroes Launchpool v2.1 21 | 7. [StarHeroes Launchpool v2 - Vesting, Staking & Integration with UniswapV2/V3](./solo/StarHeroes-Launchpool-Security-Review.pdf) 22 | 8. [GameSwift - Staking](./solo/GameSwift-Staking-Security-Review) 23 | 9. [HoneyFun - ERC20](./solo/HoneyFun-Meme-Contract-Security-Review.pdf) 24 | 10. [HoneyFun - Tokenomics v2](./solo/HoneyFun-Tokenomics-V2-Security-Review.pdf) 25 | 11. [HoneyFun - Tokenomics](./solo/HoneyFun-Tokenomics-Security-Review.pdf) 26 | 12. [GameSwift - Staking & Vesting](./solo/GameSwift-Vesting-Security-Review.pdf) 27 | 13. [HoneyFun - Berachain AI Agents](./solo/HoneyFun-Security-Review.pdf) 28 | 14. [MetisForge - Meme Generator](./solo/MetisForge-Security-Review.pdf) 29 | 15. Cookie3 - OFT Token 30 | 16. [HoneyFun Stickers- NFTs](./solo/HoneyFunStickers-Security-Review.pdf) 31 | 17. [GameSwift Launchpool - Staking](./solo/GameSwift-Launchpool-Security-Review.pdf) 32 | 18. [P2PSwap - Token Swapper](./solo/P2PSwap-Security-Review.pdf) 33 | 19. [Cookie3 - Lock contracts](./solo/Cookie3-Lock-Security-Review.pdf) 34 | 20. [Cookie3 - Farming & Airdrop](./solo/Cookie3-Farming-Security-Review.pdf) 35 | 21. [AI Agent Layer - AI Agents launching platform](./solo/AI-Agents-Layer-Security-Review.pdf) 36 | 22. [DayHub - FairLaunch](./solo/DayHub-FairLaunch-Security-Review.pdf) 37 | 23. [GamblingDAO - GameFi](./solo/GamblingDAO-Security-Review.pdf) 38 | 24. [LO-FI PEPE - NFT & Staking](./solo/Lo-Fi-Pepe-NFT-Security-Review.pdf) 39 | 25. [Cookie3 - Airdrop & Swapper](./solo/Cookie3-Security-Review.pdf) 40 | 26. [Paytr - Early Invoice Payments](./solo/Paytr-Protocol-Security-Review.pdf) 41 | 27. [Yield.Meme - AAVE, ERC4626 & Yield Farming](./solo/YieldMeme-Security-Review.pdf) 42 | 28. [DN404 - ERC721 & ERC20](https://github.com/Vectorized/dn404/blob/main/audits/guardian-audits-report.pdf) 43 | 29. [Alaska Gold Rush - Cross-chain ERC20 token](./solo/Alaska-Gold-Rush-Security-Review.pdf) 44 | 30. [DYAD - Overcollateralized stablecoin](./solo/DYAD-Security-Review.pdf) 45 | 31. Undisclosed - On-chain game quiz 46 | 32. [GameSwift - Staking](./solo/GameSwift-Security-Review-3.md) 47 | 33. [RootedToken - ERC20](./solo/RootedToken-Security-Review.md) 48 | 34. [StarHeroes - Fixed Staking & Vesting](./solo/StarHeroes-Security-Review.md) 49 | 35. [SpartaDex - Pre-Deploy Review](./solo/SpartaDex-Security-Review-2.md) 50 | 36. [GameSwift - Airdrop & Staking](./solo/GameSwift-Security-Review.md) 51 | 37. [SpartaDex - Staking, NFT & DEX ](./solo/SpartaDex-Security-Review.md) 52 | 38. SpartaDex - Airdrop & Vesting 53 | 54 | # Public Audit Contests 55 | 56 | My most notable competitive audit results: 57 | 58 | | Protocol | Rank | Description | 59 | | ------------- | ---- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 60 | | Aegis.im YUSD | 🥈 2 | [Bitcoin-backed, delta-neutral stablecoin with real-time transparency, built-in funding-rate yield, and zero reliance on the fiat banking system.](https://audits.sherlock.xyz/contests/799) | 61 | | Ondo Finance | 🥈 2 | [Institutional-Grade Finance. On-Chain. For Everyone.](https://code4rena.com/reports/2023-09-ondo) | 62 | 63 | ## Stats 64 | 65 | - Over $300,000,000 of TVL is held by the contracts that have been audited by gkrastenov. 66 | - Over 100 Critical/High severity vulnerabilities found. 67 | -------------------------------------------------------------------------------- /md/HoneyFunStickers-Security-Review.md: -------------------------------------------------------------------------------- 1 | # About KeySecurity 2 | 3 | \vspace{-0.25cm} 4 | 5 | KeySecurity is a new, innovative Web3 security company that hires top-talented security researchers for your project. We have conducted over 30 security reviews for various projects, collectively holding over $300,000,000 in TVL. For security audit inquiries, you can reach out to us on Twitter/X or Telegram [`@gkrastenov`](https://twitter.com/gkrastenov) or check our previous work [`here`](https://github.com/gkrastenov/audits). 6 | 7 | \vspace{-0.15cm} 8 | 9 | # About HoneyFun 10 | 11 | \vspace{-0.25cm} 12 | 13 | [`Honeyfun AI`](https://x.com/honeydotfun) is pioneering the co-ownership framework for AI agents specifically tailored for the Berachain ecosystem, focusing on defi, gaming and entertainment. We envision AI agents as pivotal revenue-generating entities in the future, as we believe the era of Utility AI Agents is just beginning, and in the coming years, their untapped potential across every field will be revealed. 14 | 15 | \vspace{-0.15cm} 16 | 17 | # Disclaimer 18 | 19 | \vspace{-0.25cm} 20 | 21 | Audits are a time, resource, and expertise bound effort where trained experts evaluate smart contracts using a combination of automated and manual techniques to identify as many vulnerabilities as possible. Audits can show the presence of vulnerabilities **but not their absence**. 22 | 23 | \vspace{-0.15cm} 24 | 25 | # Risk classification 26 | 27 | \vspace{-0.25cm} 28 | 29 | | Severity | Impact: High | Impact: Medium | Impact: Low | 30 | | :----------------- | :----------: | :------------: | :---------: | 31 | | Likelihood: High | Critical | High | Medium | 32 | | Likelihood: Medium | High | Medium | Low | 33 | | Likelihood: Low | Medium | Low | Low | 34 | 35 | ## Impact 36 | 37 | \vspace{-0.25cm} 38 | 39 | - **High** - leads to a significant loss of assets in the protocol or significantly harms a group of users. 40 | - **Medium** - only a small amount of funds can be lost or a functionality of the protocol is affected. 41 | - **Low** - any kind of unexpected behaviour that's not so critical. 42 | \vspace{-0.15cm} 43 | 44 | ## Likelihood 45 | 46 | \vspace{-0.25cm} 47 | 48 | - **High** - direct attack vector; the cost is relatively low to the amount of funds that can be lost. 49 | - **Medium** - only conditionally incentivized attack vector, but still relatively likely. 50 | - **Low** - too many or too unlikely assumptions; provides little or no incentive. 51 | \vspace{-0.15cm} 52 | 53 | ## Actions required by severity level 54 | 55 | \vspace{-0.25cm} 56 | 57 | - **Critical** - client **must** fix the issue. 58 | - **High** - client **must** fix the issue. 59 | - **Medium** - client **should** fix the issue. 60 | - **Low** - client **could** fix the issue. 61 | \vspace{-0.15cm} 62 | 63 | \clearpage 64 | 65 | # Executive summary 66 | 67 | \begin{center} 68 | \textbf{Overview} 69 | \end{center} 70 | 71 | \begin{center} 72 | \begin{tabular}{ | m{4cm} | m{13cm}| } 73 | \hline 74 | Project Name & HoneyFun Stickers \\ 75 | \hline 76 | Repository & https://github.com/honey-fun/honey-fun-stickers-contracts \\ 77 | \hline 78 | Commit hash & d724f990dde6a83a3c64455daa4aed83b240bbcf \\ 79 | \hline 80 | Review Commit hash & 564572948ddbe7fbb173c2a7d2bb4f6e6a30bf4a \\ 81 | \hline 82 | Documentation & https://docs.honey.fun/stickers-campaign \\ 83 | \hline 84 | Methods & Manual review \\ 85 | \hline 86 | \end{tabular} 87 | \end{center} 88 | 89 | \begin{center} 90 | \textbf{Scope} 91 | \end{center} 92 | 93 | \begin{center} 94 | \begin{tabular}{ | m{7cm} | } 95 | \hline 96 | HoneyFunStickerPacks.sol \\ 97 | \hline 98 | HoneyFunStickerPacksMinter.sol \\ 99 | \hline 100 | \end{tabular} 101 | \end{center} 102 | 103 | \begin{center} 104 | \textbf{Timeline} 105 | \end{center} 106 | 107 | \begin{center} 108 | \begin{tabular}{ | m{3.5cm} | m{4cm}| } 109 | \hline 110 | January 20, 2024 & Audit kick-off \\ 111 | \hline 112 | January 21, 2024 & Preliminary report \\ 113 | \hline 114 | January 22, 2024 & Mitigation review \\ 115 | 116 | \hline 117 | \end{tabular} 118 | \end{center} 119 | 120 | \begin{center} 121 | \textbf{Issues Found} 122 | \end{center} 123 | 124 | \begin{center} 125 | \begin{tabular}{| m{3.5cm} | m{2cm} |} 126 | \hline 127 | \textbf{Severity} & \textbf{Count} \\ 128 | \hline 129 | High & 0 \\ 130 | \hline 131 | Medium & 1 \\ 132 | \hline 133 | Low & 0 \\ 134 | \hline 135 | Information & 3 \\ 136 | \hline 137 | \textbf{Total} & \textbf{4} \\ 138 | \hline 139 | \end{tabular} 140 | \end{center} 141 | 142 | \clearpage 143 | 144 | # Findings 145 | 146 | \vspace{-0.25cm} 147 | 148 | ## Medium 149 | 150 | \vspace{-0.15cm} 151 | 152 | ### User pack IDs were incorrectly added during minting 153 | 154 | \vspace{-0.25cm} 155 | **Severity:** \textit{Medium} 156 | 157 | **Context:** [HoneyFunStickerPacks.sol#L55](https://github.com/honey-fun/honey-fun-stickers-contracts/blob/main/src/HoneyFunStickerPacks.sol#L55) 158 | 159 | **Description:** When new stickers are minted, a for loop is used to set the pack type and to push all newly minted NFTs to the `userPacksIds` mapping. However, before adding the `tokenId`, it is incorrectly incremented. As a result, the `tokenId` that is minted and the one added to the user will be different. 160 | 161 | Additionally, the function `userPacksIds` will return incorrect IDs for the user, as every ID will be bigger by 1 compared to the actual NFT IDs the user owns. 162 | 163 | ```solidity 164 | for (uint256 tokenId = nextTokenId; tokenId < lastTokenId; ) { 165 | _packTypes[tokenId] = packType; 166 | 167 | unchecked { 168 | tokenId++; 169 | } 170 | 171 | _userPacksIds[to][packType].push(tokenId); 172 | } 173 | ``` 174 | 175 | **PoC** 176 | 177 | ```solidity 178 | // forge test --match-test testCorrectlySettingUserPackToIds 179 | function testCorrectlySettingUserPackToIds() public { 180 | // Mint 1 NFT by the minter address 181 | vm.prank(minter); 182 | stickerPacks.mint(user, 1, IHoneyFunStickerPacks.PackType.BRONZE); 183 | 184 | // Check if the tokens were minted 185 | assertEq(stickerPacks.balanceOf(user), 1); 186 | // NFT with Id = 0 has a BRONZE pack type 187 | assertEq( 188 | uint(stickerPacks.typeOfPack(0)), // tokenId 189 | uint(IHoneyFunStickerPacks.PackType.BRONZE) // type 190 | ); 191 | 192 | uint256[] memory ids = stickerPacks.userPacksIds( 193 | user, 194 | IHoneyFunStickerPacks.PackType.BRONZE 195 | ); 196 | // In array ids should has only 1 NFT with Id = 0 197 | console.log(ids[0]); // print 1 198 | assertEq(0, ids[0]); // 0 != 1 revert 199 | } 200 | ``` 201 | 202 | **Recommendation:** Increment the tokenId after pushing it to \_userPacksIds. 203 | 204 | **Resolution and Client comment:** Resolved. PR: [#1](https://github.com/honey-fun/honey-fun-stickers-contracts/pull/1) 205 | 206 | ## Information 207 | 208 | \vspace{-0.15cm} 209 | 210 | ### Unnecessary calling of the \_setMinter function 211 | 212 | \vspace{-0.25cm} 213 | **Severity:** \textit{Information} 214 | 215 | **Context:** [HoneyFunStickerPacks.sol#L38](https://github.com/honey-fun/honey-fun-stickers-contracts/blob/main/src/HoneyFunStickerPacks.sol#L38) 216 | 217 | **Description:** In the constructor of the **`HoneyFunStickerPacks`** contract, the internal function `_setMinter` is called to set the minter address. At this time, the minter contract will not be deployed because, in the constructor of the minter contract, the stickers contract should be set. 218 | 219 | ```solidity 220 | constructor( 221 | HoneyFunStickerPacks stickers_, 222 | address treasury_, 223 | address owner_, 224 | address freePacksSigner_, 225 | uint256[] memory stickerPrices_ 226 | ) Ownable(owner_) { 227 | _setStickers(stickers_); 228 | _setTreasury(treasury_); 229 | _setFreePacksSigner(freePacksSigner_); 230 | _setStickerPrices(stickerPrices_); 231 | } 232 | ``` 233 | 234 | In the minter contract, the stickers contract can only be set in the constructor, meaning that the stickers contract should already be deployed. Therefore, when the constructor of the stickers contract is executed, the address of the minter contract will be `address(0)`. 235 | 236 | **Recommendation:** Do not internally call the `_setMinter` function from the constructor of the stickers contract. 237 | 238 | **Resolution and Client comment:** Resolved. PR: [#1](https://github.com/honey-fun/honey-fun-stickers-contracts/pull/1) 239 | 240 | ### Emit event in crucial places 241 | 242 | \vspace{-0.25cm} 243 | **Severity:** \textit{Information} 244 | 245 | **Context:** [HoneyFunStickerPacks.sol#L140](https://github.com/honey-fun/honey-fun-stickers-contracts/blob/main/src/HoneyFunStickerPacks.sol#L140) 246 | 247 | **Description:** Emit an event in crucial places, such as in the `_setMinter` function, when the minter contract address is set. 248 | 249 | **Recommendation:** Use the `MinterSet` event from the `IHoneyFunStickerPacks` interface. 250 | 251 | **Resolution and Client comment:** Resolved. PR: [#1](https://github.com/honey-fun/honey-fun-stickers-contracts/pull/1) 252 | 253 | ### Sticker prices can not be updated 254 | 255 | \vspace{-0.25cm} 256 | **Severity:** \textit{Information} 257 | 258 | **Context:** [HoneyFunStickerPacksMinter.sol#L145](https://github.com/honey-fun/honey-fun-stickers-contracts/blob/main/src/HoneyFunStickerPacksMinter.sol#L145) 259 | 260 | **Description:** Currently, the sticker prices are set in the constructor of the `Minter` contract. After that, it is not possible to update the sticker prices. In the case of high or low interest in the project, the admin will not be able to change the prices to align with the current market state and maximize profit. 261 | 262 | **Recommendation:** Allow the admin to update sticker prices. 263 | 264 | **Resolution and Client comment:** Resolved. PR: [#1](https://github.com/honey-fun/honey-fun-stickers-contracts/pull/1) 265 | -------------------------------------------------------------------------------- /solo/RootedToken-Security-Review.md: -------------------------------------------------------------------------------- 1 | # RootedToken Security Review 2 | 3 | A security review of the RootedToken. 4 | 5 | Author: [gkrastenov](https://twitter.com/gkrastenov) Independent Security Researcher 6 | 7 | ## Disclaimer 8 | 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. Subsequent security reviews, bug bounty programs and on-chain monitoring are strongly recommended. 9 | 10 | ## Risk classification 11 | 12 | | Severity | Impact: High | Impact: Medium | Impact: Low | 13 | | :----------------- | :----------: | :------------: | :---------: | 14 | | Likelihood: High | Critical | High | Medium | 15 | | Likelihood: Medium | High | Medium | Low | 16 | | Likelihood: Low | Medium | Low | Low | 17 | 18 | ### Impact 19 | 20 | - **High** - leads to a significant material loss of assets in the protocol or significantly harms a group of users. 21 | - **Medium** - only a small amount of funds can be lost (such as leakage of value) or a core functionality of the protocol is affected. 22 | - **Low** - can lead to any kind of unexpected behaviour with some of the protocol's functionalities that's not so critical. 23 | 24 | ### Likelihood 25 | 26 | - **High** - attack path is possible with reasonable assumptions that mimic on-chain conditions and the cost of the attack is relatively low to the amount of funds that can be stolen or lost. 27 | - **Medium** - only conditionally incentivized attack vector, but still relatively likely. 28 | - **Low** - has too many or too unlikely assumptions or requires a huge stake by the attacker with little or no incentive. 29 | 30 | ## Executive summary 31 | 32 | ### Overview 33 | | | | 34 | | :------------ | :------------------------------------------------------------------------------------------------------------------------------------------ | 35 | | Project Name | RootedToken 36 | 37 | ### Scope 38 | 39 | | Files | 40 | | :-------------- | 41 | | FeeSplitter.sol| 42 | | SwapPair.sol| 43 | | RootedTransferGate.sol| 44 | 45 | 46 | ### Issues found 47 | 48 | | Severity | Count | Fixed | Acknowledged | 49 | | :------------ | ----: | ----: | ---------: | 50 | | High risk | 2 | - | - | 51 | | Medium risk | 3 | - | - | 52 | | Low risk | 4 | - | - | 53 | | **Total** | **9** | **-** | **-** | 54 | 55 | # Findings 56 | 57 | |ID | Title | Severity | 58 | |:---| :------------------------------------------------------------------------------------| :------------ | 59 | |H-01 | Unspecified slippage allows sandwich attacks | High | 60 | |H-02 | Some functions do not work properly with meta-transactions | High | 61 | |M-01 | Use of block.timestamp for swap | Medium | 62 | |M-02 | Batch transfer/transferFrom will always fail | Medium | 63 | |M-03 | The calling of the balanceOf function can easily be blocked | Medium | 64 | |L-01 | getDumpTax() function will return 0 for small duration | Low | 65 | |L-02 | Centralization risk | Low | 66 | |L-03 | Splitter should be checked for non-zero address | Low | 67 | |L-04 |Very small transfers can bypass taxes | Low | 68 | 69 | # High 70 | 71 | ## [H-01] Unspecified slippage allows sandwich attacks 72 | ### Impact 73 | The amount getting swapped will be completely lost. 74 | 75 | ### Vulnerability Details 76 | The swap in `FeeSplitter.sol` the `payFees` function executes a swap, but the `amountOutMinimum` value, which is accountable for slippage 77 | protection is at 0, which allows the swap to yield 0 tokens in return for the amount provided. This allows for MEVs to pick the transaction up from the mempool and to sandwich it by manipulating the pool, in which the swap is happening. 78 | 79 | ### Recommendations 80 | Consider setting `amountOutMinimum` to some appropriate value, that includes a conservative amount of tolerance for price impact. 81 | 82 | ## [H-02] Some functions do not work properly with meta-transactions 83 | ### Impact 84 | Some functions do not work properly with meta-transactions which can lead to lose of funds. 85 | 86 | ### Vulnerability Details 87 | The functions `transfer`, `approve` and `transferFrom` will not work properly with meta-transactions. In meta-transactions, data is signed off-chain by one person and executed by another person who pays the gas fees. In this case, the original sender won't be `msg.sender`. Using of `msg.sender` directly is not the proper way of usage. The replayer who executed the transaction will be affected, even though they were not originally the signer of the transaction. 88 | 89 | ### Recommendations 90 | `_msgSender()` should be used instead of `msg.sender`. 91 | 92 | # Medium 93 | 94 | ## [M-01] Use of block.timestamp for swap 95 | ### Impact 96 | Not allowing users to supply their own deadline could potentially expose them to sandwich attacks. 97 | 98 | ### Vulnerability Details 99 | The current implementation can lead to significant risks including unfavorable trade outcomes and potential financial loss. Without a userspecified deadline, transactions can remain in the mempool for an extended period, resulting in execution at a potentially disadvantageous time. Moreover, by setting the deadline to `block.timestamp`, a validator can hold the transaction without any time constraints, further exposing users to the risk of price fluctuations. 100 | 101 | Advanced protocols like Automated Market Makers (AMMs) can allow users to specify a deadline parameter that enforces a time limit by which 102 | the transaction must be executed. Without a deadline parameter, the transaction may sit in the mempool and be executed at a much later time 103 | potentially resulting in a worse price for the user. 104 | 105 | ```solidity 106 | uint256[] memory amounts = router.swapExactTokensForTokens(sellAmount, 0, path, address(this), block.timestamp); 107 | ``` 108 | 109 | ### Recommendations 110 | Allow for a deadline to be set by the user, such that after the deadline the transaction never takes place. 111 | 112 | 113 | ## [M-02] Batch transfer/transferFrom will always fail 114 | ### Impact 115 | It is not possible to perform batch `transfers/transferFrom` from one user to many other users. 116 | 117 | ### Vulnerability Details 118 | 119 | The problem arises in the require statement within the `allowBalance` function, 120 | 121 | ```solidity 122 | require (last.origin != allow.origin || last.blockNumber != allow.blockNumber || last.transferFrom != allow.transferFrom, "Liquidity is locked (Please try again next block)");` 123 | ``` 124 | 125 | The conditions `last.origin != allow.origin`, `last.blockNumber != allow.blockNumber` and `last.transferFrom != allow.transferFrom` 126 | will always evaluate to false in the second transfer or transferFrom call within a batch operation, leading to a transaction revert. Users are 127 | required to wait for one block before executing multiple transfer or they can create a specific contract to bypass this requirement. 128 | 129 | ## [M-03] The calling of the balanceOf function can easily be blocked 130 | ### Impact 131 | Calling of `balanceOf` function easy can be blocked through front-running attack. 132 | 133 | ### Vulnerability Details 134 | If the `SwapPair` contract calls the `balanceOf` function and `liquidityPairLocked[pair]` is true, then it must satisfy the following require statement: 135 | 136 | ```solidity 137 | require (last.origin != allow.origin || last.blockNumber != allow.blockNumber || last.transferFrom != allow.transferFrom); 138 | ``` 139 | 140 | To meet this requirement, the caller must first invoke `transfer` or `transferFrom` to fulfill all conditions. However, if these two calls are not 141 | executed within a single transaction — for instance, if the `SwapPair` calls transfer in one transaction and then calls `balanceOf` in a separate 142 | transaction — there is a risk of being front-runned by a malicious user. 143 | 144 | The malicious user can observe the initial transaction of `SwapPair`, which is a `transfer` and subsequently execute another transfer to alter 145 | the value of `last.origin`. Consequently, the subsequent transaction involving the `balanceOf` function for the pair will result in failure. 146 | 147 | 148 | # Low 149 | ## [L-01] getDumpTax() function will return 0 for small duration 150 | The `getDumpTax()` function will return 0 if a small duration is set up by the owner or the `feeController`. This occurs due to a precision error in the 151 | following calculation: 152 | 153 | ```solidity 154 | dumpTaxStartRate * (dumpTaxEndTimestamp - block.timestamp) * 1e18 / dumpTaxDurationInSeconds / 1e18; // return 2 after 40 days` 155 | ``` 156 | 157 | ## [L-02] Centralization risk 158 | The whole contract carries a significant centralization risk. The owner can easily scam all users by altering fees or blacklisting/whitelisting 159 | addresses. 160 | 161 | ## [L-03] Splitter should be checked for non-zero address 162 | The splitter address should be checked for a non-zero value, as if the address is zero during the transfer, the fee will be transferred to 163 | `address(0)`. This will lead to funds being stuck. 164 | 165 | ## [L-04] Very small transfers can bypass taxes 166 | Very small transfers can bypass taxes due to precision loss: 167 | 168 | `return amount * feesRate / 10000;` 169 | 170 | For instance, if the feesRate is 2, then transfers with an amount of 500 will bypass taxation for the user. 171 | -------------------------------------------------------------------------------- /solo/GameSwift-Security-Review-3.md: -------------------------------------------------------------------------------- 1 | # GameSwift Security Review 2 | 3 | A security review of the [GameSwift](https://gameswift.io/). 4 | 5 | The first modular gaming blockchain based on zkEVM. 6 | 7 | Author: [gkrastenov](https://twitter.com/gkrastenov) Independent Security Researcher 8 | 9 | This audit report includes all the vulnerabilities, issues and code improvements found during the security review. 10 | 11 | ## Disclaimer 12 | 13 | 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. Subsequent security reviews, bug bounty programs and on-chain monitoring are strongly recommended. 14 | 15 | ## Risk classification 16 | 17 | | Severity | Impact: High | Impact: Medium | Impact: Low | 18 | | :----------------- | :----------: | :------------: | :---------: | 19 | | Likelihood: High | Critical | High | Medium | 20 | | Likelihood: Medium | High | Medium | Low | 21 | | Likelihood: Low | Medium | Low | Low | 22 | 23 | ### Impact 24 | 25 | - **High** - leads to a significant material loss of assets in the protocol or significantly harms a group of users. 26 | - **Medium** - only a small amount of funds can be lost (such as leakage of value) or a core functionality of the protocol is affected. 27 | - **Low** - can lead to any kind of unexpected behaviour with some of the protocol's functionalities that's not so critical. 28 | 29 | ### Likelihood 30 | 31 | - **High** - attack path is possible with reasonable assumptions that mimic on-chain conditions and the cost of the attack is relatively low to the amount of funds that can be stolen or lost. 32 | - **Medium** - only conditionally incentivized attack vector, but still relatively likely. 33 | - **Low** - has too many or too unlikely assumptions or requires a huge stake by the attacker with little or no incentive. 34 | 35 | ### Actions required by severity level 36 | 37 | - **Critical** - client **must** fix the issue. 38 | - **High** - client **must** fix the issue. 39 | - **Medium** - client **should** fix the issue. 40 | - **Low** - client **could** fix the issue. 41 | 42 | ## Executive summary 43 | 44 | ### Overview 45 | 46 | | | | 47 | | :----------------------- | :--------------------------------------- | 48 | | Project Name | GameSwift | 49 | | Language | Solidity | 50 | | Review Commit hash | 25f850c21d29846b14df6c546bcc6f341cde3235 | 51 | | Fixes Review Commit hash | 978a3731c6d3c21606b21f9ad7c26cf489eb180d | 52 | 53 | ### Scope 54 | 55 | | File | 56 | | :---------------------------------------------------------------------------------------------------------------------------------------------------- | 57 | | Contracts (2) | 58 | | [LockStaking.sol](https://github.com/KaliszukTomasz/LockStakingContractAudit/blob/25f850c21d29846b14df6c546bcc6f341cde3235/contracts/LockStaking.sol) | 59 | | [WaitingList.sol](https://github.com/KaliszukTomasz/LockStakingContractAudit/blob/25f850c21d29846b14df6c546bcc6f341cde3235/contracts/WaitingList.sol) | 60 | | **nSLOC (545)** | 61 | 62 | ### Issues found 63 | 64 | | Severity | Count | Fixed | Acknowledged | 65 | | :------------ | ----: | ----: | -----------: | 66 | | High risk | 1 | 1 | 0 | 67 | | Medium risk | 3 | 3 | 0 | 68 | | Low risk | 2 | 2 | 0 | 69 | | Informational | 1 | 0 | 1 | 70 | | **Total** | **7** | **6** | **1** | 71 | 72 | # Findings 73 | 74 | | ID | Title | Severity | 75 | | :----- | :---------------------------------------------------------------------------- | :------------ | 76 | | [H-01] | Unstaking will not work because the unstaked amount is always zero | High | 77 | | [M-01] | Lack of access control in the stakeTokens function | Medium | 78 | | [M-02] | Unwanted extending staking time | Medium | 79 | | [M-03] | Locking user's funds if they stake before the first reward period | Medium | 80 | | [L-01] | The getUserInfo function will return the wrong value for end time | Low | 81 | | [L-02] | Avoid unnecessary external call in checkEthFeeAndRefundDust modifier | Low | 82 | | [I-01] | Requrement for stakingPhase == StakingPhase.Open is unnesery in migrateToTier | Informational | 83 | 84 | # High 85 | 86 | ## [H-01] Unstaking will not work because the unstaked amount is always zero 87 | 88 | ### Impact 89 | 90 | During the creation of `UnbondInfo` for every user who decides to start unstaking their tokens, the amount and release time are recorded. Before that, the user's amount is set to zero, resulting in the creation of `UnbondInfo` with an amount equal to zero. This will block the `finishUnstaking` function because `releasedAmount += unbonding.amount` will be equal to zero, causing it to revert every time it is called. 91 | 92 | ```solidity 93 | user.amount = 0; 94 | user.unbonding = UnbondInfo({ 95 | amount : user.amount, 96 | release : block.timestamp + unbondTime 97 | }); 98 | ``` 99 | 100 | ### Recommended Mitigation Steps 101 | 102 | Make the following changes: 103 | 104 | ```diff 105 | - user.amount = 0; 106 | user.unbonding = UnbondInfo({ 107 | amount : user.amount, 108 | release : block.timestamp + unbondTime 109 | }); 110 | + user.amount = 0; 111 | emit UnstakeStarted(msg.sender, user.unbonding.amount); 112 | ``` 113 | 114 | ### Fixes Review 115 | 116 | Fixed. 117 | 118 | # Medium 119 | 120 | ## [M-01] Lack of access control in the stakeTokens function 121 | 122 | ### Impact 123 | 124 | The `stakeTokens` function should be accessible only during the `Open` phase. Unfortunately, this is not true because everyone can access it in the `WaitingList` or `Whitelist` phase and stake their tokens. The problem comes from the `notClosedPhase` modifier, where it is checked if the phase is different than `Close`. 125 | 126 | ```solidity 127 | modifier notClosedPhase() { 128 | require(activePhase != Phase.Closed, "Phase is Closed"); 129 | _; 130 | } 131 | ``` 132 | 133 | ### Recommended Mitigation Steps 134 | 135 | Make the following changes: 136 | 137 | ```diff 138 | - function stakeTokens(uint256 _amount) external payable checkEthFeeAndRefundDust(msg.value) notClosedPhase whenNotPaused { 139 | + function stakeTokens(uint256 _amount) external payable checkEthFeeAndRefundDust(msg.value) isOpenPhase whenNotPaused { 140 | stakeTokensInternal(_amount, msg.sender); 141 | } 142 | ``` 143 | 144 | ### Fixes Review 145 | 146 | Fixed. 147 | 148 | ## [M-02] Unwanted extending staking time 149 | 150 | ### Impact 151 | 152 | When the users call the `claimRewards` function to claim their rewards, the reward is calculated in the `updateRewards` function, where their staking start time will be updated. This will lead to an extending of their staking time by another 180 days. This behavior limits the user's ability to claim their tokens before the completion of the staking period. So, if someone decides to stake their tokens and after 90 days, decides to claim half of their reward, they have to wait a total of 270 days instead of the expected 180 days at the beginning of staking. 153 | 154 | ### Recommended Mitigation Steps 155 | 156 | Create two a different maps where will be stored last `stakingStartTimes` and when user start his first staking. 157 | 158 | ```diff 159 | mapping(address => uint256) public stakingStartTimes; 160 | + mapping(address => uint256) public stakingFirstTime; 161 | ``` 162 | 163 | ### Fixes Review 164 | 165 | Fixed. 166 | 167 | ## [M-03] Locking user's funds if they stake before the first reward period 168 | 169 | ### Impact 170 | 171 | If user stakes tokens before starting of first reward period his funds will be locked, no way for unstaking or migrating, in the contract because every time when `calculateReward` is called it will revert. His `lastStakeTime` will be always `<` than `rewardPeriods[i].start ` and durign calculation of `timeDelta` will lead to underflow error. 172 | 173 | ```solidity 174 | uint256 tempStart = rewardPeriods[i].start < user.lastStakeTime ? user.lastStakeTime : rewardPeriods[i].start; 175 | 176 | timeDelta = block.timestamp - tempStart; 177 | ``` 178 | 179 | The first reward start is set in the constructor, equal to the `_stake` variable. Users can stake their tokens if `_stake > block.timestamp` because the phase is set by default to `Open`. If the start of the first reward period is planned a few days after the deployment of the contract and users stake their tokens during this time difference, they will lose their tokens. 180 | 181 | ### Recommended Mitigation Steps 182 | 183 | Set `currentPhase` to `Phase.Closed` by default and change it when the first reward period is planned to start. 184 | 185 | ### Fixes Review 186 | 187 | Fixed. 188 | 189 | # Low 190 | 191 | ## [L-01] The getUserInfo function will return wrong value for end time 192 | 193 | ### Impact 194 | 195 | If unbonding is started for the user, the `endTime` will be equal to `lockupDuration` because the `stakingStartTimes` of the user is reset at the beginning of unbonding. The `endTime` should be equal to when the user is able to start unstaking of his tokens. 196 | 197 | ### Fixes Review 198 | 199 | Fixed. 200 | 201 | ## [L-02] Avoid unnecessary external call in checkEthFeeAndRefundDust modifier 202 | 203 | ### Impact 204 | 205 | When a user calls payable function, he needs to pay a fee that is greater than or equal to the fee. If `msg.value > ethFee`, the remaining fee will be returned back to the user `dust = value - ethFee`. In the case where `msg.value == ethFee`, the variable `dust` will be zero and an unnecessary external call to `msg.sender` will be executed. 206 | 207 | ### Recommended Mitigation Steps 208 | 209 | Make the following changes: 210 | 211 | ```diff 212 | modifier checkEthFeeAndRefundDust(uint256 value) { 213 | require(value >= ethFee, "Insufficient fee: the required fee must be covered"); 214 | 215 | uint256 dust = value - ethFee; 216 | - (bool sent,) = address(msg.sender).call{value : dust}(""); 217 | - require(sent, "Failed to return overpayment"); 218 | 219 | + if(dust != 0) { 220 | + (bool sent,) = address(msg.sender).call{value : dust}(""); 221 | + require(sent, "Failed to return overpayment"); 222 | } 223 | _; 224 | } 225 | ``` 226 | 227 | ### Fixes Review 228 | 229 | Fixed. 230 | 231 | # Information 232 | 233 | ## [I-01] Requrement for stakingPhase == StakingPhase.Open is unnesery in migrateToTier 234 | 235 | ### Impact 236 | 237 | Requirement for` stakingPhase == StakingPhase.Open` is unnecessary in the `migrateToTier` function because staking in the `LockStaking` contract will be possible only when `stakingPhase == StakingPhase.WaitingList`. If the user calls the `migrateToTier` function when `stakingPhase == StakingPhase.Open`, it will revert the transaction. 238 | 239 | `lockStaking.stakeWithWaitingList(migrationAmount, userAddress);` 240 | 241 | ### Recommended Mitigation Steps 242 | 243 | Make the following changes: 244 | 245 | ```diff 246 | -require(stakingPhase == StakingPhase.WaitingList || stakingPhase == StakingPhase.Open, "Phase not in WaitingList or Open"); 247 | +require(stakingPhase == StakingPhase.WaitingList, "Phase not in WaitingList"); 248 | ``` 249 | 250 | ### Fixes Review 251 | 252 | Acknowledged. 253 | -------------------------------------------------------------------------------- /solo/GameSwift-Security-Review.md: -------------------------------------------------------------------------------- 1 | # GameSwift Security Review 2 | 3 | A security review of the [GameSwift](https://gameswift.io/). GameSwift is a one-stop gaming ecosystem that aims to introduce cross-chain interoperability to Web3 gaming. 4 | 5 | Author: [gkrastenov](https://twitter.com/gkrastenov) Independent Security Researcher 6 | 7 | ## Disclaimer 8 | 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. Subsequent security reviews, bug bounty programs and on-chain monitoring are strongly recommended. 9 | 10 | ## Risk classification 11 | 12 | | Severity | Impact: High | Impact: Medium | Impact: Low | 13 | | :----------------- | :----------: | :------------: | :---------: | 14 | | Likelihood: High | Critical | High | Medium | 15 | | Likelihood: Medium | High | Medium | Low | 16 | | Likelihood: Low | Medium | Low | Low | 17 | 18 | ### Impact 19 | 20 | - **High** - leads to a significant material loss of assets in the protocol or significantly harms a group of users. 21 | - **Medium** - only a small amount of funds can be lost (such as leakage of value) or a core functionality of the protocol is affected. 22 | - **Low** - can lead to any kind of unexpected behaviour with some of the protocol's functionalities that's not so critical. 23 | 24 | ### Likelihood 25 | 26 | - **High** - attack path is possible with reasonable assumptions that mimic on-chain conditions and the cost of the attack is relatively low to the amount of funds that can be stolen or lost. 27 | - **Medium** - only conditionally incentivized attack vector, but still relatively likely. 28 | - **Low** - has too many or too unlikely assumptions or requires a huge stake by the attacker with little or no incentive. 29 | 30 | ## Executive summary 31 | 32 | ### Overview 33 | | | | 34 | | :------------ | :------------------------------------------------------------------------------------------------------------------------------------------ | 35 | | Project Name | GameSwift 36 | | Review Commit hash | 8d6c99af4b7864043234efef63f5bdf968045317| 37 | 38 | ### Scope 39 | 40 | | File | 41 | | :-------------- | 42 | | Contracts (2) | 43 | | contracts/airdrop/Airdrop.sol| 44 | | contracts/staking/Staking.sol| 45 | | **nSLOC (415)** | 46 | 47 | 48 | ### Issues found 49 | 50 | | Severity | Count | Fixed | Acknowledged | 51 | | :------------ | ----: | ----: | ---------: | 52 | | High risk | 0 | - | - | 53 | | Medium risk | 2 | - | - | 54 | | Low risk | 2 | - | - | 55 | | Informational | 7 | - | - | 56 | | Gas | 4 | - | - | 57 | | **Total** | **15** | **-** | **-** | 58 | 59 | # Findings 60 | 61 | |ID | Title | Severity | 62 | |:---| :------------------------------------------------------------------------------------| :------------ | 63 | |M-01 | Unnecessary external call in checkEthFeeAndRefundDust modifier | Medium | 64 | |M-02 | It is possible to create UnbondInfo with _amount == 0 | Medium | 65 | |L-01 | The nonReentrant modifier should occur before all other modifiers | Low | 66 | |L-02 | Use msg.sender instead of owner() | Low | 67 | |I-01 | Redundant if condition | Informational | 68 | |I-02 | Add 100% test coverage | Informational | 69 | |I-03 | Missing non-bytes32(0) checks | Informational | 70 | |I-04 | Use safeTransfer instead of transfer | Informational | 71 | |I-05 | Add NatSpec documentation | Informational | 72 | |I-06 | Missing non-zero address checks | Informational | 73 | |I-07 | Import declarations should import specific identifiers | Informational | 74 | |G-01| Use custom error instead of require statement | Gas | 75 | |G-02| ++i/i++ should be unchecked{++i}/unchecked{i++} when it is not possible for them to overflow | Gas | 76 | |G-03| Use constant for REWARDS_PRECISION | Gas | 77 | |G-04| Use extnernal modifier instead of public | Gas | 78 | 79 | # Medium 80 | 81 | ## [M-01] Unnecessary external call in checkEthFeeAndRefundDust modifier 82 | ### Impact 83 | When a user calls `stake`, `startUnstaking`, `claim`, or `restake` functions, he needs to pay a fee that is greater than or equal to the storage variable `ethFee`. If `msg.value` is larger than `ethFee`, the remaining fee will be returned back to the user `dust = value - ethFee`. In the case where `msg.value == ethFee`, the variable `dust` will be zero, and an unnecessary external call to `msg.sender` will be executed. This will increase the transaction cost. 84 | 85 | ```solidity 86 | modifier checkEthFeeAndRefundDust(uint256 value) { 87 | require(value >= ethFee, "Insufficient fee: the required fee must be covered"); 88 | 89 | //@audit if ethFee == value will make unnecessary call. 90 | 91 | uint256 dust = value - ethFee; 92 | (bool sent,) = address(msg.sender).call{value : dust}(""); 93 | require(sent, "Failed to return overpayment"); 94 | _; 95 | } 96 | ``` 97 | 98 | ### Recommended Mitigation Steps 99 | Return remaning fee back to user only when `dust != 0` 100 | 101 | ```diff 102 | modifier checkEthFeeAndRefundDust(uint256 value) { 103 | require(value >= ethFee, "Insufficient fee: the required fee must be covered"); 104 | 105 | uint256 dust = value - ethFee; 106 | - (bool sent,) = address(msg.sender).call{value : dust}(""); 107 | - require(sent, "Failed to return overpayment"); 108 | 109 | + if(dust != 0) { 110 | + (bool sent,) = address(msg.sender).call{value : dust}(""); 111 | + require(sent, "Failed to return overpayment"); 112 | } 113 | _; 114 | } 115 | ``` 116 | 117 | ## [M-02] It is possible to create UnbondInfo with _amount == 0 118 | ### Impact 119 | Every user can call the `startUnstaking` function with `_amount` equal to 0 and to create an `UnbondInfo` position. They do not need to stake any amount before that, and it will emit the `UnstakeStarted(msg.sender, _amount)` event. 120 | 121 | ### Recommended Mitigation Steps 122 | Add additional check for that. 123 | 124 | ```diff 125 | function startUnstaking(uint256 _amount) public payable checkEthFeeAndRefundDust(msg.value) nonReentrant { 126 | UserInfo storage user = userInfo[msg.sender]; 127 | 128 | + require(_amount > 0, "Zero amount"); 129 | require(user.unbondings.length < unbondLimit, "startUnstaking: limit reached"); 130 | require(user.amount >= _amount, "startUnstaking: not enough staked amount"); 131 | ``` 132 | 133 | # Low severity 134 | 135 | ## [L-01] The nonReentrant modifier should occur before all other modifiers 136 | ### Impact 137 | The modifiers are read one by one depending on their order. The `checkEthFeeAndRefundDust` modifier is placed before the `nonReentrant` modifier, which allows the possibility for a user to execute an arbitrary call or attempt a reentrancy attack. 138 | 139 | This is a best practice to protect against reentrancy in other modifiers. 140 | ### Recommended Mitigation Steps 141 | Make the following changes for `stake`, `startUnstaking`, `claim`, and `restake` functions: 142 | 143 | ```diff 144 | -function stake(uint256 _amount) public payable checkEthFeeAndRefundDust(msg.value) nonReentrant 145 | +function stake(uint256 _amount) public payable nonReentrant checkEthFeeAndRefundDust(msg.value) 146 | ``` 147 | 148 | ## [L-02] Use msg.sender instead of owner() 149 | ### Impact 150 | When a function is only accessible by the owner due to the presence of the `onlyOwner` modifier, there is no need to call an internal function to obtain the owner's address. In this case, `msg.sender` is equal to `owner()`. 151 | 152 | ```solidity 153 | function withdrawEth(uint256 amount) external onlyOwner { 154 | //@audit use directly msg.sender 155 | (bool success,) = payable(owner()).call{value : amount}(""); 156 | if (!success) { 157 | revert WithdrawFailed(); 158 | } 159 | 160 | emit WithdrawEth(owner(), amount); 161 | } 162 | ``` 163 | 164 | ### Recommended Mitigation Steps 165 | Make the following changes: 166 | 167 | ```diff 168 | - (bool success,) = payable(owner()).call{value : amount}(""); 169 | + (bool success,) = payable(msg.sender).call{value : amount}(""); 170 | 171 | ``` 172 | 173 | # Information 174 | 175 | ## [I-01] Redundant if condition 176 | The condition `block.timestamp < vestingStartDate` in the `getVestedAmount` function is redundant because there is already a check before calling the function for `block.timestamp > vestingStartDate`. This means that `block.timestamp` will always be greater than `vestingStartDate` in the `getVestedAmount` function. 177 | 178 | in `getClaimableAmount`function: 179 | 180 | ```solidity 181 | if (block.timestamp > vestingStartDate) { 182 | claimable += getVestedAmount(halfOfAirdrop); 183 | } 184 | ``` 185 | 186 | ## [I-02] Add 100% test coverage 187 | Currently, the contracts have 0% test coverage. Increasing the test coverage and testing every unit of code can help prevent unexpected behavior. 188 | 189 | ## [I-03] Missing non-bytes32(0) checks 190 | Add non-bytes32(0) checks for all places where `merkleRoot` is set from the owner. 191 | 192 | ## [I-04] Use safeTransfer instead of transfer 193 | Using safeTransfer instead of transfer will automatically handle the returning of the bool parameter. 194 | 195 | ### Recommended Mitigation Steps 196 | Example: 197 | 198 | ```diff 199 | -require(token.transfer(msg.sender, claimableAmount), "Airdrop transfer failed.");` 200 | +token.safeTransfer(msg.sender, claimableAmount);` 201 | ``` 202 | 203 | ## [I-05] Add NatSpec documentation 204 | NatSpec documentation to all public methods and variables is essential for better understanding of the code by developers and auditors and is strongly recommended. 205 | 206 | ## [I-06] Missing non-zero address checks 207 | Add non-zero address checks for all address type arguments. 208 | 209 | ## [I-07] Import declarations should import specific identifiers, rather than the whole file 210 | Using import declarations of the form `import {} from "some/file.sol"` avoids polluting the symbol namespace making flattened files smaller and speeds up compilation 211 | 212 | # Gas 213 | 214 | ## [G-01] Use custom error instead of require statement 215 | Use custom errors instead of require statements. It saves more gas. 216 | 217 | ## [G-02] ++i/i++ should be unchecked{++i}/unchecked{i++} when it is not possible for them to overflow 218 | The unchecked keyword is new in solidity version 0.8.0, so this only applies to that version or higher, which these instances are. This saves 30-40 gas [per loop](https://gist.github.com/hrkrshnn/ee8fabd532058307229d65dcd5836ddc#the-increment-in-for-loop-post-condition-can-be-made-unchecked) 219 | 220 | ## [G-03] Use constant for REWARDS_PRECISION 221 | Use a constant variable for REWARDS_PRECISION instead of `1e12`. Constant variables are saved in bytecode and help save gas. 222 | 223 | ## [G-04] Use external access modifier instead of public 224 | For the functions `stake`, `restake`, `startUnstaking`, `finishUnstaking`, `restake`, `getUserUnbondings`, and claim, use the external access modifier because they will be called only externally and not internally. -------------------------------------------------------------------------------- /solo/SpartaDex-Security-Review-2.md: -------------------------------------------------------------------------------- 1 | # SpartaDex Security Review 2 | 3 | A second security review of the [SpartaDex](https://spartadex.io/) before deploying of their contracts on Arbitrum. 4 | 5 | Author: [gkrastenov](https://twitter.com/gkrastenov) Independent Security Researcher 6 | 7 | This audit report includes all the vulnerabilities, issues and code improvements found during the security review. 8 | 9 | ## Disclaimer 10 | 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. Subsequent security reviews, bug bounty programs and on-chain monitoring are strongly recommended. 11 | 12 | ## Risk classification 13 | 14 | | Severity | Impact: High | Impact: Medium | Impact: Low | 15 | | :----------------- | :----------: | :------------: | :---------: | 16 | | Likelihood: High | Critical | High | Medium | 17 | | Likelihood: Medium | High | Medium | Low | 18 | | Likelihood: Low | Medium | Low | Low | 19 | 20 | ### Impact 21 | 22 | - **High** - leads to a significant material loss of assets in the protocol or significantly harms a group of users. 23 | - **Medium** - only a small amount of funds can be lost (such as leakage of value) or a core functionality of the protocol is affected. 24 | - **Low** - can lead to any kind of unexpected behaviour with some of the protocol's functionalities that's not so critical. 25 | 26 | ### Likelihood 27 | 28 | - **High** - attack path is possible with reasonable assumptions that mimic on-chain conditions and the cost of the attack is relatively low to the amount of funds that can be stolen or lost. 29 | - **Medium** - only conditionally incentivized attack vector, but still relatively likely. 30 | - **Low** - has too many or too unlikely assumptions or requires a huge stake by the attacker with little or no incentive. 31 | 32 | ## Executive summary 33 | 34 | ### Overview 35 | | | | 36 | | :------------ | :------------------------------------------------------------------------------------------------------------------------------------------ | 37 | | Project Name | SpartaDex 38 | | Review Commit hash | [fb6b6a69fe5ecba792d49fa460e94d0b98712a8b](https://github.com/SpartaDEX/sdex-smart-contracts/tree/fb6b6a69fe5ecba792d49fa460e94d0b98712a8b) | 39 | | Fixes Review Commit hash | [d7e8823e0bbaa6b791f918ffa01c923e55147a9e](https://github.com/SpartaDEX/sdex-smart-contracts/tree/d7e8823e0bbaa6b791f918ffa01c923e55147a9e) | 40 | 41 | ### Scope 42 | 43 | | File | 44 | | :-------------- | 45 | | Contracts (6) | 46 | | [contracts/tokens/Polis.sol](https://github.com/SpartaDEX/sdex-smart-contracts/blob/fb6b6a69fe5ecba792d49fa460e94d0b98712a8b/contracts/tokens/Polis.sol) | 47 | | [contracts/dex/core/SpartaDexFactory.sol](https://github.com/SpartaDEX/sdex-smart-contracts/blob/fb6b6a69fe5ecba792d49fa460e94d0b98712a8b/contracts/dex/core/SpartaDexFactory.sol) | 48 | | [contracts/staking/SpartaStaking.sol](https://github.com/SpartaDEX/sdex-smart-contracts/blob/fb6b6a69fe5ecba792d49fa460e94d0b98712a8b/contracts/staking/SpartaStaking.sol) | 49 | | [contracts/SpartaRewardLpLinearStaking.sol](https://github.com/SpartaDEX/sdex-smart-contracts/blob/fb6b6a69fe5ecba792d49fa460e94d0b98712a8b/contracts/staking/SpartaRewardLpLinearStaking.sol) | 50 | | [contracts/ContractsRepository.sol](https://github.com/SpartaDEX/sdex-smart-contracts/blob/fb6b6a69fe5ecba792d49fa460e94d0b98712a8b/contracts/ContractsRepository.sol) | 51 | | [contracts/vesting/SpartaVesting.sol](https://github.com/SpartaDEX/sdex-smart-contracts/blob/fb6b6a69fe5ecba792d49fa460e94d0b98712a8b/contracts/vesting/SpartaVesting.sol) | 52 | 53 | ### Issues found 54 | 55 | | Severity | Count | Fixed | Acknowledged | 56 | | :------------ | ----: | ----: | ---------: | 57 | | Critical risk | 3 | 3 | 0 | 58 | | High risk | 1 | 1 | 0 | 59 | | Medium risk | 0 | 0 | 0 | 60 | | Low risk | 0 | 0 | 0 | 61 | | Informational | 6 | 6 | 0 | 62 | | **Total** | **10** | **10** | **0** | 63 | 64 | # Findings 65 | |ID | Title | Severity | 66 | |:---| :-------------------------------------------------------------------------------------| :------------ | 67 | |C-01 | The `claim()` function will always revert | Critical | 68 | |C-02 | The `currentImplementation()` function will always revert | Critical | 69 | |C-03 | Users can double their StakedSparta tokens | Critical | 70 | |H-01 | Only the staked amount is moved without the reward | High | 71 | |I-01 | Add an additional check: If `claimableAmount > 0` | Informational | 72 | |I-02 | The deactivate function is redundant | Informational | 73 | |I-03 | Use external modifier instead of public | Informational | 74 | |I-04 | In the RewardTaken event, toTransfer should be used instead of reward | Informational | 75 | |I-05 | Emit event after changing the `baseTokenURI/contractURI` | Informational | 76 | |I-06 | In the event MovedToNextImplementation, `total` should be used instead of` balance ` | Informational | 77 | 78 | # Critical 79 | ## [C-01] The claim() function will always revert 80 | When the `claim()` function is called, a new Sparta staking address is retrieved from the `Repository` contract. The argument `"SPARTA_STAKING"` is incorrectly used in the `getContract()` function; it should be the `keccak256` hash of `"SPARTA_STAKING"`. The `getContract()` function will return `address(0)` and the function will revert when trying to stake the claimed amount. 81 | 82 | ### Impact 83 | 84 | ```solidity 85 | address spartaStakingAddress = contractsRepository.getContract( 86 | "SPARTA_STAKING" //@audit-issue wrong input 87 | ); 88 | ``` 89 | ### Recommended Mitigation Steps 90 | Users have to move the tokens from one iteration to the next without unstaking period and fees 91 | 92 | ```diff 93 | address spartaStakingAddress = contractsRepository.getContract( 94 | - "SPARTA_STAKING" 95 | + keccak256(“SPARTA_STAKING”) 96 | ); 97 | ``` 98 | 99 | ### Fixes Review 100 | Fixed. 101 | 102 | ## [C-02] The currentImplementation() function will always revert 103 | ### Impact 104 | The idea behind `currentImplementation()` is to allow users to move tokens from one iteration to the next without a unstaking period and fees. The `getContract()` function will return the last iteration from the `Repository` contract. The problem arises in the if condition because `spartaStakingAddress == address(spartaStakingAddress)` is always true and the `currentImplementation()` function will always revert when called. Users will not be able to move their tokens. 105 | 106 | 107 | ```solidity 108 | function currentImplementation() public view returns (ISpartaStaking) { 109 | address spartaStakingAddress = contractsRepository.getContract( 110 | SPARTA_STAKING_CONTRACT_ID 111 | ); 112 | 113 | if (spartaStakingAddress == address(spartaStakingAddress)) { 114 | revert CurrentImplementation(); //@audit-issue it will always revert 115 | } 116 | 117 | return SpartaStaking(spartaStakingAddress); 118 | } 119 | ``` 120 | 121 | ### Proof of Concept 122 | ```javascript 123 | it("C-02, PoC", async function () { 124 | // Some user logic here: 125 | await mintTokensToContract(); 126 | await mintTokensToStakerAndApprove(); 127 | await initialize(); 128 | await time.setNextBlockTimestamp(stakingStart); 129 | await instance.connect(staker).stake(stakedTokens); 130 | 131 | // PoC start: 132 | const contractId = ethers.utils.id("SPARTA_STAKING"); 133 | 134 | contractsRepository.setContract( 135 | ethers.utils.solidityKeccak256(["string"], ["SPARTA_STAKING"]), 136 | "0x0000000000000000000000000000000000000000" 137 | ); 138 | 139 | expect(await contractsRepository.connect(stakingOwner).tryGetContract(contractId)).to.be.eq("0x0000000000000000000000000000000000000000"); 140 | 141 | // first call of currentImplementation 142 | await expect(instance.connect(staker).currentImplementation()) 143 | .to.be.reverted; 144 | 145 | // Creating new instances of SpartaStaking 146 | let instance2: Contract; 147 | 148 | const SpartaStaking = await ethers.getContractFactory("SpartaStaking"); 149 | 150 | instance2 = await SpartaStaking.deploy( 151 | sparta.address, 152 | stakedSparta.address, 153 | acl.address, 154 | contractsRepository.address, 155 | treasury.address, 156 | fees 157 | ); 158 | 159 | // the addresses are different 160 | expect(instance2.address).to.not.eq(instance.address); 161 | 162 | // overide the address in the repository 163 | // stakingOwner is REPOSITORY_OWNER 164 | 165 | await expect(await contractsRepository.connect(stakingOwner).setContract(contractId, instance2.address)) 166 | .to.not.be.reverted; 167 | 168 | expect(await contractsRepository.connect(stakingOwner).getContract(contractId)).to.be.eq(instance2.address); 169 | 170 | // second call of currentImplementation 171 | await expect(instance.connect(staker).currentImplementation()) 172 | .to.be.reverted; 173 | }); 174 | ``` 175 | ### Recommended Mitigation Steps 176 | Make the following changes: 177 | 178 | ```diff 179 | function currentImplementation() public view returns (ISpartaStaking) { 180 | address spartaStakingAddress = contractsRepository.getContract( 181 | SPARTA_STAKING_CONTRACT_ID 182 | ); 183 | 184 | - if (spartaStakingAddress == address(spartaStakingAddress)) 185 | + if (spartaStakingAddress == address(this)) { 186 | revert CurrentImplementation(); 187 | } 188 | 189 | return SpartaStaking(spartaStakingAddress); 190 | } 191 | ``` 192 | 193 | ### Fixes Review 194 | Fixed. 195 | 196 | ## [C-03] Users can double their StakedSparta tokens 197 | ### Impact 198 | When a new staking Sparta implementation is added and users decide to move their staked and reward amounts, they will call the `stakeAs` function, which will mint newly StakedSparta tokens. This is not the correct behavior because it will double their token balance. They should only move their existing balance instead of doubling it. 199 | 200 | ### Recommended Mitigation Steps 201 | Burn the StakedSparta tokens during moving to the new round implementation. 202 | 203 | ### Fixes Review 204 | Fixed. 205 | 206 | 207 | # High 208 | ## [H-01] Only the staked amount is moved without the reward 209 | ### Impact 210 | When a user calls `moveToNextSpartaStakingWithReward()`, he have to move both the already staked amount and the reward to the new Sparta staking implementation. Currently, only the user's balance is moved without the reward amount. 211 | 212 | ```solidity 213 | uint256 total = balance + reward; 214 | if (total == 0) { 215 | revert ZeroAmount(); 216 | } 217 | balanceOf[msg.sender] = 0; 218 | rewards[msg.sender] = 0; 219 | totalSupply -= balance; 220 | sparta.forceApprove(address(current), balance); //@audit-issue it should be total instead of balance 221 | current.stakeAs(msg.sender, balance); 222 | ``` 223 | ### Recommended Mitigation Steps 224 | Make the following changes: 225 | 226 | ```diff 227 | - sparta.forceApprove(address(current), balance); 228 | - current.stakeAs(msg.sender, balance); 229 | + sparta.forceApprove(address(current), total); 230 | + current.stakeAs(msg.sender, total); 231 | ``` 232 | 233 | ### Fixes Review 234 | Fixed. 235 | 236 | 237 | # Informational 238 | ## [I-01] Add an additional check: If claimableAmount > 0 239 | To avoid zero staking and transferring, add an additional check if `claimableAmount > 0`. 240 | ### Fixes Review 241 | Fixed. 242 | 243 | ## [I-02] The deactivate function is redundant 244 | The `deactivate()` function is redundant because you can update the restricted variable with the `setActivated()` function. 245 | 246 | ```soldiity 247 | //@audit-issue redundant function 248 | function deactivate() external onlyLiquidityController { 249 | restricted = false; 250 | } 251 | 252 | function setActivated(bool value) external onlyLiquidityController { 253 | if (restricted != value) { 254 | restricted = value; 255 | } 256 | } 257 | ``` 258 | ### Fixes Review 259 | Fixed. 260 | 261 | ## [I-03] Use external modifier instead of public 262 | In the `SpartaRewardLpLinearStaking` contract, use the `external` modifier instead of `public` since the functions are not used internally. 263 | ### Fixes Review 264 | Fixed. 265 | 266 | ## [I-04] In the RewardTaken event, toTransfer should be used instead of reward 267 | In the `RewardTaken` event, `toTransfer` should be used instead of `reward`. The purpose of the event is to emit the wallet address (`msg.sender`, in your case) and the amount he will receive. If `spartaStakingAddress != address(0)` is true, then the user will receive only 25% of the reward. 268 | ### Fixes Review 269 | Fixed. 270 | 271 | ## [I-05] Emit event after changing the baseTokenURI/contractURI 272 | In the `Polis` contract, the event should be emitted after changing the `baseTokenURI/contractURI`, not before. 273 | ### Fixes Review 274 | Fixed. 275 | 276 | ## [I-06] In the event MovedToNextImplementation, total should be used instead of balance 277 | In the event `MovedToNextImplementation`, `total` should be used instead of `balance`. 278 | 279 | Make the following changes: 280 | ```diff 281 | -emit MovedToNextImplementation(msg.sender, balance, reward); 282 | +emit MovedToNextImplementation(msg.sender, total, reward); 283 | ``` 284 | ### Fixes Review 285 | Fixed. -------------------------------------------------------------------------------- /solo/StarHeroes-Security-Review.md: -------------------------------------------------------------------------------- 1 | # StarHeroes Security Review 2 | 3 | A security review of the [StarHeroes](https://twitter.com/StarHeroes_game). 4 | 5 | The first multiplayer third-person space shooter designed for esports. With its top-quality dynamic and competitive gameplay, StarHeroes provides players with what they crave most: true gaming emotions. 6 | 7 | Author: [gkrastenov](https://twitter.com/gkrastenov) Independent Security Researcher 8 | 9 | This audit report includes all the vulnerabilities, issues and code improvements found during the security review. 10 | 11 | ## Disclaimer 12 | 13 | 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. Subsequent security reviews, bug bounty programs and on-chain monitoring are strongly recommended. 14 | 15 | ## Risk classification 16 | 17 | | Severity | Impact: High | Impact: Medium | Impact: Low | 18 | | :----------------- | :----------: | :------------: | :---------: | 19 | | Likelihood: High | Critical | High | Medium | 20 | | Likelihood: Medium | High | Medium | Low | 21 | | Likelihood: Low | Medium | Low | Low | 22 | 23 | ### Impact 24 | 25 | - **High** - leads to a significant material loss of assets in the protocol or significantly harms a group of users. 26 | - **Medium** - only a small amount of funds can be lost (such as leakage of value) or a core functionality of the protocol is affected. 27 | - **Low** - can lead to any kind of unexpected behaviour with some of the protocol's functionalities that's not so critical. 28 | 29 | ### Likelihood 30 | 31 | - **High** - attack path is possible with reasonable assumptions that mimic on-chain conditions and the cost of the attack is relatively low to the amount of funds that can be stolen or lost. 32 | - **Medium** - only conditionally incentivized attack vector, but still relatively likely. 33 | - **Low** - has too many or too unlikely assumptions or requires a huge stake by the attacker with little or no incentive. 34 | 35 | ### Actions required by severity level 36 | 37 | - **Critical** - client **must** fix the issue. 38 | - **High** - client **must** fix the issue. 39 | - **Medium** - client **should** fix the issue. 40 | - **Low** - client **could** fix the issue. 41 | 42 | ## Executive summary 43 | 44 | ### Overview 45 | 46 | | | | 47 | | :----------------------- | :--------------------------------------- | 48 | | Project Name | StarHeroes | 49 | | Review Commit hash | 9010b162adfae5f78ff913047dfe9fd223934deb | 50 | | Fixes Review Commit hash | 4662eec97cfe065f3b857a8109f6b5aa75cbdf11 | 51 | 52 | ### Scope 53 | 54 | | File | 55 | | :-------------------------------------- | 56 | | Contracts (2) | 57 | | contracts/staking/FixedStaking.sol | 58 | | contracts/vesting/FixedVestingCliff.sol | 59 | | **nSLOC (364)** | 60 | 61 | ### Issues found 62 | 63 | | Severity | Count | Fixed | Acknowledged | 64 | | :--------------- | -----: | -----: | -----------: | 65 | | High risk | 0 | 0 | 0 | 66 | | Medium risk | 5 | 4 | 1 | 67 | | Low risk | 4 | 3 | 1 | 68 | | Informational | 9 | 6 | 3 | 69 | | Code improvement | 2 | 2 | 0 | 70 | | Gas | 7 | 6 | 1 | 71 | | **Total** | **27** | **21** | **6** | 72 | 73 | # Findings 74 | 75 | | ID | Title | Severity | 76 | | :------ | :------------------------------------------------------------------------------------------- | :--------------- | 77 | | [M-01] | It is possible to create UnbondInfo with \_amount == 0 | Medium | 78 | | [M-02] | Owner can register total vesting amount that exceeds what the contract currently holds | Medium | 79 | | [M-03] | Possible reverting of releasableAmount | Medium | 80 | | [M-04] | Unnecessary external call in checkEthFeeAndRefundDust modifier | Medium | 81 | | [M-05] | getStakingRewards will revert after changing of APY | Medium | 82 | | [L-01] | getStakingRewards return userReward for freezed account | Low | 83 | | [L-02] | It is possible to register frozen accounts | Low | 84 | | [L-03] | calculateReward will revert if double staking is made | Low | 85 | | [L-04] | The nonReentrant modifier should occur before all other modifiers | Low | 86 | | [I-01] | Use msg.sender instead of owner() | Informational | 87 | | [I-02] | Add an additional parameter for the FreezeAccount event | Informational | 88 | | [I-03] | Using SafeMath when compiler is ^0.8.0 | Informational | 89 | | [I-04] | Add NatSpec documentation | Informational | 90 | | [I-05] | Use safeTransfer/safeTransferFrom instead of transfer/transferFrom | Informational | 91 | | [I-06] | Include old fee value in event | Informational | 92 | | [I-06] | Import declarations should import specific identifiers, rather than the whole file | Informational | 93 | | [I-08] | amount argument can be bigger than the balance | Informational | 94 | | [I-09] | Check if constructor argument is in the past | Informational | 95 | | [CI-01] | tempStart can be calculated before if condition | Code improvement | 96 | | [CI-02] | Unnecessary updating of lastStakeTime | Code improvement | 97 | | [G-01] | Use external modifier instead of public | Gas | 98 | | [G-02] | Use custom error instead of require statement | Gas | 99 | | [G-03] | ++i/i++ should be unchecked{++i}/unchecked{i++} when it is not possible for them to overflow | Gas | 100 | | [G-04] | Use != 0 instead of > 0 | Gas | 101 | | [G-05] | Add unchecked {} for subtractions where the operands can not underflow | Gas | 102 | | [G-06] | Use immutable when is possible | Gas | 103 | | [G-07] | Default int values are manually set | Gas | 104 | 105 | # Medium 106 | 107 | ## [M-01] It is possible to create UnbondInfo with \_amount == 0 108 | 109 | ### Impact 110 | 111 | Every user can call the `startUnstaking` function with `_amount` equal to 0 and to create an `UnbondInfo` position. They do not need to stake any amount before that and it will emit the `UnstakeStarted(msg.sender, _amount)` event. 112 | 113 | ### Recommended Mitigation Steps 114 | 115 | ```diff 116 | function startUnstaking(uint256 _amount) public payable checkEthFeeAndRefundDust(msg.value) nonReentrant { 117 | UserInfo storage user = userInfo[msg.sender]; 118 | 119 | + require(_amount > 0, "Zero amount"); 120 | require(user.unbondings.length < unbondLimit, "startUnstaking: limit reached"); 121 | require(user.amount >= _amount, "startUnstaking: not enough staked amount"); 122 | ``` 123 | 124 | ### Fixes Review 125 | 126 | Fixed. 127 | 128 | ## [M-02] Owner can register total vesting amount that exceeds what the contract currently holds 129 | 130 | ### Impact 131 | 132 | Owner can register a vesting amount for every user. If the total registered vested amount exceeds what the contract currently holds, some users will not be able to receive their tokens. 133 | 134 | ```solidity 135 | function registerVestingAccounts(address[] memory _userAddresses, uint256[] memory _amounts) external onlyOwner { 136 | require(_amounts.length == _userAddresses.length, "Amounts and userAddresses must have the same length"); 137 | 138 | for (uint i = 0; i < _userAddresses.length; i++) { 139 | userTotal[_userAddresses[i]] = _amounts[i]; 140 | //@audit contract has to have enough balance 141 | } 142 | } 143 | ``` 144 | 145 | ### Fixes Review 146 | 147 | Acknowledged. 148 | 149 | ### Recommended Mitigation Steps 150 | 151 | In every for loop iteration, check if the total registered vesting amount is bigger than the balance of the contract. Also, include the expected APY in the calculation. 152 | 153 | ## [M-03] Possible reverting of releasableAmount 154 | 155 | ### Impact 156 | 157 | The releaseable amount is calculated based on the vested amount minus the amount already released. The vested amount is equal to the total tokens multiplied by the elapsed time and divided by the total vesting duration. 158 | 159 | `uint256 vestedAmount = (totalTokens * elapsedTime) / totalVestingTime;` 160 | 161 | If the owner initially registers a big amount of vesting for a user, the user is allowed after some time to release a part of this amount based on the above formula. The owner has the rights to change the vesting amount for every user and if he decreases the vested amount of a user, it can open potential vulnerabilities. It is possible for` tokensReleased[userAddress]` to be bigger than the newly calculated vested amount `vestedAmount` and the transaction to revert because of arithmetic underflow. 162 | 163 | ```solidity 164 | uint256 elapsedTime = block.timestamp - config.startDate; 165 | uint256 totalVestingTime = config.endDate - config.startDate; 166 | 167 | uint256 vestedAmount = (totalTokens * elapsedTime) / totalVestingTime; 168 | 169 | return vestedAmount - tokensReleased[userAddress]; //@audit potential vulnerabilities 170 | ``` 171 | 172 | ## Recommended Mitigation Steps 173 | 174 | Make the following changes: 175 | 176 | ```diff 177 | -return vestedAmount.sub(tokensReleased[userAddress]); 178 | +return vestedAmount < tokensReleased[userAddress] ? 0 : vestedAmount - tokensReleased[userAddress]; 179 | ``` 180 | 181 | ### Fixes Review 182 | 183 | Fixed. 184 | 185 | ## [M-04] Unnecessary external call in checkEthFeeAndRefundDust modifier 186 | 187 | ### Impact 188 | 189 | When a user calls payable function, he needs to pay a fee that is greater than or equal to the storage variable `ethFee`. If `msg.value `is larger than `ethFee`, the remaining fee will be returned back to the user` dust = value - ethFee`. In the case where `msg.value == ethFee`, the variable `dust` will be zero, and an unnecessary external call to `msg.sender` will be executed. This will increase the transaction cost. 190 | 191 | ## Recommended Mitigation Steps 192 | 193 | I recommend to add more stricly condiiton for fee 194 | 195 | ```diff 196 | -require(value >= ethFee, "Insufficient fee: the required fee must be covered"); 197 | +require(value == ethFee, "Insufficient fee: the required fee must be covered"); 198 | ``` 199 | 200 | or to return remaning fee back to user only when `dust != 0`. 201 | 202 | ```diff 203 | modifier checkEthFeeAndRefundDust(uint256 value) { 204 | require(value >= ethFee, "Insufficient fee: the required fee must be covered"); 205 | 206 | uint256 dust = value - ethFee; 207 | - (bool sent,) = address(msg.sender).call{value : dust}(""); 208 | - require(sent, "Failed to return overpayment"); 209 | 210 | + if(dust != 0) { 211 | + (bool sent,) = address(msg.sender).call{value : dust}(""); 212 | + require(sent, "Failed to return overpayment"); 213 | } 214 | _; 215 | } 216 | ``` 217 | 218 | ### Fixes Review 219 | 220 | Fixed. 221 | 222 | ## [M-05] getStakingRewards will revert after changing of APY 223 | 224 | ### Impact 225 | 226 | Owner has the right to change APY at any time. He need to add a new reward rate by calling the `setRewardRate` function. The problem comes in the calculation of staking rewards. When a new reward rate is added, `rewardPeriods.length` becomes greater than 1. In the last iteration of the for loop in the `getStakingRewards` function, `elapsedTime` will be calculated as follows: 227 | 228 | `uint256 elapsedTime = block.timestamp < config.startDate ? block.timestamp - rewardPeriods[i].start : config.startDate - rewardPeriods[i].start;` 229 | 230 | The statement `block.timestamp < config.startDate` is always false, and `elapsedTime` is equal to `config.startDate - rewardPeriods[i].start`. Unfortunately, `rewardPeriods[i].start` is always greater than `config.startDate`, and this will revert with panic code 17, indicating arithmetic underflow. 231 | 232 | The issue is marked as `medium` because the owner is trusted. 233 | 234 | ```solidity 235 | for (uint256 i = 0; i < rewardPeriods.length; i++) { 236 | 237 | if (i == rewardPeriods.length - 1) { 238 | //@audit H -> revert if length > 1 in config.startDate - rewardPeriods[i].start; 239 | uint256 elapsedTime = block.timestamp < config.startDate ? block.timestamp - rewardPeriods[i].start : config.startDate - rewardPeriods[i].start; 240 | 241 | userReward += userTotal[_userAddress] * elapsedTime * rewardPeriods[i].rate / VESTING_DIVIDER / YEAR_DIVIDER; 242 | } else { 243 | uint256 duration = rewardPeriods[i + 1].start - rewardPeriods[i].start; 244 | userReward += userTotal[_userAddress] * duration * rewardPeriods[i].rate / VESTING_DIVIDER / YEAR_DIVIDER; 245 | } 246 | } 247 | ``` 248 | 249 | ### Proof of concept 250 | 251 | ```javascript 252 | import { time } from "@nomicfoundation/hardhat-network-helpers"; 253 | import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; 254 | import { expect } from "chai"; 255 | import { Contract } from "ethers"; 256 | import { ethers } from "hardhat"; 257 | 258 | describe.only("TokenVesting", () => { 259 | let vesting: Contract; 260 | 261 | let user: SignerWithAddress; 262 | let owner: SignerWithAddress; 263 | 264 | beforeEach(async () => { 265 | [owner, user] = await ethers.getSigners(); 266 | 267 | const Vesting = await ethers.getContractFactory("FixedVestingCliff"); 268 | 269 | await time.increaseTo(1699900000); 270 | const endDate = 1700050000 + 2629743; // end after 1 month 271 | 272 | vesting = await Vesting.deploy( 273 | 1700000000, // tge time 274 | 1700050000, // start time 275 | endDate, 276 | "0x580e933d90091b9ce380740e3a4a39c67eb85b4c", // token address 277 | 5000, // 5% 278 | 0 // zero fee 279 | ); 280 | }); 281 | 282 | it.only("PoC", async () => { 283 | const amount = ethers.utils.parseUnits("9", 18); 284 | 285 | // After 5k block, the owner registers vesting accounts. 286 | await time.increaseTo(1700040000); 287 | await vesting 288 | .connect(owner) 289 | .registerVestingAccounts([user.address], [amount]); 290 | 291 | expect( 292 | await vesting.connect(user.address).getStakingRewards(user.address) 293 | ).to.not.be.eq("0"); 294 | 295 | // After 10k block, the owner updates the reward rate to 3% (the percentage does not matter) 296 | await time.increaseTo(1700140000); 297 | await vesting.connect(owner).setRewardRate(3000); 298 | 299 | // After 1k block, user calls the getStakingRewards function. 300 | await time.increaseTo(1700150000); 301 | 302 | // Revert with panic code 17: Arithmetic operation underflowed 303 | await vesting.connect(user).getStakingRewards(user.address); 304 | }); 305 | }); 306 | ``` 307 | 308 | ### Recommended Mitigation Steps 309 | 310 | Add an additional check for `block.timestamp < config.startDate` in the `setRewardRate` function.. 311 | 312 | ### Fixes Review 313 | 314 | Fixed. 315 | 316 | # Low 317 | 318 | ## [L-01] getStakingRewards return userReward for freezed account 319 | 320 | ### Impact 321 | 322 | Function `getStakingRewards` returns the calculated `userReward` for frozen accounts, which are expected to not receive staking rewards. 323 | 324 | ### Recommended Mitigation Steps 325 | 326 | Add `accountNotFrozen` modifier for `getStakingRewards` function. 327 | 328 | ### Fixes Review 329 | 330 | Fixed. 331 | 332 | ## [L-02] It is possible to register frozen accounts 333 | 334 | ### Impact 335 | 336 | Frozen accounts can be registered by the owner. This should not be possible because when an account is frozen, it is not able to participate in vesting. 337 | 338 | ```solidity 339 | require(_amounts.length == _userAddresses.length, "Amounts and userAddresses must have the same length"); 340 | 341 | for (uint i = 0; i < _userAddresses.length; i++) { 342 | //@audit can register frozen account 343 | userTotal[_userAddresses[i]] = _amounts[i]; 344 | } 345 | ``` 346 | 347 | ### Recommended Mitigation Steps 348 | 349 | Add an additional check for every account that will be registered. 350 | 351 | ### Fixes Review 352 | 353 | Acknowledged. 354 | 355 | ## [L-03] calculateReward will revert if double staking is made 356 | 357 | ### Impact 358 | 359 | The `calculateReward` function will revert if a user made a double staking before `rewardPeriods[i].start`. The variable `tempStart` will be equal to `rewardPeriods[i].start`, which is less than `block.timestamp` because start is in the future. 360 | 361 | ```solidity 362 | uint256 tempStart = rewardPeriods[i].start < user.lastStakeTime ? user.lastStakeTime : rewardPeriods[i].start; 363 | 364 | //@audit this will revert if double staking is made before rewardPeriods[i].start 365 | // tempstart = rewardPeriods[i].start which is < than block.timestamp because start is in the future 366 | timeDelta = block.timestamp - tempStart; 367 | ``` 368 | 369 | ### Recommended Mitigation Steps 370 | 371 | This is a rare case and as we discussed, the difference between deploying the contract and starting the staking will be small. I recommend to notify the user to not stake until the staking period has started. 372 | 373 | ### Fixes Review 374 | 375 | Fixed. An additional check has been added 376 | `require(block.timestamp >= rewardPeriods[0].start, "Staking not started.");` 377 | 378 | ## [L-04] The nonReentrant modifier should occur before all other modifiers 379 | 380 | ### Impact 381 | 382 | The modifiers are read one by one depending on their order. The `checkEthFeeAndRefundDust` modifier is placed before the `nonReentrant` modifier, which allows the possibility for a user to execute an arbitrary call or attempt a reentrancy attack. 383 | 384 | This is a best practice to protect against reentrancy in other modifiers. 385 | 386 | ### Fixes Review 387 | 388 | Fixed. 389 | 390 | # Information 391 | 392 | ## [I-01] Use msg.sender instead of owner() 393 | 394 | ### Impact 395 | 396 | When a function is only accessible by the owner due to the presence of the `onlyOwner` modifier, there is no need to call an internal function to obtain the owner's address. In this case,` msg.sender` is equal to `owner()`. 397 | 398 | ### Recommended Mitigation Steps 399 | 400 | Make the following changes: 401 | 402 | ```diff 403 | function withdrawEth(uint256 amount) external onlyOwner { 404 | 405 | - (bool success,) = payable(owner()).call{value : amount}(""); 406 | + (bool success,) = payable(msg.sender).call{value : amount}(""); 407 | if (!success) { 408 | revert WithdrawFailed(); 409 | } 410 | - emit WithdrawEth(owner(), amount); 411 | + emit WithdrawEth(msg.sender, amount); 412 | } 413 | ``` 414 | 415 | ### Fixes Review 416 | 417 | Fixed. 418 | 419 | ## [I-02] Add an additional parameter for the FreezeAccount event 420 | 421 | Owner can freeze and unfreeze any user at any time. It would be beneficial to add the `_freeze` parameter to the `FreezeAccount` event to clarify which action is being performed. 422 | 423 | ```diff 424 | - emit FreezeAccount(_userAddress); 425 | + emit FreezeAccount(_userAddress, _freeze); 426 | ``` 427 | 428 | ### Fixes Review 429 | 430 | Fixed. 431 | 432 | ## [I-03] Using SafeMath when compiler is ^0.8.0 433 | 434 | There is no need to use `SafeMath` when compiler is ^0.8.0 because it has built-in under/overflow checks. 435 | 436 | ### Fixes Review 437 | 438 | Fixed. 439 | 440 | ## [I-04] Add NatSpec documentation 441 | 442 | NatSpec documentation to all public methods and variables is essential for better understanding of the code by developers and auditors and is strongly recommended. 443 | 444 | ### Fixes Review 445 | 446 | Acknowledged. 447 | 448 | ## [I-05] Use safeTransfer/safeTransferFrom instead of transfer/transferFrom 449 | 450 | Using safeTransfer/safeTransferFrom instead of transfer/transferFrom. It will automatically handle the returning of the bool parameter. 451 | 452 | ### Recommended Mitigation Steps 453 | 454 | Example: 455 | 456 | ```diff 457 | -require(token.transferFrom(msg.sender, address(this), _amount), "Stake transfer failed.");` 458 | +token.safeTransferFrom(msg.sender, address(this), _amount);` 459 | ``` 460 | 461 | ### Fixes Review 462 | 463 | Acknowledged. 464 | 465 | ## [I-06] Include old fee value in event 466 | 467 | After changing the fee value in the `updateEthFee` function, it would be good to include the old value of the fee in the `UpdateFee` event. 468 | 469 | ```diff 470 | -emit UpdateFee(_newFee); 471 | +emit UpdateFee(oldFee, _newFee); 472 | ``` 473 | 474 | ### Fixes Review 475 | 476 | Fixed. 477 | 478 | ## [I-07] Import declarations should import specific identifiers, rather than the whole file 479 | 480 | Using import declarations of the form `import {} from "some/file.sol"` avoids polluting the symbol namespace making flattened files smaller and speeds up compilation 481 | 482 | ### Fixes Review 483 | 484 | Fixed. 485 | 486 | ## [I-08] amount argument can be bigger than the balance 487 | 488 | The `amount` argument can be bigger than the balance of the contract and transaction will revert. 489 | Add an additional check for that which returns a proper error with good information if this happens. 490 | 491 | ```solidity 492 | function withdrawEth(uint256 amount) external onlyOwner { 493 | //@audit amount > balance 494 | (bool success,) = payable(owner()).call{value : amount}(""); 495 | if (!success) { 496 | revert WithdrawFailed(); 497 | } 498 | emit WithdrawEth(owner(), amount); 499 | } 500 | ``` 501 | 502 | ### Fixes Review 503 | 504 | Fixed. 505 | 506 | ## [I-09] Check if constructor argument is in the past 507 | 508 | Check if the `_start` argument is in the past in the constructor of the `FixedStaking` contract. This can also be done for the `_tgeDate` in the constructor of the `FixedVestingCliff` contract. 509 | 510 | ### Fixes Review 511 | 512 | Acknowledged. 513 | 514 | # Code Improvements 515 | 516 | ## [CI-01] tempStart can be calculated before if condition 517 | 518 | ### Details 519 | 520 | Variable `tempStart` can be calculated before if condition. 521 | 522 | ### Recommended Mitigation Steps 523 | 524 | Make the following changes: 525 | 526 | ```diff 527 | for (uint256 i = startIndex; i < rewardPeriods.length; i++) { 528 | 529 | uint256 timeDelta; 530 | + uint256 tempStart = rewardPeriods[i].start < user.lastStakeTime ? user.lastStakeTime : rewardPeriods[i].start; 531 | if (i < rewardPeriods.length - 1) { 532 | 533 | - uint256 tempStart = rewardPeriods[i].start < user.lastStakeTime ? user.lastStakeTime : rewardPeriods[i].start; 534 | timeDelta = rewardPeriods[i + 1].start - tempStart; 535 | reward += user.amount * rewardPeriods[i].rate * timeDelta / YEAR_DIVIDER / RATE_DIVIDER; 536 | } else { 537 | 538 | 539 | - uint256 tempStart = rewardPeriods[i].start < user.lastStakeTime ? user.lastStakeTime : rewardPeriods[i].start; 540 | timeDelta = block.timestamp - tempStart; 541 | reward += user.amount * rewardPeriods[i].rate * timeDelta / YEAR_DIVIDER / RATE_DIVIDER; 542 | } 543 | } 544 | ``` 545 | 546 | ### Fixes Review 547 | 548 | Fixed. 549 | 550 | ## [CI-02] Unnecessary updating of lastStakeTime 551 | 552 | ### Details 553 | 554 | Unnecessary updating of the `lastStakeTime` variable if `pending > 0`. At the end of the function, `lastStakeTime` is updated every time. 555 | 556 | ```solidity 557 | user.amount += _amount; 558 | user.lastStakeTime = block.timestamp; 559 | emit StakeStarted(msg.sender, _amount); 560 | ``` 561 | 562 | ### Recommended Mitigation Steps 563 | 564 | Make the following changes: 565 | 566 | ```diff 567 | if (user.amount != 0) { 568 | uint256 pending = calculateReward(msg.sender); 569 | 570 | if (pending > 0) { //@audit-r != 0 better 571 | user.waitingRewards += pending; 572 | 573 | //@audit code-improvement 574 | - user.lastStakeTime = block.timestamp; 575 | } 576 | } 577 | 578 | user.amount += _amount; 579 | user.lastStakeTime = block.timestamp; 580 | emit StakeStarted(msg.sender, _amount); 581 | ``` 582 | 583 | ### Fixes Review 584 | 585 | Fixed. 586 | 587 | # Gas 588 | 589 | ## [G-01] Use external modifier instead of public 590 | 591 | The `external` modifier should be used instead of `public` for `claimStakingRewards` function since is not used internally. 592 | 593 | ## [G-02] Use custom error instead of require statement 594 | 595 | Use custom errors instead of require statements. It saves more gas. 596 | 597 | ## [G-03] ++i/i++ should be unchecked{++i}/unchecked{i++} when it is not possible for them to overflow 598 | 599 | The unchecked keyword is new in solidity version 0.8.0, so this only applies to that version or higher, which these instances are. This saves 30-40 gas per loop 600 | 601 | ## [G-04] Use != 0 instead of > 0 602 | 603 | Replace spotted instances with != 0 for uints as this uses less gas. 604 | 605 | ## [G-05] Add unchecked {} for subtractions where the operands can not underflow 606 | 607 | In some places in the codebase, `unchecked {}` can be used for subtractions because they are already checked. 608 | 609 | ## [G-06] Use immutable when is possible 610 | 611 | The storage variable `token` can be immutable in the `FixedStaking` contract because it will never be changed. 612 | 613 | ## [G-07] Default int values are manually set 614 | 615 | In instances where a new variable is defined, there is no need to set it to it's default value. 616 | -------------------------------------------------------------------------------- /solo/SpartaDex-Security-Review.md: -------------------------------------------------------------------------------- 1 | # SpartaDex Security Review 2 | 3 | A security review of the [SpartaDex](https://spartadex.io/). The first DEX with gamified yeild. 4 | 5 | Author: [gkrastenov](https://twitter.com/gkrastenov) Independent Security Researcher 6 | 7 | This audit report includes all the vulnerabilities, issues and code improvements found during the security review. 8 | 9 | ## Disclaimer 10 | 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. Subsequent security reviews, bug bounty programs and on-chain monitoring are strongly recommended. 11 | 12 | ## Risk classification 13 | 14 | | Severity | Impact: High | Impact: Medium | Impact: Low | 15 | | :----------------- | :----------: | :------------: | :---------: | 16 | | Likelihood: High | Critical | High | Medium | 17 | | Likelihood: Medium | High | Medium | Low | 18 | | Likelihood: Low | Medium | Low | Low | 19 | 20 | ### Impact 21 | 22 | - **High** - leads to a significant material loss of assets in the protocol or significantly harms a group of users. 23 | - **Medium** - only a small amount of funds can be lost (such as leakage of value) or a core functionality of the protocol is affected. 24 | - **Low** - can lead to any kind of unexpected behaviour with some of the protocol's functionalities that's not so critical. 25 | 26 | ### Likelihood 27 | 28 | - **High** - attack path is possible with reasonable assumptions that mimic on-chain conditions and the cost of the attack is relatively low to the amount of funds that can be stolen or lost. 29 | - **Medium** - only conditionally incentivized attack vector, but still relatively likely. 30 | - **Low** - has too many or too unlikely assumptions or requires a huge stake by the attacker with little or no incentive. 31 | 32 | ## Executive summary 33 | 34 | ### Overview 35 | | | | 36 | | :------------ | :------------------------------------------------------------------------------------------------------------------------------------------ | 37 | | Project Name | SpartaDex 38 | | Review Commit hash | [18941ef176e74c81da9d7a98ff90ffd1c4ab9d80](https://github.com/SpartaDEX/sdex-smart-contracts/tree/18941ef176e74c81da9d7a98ff90ffd1c4ab9d80) | 39 | | Fixes Review Commit hash | [5cf7351353e3989c6c8272c3afa9dfdeb4bddccb](https://github.com/SpartaDEX/sdex-smart-contracts/tree/5cf7351353e3989c6c8272c3afa9dfdeb4bddccb) | 40 | 41 | ### Scope 42 | 43 | | File | 44 | | :-------------- | 45 | | Contracts (15) | 46 | | [contracts/tokens/StakedSparta.sol](https://github.com/SpartaDEX/sdex-smart-contracts/blob/main/contracts/tokens/StakedSparta.sol) | 47 | | [contracts/tokens/PolisMinter.sol](https://github.com/SpartaDEX/sdex-smart-contracts/blob/main/contracts/tokens/PolisMinter.sol) | 48 | | [contracts/tokens/Polis.sol](https://github.com/SpartaDEX/sdex-smart-contracts/blob/main/contracts/tokens/Polis.sol) | 49 | | [contracts/tokens/Sparta.sol](hhttps://github.com/SpartaDEX/sdex-smart-contracts/blob/main/contracts/tokens/Sparta.sol) | 50 | | [contracts/PolisManager.sol](https://github.com/SpartaDEX/sdex-smart-contracts/blob/main/contracts/PolisManager.sol) | 51 | | [contracts/dex/periphery/SpartaDexRouter.sol](https://github.com/SpartaDEX/sdex-smart-contracts/blob/main/contracts/dex/periphery/SpartaDexRouter.sol) | 52 | | [contracts/dex/core/SpartaDexFactory.sol](https://github.com/SpartaDEX/sdex-smart-contracts/blob/main/contracts/dex/core/SpartaDexFactory.sol) | 53 | | [contracts/dex/core/SpartaDexERC20.sol](https://github.com/SpartaDEX/sdex-smart-contracts/blob/main/contracts/dex/core/SpartaDexERC20.sol) | 54 | | [contracts/dex/core/SpartaDexPair.sol](https://github.com/SpartaDEX/sdex-smart-contracts/blob/main/contracts/dex/core/SpartaDexPair.sol) | 55 | | [contracts/staking/LinearStaking.sol](https://github.com/SpartaDEX/sdex-smart-contracts/blob/main/contracts/staking/LinearStaking.sol) | 56 | | [contracts/staking/SpartaStaking.sol](https://github.com/SpartaDEX/sdex-smart-contracts/blob/main/contracts/staking/SpartaStaking.sol) | 57 | | [contracts/staking/LPLinearStaking.sol](https://github.com/SpartaDEX/sdex-smart-contracts/blob/main/contracts/staking/LPLinearStaking.sol) | 58 | | [contracts/SpartaRewardLpLinearStaking.sol](https://github.com/SpartaDEX/sdex-smart-contracts/blob/main/contracts/staking/SpartaRewardLpLinearStaking.sol) | 59 | | [contracts/staking/LPLinearStaking.sol](https://github.com/SpartaDEX/sdex-smart-contracts/blob/main/contracts/staking/LPLinearStaking.sol) | 60 | | [contracts/WithFees.sol](https://github.com/SpartaDEX/sdex-smart-contracts/blob/main/contracts/WithFees.sol) | 61 | | Interfaces (9) | 62 | | [ccontracts/IPolisManager.sol](https://github.com/SpartaDEX/sdex-smart-contracts/blob/main/contracts/IPolisManager.sol) | 63 | | [contracts/tokens/interfaces/IPolisMinter.sol](https://github.com/SpartaDEX/sdex-smart-contracts/blob/main/contracts/tokens/interfaces/IPolisMinter.sol) | 64 | | [contracts/tokens/interfaces/IStakedSparta.sol](https://github.com/SpartaDEX/sdex-smart-contracts/blob/main/contracts/tokens/interfaces/IStakedSparta.sol) | 65 | | [contracts/tokens/interfaces/IPolis.sol](https://github.com/SpartaDEX/sdex-smart-contracts/blob/main/contracts/tokens/interfaces/IPolis.sol) | 66 | | [contracts/tokens/interfaces/ISparta.sol](https://github.com/SpartaDEX/sdex-smart-contracts/blob/main/contracts/tokens/interfaces/ISparta.sol) | 67 | | [contracts/IWithFees.sol](https://github.com/SpartaDEX/sdex-smart-contracts/blob/main/contracts/IWithFees.sol) | 68 | | [contracts/staking/interfaces/ILinearStaking.sol](https://github.com/SpartaDEX/sdex-smart-contracts/blob/main/contracts/staking/interfaces/ILinearStaking.sol) | 69 | | [contracts/staking/interfaces/ISpartaStaking.sol](https://github.com/SpartaDEX/sdex-smart-contracts/blob/main/contracts/staking/interfaces/ISpartaStaking.sol) | 70 | | [contracts/payment-receiver/IPaymentReceiver.sol](https://github.com/SpartaDEX/sdex-smart-contracts/blob/main/contracts/payment-receiver/IPaymentReceiver.sol) | 71 | | **Total (24)** | 72 | | **nSLOC (1966)** | 73 | 74 | 75 | ### Issues found 76 | 77 | | Severity | Count | Fixed | Acknowledged | 78 | | :------------ | ----: | ----: | ---------: | 79 | | High risk | 2 | 2 | 0 | 80 | | Medium risk | 6 | 6 | 0 | 81 | | Low risk | 8 | 8 | 0 | 82 | | Informational | 18 | 16 | 2 | 83 | | Gas | 6 | 5 | 1 | 84 | | **Total** | **40** | **37** | **3** | 85 | 86 | # Findings 87 | 88 | |ID | Title | Severity | 89 | |:---| :------------------------------------------------------------------------------------| :------------ | 90 | |H-01 | Token can be stucked in the staking contract | High | 91 | |H-02 | Wrong implementation of swapTokensForExactTokens | High | 92 | |M-01 | weiRewardRatioPerTokenStored() will not work most of the time | Medium | 93 | |M-02 | The owner can not lock tokens without granting approval rights to another user | Medium | 94 | |M-03 | Signature replay attack is possible | Medium | 95 | |M-04 | ChainId is missed in EIP712 Type hash | Medium | 96 | |M-05 | withdrawTokensToClaimFromRounds will not work as expected | Medium | 97 | |M-06 | Use safeTransferFrom() instead of transferFrom() | Medium | 98 | |L-01 | Double approving to same address is possible | Low | 99 | |L-02 | Hard-coded chainId in DOMAIN_SEPARATOR | Low | 100 | |L-03 | The tokenId is not checked for existence during the buying of gems | Low | 101 | |L-04 | The wallet can be equal to address(0) during the buying of gems | Low | 102 | |L-05 | Typos in EIP712Domain separator and UPGRADE_TYPE | Low | 103 | |L-06 | Transferring ownership in the constructor is unnecessary | Low | 104 | |L-07 | Upgrading of not existing token is possible | Low | 105 | |L-08 | UPGRADE_TYPE and BUY_TYPE contain unnecessary parametars | Low | 106 | |I-01 | Missed license | Informational | 107 | |I-02 | Redundant events or errors | Informational | 108 | |I-03 | Library ECDSA is never used in Polis contract | Informational | 109 | |I-04 | Argument in modifier is redundant | Informational | 110 | |I-05 | Approving to zero address is possible | Informational | 111 | |I-06 | updateReward modifier is unnecessary in initialize function | Informational | 112 | |I-07 | Redundant function | Informational | 113 | |I-08 | Naming collision in WithFees contract | Informational | 114 | |I-09 | Emit event in crucial places | Informational | 115 | |I-10 | Use custom error instead of require statement | Informational | 116 | |I-11 | Rename function setup to initialize | Informational | 117 | |I-12 | Modifiers should be declared before functions | Informational | 118 | |I-13 | Add NatSpec documentation | Informational | 119 | |I-14 | Missing non-zero address checks | Informational | 120 | |I-15 | Import declarations should import specific identifiers, rather than the whole file | Informational | 121 | |I-16 | Remove redundant import | Informational | 122 | |I-17 | Add additional checks for the variables duration and start | Informational | 123 | |I-18 | Override keyword is missed in some functions | Informational | 124 | |G-01| Using private rather than public for constants, saves gas | Gas | 125 | |G-02| Constructors can be marked payable | Gas | 126 | |G-03| ++i/i++ should be unchecked{++i}/unchecked{i++} when it is not possible for them to overflow | Gas | 127 | |G-04| Don’t initialize variables with default value | Gas | 128 | |G-05| Cache storage values in memory to minimize SLOADs | Gas | 129 | |G-06| Use calldata instead of memory | Gas | 130 | 131 | 132 | 133 | # High 134 | 135 | ## [H-01] Token can be stucked in staking contract 136 | ### Impact 137 | If the owner grants rights to another user to lock his token but later decides to remove those rights, the token will become stuck in the contract because only the locker can unlock. 138 | 139 | ```solidity 140 | function unlock(uint256 _tokenId) external override { 141 | if (lockerOf(_tokenId) != msg.sender) { 142 | //@audit can be stucked if approved operator is removed 143 | revert CannotUnlock(); 144 | } 145 | 146 | delete _lockers[_tokenId]; 147 | 148 | emit Unlock(_tokenId, msg.sender); 149 | } 150 | ``` 151 | ### Recommended Mitigation Steps 152 | Owner should be able to unlock his token. 153 | 154 | 155 | ### Fixes Review 156 | Fixed. 157 | 158 | ## [H-02] Wrong implementation of swapTokensForExactTokens 159 | ### Impact 160 | The check for excessive input amount is wrongly implemented in the `swapTokensForExactTokens` function. The swap will be successful only when `amounts[0]` is greater than or equal to `amountInMax` 161 | 162 | `amounts[0]` represents the actual amount of input tokens that will be spent during the token swap. It is calculated based on the token exchange rates and liquidity available in the SpartaDex pairs. 163 | `amountInMax` is the maximum amount of input tokens that you are willing to spend. 164 | 165 | The actual amount of input tokens should not be bigger than the maximum amount that the user is willing to spend. 166 | 167 | For reference: https://github.com/Uniswap/v2-periphery/blob/master/contracts/UniswapV2Router02.sol#L246 168 | 169 | ```solidity 170 | function swapTokensForExactTokens( 171 | uint amountOut, 172 | uint amountInMax, 173 | address[] calldata path, 174 | address to, 175 | uint deadline 176 | ) 177 | external 178 | virtual 179 | override 180 | ensure(deadline) 181 | returns (uint[] memory amounts) 182 | { 183 | amounts = UniswapV2Library.getAmountsIn(factory, amountOut, path); 184 | //@audit wrong if condition 185 | if (amounts[0] < amountInMax) { 186 | revert ExcessiveInputAmount(); 187 | } 188 | 189 | TransferHelper.safeTransferFrom( 190 | path[0], 191 | msg.sender, 192 | UniswapV2Library.pairFor(factory, path[0], path[1]), 193 | amounts[0] 194 | ); 195 | _swap(amounts, path, to); 196 | } 197 | ``` 198 | ### Recommended Mitigation Steps 199 | Make the following changes: 200 | 201 | ```diff 202 | - if (amounts[0] < amountInMax) 203 | + if (amounts[0] > amountInMax) 204 | ``` 205 | 206 | ### Fixes Review 207 | Fixed. 208 | 209 | # Medium 210 | 211 | ## [M-01] weiRewardRatioPerTokenStored() will not work most of the time 212 | ### Impact 213 | This function calculates the ratio between the reward wei and the stored tokens. If the `totalSupply` is zero, it returns 0. When the `totalSupply` is not equal to 0, the return value is the division of the `rewardRate` and the `totalSupply`. If the `totalSupply` is greater than the `rewardRate`, precision loss may occur. 214 | 215 | ```solidity 216 | function weiRewardRatioPerTokenStored() external view returns (uint256) { 217 | //@audit precision loss if totalSupply > rewardRate 218 | return totalSupply == 0 ? 0 : rewardRate / totalSupply; 219 | } 220 | ``` 221 | 222 | ### PoC 223 | ```javascript 224 | it.only("Medium 1: weiRewardRatioPerTokenStored() -> precision loss", async function () { 225 | const initialAmount = ethers.utils.parseEther("30000000"); 226 | await mintTokens(instance.address, initialAmount); 227 | 228 | await mintTokensToContract(); 229 | 230 | const stakerAmount = ethers.utils.parseEther("100"); 231 | await mintTokens(staker.address, stakerAmount); 232 | await sparta.connect(staker).approve(instance.address, stakerAmount); 233 | 234 | const aliceAmount = ethers.utils.parseEther("200"); 235 | await mintTokens(alice.address, aliceAmount); 236 | await sparta.connect(alice).approve(instance.address, aliceAmount); 237 | 238 | const bobAmount = ethers.utils.parseEther("1200"); 239 | await mintTokens(bob.address, bobAmount); 240 | await sparta.connect(bob).approve(instance.address, bobAmount); 241 | 242 | const duration = time.duration.seconds(2500000); // around 30 days (28.93) 243 | await instance 244 | .connect(stakingOwner) 245 | .initialize(initialAmount, stakingStart, duration); 246 | // rewardRate = 12*1e18 247 | await time.setNextBlockTimestamp(stakingStart); 248 | 249 | await instance.connect(staker).stake(stakerAmount); // 100*1e18 250 | // totalSupply = 100*1e18 which is bigger than rewardRate 251 | let ratio = await instance.weiRewardRatioPerTokenStored(); // 100*1e18 > rewardRate 252 | expect(ratio).to.be.eq('0'); 253 | 254 | await instance.connect(alice).stake(aliceAmount); // 200*1e18 255 | // totalSupply = 300*1e18 256 | ratio = await instance.weiRewardRatioPerTokenStored(); // 300*1e18 > rewardRate 257 | expect(ratio).to.be.eq('0'); 258 | 259 | 260 | await instance.connect(bob).stake(bobAmount); // 1200*1e18 261 | // totalSupply = 1500*1e18 262 | ratio = await instance.weiRewardRatioPerTokenStored(); // 1500*1e18 > rewardRate 263 | expect(ratio).to.be.eq('0'); 264 | }); 265 | ``` 266 | 267 | ### Recommended Mitigation Steps 268 | Find a different way to calculate this ratio. 269 | 270 | ### Fixes Review 271 | Fixed. 272 | 273 | ## [M-02] The owner can not lock tokens without granting approval rights to another user 274 | ### Impact 275 | The owner is not able to lock his own token because first he has to approve the operator/locker. He is forced to always grant permissions to someone else which may not be desirable in some scenarios. For example, he may not trust another user. 276 | 277 | ### PoC 278 | ```javascript 279 | it.only("Medium 2: The owner can not lock their tokens", async function () { 280 | await instance.connect(tokenOwner).mint(); 281 | await instance.connect(tokenOwner).lock(1); // revert with CannotLock() 282 | // 283 | expect(await instance.lockerOf(1)).to.be.equal(tokenOwner.address); 284 | expect(await instance.totalSupply()).to.be.equal(1); 285 | }); 286 | ``` 287 | 288 | ### Recommended Mitigation Steps 289 | If `msg.sender` is the owner of the token, then allow them to lock their token 290 | 291 | ### Fixes Review 292 | Fixed. 293 | 294 | ## [M-03] Signature replay attack is possible 295 | ### Impact 296 | A signature replay attack is possible during token upgrades. If the deadline is set too far in the future or has not yet expired, a previously used signature for upgrading can be reused. 297 | 298 | ### PoC 299 | ```javascript 300 | it.only("Signature replay attack", async () => { 301 | await polis.mintAsMinter(owner1.address); 302 | const levelBefore = await polis.senateLevel(1); 303 | await polis.connect(owner1).setLockApprovalForAll(instance.address, true); 304 | await instance.connect(owner1).lock(1); 305 | const hash = await instance.upgradeHash(1, 1); 306 | const signature = deployer.signMessage(ethers.utils.arrayify(hash)); 307 | 308 | await instance.upgradeWithSignature(1, 1, signature); 309 | let lv = await polis.senateLevel(1); 310 | console.log(lv); // senate = 1 311 | 312 | expect(await polis.senateLevel(1)).to.be.equal(levelBefore + 1); 313 | 314 | // Attack: 315 | const maxLevel = 254; 316 | for (let i = 0; i < maxLevel; i++) { 317 | await instance.upgradeWithSignature(1, 1, signature); 318 | } 319 | lv = await polis.senateLevel(1); 320 | console.log(lv); // senate = 255 321 | 322 | // Reached max level: 323 | expect(await polis.senateLevel(1)).to.be.equal(levelBefore + 255); // => true 324 | }); 325 | ``` 326 | 327 | ### Recommended Mitigation Steps 328 | Add a nonce for every signature that the user signs. 329 | 330 | ### Fixes Review 331 | Fixed. 332 | 333 | ## [M-04] ChainId is missed in EIP712 Type hash 334 | ### Impact 335 | ChainId is missed in EIP712 Type hash in `PolisManager` and `PaymentReceiver` contracts. 336 | 337 | ```solidity 338 | function domainSeperator() internal view returns (bytes32) { 339 | return 340 | keccak256( 341 | abi.encode( 342 | keccak256( //@audit Wrong domain separator, missed chainId 343 | "EIP712Domain(string name, string version, address verifyingContract, uint256 signedAt)" 344 | ), 345 | keccak256(bytes("Sparta")), 346 | keccak256(bytes("1")), 347 | _chainId(), 348 | address(this), 349 | keccak256(bytes("Sparta")) 350 | ) 351 | ); 352 | } 353 | ``` 354 | 355 | ### Recommended Mitigation Steps 356 | Change type hash to: 357 | ```diff 358 | - "EIP712Domain(string name, string version, address verifyingContract, uint256 signedAt)" 359 | + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract,uint256 signedAt)" 360 | ``` 361 | 362 | ### Fixes Review 363 | Fixed. 364 | 365 | ## [M-05] withdrawTokensToClaimFromRounds will not work as expected 366 | ### Impact 367 | `withdrawTokensToClaimFromRounds` function will not work as expected because every time `roundsLength` is equal to 0 and the for loop will not cycle through all the given rounds. 368 | 369 | ```solidity 370 | function withdrawTokensToClaimFromRounds(uint256[] memory rounds) external { 371 | uint256 roundsLength = 0; //@audit M: roundsLength = rounds.length 372 | for (uint roundIndex = 0; roundIndex < roundsLength; ++roundIndex) { 373 | withdrawTokensToClaim(rounds[roundIndex]); 374 | } 375 | } 376 | ``` 377 | ### Recommended Mitigation Steps 378 | Make the following changes: 379 | 380 | ```diff 381 | - uint256 roundsLength = 0; 382 | + uint256 roundsLength = rounds.length; 383 | ``` 384 | 385 | ### Fixes Review 386 | Fixed. 387 | 388 | ## [M-06] Use safeTransferFrom() instead of transferFrom() 389 | It is more preferable to use safeTransferFrom instead of transferFrom because if ‘_to ‘ is a contract address that does not support ERC721, the NFT can be frozen in that contract. 390 | 391 | Also, OpenZeppelin’s documentation discourages the use of transferFrom(). Use safeTransferFrom() whenever possible because transferFrom() can not check whether the receiving address know how to handle ERC721 tokens. 392 | 393 | ### Fixes Review 394 | Fixed. 395 | 396 | # Low severity 397 | 398 | ## [L-01] Double approving to same address is possible 399 | ### Impact 400 | It is possible to double-approve the same address in the `lockApprove()` function of the `Polis` contract. This behavior is not expected, and the `LockApproval` event with the same `owner`, `to`, and `tokenId` will be emitted again. 401 | 402 | ```solidity 403 | function lockApprove( 404 | address _to, 405 | uint256 _tokenId 406 | ) public virtual isNotLocked(_tokenId) { 407 | //@audit double approving to same address is possible 408 | address owner = ERC721A.ownerOf(_tokenId); 409 | if (_to == owner) { 410 | revert SelfApproval(); 411 | } 412 | bool canApprove = msg.sender == owner || 413 | _lockOperatorApprovals[owner][msg.sender]; 414 | 415 | if (!canApprove) { 416 | revert CannotApprove(); 417 | } 418 | 419 | _lockApprove(owner, _to, _tokenId); 420 | } 421 | ``` 422 | ### Recommended Mitigation Steps 423 | Add additional check for that. 424 | 425 | ```solidity 426 | if (lockApprovals[tokenId] == _to) { 427 | revert AlreadyApproved(); 428 | } 429 | ``` 430 | 431 | ### Fixes Review 432 | Fixed. 433 | 434 | ## [L-02] Hard-coded chainId in DOMAIN_SEPARATOR 435 | ### Impact 436 | Chain ID should be computed dynamically rather than being hard-coded into the DOMAIN_SEPARATOR during initialization. 437 | 438 | ```solidity 439 | constructor() { 440 | DOMAIN_SEPARATOR = keccak256( 441 | abi.encode( 442 | keccak256( 443 | "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" 444 | ), 445 | keccak256(bytes(name)), 446 | keccak256(bytes("1")), 447 | 1729, //@audit hard-coded chainId 448 | address(this) 449 | ) 450 | ); 451 | } 452 | ``` 453 | ### Recommended Mitigation Steps 454 | Use similar approach as in the `PaymentReceiver` contract. 455 | 456 | ### Fixes Review 457 | Fixed. 458 | 459 | ## [L-03] The tokenId is not checked for existence during the buying of gems 460 | ### Impact 461 | Gems can be bought and a `GemsPurchased` event can be emitted when the `tokenId` does not exist. 462 | 463 | ```solidity 464 | function buyGems( 465 | address _wallet, 466 | uint256 _tokenId, // polis nft 467 | uint256 _amount, 468 | uint256 _price, 469 | uint256 _deadline, 470 | bytes calldata _signature 471 | ) external override deadlineIsNotMissed(_deadline) { 472 | //@audit _tokenId is not checked if exist 473 | 474 | bytes32 hash = buyGemsnHash( 475 | _wallet, 476 | _tokenId, 477 | _amount, 478 | _price, 479 | _deadline 480 | ); 481 | 482 | address signer = hash.toEthSignedMessageHash().recover(_signature); 483 | _esnureHasGemsTraderRole(signer); 484 | 485 | if (!paymentToken.transferFrom(msg.sender, treasury, _price)) { 486 | revert TransferFailed(); 487 | } 488 | 489 | emit GemsPurchased( 490 | _wallet, 491 | collection, 492 | _tokenId, 493 | msg.sender, 494 | _amount, 495 | _price 496 | ); 497 | } 498 | ``` 499 | 500 | ### Recommended Mitigation Steps 501 | Add an additional check if the `tokenId` exists. 502 | 503 | ```solidity 504 | if (collection.ownerOf(_tokenId) == address(0)) { 505 | revert TokenNotExists(); 506 | } 507 | ``` 508 | ### Fixes Review 509 | Fixed. 510 | 511 | ## [L-04] The wallet can be equal to address(0) during the buying of gems 512 | ### Impact 513 | Gems can be bought and a `GemsPurchased` event can be emitted when the wallet is equal to address(0). 514 | 515 | ```solidity 516 | function buyGems( 517 | address _wallet, 518 | uint256 _tokenId, // polis nft 519 | uint256 _amount, 520 | uint256 _price, 521 | uint256 _deadline, 522 | bytes calldata _signature 523 | ) external override deadlineIsNotMissed(_deadline) { 524 | //@audit _wallet can be eqaul to zero 525 | 526 | bytes32 hash = buyGemsnHash( 527 | _wallet, 528 | _tokenId, 529 | _amount, 530 | _price, 531 | _deadline 532 | ); 533 | 534 | address signer = hash.toEthSignedMessageHash().recover(_signature); 535 | _esnureHasGemsTraderRole(signer); 536 | 537 | if (!paymentToken.transferFrom(msg.sender, treasury, _price)) { 538 | revert TransferFailed(); 539 | } 540 | 541 | emit GemsPurchased( 542 | _wallet, 543 | collection, 544 | _tokenId, 545 | msg.sender, 546 | _amount, 547 | _price 548 | ); 549 | } 550 | ``` 551 | 552 | 553 | ### Recommended Mitigation Steps 554 | Add an additional check if wallet is eqaul to zero address. 555 | 556 | ```solidity 557 | if (_wallet_ == address(0)) { 558 | revert ZeroAddress(); 559 | } 560 | ``` 561 | ### Fixes Review 562 | Fixed. 563 | 564 | ## [L-05] Typos in EIP712Domain separator and UPGRADE_TYPE 565 | ### Impact 566 | Typos occur in the EIP712 Domain separator and the UPGRADE_TYPE constant variable. 567 | 568 | For additional information: https://eips.ethereum.org/EIPS/eip-712 569 | 570 | ### Recommended Mitigation Steps 571 | Make the following changes: 572 | 573 | ```diff 574 | - string constant UPGRADE_TYPE = "upgrade(uint256 tokenId, uint8 level, bytes signature)"; 575 | + string constant UPGRADE_TYPE = "upgrade(uint256 tokenId,uint8 level,bytes signature)"; 576 | ``` 577 | 578 | ```diff 579 | - "EIP712Domain(string name, string version, address verifyingContract, uint256 signedAt)" 580 | + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract,uint256 signedAt)" 581 | ``` 582 | 583 | ## [L-06] Transferring ownership in the constructor is unnecessary 584 | ### Impact 585 | Transferring ownership in the constructor is unnecessary because it is already done in the base class. 586 | 587 | ```solidity 588 | constructor( 589 | IERC20 sparta_, 590 | IStakedSparta stakedSparta_, 591 | IAccessControl _acl, 592 | address _treasury, 593 | uint256 _value 594 | ) WithFees(_acl, _treasury, _value) { 595 | sparta = sparta_; 596 | stakedSparta = stakedSparta_; 597 | 598 | // @audit unnecessary 599 | _transferOwnership(msg.sender); 600 | } 601 | ``` 602 | 603 | ### Fixes Review 604 | Fixed. 605 | 606 | ## [L-07] Upgrading of not existing token is possible 607 | ### Impact 608 | Upgrading a non-existing token is possible in the `Polis` contract. 609 | 610 | ```solidity 611 | function upgrade(uint256 tokenId) external onlyUpgradeRoleAccess { 612 | //@audit Non-existing tokens can be upgraded. 613 | _upgrade(tokenId); 614 | } 615 | ``` 616 | ### Recommended Mitigation Steps 617 | Add an additional check to verify if the token exists. 618 | 619 | ### Fixes Review 620 | Fixed. 621 | 622 | ## [L-08] UPGRADE_TYPE and BUY_TYPE contain unnecessary parametars 623 | The parameter `signature` is unnecessary because it is never used during the creation of the hash. 624 | [UPGRADE_TYPE](https://github.com/SpartaDEX/sdex-smart-contracts/blob/main/contracts/PolisManager.sol#L124) 625 | [BUY_TYPE](https://github.com/SpartaDEX/sdex-smart-contracts/blob/main/contracts/payment-receiver/PaymentReceiver.sol#L106) 626 | 627 | If you are not sure how to implement EIP712 in your smart contract. 628 | 629 | Than you can follow this article: [ref](https://medium.com/coinmonks/eip712-a-full-stack-example-e12185b03d54) 630 | 631 | ### Fixes Review 632 | Fixed. 633 | 634 | # Information 635 | 636 | ## [I-01] Missed license in SpartaDexRouter 637 | The license `SPDX-License-Identifier: Unlicense` is missing in `SpartaDexRouter` and most of the files in the .../dex/periphery folder. 638 | 639 | ### Fixes Review 640 | Fixed. 641 | 642 | ## [I-02] Redundant events or errors 643 | Following erros and events are redundant 644 | `error OnlyLockerRole()`, 645 | `error SignatureExceeded(uint256 deadline)`, 646 | `error AlreadyLocked()` in `PolisMinter` contract 647 | 648 | ### Fixes Review 649 | Fixed. 650 | 651 | ## [I-03] Library ECDSA is never used in Polis contract 652 | Library ECDSA is never used in `Polis` contract. 653 | 654 | ```solidity 655 | using ECDSA for bytes32; //@audit never used in this contract 656 | ``` 657 | ### Fixes Review 658 | Fixed. 659 | 660 | ## [I-04] Argument in modifier is redundant 661 | Argument `tokenId` is redundant in `onlyOneTokenLockedByUser` modifier. 662 | 663 | ```solidity 664 | modifier onlyOneTokenLockedByUser(uint256 tokenId) { //@audit tokenId argument is not used 665 | if (lockedTokens[msg.sender] != 0) { 666 | revert OnlyOneTokenLocked(); 667 | } 668 | _; 669 | } 670 | ``` 671 | ### Fixes Review 672 | Fixed. 673 | 674 | ## [I-05] Approving to zero address is possible 675 | The owner can approve address(0) to have rights to lock tokens. This action will not have any negative impact but this is not expected behavior 676 | 677 | ```solidity 678 | function setLockApprovalForAll( 679 | address operator, 680 | bool approved 681 | ) public virtual { 682 | //@audit approving to zero address is possible 683 | if (msg.sender == operator) { 684 | revert SelfApproval(); 685 | } 686 | _setLockApprovalForAll(msg.sender, operator, approved); 687 | } 688 | ``` 689 | ### Fixes Review 690 | Fixed. 691 | 692 | ## [I-06] updateReward modifier is unnecessary in initialize function 693 | The `updateReward` modifier is unnecessary in the `initialize` function because the variable `updatedAt` is set to block.timestamp within the `initialize` function and `rewardPerTokenStored` is set to zero by default. 694 | ### Fixes Review 695 | Fixed. 696 | 697 | ## [I-07] Redundant function 698 | Remove the redundant function `foo` in the `SpartaDexFactory` contract. 699 | 700 | `function foo() external pure {} ` 701 | ### Fixes Review 702 | Fixed. 703 | 704 | ## [I-08] Naming collision in WithFees contract 705 | A naming collision occurs in the `WithFees` contract between the storage variable `value` and the transfer of ether using the `call` function. This will not have any negative impact while sending money. 706 | 707 | ```solidity 708 | (bool sent, ) = treasury.call{value: address(this).balance}(""); 709 | //@audit naming collision with storage variable value 710 | ``` 711 | 712 | ### Recommended Mitigation Steps 713 | Make the following changes: 714 | 715 | ```diff 716 | - uint256 public immutable override value; 717 | + uint256 public immutable override fee; 718 | ``` 719 | ### Fixes Review 720 | Fixed. 721 | 722 | ## [I-09] Emit event in crucial places 723 | Emit event in crucial place like in `setup()` function in `LinearStaking` contract. 724 | 725 | ```diff 726 | function setup( 727 | uint256 _amount, 728 | uint256 _duration, 729 | uint256 _start 730 | ) external notInitialized onlyOwner updateReward(address(0)) { 731 | require( 732 | _amount <= rewardToken.balanceOf(address(this)), 733 | "reward amount > balance" 734 | //@fix: use error 735 | ); 736 | 737 | duration = _duration; 738 | rewardRate = _amount / duration; //gas: use _duration 739 | start = _start; 740 | updatedAt = block.timestamp; 741 | 742 | initialized = true; 743 | 744 | + emit Initialized(_start, _duration, _amount); 745 | } 746 | ``` 747 | ### Fixes Review 748 | Fixed. 749 | 750 | ## [I-10] Use custom error instead of require statement 751 | Use custom error instead of a require statement in `setup` function in `LinearStaking` contract. 752 | 753 | ``` 754 | function setup( //@audit follow good standard should be initialize 755 | uint256 _amount, 756 | uint256 _duration, 757 | uint256 _start 758 | ) external notInitialized onlyOwner updateReward(address(0)) { 759 | require( 760 | _amount <= rewardToken.balanceOf(address(this)), 761 | "reward amount > balance" 762 | //@audit nc: use custom error 763 | ); 764 | } 765 | ``` 766 | ### Fixes Review 767 | Fixed. 768 | 769 | ## [I-11] Rename function setup to initialize 770 | Rename function `setup` to `initialize` in `LinearStaking` contract. It is recommended to follow established design standards. 771 | 772 | ```solidity 773 | function setup( //@audit follow good standards 774 | uint256 _amount, 775 | uint256 _duration, 776 | uint256 _start 777 | ) external notInitialized onlyOwner updateReward(address(0)) 778 | ``` 779 | ### Fixes Review 780 | Fixed. 781 | 782 | ## [I-12] Modifiers should be declared before functions 783 | It is good practice modifiers to be declared before function. Currently, they are below the functios. 784 | ### Fixes Review 785 | Fixed. 786 | 787 | ## [I-13] Add NatSpec documentation 788 | NatSpec documentation to all public methods and variables is essential for better understanding of the code by developers and auditors and is strongly recommended. 789 | ### Fixes Review 790 | Acknowledged. 791 | 792 | ## [I-14] Missing non-zero address checks 793 | Add non-zero address checks for all address type arguments. 794 | ### Fixes Review 795 | Fixed. 796 | 797 | ## [I-15] Import declarations should import specific identifiers, rather than the whole file 798 | Using import declarations of the form `import {} from "some/file.sol"` avoids polluting the symbol namespace making flattened files smaller and speeds up compilation 799 | ### Fixes Review 800 | Acknowledged. 801 | 802 | ## [I-16] Remove redundant import 803 | The interface IUniswapV2Pair is not used in the `Polis` contract. 804 | 805 | `import "../dex/core/interfaces/IUniswapV2Pair.sol";` 806 | ### Fixes Review 807 | Fixed. 808 | 809 | ## [I-17] Add additional checks for the variables duration and start 810 | Add additional checks for the variables `duration` and `start` in the `initialize` function. The `duration` should be greater than zero, and the `start` variable should be greater than or equal to block.timestamp 811 | ### Fixes Review 812 | Fixed. 813 | 814 | ## [I-18] Override keyword is missed in some functions 815 | The override keyword is missed in the `mintTo` and `burnFrom` functions in the `StakedSparta` contract. 816 | ### Fixes Review 817 | Fixed. 818 | 819 | # Gas 820 | 821 | ## [G-01] Using private rather than public for constants, saves gas 822 | If needed, the values can be read from the verified contract source code, or if there are multiple values there can be a single getter function that [returns a tuple ](https://github.com/code-423n4/2022-08-frax/blob/90f55a9ce4e25bceed3a74290b854341d8de6afa/src/contracts/FraxlendPair.sol#L156-L178) of the values of all currently-public constants. Saves 3406-3606 gas in deployment gas due to the compiler not having to create non-payable getter functions for deployment calldata, not having to store the bytes of the value outside of where it's used, and not adding another entry to the method ID table. 823 | 824 | Instances: 825 | WithFees: [12](https://github.com/SpartaDEX/sdex-smart-contracts/blob/main/contracts/WithFees.sol#L12); 826 | PolisManager: [11](https://github.com/SpartaDEX/sdex-smart-contracts/blob/main/contracts/PolisManager.sol#L11), [14](https://github.com/SpartaDEX/sdex-smart-contracts/blob/main/contracts/PolisManager.sol#L14); 827 | PaymentReceiver: [17](https://github.com/SpartaDEX/sdex-smart-contracts/blob/main/contracts/payment-receiver/PaymentReceiver.sol#L17); 828 | Polis: [14-15](https://github.com/SpartaDEX/sdex-smart-contracts/blob/main/contracts/tokens/Polis.sol#L14-L15); 829 | PolisMinter: [13](https://github.com/SpartaDEX/sdex-smart-contracts/blob/main/contracts/tokens/PolisMinter.sol#L13); 830 | Sparta: [8](https://github.com/SpartaDEX/sdex-smart-contracts/blob/main/contracts/tokens/Sparta.sol#L8); 831 | StakedSparta: [17](https://github.com/SpartaDEX/sdex-smart-contracts/blob/main/contracts/tokens/StakedSparta.sol#L17); 832 | ### Fixes Review 833 | Fixed. 834 | 835 | ## [G-02] Constructors can be marked payable 836 | Payable functions cost less gas to execute, since the compiler does not have to add extra checks to ensure that a payment wasn't provided. A constructor can safely be marked as payable, since only the deployer would be able to pass funds, and the project itself would not pass any funds. 837 | ### Fixes Review 838 | Acknowledged. 839 | 840 | ## [G-03] ++i/i++ should be unchecked{++i}/unchecked{i++} when it is not possible for them to overflow 841 | The unchecked keyword is new in solidity version 0.8.0, so this only applies to that version or higher, which these instances are. This saves 30-40 gas [per loop](https://gist.github.com/hrkrshnn/ee8fabd532058307229d65dcd5836ddc#the-increment-in-for-loop-post-condition-can-be-made-unchecked) 842 | 843 | Instances: 844 | SpartaStaking: [224](https://github.com/SpartaDEX/sdex-smart-contracts/blob/main/contracts/staking/SpartaStaking.sol#L224) 845 | ### Fixes Review 846 | Fixed. 847 | 848 | ## [G-04] Do not initialize variables with default value 849 | Uninitialized variables are assigned with the types default value. Explicitly initializing a variable with it’s default value costs unnecesary gas. 850 | 851 | Instances: 852 | ToInitialize: [8](https://github.com/SpartaDEX/sdex-smart-contracts/blob/main/contracts/ToInitialize.sol#L8); 853 | SpartaStaking: [194](https://github.com/SpartaDEX/sdex-smart-contracts/blob/main/contracts/staking/SpartaStaking.sol#L194), [195](https://github.com/SpartaDEX/sdex-smart-contracts/blob/main/contracts/staking/SpartaStaking.sol#L195), [201](https://github.com/SpartaDEX/sdex-smart-contracts/blob/main/contracts/staking/SpartaStaking.sol#L201), [203](https://github.com/SpartaDEX/sdex-smart-contracts/blob/main/contracts/staking/SpartaStaking.sol#L203), [224](https://github.com/SpartaDEX/sdex-smart-contracts/blob/main/contracts/staking/SpartaStaking.sol#L224), 854 | ### Fixes Review 855 | Fixed. 856 | 857 | ## [G-05] Cache storage values in memory to minimize SLOADs 858 | Use directly `_duration` instead of storage variable `duration`: [1](https://github.com/SpartaDEX/sdex-smart-contracts/blob/main/contracts/staking/LinearStaking.sol#L144), [2](https://github.com/SpartaDEX/sdex-smart-contracts/blob/main/contracts/staking/SpartaStaking.sol#L53) 859 | ### Fixes Review 860 | Fixed. 861 | 862 | ## [G-08] Use calldata instead of memory 863 | Use calldata instead of memory for function parameters saves gas if the function argument is only read. 864 | Instances: 865 | SpartaStaking: [193](https://github.com/SpartaDEX/sdex-smart-contracts/blob/main/contracts/staking/SpartaStaking.sol#L193) 866 | ### Fixes Review 867 | Fixed. --------------------------------------------------------------------------------